# Sprint 18 — Room Chests & Item Spawning Design Spec ## Goal Implement a `RoomChest` system: tappable storage nodes in every room that spawn `HoldableItem`/`OutfitItem` instances with a fly-out tween. Items persist in the world until manually dragged back to their chest. All 12 existing rooms get populated with placeholder items (string IDs, no textures yet). ## Architecture ### Data Layer **`ChestItemData`** — `Resource` subclass, one entry per item slot in a chest. | Field | Type | Purpose | |---|---|---| | `item_id` | `String` | Canonical item identifier, e.g. `"stethoscope"` | | `item_type` | `String` | `"holdable"` or `"outfit"` | | `outfit_layer` | `int` | 1–3, only relevant when `item_type == "outfit"` | | `spawn_offset` | `Vector2` | Relative target position for fly-out tween | **`RoomChest`** — `Node2D` subclass. | Field | Type | Purpose | |---|---|---| | `chest_id` | `String` | Unique identifier, e.g. `"pharmacy_medicine"` | | `items` | `Array[ChestItemData]` | Configured items for this chest | | `_spawned_items` | `Array` | Currently live item nodes in the world | ### Spawn Behaviour 1. Player taps `RoomChest`. 2. If `_spawned_items` is not empty → no-op (already spawned). 3. Otherwise: for each `ChestItemData`, instantiate a `HoldableItem` or `OutfitItem` node. Set `item.home_chest = self`. Reparent to the room scene (not the chest). Tween `global_position` from `chest.global_position` to `chest.global_position + item_data.spawn_offset`. ### Return Behaviour `HoldableItem._on_drag_released` checks (in priority order): 1. **Chest return** — if `home_chest != null` and `global_position.distance_to(home_chest.global_position) < CHEST_RETURN_RADIUS (80.0)`: tween back to chest → `home_chest.receive_item(self)` → `queue_free()`. 2. **Outfit apply** — existing OutfitItem logic (80 px body proximity). 3. **Hand slot attach** — existing HoldableItem logic (60 px hand slot proximity). `RoomChest.receive_item(item: HoldableItem)` removes the item from `_spawned_items`. ### GameState v3 Extend `GameState` with `_chest_states: Dictionary` mapping `chest_id → Array[String]` of currently spawned `item_id`s. Persist in save data as `"version": 3`. Item positions within the room are **not** persisted (deliberate simplification). On load: if a chest has entries in `_chest_states`, re-spawn those items via the normal fly-out tween from the chest — they land at their configured `spawn_offset`, not their last dragged position. ## Room Population All 12 existing room `.tscn` files get `RoomChest` nodes. No textures in this sprint — `item_id` strings only. | Room | chest_id | item_ids | |---|---|---| | Reception | `reception_desk` | `clipboard`, `pen`, `bandage` | | GiftShop | `giftshop_shelf` | `gift_box`, `ribbon`, `balloon` | | Restaurant | `restaurant_counter` | `teacup`, `plate`, `spoon` | | Emergency | `emergency_cabinet` | `bandage_roll`, `syringe`, `ice_pack` | | XRay | `xray_cabinet` | `xray_sheet`, `lead_apron`*, `marker` | | Pharmacy | `pharmacy_medicine` | `pill_bottle`, `syrup` | | Pharmacy | `pharmacy_tools` | `mortar`, `spatula` | | Lab | `lab_bench` | `test_tube`, `pipette`, `microscope_slide` | | PatientRooms | `patient_cabinet` | `thermometer`, `stethoscope`*, `pillow` | | Ultrasound | `ultrasound_cart` | `gel_tube`, `probe`, `towel` | | DeliveryRoom | `delivery_cabinet` | `swaddle`*, `scissors`, `cord_clamp` | | Nursery | `nursery_shelf` | `bottle`, `rattle`, `blanket`* | | GardenParty | `garden_table` | `teapot`, `cake` | | GardenParty | `garden_storage` | `confetti`, `party_hat` | `*` = `item_type: "outfit"` (layer: `lead_apron`→1, `stethoscope`→2, `swaddle`→1, `blanket`→1) ## Testing - `test/unit/test_room_chest.gd` — unit tests for `RoomChest` and `ChestItemData` - `test/unit/test_game_state.gd` — appended tests for GameState v3 chest state - `test/unit/test_holdable_item.gd` — appended tests for chest-return priority Tests follow GUT v9.6.0, `extends GutTest`, `add_child_autofree`. ## Out of Scope - Item textures / sprites (Sprint 18 uses null textures throughout) - Chest open/close sprite animation (visual polish, later sprint) - Per-item sounds (AudioManager sprint) - Chest state per save-slot (single save file, existing SaveManager)