From 2e0b96152077c9264d34bd813abf65e083df8a6f Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 00:43:23 +0200 Subject: [PATCH] docs: add Sprint 18 Room Chests design spec --- .../specs/2026-05-09-sprint-18-room-chests.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-09-sprint-18-room-chests.md diff --git a/docs/superpowers/specs/2026-05-09-sprint-18-room-chests.md b/docs/superpowers/specs/2026-05-09-sprint-18-room-chests.md new file mode 100644 index 0000000..49810b3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-09-sprint-18-room-chests.md @@ -0,0 +1,84 @@ +# 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`. On load: if a chest has entries in `_chest_states`, skip auto-spawn so items resume where they were. + +## 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)