1087 lines
34 KiB
Markdown
1087 lines
34 KiB
Markdown
# 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("<neue_id>_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("<neue_id>_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("<neue_id>_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("<neue_id>_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("<neue_id>_chest")
|
|
chest_id = "garden_table"
|
|
|
|
[node name="GardenStorage" type="Node2D" parent="."]
|
|
position = Vector2(900.0, 400.0)
|
|
script = ExtResource("<neue_id>_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) |
|