Files
Cozypaw-Hospital/docs/superpowers/plans/2026-05-08-sprint-16-snap-points.md
T

22 KiB
Raw Blame History

Sprint 16 — Snap-Point System Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add SnapPoint nodes to all furniture in all 12 rooms so characters can snap into sitting/lying poses when released near a piece of furniture.

Architecture: Each room .tscn gets one ext_resource entry for snap_point.gd and one [node] entry per snap position. SnapPoints are direct children of the room node (not children of ColorRect furniture, since ColorRects have no Node2D hierarchy). All SnapPoints auto-register in the "snap_points" group via _ready(). No new GDScript is written — this sprint is purely scene data.

Tech Stack: Godot 4.6.2, .tscn text format, GUT v9.6.0 (TDD), headless runner.

GDD Reference: docs/game-design.md — Kapitel 2 (Raum-Übersicht) + Kapitel 5.1 (Snap-Point System).

Headless runner:

"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1

Existing tests must stay green: 90 tests passing before this sprint starts.


File Map

Action Path SnapPoints added
Modify scenes/rooms/floor0/Reception.tscn 4 × sitting (2 benches × 2)
Modify scenes/rooms/floor0/GiftShop.tscn 1 × sitting (counter)
Modify scenes/rooms/floor0/Restaurant.tscn 6 × sitting (3 tables × 2)
Modify scenes/rooms/floor0/EmergencyRoom.tscn 1 × lying (medical table)
Modify scenes/rooms/floor1/XRay.tscn 1 × lying (exam table)
Modify scenes/rooms/floor1/Pharmacy.tscn 1 × sitting (counter)
Modify scenes/rooms/floor1/Lab.tscn 2 × sitting (lab bench)
Modify scenes/rooms/floor1/PatientRoom.tscn 2 × lying (2 beds)
Modify scenes/rooms/floor2/Ultrasound.tscn 1 × lying (exam table)
Modify scenes/rooms/floor2/DeliveryRoom.tscn 1 × lying (delivery bed)
Modify scenes/rooms/floor2/Nursery.tscn 3 × lying, baby_only (3 cradles)
Modify scenes/rooms/home/GardenParty.tscn 2 × sitting (garden table)
Create test/unit/test_snap_points_floor0.gd Tests for EG (12 snap points)
Create test/unit/test_snap_points_floor1.gd Tests for 1.OG (6 snap points)
Create test/unit/test_snap_points_floor2.gd Tests for 2.OG + Garten (7 snap points)

Total: 25 SnapPoints across 12 rooms.


How to add a SnapPoint to a .tscn

1. Increment load_steps in the header by 1

[gd_scene load_steps=3 format=3 ...]   ← was 2

2. Add ext_resource for snap_point.gd after the last existing ext_resource line

For rooms with currently 1 ext_resource (InteractiveObject only), use id "2_snap":

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

For rooms with currently 2 ext_resources, use id "3_snap":

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

For GardenParty (3 ext_resources), use id "4_snap":

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="4_snap"]

3. Append node entries at the end of the file

Sitting (default pose, no need to set pose or baby_only):

[node name="SnapBenchLeft1" type="Node2D" parent="."]
position = Vector2(150, 555)
script = ExtResource("2_snap")

Lying (must set pose):

[node name="SnapExamTable" type="Node2D" parent="."]
position = Vector2(640, 480)
script = ExtResource("2_snap")
pose = "lying"

Lying + baby only:

[node name="SnapCradle1" type="Node2D" parent="."]
position = Vector2(340, 240)
script = ExtResource("3_snap")
pose = "lying"
baby_only = true

Helper: how tests count SnapPoints in a room

func _count_snaps(room: Node2D) -> int:
    var count: int = 0
    for child: Node in room.get_children():
        if child is SnapPoint:
            count += 1
    return count

func _count_snaps_with_pose(room: Node2D, pose: String) -> int:
    var count: int = 0
    for child: Node in room.get_children():
        var snap: SnapPoint = child as SnapPoint
        if snap != null and snap.pose == pose:
            count += 1
    return count

func _count_snaps_baby_only(room: Node2D) -> int:
    var count: int = 0
    for child: Node in room.get_children():
        var snap: SnapPoint = child as SnapPoint
        if snap != null and snap.baby_only:
            count += 1
    return count

Task 1: Floor 0 — Reception, GiftShop, Restaurant, EmergencyRoom

