From 9786cf5895159037d815623b8a42ac5882bbc302 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 00:54:19 +0200 Subject: [PATCH] docs: add Sprint 18 Room Chests implementation plan --- .../plans/2026-05-09-sprint-18-room-chests.md | 1086 +++++++++++++++++ 1 file changed, 1086 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-09-sprint-18-room-chests.md diff --git a/docs/superpowers/plans/2026-05-09-sprint-18-room-chests.md b/docs/superpowers/plans/2026-05-09-sprint-18-room-chests.md new file mode 100644 index 0000000..3a703ab --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-sprint-18-room-chests.md @@ -0,0 +1,1086 @@ +# Sprint 18 — Room Chests & Item Spawning 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 tappable `RoomChest` nodes to all 12 rooms that spawn `HoldableItem`/`OutfitItem` instances with a fly-out tween. Items persist in the world until dragged back to their chest. + +**Architecture:** `ChestItemData` (Resource) holds item config per slot. `RoomChestConfig` (static class) maps chest IDs to item arrays — keeps .tscn files simple (only `chest_id` + `position` exported). `RoomChest` (Node2D) reads config in `_ready()`, spawns on demand, receives items on return. `HoldableItem` gains `home_chest` property and `_try_return_to_chest()` — highest priority check in `_on_drag_released`. `OutfitItem` also calls `_try_return_to_chest()` before outfit apply. `GameState` extended to v3 with `_chest_states` Dictionary. + +**Tech Stack:** GDScript (statisch typisiert), GUT v9.6.0, Godot 4.6.2 + +**Test 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 +``` + +**Branch:** `sprint/18-room-chests` (worktree at `.worktrees/sprint-18-room-chests`) + +--- + +## File Map + +| Action | File | Responsibility | +|---|---|---| +| Create | `scripts/objects/chest_item_data.gd` | Resource with item_id, item_type, outfit_layer, spawn_offset | +| Create | `scripts/objects/room_chest_config.gd` | Static config: chest_id → Array[ChestItemData] for all 14 chests | +| Create | `scripts/objects/room_chest.gd` | Node2D: spawn items, receive returns, persist via GameState | +| Modify | `scripts/objects/holdable_item.gd` | Add home_chest, CHEST_RETURN_RADIUS, _try_return_to_chest() | +| Modify | `scripts/objects/outfit_item.gd` | Call _try_return_to_chest() before character check | +| Modify | `scripts/autoload/GameState.gd` | v3: _chest_states, get/set/clear_chest_state | +| Create | `test/unit/test_room_chest.gd` | 10 tests for ChestItemData, RoomChestConfig, RoomChest logic | +| Modify | `test/unit/test_holdable_item.gd` | Append 3 chest-return tests | +| Modify | `test/unit/test_game_state.gd` | Append 6 v3 chest state tests | +| Modify | `scenes/rooms/floor0/Reception.tscn` | Add ReceptionDesk RoomChest | +| Modify | `scenes/rooms/floor0/GiftShop.tscn` | Add GiftShopShelf RoomChest | +| Modify | `scenes/rooms/floor0/Restaurant.tscn` | Add RestaurantCounter RoomChest | +| Modify | `scenes/rooms/floor0/EmergencyRoom.tscn` | Add EmergencyCabinet RoomChest | +| Modify | `scenes/rooms/floor1/XRay.tscn` | Add XRayCabinet RoomChest | +| Modify | `scenes/rooms/floor1/Pharmacy.tscn` | Add PharmacyMedicine + PharmacyTools RoomChest | +| Modify | `scenes/rooms/floor1/Lab.tscn` | Add LabBench RoomChest | +| Modify | `scenes/rooms/floor1/PatientRoom.tscn` | Add PatientCabinet RoomChest | +| Modify | `scenes/rooms/floor2/Ultrasound.tscn` | Add UltrasoundCart RoomChest | +| Modify | `scenes/rooms/floor2/DeliveryRoom.tscn` | Add DeliveryCabinet RoomChest | +| Modify | `scenes/rooms/floor2/Nursery.tscn` | Add NurseryShelf RoomChest | +| Modify | `scenes/rooms/home/GardenParty.tscn` | Add GardenTable + GardenStorage RoomChest | +| Create | `test/unit/test_room_chests_floor0.gd` | 8 scene tests (presence + item count per chest) | +| Create | `test/unit/test_room_chests_floor1.gd` | 10 scene tests | +| Create | `test/unit/test_room_chests_floor2_home.gd` | 10 scene tests | + +--- + +## Task 1: ChestItemData + RoomChestConfig + +**Files:** +- Create: `scripts/objects/chest_item_data.gd` +- Create: `scripts/objects/room_chest_config.gd` +- Create: `test/unit/test_room_chest.gd` (first 4 tests only) + +- [ ] **Step 1: Failing tests schreiben** + +```gdscript +## Tests for ChestItemData resource and RoomChestConfig static config. +extends GutTest + + +func test_chest_item_data_default_item_type_is_holdable() -> void: + var d: ChestItemData = ChestItemData.new() + assert_eq(d.item_type, "holdable") + + +func test_chest_item_data_default_outfit_layer_is_one() -> void: + var d: ChestItemData = ChestItemData.new() + assert_eq(d.outfit_layer, 1) + + +func test_room_chest_config_reception_desk_has_three_items() -> void: + var items: Array[ChestItemData] = RoomChestConfig.get_items("reception_desk") + assert_eq(items.size(), 3) + + +func test_room_chest_config_unknown_id_returns_empty() -> void: + var items: Array[ChestItemData] = RoomChestConfig.get_items("does_not_exist") + assert_eq(items.size(), 0) +``` + +- [ ] **Step 2: Test ausführen — muss FAIL sein** + +Expected: `ChestItemData: Identifier not found` oder ähnlich. + +- [ ] **Step 3: `chest_item_data.gd` implementieren** + +```gdscript +## ChestItemData — configuration for a single item slot inside a RoomChest. +class_name ChestItemData extends Resource + +@export var item_id: String = "" +@export var item_type: String = "holdable" +@export var outfit_layer: int = 1 +@export var spawn_offset: Vector2 = Vector2.ZERO +``` + +- [ ] **Step 4: `room_chest_config.gd` implementieren** + +```gdscript +## RoomChestConfig — static item configuration for all room chests. +## Maps chest_id strings to ChestItemData arrays. No assets needed: item_id strings only. +class_name RoomChestConfig + + +static func get_items(chest_id: String) -> Array[ChestItemData]: + match chest_id: + "reception_desk": + return _make([ + ["clipboard", "holdable", 1, Vector2(-70.0, -60.0)], + ["pen", "holdable", 1, Vector2(0.0, -80.0)], + ["bandage", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "giftshop_shelf": + return _make([ + ["gift_box", "holdable", 1, Vector2(-70.0, -60.0)], + ["ribbon", "holdable", 1, Vector2(0.0, -80.0)], + ["balloon", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "restaurant_counter": + return _make([ + ["teacup", "holdable", 1, Vector2(-70.0, -60.0)], + ["plate", "holdable", 1, Vector2(0.0, -80.0)], + ["spoon", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "emergency_cabinet": + return _make([ + ["bandage_roll", "holdable", 1, Vector2(-70.0, -60.0)], + ["syringe", "holdable", 1, Vector2(0.0, -80.0)], + ["ice_pack", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "xray_cabinet": + return _make([ + ["xray_sheet", "holdable", 1, Vector2(-70.0, -60.0)], + ["lead_apron", "outfit", 1, Vector2(0.0, -80.0)], + ["marker", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "pharmacy_medicine": + return _make([ + ["pill_bottle", "holdable", 1, Vector2(-50.0, -60.0)], + ["syrup", "holdable", 1, Vector2(50.0, -60.0)], + ]) + "pharmacy_tools": + return _make([ + ["mortar", "holdable", 1, Vector2(-50.0, -60.0)], + ["spatula", "holdable", 1, Vector2(50.0, -60.0)], + ]) + "lab_bench": + return _make([ + ["test_tube", "holdable", 1, Vector2(-70.0, -60.0)], + ["pipette", "holdable", 1, Vector2(0.0, -80.0)], + ["microscope_slide", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "patient_cabinet": + return _make([ + ["thermometer", "holdable", 1, Vector2(-70.0, -60.0)], + ["stethoscope", "outfit", 2, Vector2(0.0, -80.0)], + ["pillow", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "ultrasound_cart": + return _make([ + ["gel_tube", "holdable", 1, Vector2(-70.0, -60.0)], + ["probe", "holdable", 1, Vector2(0.0, -80.0)], + ["towel", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "delivery_cabinet": + return _make([ + ["swaddle", "outfit", 1, Vector2(-70.0, -60.0)], + ["scissors", "holdable", 1, Vector2(0.0, -80.0)], + ["cord_clamp", "holdable", 1, Vector2(70.0, -60.0)], + ]) + "nursery_shelf": + return _make([ + ["bottle", "holdable", 1, Vector2(-70.0, -60.0)], + ["rattle", "holdable", 1, Vector2(0.0, -80.0)], + ["blanket", "outfit", 1, Vector2(70.0, -60.0)], + ]) + "garden_table": + return _make([ + ["teapot", "holdable", 1, Vector2(-50.0, -60.0)], + ["cake", "holdable", 1, Vector2(50.0, -60.0)], + ]) + "garden_storage": + return _make([ + ["confetti", "holdable", 1, Vector2(-50.0, -60.0)], + ["party_hat", "outfit", 1, Vector2(50.0, -60.0)], + ]) + return [] + + +static func _make(data: Array) -> Array[ChestItemData]: + var result: Array[ChestItemData] = [] + for entry: Array in data: + var d: ChestItemData = ChestItemData.new() + d.item_id = entry[0] + d.item_type = entry[1] + d.outfit_layer = entry[2] + d.spawn_offset = entry[3] + result.append(d) + return result +``` + +- [ ] **Step 5: Tests ausführen — müssen PASS sein** + +Expected: `4/4 passed` + +- [ ] **Step 6: Committen** + +``` +git add scripts/objects/chest_item_data.gd scripts/objects/room_chest_config.gd test/unit/test_room_chest.gd +git commit -m "feat(items): add ChestItemData resource and RoomChestConfig static config" +``` + +--- + +## Task 2: RoomChest Node2D — Spawn + Receive + +**Files:** +- Create: `scripts/objects/room_chest.gd` +- Modify: `test/unit/test_room_chest.gd` (append 6 tests) + +- [ ] **Step 1: Failing tests anhängen** + +Append to `test/unit/test_room_chest.gd`: + +```gdscript +func test_are_items_spawned_false_initially() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + assert_false(chest.are_items_spawned()) + + +func test_get_spawned_count_zero_initially() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + assert_eq(chest.get_spawned_count(), 0) + + +func test_get_item_config_count_matches_config() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + assert_eq(chest.get_item_config_count(), 3) + + +func test_spawn_items_creates_correct_count() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + chest.spawn_items() + assert_eq(chest.get_spawned_count(), 3) + + +func test_double_spawn_is_noop() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + chest.spawn_items() + chest.spawn_items() + assert_eq(chest.get_spawned_count(), 3) + + +func test_receive_item_decrements_spawned_count() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + chest.spawn_items() + var item: HoldableItem = chest._spawned_items[0] as HoldableItem + chest.receive_item(item) + assert_eq(chest.get_spawned_count(), 2) +``` + +- [ ] **Step 2: Test ausführen — müssen FAIL sein** + +Expected: `RoomChest: Identifier not found` + +- [ ] **Step 3: `room_chest.gd` implementieren** + +```gdscript +## RoomChest — tappable storage node. Spawns HoldableItem/OutfitItem instances on demand. +## Items fly out with a tween. Receives items back via receive_item(). +class_name RoomChest extends Node2D + +signal items_spawned(chest: RoomChest) +signal item_received(chest: RoomChest, item_id: String) + +const SPAWN_TWEEN_DURATION: float = 0.3 + +@export var chest_id: String = "" + +var _spawned_items: Array = [] +var _item_configs: Array[ChestItemData] = [] + + +func _ready() -> void: + add_to_group("room_chests") + _item_configs = RoomChestConfig.get_items(chest_id) + if not chest_id.is_empty() and not GameState.get_chest_state(chest_id).is_empty(): + spawn_items() + + +func spawn_items() -> void: + if not _spawned_items.is_empty(): + return + var parent: Node = get_parent() + for config: ChestItemData in _item_configs: + var item: HoldableItem = _create_item(config) + item.home_chest = self + if parent != null: + parent.add_child(item) + else: + add_child(item) + item.global_position = global_position + _spawned_items.append(item) + _tween_item_out(item, config.spawn_offset) + GameState.set_chest_state(chest_id, _get_spawned_ids()) + items_spawned.emit(self) + + +func receive_item(item: HoldableItem) -> void: + _spawned_items.erase(item) + if _spawned_items.is_empty(): + GameState.clear_chest_state(chest_id) + else: + GameState.set_chest_state(chest_id, _get_spawned_ids()) + item_received.emit(self, item.item_id) + _tween_item_in(item) + + +func are_items_spawned() -> bool: + return not _spawned_items.is_empty() + + +func get_spawned_count() -> int: + return _spawned_items.size() + + +func get_item_config_count() -> int: + return _item_configs.size() + + +func _create_item(config: ChestItemData) -> HoldableItem: + var item: HoldableItem + if config.item_type == "outfit": + var outfit: OutfitItem = OutfitItem.new() + outfit.outfit_layer = config.outfit_layer + item = outfit + else: + item = HoldableItem.new() + item.item_id = config.item_id + return item + + +func _get_spawned_ids() -> Array: + var ids: Array = [] + for item: HoldableItem in _spawned_items: + ids.append(item.item_id) + return ids + + +func _tween_item_out(item: HoldableItem, offset: Vector2) -> void: + var tween: Tween = create_tween() + tween.tween_property(item, "global_position", global_position + offset, SPAWN_TWEEN_DURATION) + + +func _tween_item_in(item: HoldableItem) -> void: + var tween: Tween = create_tween() + tween.tween_property(item, "global_position", global_position, SPAWN_TWEEN_DURATION) + tween.tween_callback(item.queue_free) +``` + +- [ ] **Step 4: Tests ausführen — müssen PASS sein** + +Expected: `10/10 passed` + +- [ ] **Step 5: Committen** + +``` +git add scripts/objects/room_chest.gd test/unit/test_room_chest.gd +git commit -m "feat(items): add RoomChest with spawn and receive logic" +``` + +--- + +## Task 3: HoldableItem Chest-Return + OutfitItem + GameState v3 + +**Files:** +- Modify: `scripts/objects/holdable_item.gd` +- Modify: `scripts/objects/outfit_item.gd` +- Modify: `scripts/autoload/GameState.gd` +- Modify: `test/unit/test_holdable_item.gd` (append 3 tests) +- Modify: `test/unit/test_game_state.gd` (append 6 tests) + +- [ ] **Step 1: Failing tests anhängen** + +Append to `test/unit/test_holdable_item.gd`: + +```gdscript +func test_try_return_to_chest_false_when_no_home_chest() -> void: + var item: HoldableItem = HoldableItem.new() + add_child_autofree(item) + assert_false(item._try_return_to_chest()) + + +func test_try_return_to_chest_false_when_beyond_radius() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + chest.global_position = Vector2.ZERO + var item: HoldableItem = HoldableItem.new() + add_child_autofree(item) + item.home_chest = chest + item.global_position = Vector2(200.0, 0.0) + assert_false(item._try_return_to_chest()) + + +func test_try_return_to_chest_true_when_within_radius() -> void: + var chest: RoomChest = RoomChest.new() + chest.chest_id = "reception_desk" + add_child_autofree(chest) + chest.global_position = Vector2.ZERO + var item: HoldableItem = HoldableItem.new() + add_child_autofree(item) + item.home_chest = chest + item.global_position = Vector2(40.0, 0.0) + assert_true(item._try_return_to_chest()) +``` + +Append to `test/unit/test_game_state.gd`: + +```gdscript +func test_get_chest_state_returns_empty_for_unknown_id() -> void: + assert_eq(GameState.get_chest_state("nonexistent_chest_xyz"), []) + + +func test_set_and_get_chest_state() -> void: + GameState.set_chest_state("pharmacy_medicine_test", ["pill_bottle", "syrup"]) + assert_eq(GameState.get_chest_state("pharmacy_medicine_test"), ["pill_bottle", "syrup"]) + + +func test_clear_chest_state_removes_entry() -> void: + GameState.set_chest_state("lab_bench_test", ["test_tube"]) + GameState.clear_chest_state("lab_bench_test") + assert_eq(GameState.get_chest_state("lab_bench_test"), []) + + +func test_chest_state_included_in_save_data() -> void: + GameState.set_chest_state("xray_cabinet_test", ["xray_sheet"]) + var data: Dictionary = GameState.get_save_data() + assert_true(data.has("chest_states")) + assert_eq(data["chest_states"]["xray_cabinet_test"], ["xray_sheet"]) + + +func test_save_data_version_is_three() -> void: + var data: Dictionary = GameState.get_save_data() + assert_eq(data["version"], 3) + + +func test_apply_save_data_restores_chest_state() -> void: + var data: Dictionary = { + "version": 3, + "chest_states": {"reception_desk_test": ["clipboard", "pen"]}, + } + GameState.apply_save_data(data) + assert_eq(GameState.get_chest_state("reception_desk_test"), ["clipboard", "pen"]) +``` + +- [ ] **Step 2: Tests ausführen — müssen FAIL sein** + +Expected: `_try_return_to_chest: Method not found` + version still 2. + +- [ ] **Step 3: `holdable_item.gd` modifizieren** + +Ergänze nach der bestehenden `HAND_SLOT_RADIUS`-Konstante und dem `@export var item_id`: + +```gdscript +const CHEST_RETURN_RADIUS: float = 80.0 + +var home_chest: RoomChest = null +``` + +Ersetze `_on_drag_released`: + +```gdscript +func _on_drag_released(_pos: Vector2) -> void: + if _try_return_to_chest(): + return + var result: Array = _find_nearest_free_hand_slot() + if not result.is_empty(): + var character: Character = result[0] as Character + var hand: String = result[1] as String + character.attach_item(hand, self) + item_placed.emit(self) +``` + +Neue Methode nach `is_in_hand_slot()` einfügen: + +```gdscript +func _try_return_to_chest() -> bool: + if home_chest == null: + return false + if global_position.distance_to(home_chest.global_position) >= CHEST_RETURN_RADIUS: + return false + home_chest.receive_item(self) + return true +``` + +- [ ] **Step 4: `outfit_item.gd` modifizieren** + +Ersetze `_on_drag_released`: + +```gdscript +func _on_drag_released(_pos: Vector2) -> void: + if _try_return_to_chest(): + return + var character: Character = _find_nearest_character() + if character != null: + character.apply_outfit_item(outfit_layer, item_id, outfit_sprite, self) + return + super._on_drag_released(_pos) +``` + +- [ ] **Step 5: `GameState.gd` modifizieren** + +Ergänze nach `_object_states`: + +```gdscript +var _chest_states: Dictionary = {} +``` + +Neue Methoden nach `set_object_state()` einfügen: + +```gdscript +func get_chest_state(chest_id: String) -> Array: + return _chest_states.get(chest_id, []) + + +func set_chest_state(chest_id: String, spawned_item_ids: Array) -> void: + _chest_states[chest_id] = spawned_item_ids + state_changed.emit() + + +func clear_chest_state(chest_id: String) -> void: + _chest_states.erase(chest_id) + state_changed.emit() +``` + +In `get_save_data()`, ersetze `"version": 2` mit `"version": 3` und ergänze `"chest_states"`: + +```gdscript +return { + "version": 3, + "character_positions": positions, + "character_outfits": _character_outfits.duplicate(true), + "character_held_items": _character_held_items.duplicate(true), + "object_states": _object_states, + "chest_states": _chest_states.duplicate(true), + "current_room": current_room, + "music_volume": music_volume, + "sfx_volume": sfx_volume, +} +``` + +In `apply_save_data()`, am Ende ergänzen: + +```gdscript + if data.has("chest_states"): + _chest_states = data["chest_states"].duplicate(true) + else: + _chest_states = {} +``` + +- [ ] **Step 6: Tests ausführen — müssen PASS sein** + +Expected: alle bisherigen Tests + 9 neue = grün. + +- [ ] **Step 7: Committen** + +``` +git add scripts/objects/holdable_item.gd scripts/objects/outfit_item.gd scripts/autoload/GameState.gd test/unit/test_holdable_item.gd test/unit/test_game_state.gd +git commit -m "feat(items): add chest-return priority to HoldableItem and GameState v3 chest states" +``` + +--- + +## Task 4: Room-Scenes Floor 0 + Tests + +**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_room_chests_floor0.gd` + +**Vorgehen für jede .tscn-Datei:** +1. Datei lesen +2. `load_steps=N` → `load_steps=N+1` +3. Neue ext_resource direkt nach dem letzten bestehenden `[ext_resource]`-Block einfügen. Wähle als ID `"N_chest"` wobei N die neue Zahl ist. +4. Einen `[node]`-Block am Ende der Datei anhängen + +- [ ] **Step 1: Failing tests schreiben** + +`test/unit/test_room_chests_floor0.gd`: + +```gdscript +## Tests for RoomChest presence and item counts in Floor 0 rooms. +extends GutTest + + +func test_reception_has_chest_reception_desk() -> void: + var room: Node = preload("res://scenes/rooms/floor0/Reception.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("ReceptionDesk")) + + +func test_reception_desk_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor0/Reception.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("ReceptionDesk") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_giftshop_has_chest_giftshop_shelf() -> void: + var room: Node = preload("res://scenes/rooms/floor0/GiftShop.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("GiftShopShelf")) + + +func test_giftshop_shelf_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor0/GiftShop.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("GiftShopShelf") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_restaurant_has_chest_restaurant_counter() -> void: + var room: Node = preload("res://scenes/rooms/floor0/Restaurant.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("RestaurantCounter")) + + +func test_restaurant_counter_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor0/Restaurant.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("RestaurantCounter") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_emergency_has_chest_emergency_cabinet() -> void: + var room: Node = preload("res://scenes/rooms/floor0/EmergencyRoom.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("EmergencyCabinet")) + + +func test_emergency_cabinet_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor0/EmergencyRoom.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("EmergencyCabinet") as RoomChest + assert_eq(chest.get_item_config_count(), 3) +``` + +- [ ] **Step 2: Tests ausführen — müssen FAIL sein** + +Expected: `ReceptionDesk: Node not found` + +- [ ] **Step 3: Reception.tscn modifizieren** + +Reception.tscn hat aktuell `load_steps=3`. Änderungen: + +Header: `load_steps=3` → `load_steps=4` + +Nach dem letzten `[ext_resource]`-Block einfügen: +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] +``` + +Am Ende der Datei anhängen: +``` +[node name="ReceptionDesk" type="Node2D" parent="."] +position = Vector2(120.0, 555.0) +script = ExtResource("3_chest") +chest_id = "reception_desk" +``` + +- [ ] **Step 4: GiftShop.tscn modifizieren** + +GiftShop.tscn hat `load_steps=3`. Selbes Muster (id=`"3_chest"`): + +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] +``` + +``` +[node name="GiftShopShelf" type="Node2D" parent="."] +position = Vector2(120.0, 300.0) +script = ExtResource("3_chest") +chest_id = "giftshop_shelf" +``` + +- [ ] **Step 5: Restaurant.tscn modifizieren** + +Restaurant.tscn hat `load_steps=3`. Selbes Muster (id=`"3_chest"`): + +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] +``` + +``` +[node name="RestaurantCounter" type="Node2D" parent="."] +position = Vector2(120.0, 555.0) +script = ExtResource("3_chest") +chest_id = "restaurant_counter" +``` + +- [ ] **Step 6: EmergencyRoom.tscn modifizieren** + +EmergencyRoom.tscn hat `load_steps=4` (iobj, ambulance, snap). Neue ID=`"4_chest"`: + +Header: `load_steps=4` → `load_steps=5` + +Nach dem letzten `[ext_resource]`-Block einfügen: +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] +``` + +``` +[node name="EmergencyCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "emergency_cabinet" +``` + +- [ ] **Step 7: Tests ausführen — müssen PASS sein** + +Expected: `8/8 passed` + +- [ ] **Step 8: Committen** + +``` +git add scenes/rooms/floor0/Reception.tscn scenes/rooms/floor0/GiftShop.tscn scenes/rooms/floor0/Restaurant.tscn scenes/rooms/floor0/EmergencyRoom.tscn test/unit/test_room_chests_floor0.gd +git commit -m "feat(rooms): add RoomChest nodes to Floor 0 rooms" +``` + +--- + +## Task 5: Room-Scenes Floor 1 + Tests + +**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_room_chests_floor1.gd` + +- [ ] **Step 1: Failing tests schreiben** + +`test/unit/test_room_chests_floor1.gd`: + +```gdscript +## Tests for RoomChest presence and item counts in Floor 1 rooms. +extends GutTest + + +func test_xray_has_chest_xray_cabinet() -> void: + var room: Node = preload("res://scenes/rooms/floor1/XRay.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("XRayCabinet")) + + +func test_xray_cabinet_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor1/XRay.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("XRayCabinet") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_pharmacy_has_chest_pharmacy_medicine() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("PharmacyMedicine")) + + +func test_pharmacy_medicine_has_two_items() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("PharmacyMedicine") as RoomChest + assert_eq(chest.get_item_config_count(), 2) + + +func test_pharmacy_has_chest_pharmacy_tools() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("PharmacyTools")) + + +func test_pharmacy_tools_has_two_items() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Pharmacy.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("PharmacyTools") as RoomChest + assert_eq(chest.get_item_config_count(), 2) + + +func test_lab_has_chest_lab_bench() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Lab.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("LabBench")) + + +func test_lab_bench_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor1/Lab.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("LabBench") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_patient_room_has_chest_patient_cabinet() -> void: + var room: Node = preload("res://scenes/rooms/floor1/PatientRoom.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("PatientCabinet")) + + +func test_patient_cabinet_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor1/PatientRoom.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("PatientCabinet") as RoomChest + assert_eq(chest.get_item_config_count(), 3) +``` + +- [ ] **Step 2: Tests ausführen — müssen FAIL sein** + +- [ ] **Step 3: XRay.tscn modifizieren** + +XRay.tscn hat `load_steps=4` (iobj, xraymachine, snap). Neue ID=`"4_chest"`: + +Header: `load_steps=4` → `load_steps=5` + +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] +``` + +``` +[node name="XRayCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "xray_cabinet" +``` + +- [ ] **Step 4: Pharmacy.tscn modifizieren** + +Pharmacy.tscn hat `load_steps=3`. Neue ID=`"3_chest"`: + +Header: `load_steps=3` → `load_steps=4` + +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] +``` + +Zwei Nodes am Ende anhängen: +``` +[node name="PharmacyMedicine" type="Node2D" parent="."] +position = Vector2(350.0, 320.0) +script = ExtResource("3_chest") +chest_id = "pharmacy_medicine" + +[node name="PharmacyTools" type="Node2D" parent="."] +position = Vector2(900.0, 320.0) +script = ExtResource("3_chest") +chest_id = "pharmacy_tools" +``` + +- [ ] **Step 5: Lab.tscn modifizieren** + +Lab.tscn lesen und `load_steps` prüfen. Selbes Muster: neue ext_resource für room_chest.gd einfügen, Node am Ende: + +``` +[node name="LabBench" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("_chest") +chest_id = "lab_bench" +``` + +- [ ] **Step 6: PatientRoom.tscn modifizieren** + +PatientRoom.tscn lesen, selbes Muster: + +``` +[node name="PatientCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("_chest") +chest_id = "patient_cabinet" +``` + +- [ ] **Step 7: Tests ausführen — müssen PASS sein** + +Expected: `10/10 passed` + +- [ ] **Step 8: Committen** + +``` +git add scenes/rooms/floor1/XRay.tscn scenes/rooms/floor1/Pharmacy.tscn scenes/rooms/floor1/Lab.tscn scenes/rooms/floor1/PatientRoom.tscn test/unit/test_room_chests_floor1.gd +git commit -m "feat(rooms): add RoomChest nodes to Floor 1 rooms" +``` + +--- + +## Task 6: Room-Scenes Floor 2 + Home + Tests + +**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_room_chests_floor2_home.gd` + +- [ ] **Step 1: Failing tests schreiben** + +`test/unit/test_room_chests_floor2_home.gd`: + +```gdscript +## Tests for RoomChest presence and item counts in Floor 2 and Home rooms. +extends GutTest + + +func test_ultrasound_has_chest_ultrasound_cart() -> void: + var room: Node = preload("res://scenes/rooms/floor2/Ultrasound.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("UltrasoundCart")) + + +func test_ultrasound_cart_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor2/Ultrasound.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("UltrasoundCart") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_delivery_room_has_chest_delivery_cabinet() -> void: + var room: Node = preload("res://scenes/rooms/floor2/DeliveryRoom.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("DeliveryCabinet")) + + +func test_delivery_cabinet_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor2/DeliveryRoom.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("DeliveryCabinet") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_nursery_has_chest_nursery_shelf() -> void: + var room: Node = preload("res://scenes/rooms/floor2/Nursery.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("NurseryShelf")) + + +func test_nursery_shelf_has_three_items() -> void: + var room: Node = preload("res://scenes/rooms/floor2/Nursery.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("NurseryShelf") as RoomChest + assert_eq(chest.get_item_config_count(), 3) + + +func test_garden_party_has_chest_garden_table() -> void: + var room: Node = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("GardenTable")) + + +func test_garden_table_has_two_items() -> void: + var room: Node = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("GardenTable") as RoomChest + assert_eq(chest.get_item_config_count(), 2) + + +func test_garden_party_has_chest_garden_storage() -> void: + var room: Node = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() + add_child_autofree(room) + assert_not_null(room.get_node_or_null("GardenStorage")) + + +func test_garden_storage_has_two_items() -> void: + var room: Node = preload("res://scenes/rooms/home/GardenParty.tscn").instantiate() + add_child_autofree(room) + var chest: RoomChest = room.get_node_or_null("GardenStorage") as RoomChest + assert_eq(chest.get_item_config_count(), 2) +``` + +- [ ] **Step 2: Tests ausführen — müssen FAIL sein** + +- [ ] **Step 3: Ultrasound.tscn modifizieren** + +Ultrasound.tscn hat `load_steps=4` (iobj, ultrasound, snap). Neue ID=`"4_chest"`: + +Header: `load_steps=4` → `load_steps=5` + +``` +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] +``` + +``` +[node name="UltrasoundCart" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "ultrasound_cart" +``` + +- [ ] **Step 4: DeliveryRoom.tscn modifizieren** + +DeliveryRoom.tscn lesen. Selbes Muster, passende neue ID wählen: + +``` +[node name="DeliveryCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("_chest") +chest_id = "delivery_cabinet" +``` + +- [ ] **Step 5: Nursery.tscn modifizieren** + +Nursery.tscn lesen. Selbes Muster: + +``` +[node name="NurseryShelf" type="Node2D" parent="."] +position = Vector2(640.0, 260.0) +script = ExtResource("_chest") +chest_id = "nursery_shelf" +``` + +- [ ] **Step 6: GardenParty.tscn modifizieren** + +GardenParty.tscn lesen. Zwei Nodes, selbe ext_resource ID verwenden: + +``` +[node name="GardenTable" type="Node2D" parent="."] +position = Vector2(200.0, 400.0) +script = ExtResource("_chest") +chest_id = "garden_table" + +[node name="GardenStorage" type="Node2D" parent="."] +position = Vector2(900.0, 400.0) +script = ExtResource("_chest") +chest_id = "garden_storage" +``` + +- [ ] **Step 7: Alle Tests ausführen — müssen PASS sein** + +Expected output: +``` +Scripts 15 +Tests 185 +Passing Tests 185 +---- All tests passed! ---- +``` + +- [ ] **Step 8: Committen** + +``` +git add scenes/rooms/floor2/Ultrasound.tscn scenes/rooms/floor2/DeliveryRoom.tscn scenes/rooms/floor2/Nursery.tscn scenes/rooms/home/GardenParty.tscn test/unit/test_room_chests_floor2_home.gd +git commit -m "feat(rooms): add RoomChest nodes to Floor 2 and Home rooms" +``` + +--- + +## Spec Self-Check + +| Requirement | Task | +|---|---| +| ChestItemData Resource | Task 1 | +| RoomChestConfig für alle 14 Truhen | Task 1 | +| RoomChest spawn_items() + receive_item() | Task 2 | +| Doppel-Spawn no-op | Task 2 | +| Tween fly-out / fly-in | Task 2 (visual, nicht getestet) | +| HoldableItem chest-return Priorität | Task 3 | +| OutfitItem chest-return vor outfit-apply | Task 3 | +| GameState v3 chest_states | Task 3 | +| Floor 0 alle 4 Räume befüllt | Task 4 | +| Floor 1 alle 4 Räume befüllt | Task 5 | +| Floor 2 + Home alle 4 Räume befüllt | Task 6 | +| Auto-restore auf load (via _ready) | Task 2 (room_chest.gd _ready()) | +| Item-Positionen NICHT persistiert | — (by design, spec §GameState v3) |