# 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)