Files:

  • Modify: scenes/rooms/floor0/Reception.tscn
  • Modify: scenes/rooms/floor0/GiftShop.tscn
  • Modify: scenes/rooms/floor0/Restaurant.tscn
  • Modify: scenes/rooms/floor0/EmergencyRoom.tscn
  • Create: test/unit/test_snap_points_floor0.gd

Step 1: Write the failing tests

Create test/unit/test_snap_points_floor0.gd:

## Tests verifying SnapPoints exist in all EG (Erdgeschoss) room scenes.
extends GutTest


func _count_snaps(room: Node2D) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		if child is SnapPoint:
			count += 1
	return count


func _count_snaps_with_pose(room: Node2D, pose: String) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		var snap: SnapPoint = child as SnapPoint
		if snap != null and snap.pose == pose:
			count += 1
	return count


func test_reception_has_four_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/Reception.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 4)


func test_reception_all_snaps_are_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/Reception.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 4)


func test_giftshop_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/GiftShop.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_giftshop_snap_is_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/GiftShop.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 1)


func test_restaurant_has_six_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/Restaurant.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 6)


func test_restaurant_all_snaps_are_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/Restaurant.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 6)


func test_emergency_room_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/EmergencyRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_emergency_room_snap_is_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor0/EmergencyRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 1)

Step 2: Run tests — verify they FAIL

"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1

Expected: 8 new failures. 90 existing pass.

Step 3: Edit scenes/rooms/floor0/Reception.tscn

Current header: [gd_scene load_steps=2 format=3 uid="uid://cozypaw_reception"]

Change to: [gd_scene load_steps=3 format=3 uid="uid://cozypaw_reception"]

After [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"], add:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append at the end of the file:

[node name="SnapBenchLeft1" type="Node2D" parent="."]
position = Vector2(150, 555)
script = ExtResource("2_snap")

[node name="SnapBenchLeft2" type="Node2D" parent="."]
position = Vector2(240, 555)
script = ExtResource("2_snap")

[node name="SnapBenchRight1" type="Node2D" parent="."]
position = Vector2(990, 555)
script = ExtResource("2_snap")

[node name="SnapBenchRight2" type="Node2D" parent="."]
position = Vector2(1080, 555)
script = ExtResource("2_snap")

Step 4: Edit scenes/rooms/floor0/GiftShop.tscn

Change header to load_steps=3.

Add after InteractiveObject ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append:

[node name="SnapCounter" type="Node2D" parent="."]
position = Vector2(640, 528)
script = ExtResource("2_snap")

Step 5: Edit scenes/rooms/floor0/Restaurant.tscn

Change header to load_steps=3.

Add after InteractiveObject ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append:

[node name="SnapTable1Left" type="Node2D" parent="."]
position = Vector2(160, 510)
script = ExtResource("2_snap")

[node name="SnapTable1Right" type="Node2D" parent="."]
position = Vector2(280, 510)
script = ExtResource("2_snap")

[node name="SnapTable2Left" type="Node2D" parent="."]
position = Vector2(580, 510)
script = ExtResource("2_snap")

[node name="SnapTable2Right" type="Node2D" parent="."]
position = Vector2(700, 510)
script = ExtResource("2_snap")

[node name="SnapTable3Left" type="Node2D" parent="."]
position = Vector2(1000, 510)
script = ExtResource("2_snap")

[node name="SnapTable3Right" type="Node2D" parent="."]
position = Vector2(1120, 510)
script = ExtResource("2_snap")

Step 6: Edit scenes/rooms/floor0/EmergencyRoom.tscn

Current header: load_steps=3 (has Ambulance ext_resource). Change to load_steps=4.

Add after the last ext_resource line (Ambulance):

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

Append:

[node name="SnapMedicalTable" type="Node2D" parent="."]
position = Vector2(310, 480)
script = ExtResource("3_snap")
pose = "lying"

Step 7: Run tests — verify 8 new tests PASS (98 total)

Run --headless --import first if needed, then the test runner.

Expected: 98/98 passed.

Step 8: Commit

cd "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-16-snap-points"
git add scenes/rooms/floor0/Reception.tscn scenes/rooms/floor0/GiftShop.tscn
git add scenes/rooms/floor0/Restaurant.tscn scenes/rooms/floor0/EmergencyRoom.tscn
git add test/unit/test_snap_points_floor0.gd
git commit -m "feat(snap-points): add SnapPoints to all EG rooms (Reception, GiftShop, Restaurant, EmergencyRoom)"

Task 2: Floor 1 — XRay, Pharmacy, Lab, PatientRoom

Files:

  • Modify: scenes/rooms/floor1/XRay.tscn
  • Modify: scenes/rooms/floor1/Pharmacy.tscn
  • Modify: scenes/rooms/floor1/Lab.tscn
  • Modify: scenes/rooms/floor1/PatientRoom.tscn
  • Create: test/unit/test_snap_points_floor1.gd

Step 1: Write the failing tests

Create test/unit/test_snap_points_floor1.gd:

## Tests verifying SnapPoints exist in all 1.OG room scenes.
extends GutTest


func _count_snaps(room: Node2D) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		if child is SnapPoint:
			count += 1
	return count


func _count_snaps_with_pose(room: Node2D, pose: String) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		var snap: SnapPoint = child as SnapPoint
		if snap != null and snap.pose == pose:
			count += 1
	return count


func test_xray_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/XRay.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_xray_snap_is_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/XRay.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 1)


