Files
Cozypaw-Hospital/docs/superpowers/plans/2026-05-09-sprint-18-room-chests.md
T
2026-05-09 00:54:19 +02:00

34 KiB

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

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

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

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:

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:

const CHEST_RETURN_RADIUS: float = 80.0

var home_chest: RoomChest = null

Ersetze _on_drag_released:

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:

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:

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:

var _chest_states: Dictionary = {}

Neue Methoden nach set_object_state() einfügen:

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":

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:

	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=Nload_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:

## 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=3load_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=4load_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:

## 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=4load_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=3load_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:

## 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=4load_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)