func test_pharmacy_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_pharmacy_snap_is_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 1)


func test_lab_has_two_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/Lab.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 2)


func test_lab_all_snaps_are_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/Lab.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 2)


func test_patient_room_has_two_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/PatientRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 2)


func test_patient_room_all_snaps_are_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor1/PatientRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 2)

Step 2: Run tests — verify 8 new tests FAIL

Expected: 8 failures. 98 existing pass.

Step 3: Edit scenes/rooms/floor1/XRay.tscn

Current header: load_steps=3 (has XRayMachine). Change to load_steps=4.

Add after the last ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

Append:

[node name="SnapExamTable" type="Node2D" parent="."]
position = Vector2(640, 480)
script = ExtResource("3_snap")
pose = "lying"

Step 4: Edit scenes/rooms/floor1/Pharmacy.tscn

Current header: load_steps=2. Change to load_steps=3.

Add after ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append:

[node name="SnapCounter" type="Node2D" parent="."]
position = Vector2(640, 520)
script = ExtResource("2_snap")

Step 5: Edit scenes/rooms/floor1/Lab.tscn

Current header: load_steps=2. Change to load_steps=3.

Add after ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append:

[node name="SnapLabBench1" type="Node2D" parent="."]
position = Vector2(450, 470)
script = ExtResource("2_snap")

[node name="SnapLabBench2" type="Node2D" parent="."]
position = Vector2(750, 470)
script = ExtResource("2_snap")

Step 6: Edit scenes/rooms/floor1/PatientRoom.tscn

Current header: load_steps=2. Change to load_steps=3.

Add after ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"]

Append:

[node name="SnapBed1" type="Node2D" parent="."]
position = Vector2(250, 465)
script = ExtResource("2_snap")
pose = "lying"

[node name="SnapBed2" type="Node2D" parent="."]
position = Vector2(810, 465)
script = ExtResource("2_snap")
pose = "lying"

Step 7: Run tests — verify 8 new tests PASS (106 total)

Run --headless --import first if needed, then the test runner.

Expected: 106/106 passed.

Step 8: Commit

cd "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-16-snap-points"
git add scenes/rooms/floor1/XRay.tscn scenes/rooms/floor1/Pharmacy.tscn
git add scenes/rooms/floor1/Lab.tscn scenes/rooms/floor1/PatientRoom.tscn
git add test/unit/test_snap_points_floor1.gd
git commit -m "feat(snap-points): add SnapPoints to all 1.OG rooms (XRay, Pharmacy, Lab, PatientRoom)"

Task 3: Floor 2 + Garten — Ultrasound, DeliveryRoom, Nursery, GardenParty

Files:

  • Modify: scenes/rooms/floor2/Ultrasound.tscn
  • Modify: scenes/rooms/floor2/DeliveryRoom.tscn
  • Modify: scenes/rooms/floor2/Nursery.tscn
  • Modify: scenes/rooms/home/GardenParty.tscn
  • Create: test/unit/test_snap_points_floor2.gd

Step 1: Write the failing tests

Create test/unit/test_snap_points_floor2.gd:

## Tests verifying SnapPoints exist in all 2.OG and Garten room scenes.
extends GutTest


func _count_snaps(room: Node2D) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		if child is SnapPoint:
			count += 1
	return count


func _count_snaps_with_pose(room: Node2D, pose: String) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		var snap: SnapPoint = child as SnapPoint
		if snap != null and snap.pose == pose:
			count += 1
	return count


func _count_snaps_baby_only(room: Node2D) -> int:
	var count: int = 0
	for child: Node in room.get_children():
		var snap: SnapPoint = child as SnapPoint
		if snap != null and snap.baby_only:
			count += 1
	return count


func test_ultrasound_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/Ultrasound.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_ultrasound_snap_is_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/Ultrasound.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 1)


func test_delivery_room_has_one_snap_point() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/DeliveryRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 1)


func test_delivery_room_snap_is_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/DeliveryRoom.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 1)


func test_nursery_has_three_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/Nursery.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 3)


func test_nursery_all_snaps_are_baby_only() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/Nursery.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_baby_only(room), 3)


func test_nursery_all_snaps_are_lying() -> void:
	var room: Node2D = preload("res://scenes/rooms/floor2/Nursery.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "lying"), 3)


func test_garden_party_has_two_snap_points() -> void:
	var room: Node2D = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps(room), 2)


func test_garden_party_all_snaps_are_sitting() -> void:
	var room: Node2D = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() as Node2D
	add_child_autofree(room)
	assert_eq(_count_snaps_with_pose(room, "sitting"), 2)

Step 2: Run tests — verify 9 new tests FAIL

Expected: 9 failures. 106 existing pass.

Step 3: Edit scenes/rooms/floor2/Ultrasound.tscn

Current header: load_steps=3 (has UltrasoundMachine). Change to load_steps=4.

Add after last ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

Append:

[node name="SnapExamTable" type="Node2D" parent="."]
position = Vector2(470, 480)
script = ExtResource("3_snap")
pose = "lying"

Step 4: Edit scenes/rooms/floor2/DeliveryRoom.tscn

Current header: load_steps=3 (has DeliveryBed). Change to load_steps=4.

Add after last ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

Append:

[node name="SnapDeliveryBed" type="Node2D" parent="."]
position = Vector2(540, 480)
script = ExtResource("3_snap")
pose = "lying"

Step 5: Edit scenes/rooms/floor2/Nursery.tscn

Current header: load_steps=3 (has Cradle). Change to load_steps=4.

Add after last ext_resource:

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"]

Append:

[node name="SnapCradle1" type="Node2D" parent="."]
position = Vector2(340, 240)
script = ExtResource("3_snap")
pose = "lying"
baby_only = true

[node name="SnapCradle2" type="Node2D" parent="."]
position = Vector2(600, 240)
script = ExtResource("3_snap")
pose = "lying"
baby_only = true

[node name="SnapCradle3" type="Node2D" parent="."]
position = Vector2(860, 240)
script = ExtResource("3_snap")
pose = "lying"
baby_only = true

Step 6: Edit scenes/rooms/home/GardenParty.tscn

Current header: load_steps=4 (has GiftBox, TeaPot, HomeButton). Change to load_steps=5.

Add after the last ext_resource (HomeButton):

[ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="4_snap"]

Append:

[node name="SnapTableLeft" type="Node2D" parent="."]
position = Vector2(530, 455)
script = ExtResource("4_snap")

[node name="SnapTableRight" type="Node2D" parent="."]
position = Vector2(750, 455)
script = ExtResource("4_snap")

Step 7: Run tests — verify 9 new tests PASS (115 total)

Run --headless --import first, then the test runner.

Expected: 115/115 passed.

Step 8: Commit

cd "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-16-snap-points"
git add scenes/rooms/floor2/Ultrasound.tscn scenes/rooms/floor2/DeliveryRoom.tscn
git add scenes/rooms/floor2/Nursery.tscn scenes/rooms/home/GardenParty.tscn
git add test/unit/test_snap_points_floor2.gd
git commit -m "feat(snap-points): add SnapPoints to all 2.OG and Garten rooms (Ultrasound, DeliveryRoom, Nursery, GardenParty)"

Final Check

  • Run full test suite one last time
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1

Expected output:

Scripts              10
Tests               115
Passing Tests       115
---- All tests passed! ----
  • Verify git log shows 3 clean commits
git log --oneline -4

Expected:

feat(snap-points): add SnapPoints to all 2.OG and Garten rooms...
feat(snap-points): add SnapPoints to all 1.OG rooms...
feat(snap-points): add SnapPoints to all EG rooms...