From b97b11087674e828f1abecdeb1f54c353ba7876d Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 00:58:32 +0200 Subject: [PATCH 1/9] feat(items): add ChestItemData resource and RoomChestConfig static config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD: 4 tests written first (FAIL), then implemented — all 151 tests pass. --- scripts/objects/chest_item_data.gd | 7 ++ scripts/objects/room_chest_config.gd | 100 +++++++++++++++++++++++++++ test/unit/test_room_chest.gd | 22 ++++++ 3 files changed, 129 insertions(+) create mode 100644 scripts/objects/chest_item_data.gd create mode 100644 scripts/objects/room_chest_config.gd create mode 100644 test/unit/test_room_chest.gd diff --git a/scripts/objects/chest_item_data.gd b/scripts/objects/chest_item_data.gd new file mode 100644 index 0000000..f704578 --- /dev/null +++ b/scripts/objects/chest_item_data.gd @@ -0,0 +1,7 @@ +## 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 diff --git a/scripts/objects/room_chest_config.gd b/scripts/objects/room_chest_config.gd new file mode 100644 index 0000000..6965ef0 --- /dev/null +++ b/scripts/objects/room_chest_config.gd @@ -0,0 +1,100 @@ +## 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 diff --git a/test/unit/test_room_chest.gd b/test/unit/test_room_chest.gd new file mode 100644 index 0000000..dfa3c5a --- /dev/null +++ b/test/unit/test_room_chest.gd @@ -0,0 +1,22 @@ +## 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) From 4e4743f14ffdadd97bf2f9911ef2952439cca488 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:01:50 +0200 Subject: [PATCH 2/9] refactor(items): use ItemType enum and offset constants in ChestItemData/RoomChestConfig Co-Authored-By: Claude Sonnet 4.6 --- scripts/objects/chest_item_data.gd | 6 +- scripts/objects/room_chest_config.gd | 83 +++++++++++++++------------- test/unit/test_room_chest.gd | 17 +++++- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/scripts/objects/chest_item_data.gd b/scripts/objects/chest_item_data.gd index f704578..df66e32 100644 --- a/scripts/objects/chest_item_data.gd +++ b/scripts/objects/chest_item_data.gd @@ -1,7 +1,9 @@ ## ChestItemData — configuration for a single item slot inside a RoomChest. class_name ChestItemData extends Resource +enum ItemType { HOLDABLE, OUTFIT } + @export var item_id: String = "" -@export var item_type: String = "holdable" -@export var outfit_layer: int = 1 +@export var item_type: ItemType = ItemType.HOLDABLE +@export_range(1, 3) var outfit_layer: int = 1 @export var spawn_offset: Vector2 = Vector2.ZERO diff --git a/scripts/objects/room_chest_config.gd b/scripts/objects/room_chest_config.gd index 6965ef0..ee4cfae 100644 --- a/scripts/objects/room_chest_config.gd +++ b/scripts/objects/room_chest_config.gd @@ -2,88 +2,94 @@ ## Maps chest_id strings to ChestItemData arrays. No assets needed: item_id strings only. class_name RoomChestConfig +const _OFFSET_LEFT: Vector2 = Vector2(-70.0, -60.0) +const _OFFSET_CENTER: Vector2 = Vector2(0.0, -80.0) +const _OFFSET_RIGHT: Vector2 = Vector2(70.0, -60.0) +const _OFFSET_LEFT_2: Vector2 = Vector2(-50.0, -60.0) +const _OFFSET_RIGHT_2: Vector2 = Vector2(50.0, -60.0) + 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)], + ["clipboard", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["pen", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["bandage", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["gift_box", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["ribbon", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["balloon", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["teacup", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["plate", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["spoon", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["bandage_roll", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["syringe", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["ice_pack", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["xray_sheet", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["lead_apron", ChestItemData.ItemType.OUTFIT, 1, _OFFSET_CENTER], + ["marker", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "pharmacy_medicine": return _make([ - ["pill_bottle", "holdable", 1, Vector2(-50.0, -60.0)], - ["syrup", "holdable", 1, Vector2(50.0, -60.0)], + ["pill_bottle", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT_2], + ["syrup", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT_2], ]) "pharmacy_tools": return _make([ - ["mortar", "holdable", 1, Vector2(-50.0, -60.0)], - ["spatula", "holdable", 1, Vector2(50.0, -60.0)], + ["mortar", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT_2], + ["spatula", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT_2], ]) "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)], + ["test_tube", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["pipette", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["microscope_slide", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["thermometer", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["stethoscope", ChestItemData.ItemType.OUTFIT, 2, _OFFSET_CENTER], + ["pillow", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["gel_tube", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["probe", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["towel", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["swaddle", ChestItemData.ItemType.OUTFIT, 1, _OFFSET_LEFT], + ["scissors", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["cord_clamp", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT], ]) "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)], + ["bottle", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT], + ["rattle", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_CENTER], + ["blanket", ChestItemData.ItemType.OUTFIT, 1, _OFFSET_RIGHT], ]) "garden_table": return _make([ - ["teapot", "holdable", 1, Vector2(-50.0, -60.0)], - ["cake", "holdable", 1, Vector2(50.0, -60.0)], + ["teapot", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT_2], + ["cake", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_RIGHT_2], ]) "garden_storage": return _make([ - ["confetti", "holdable", 1, Vector2(-50.0, -60.0)], - ["party_hat", "outfit", 1, Vector2(50.0, -60.0)], + ["confetti", ChestItemData.ItemType.HOLDABLE, 1, _OFFSET_LEFT_2], + ["party_hat", ChestItemData.ItemType.OUTFIT, 1, _OFFSET_RIGHT_2], ]) return [] @@ -91,6 +97,7 @@ static func get_items(chest_id: String) -> Array[ChestItemData]: static func _make(data: Array) -> Array[ChestItemData]: var result: Array[ChestItemData] = [] for entry: Array in data: + assert(entry.size() == 4, "ChestItemData entry must have 4 elements: " + str(entry)) var d: ChestItemData = ChestItemData.new() d.item_id = entry[0] d.item_type = entry[1] diff --git a/test/unit/test_room_chest.gd b/test/unit/test_room_chest.gd index dfa3c5a..3638df5 100644 --- a/test/unit/test_room_chest.gd +++ b/test/unit/test_room_chest.gd @@ -4,7 +4,7 @@ extends GutTest func test_chest_item_data_default_item_type_is_holdable() -> void: var d: ChestItemData = ChestItemData.new() - assert_eq(d.item_type, "holdable") + assert_eq(d.item_type, ChestItemData.ItemType.HOLDABLE) func test_chest_item_data_default_outfit_layer_is_one() -> void: @@ -20,3 +20,18 @@ func test_room_chest_config_reception_desk_has_three_items() -> void: 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) + + +func test_room_chest_config_reception_desk_first_item_fields() -> void: + var items: Array[ChestItemData] = RoomChestConfig.get_items("reception_desk") + assert_eq(items[0].item_id, "clipboard") + assert_eq(items[0].item_type, ChestItemData.ItemType.HOLDABLE) + assert_eq(items[0].spawn_offset, Vector2(-70.0, -60.0)) + + +func test_room_chest_config_patient_cabinet_stethoscope_outfit_layer_two() -> void: + var items: Array[ChestItemData] = RoomChestConfig.get_items("patient_cabinet") + var stethoscope: ChestItemData = items[1] + assert_eq(stethoscope.item_id, "stethoscope") + assert_eq(stethoscope.item_type, ChestItemData.ItemType.OUTFIT) + assert_eq(stethoscope.outfit_layer, 2) From b9c73b80ea69e195f7bc654bb98a3e19a3af3948 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:04:34 +0200 Subject: [PATCH 3/9] feat(items): add RoomChest with spawn and receive logic Co-Authored-By: Claude Sonnet 4.6 --- scripts/objects/holdable_item.gd | 2 + scripts/objects/room_chest.gd | 93 ++++++++++++++++++++++++++++++++ test/unit/test_room_chest.gd | 48 +++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 scripts/objects/room_chest.gd diff --git a/scripts/objects/holdable_item.gd b/scripts/objects/holdable_item.gd index 59d7bf6..be93faa 100644 --- a/scripts/objects/holdable_item.gd +++ b/scripts/objects/holdable_item.gd @@ -10,6 +10,8 @@ const HAND_SLOT_RADIUS: float = 60.0 @export var item_id: String = "" +var home_chest: Node2D = null + func _ready() -> void: var drag: DragDropComponent = get_node_or_null("DragDropComponent") as DragDropComponent diff --git a/scripts/objects/room_chest.gd b/scripts/objects/room_chest.gd new file mode 100644 index 0000000..324a702 --- /dev/null +++ b/scripts/objects/room_chest.gd @@ -0,0 +1,93 @@ +## 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 GameState.has_method("get_chest_state"): + if 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) + if GameState.has_method("set_chest_state"): + GameState.set_chest_state(chest_id, _get_spawned_ids()) + items_spawned.emit(self) + + +func receive_item(item: HoldableItem) -> void: + _spawned_items.erase(item) + if GameState.has_method("set_chest_state"): + 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 == ChestItemData.ItemType.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) diff --git a/test/unit/test_room_chest.gd b/test/unit/test_room_chest.gd index 3638df5..69ca74a 100644 --- a/test/unit/test_room_chest.gd +++ b/test/unit/test_room_chest.gd @@ -35,3 +35,51 @@ func test_room_chest_config_patient_cabinet_stethoscope_outfit_layer_two() -> vo assert_eq(stethoscope.item_id, "stethoscope") assert_eq(stethoscope.item_type, ChestItemData.ItemType.OUTFIT) assert_eq(stethoscope.outfit_layer, 2) + + +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) From 4f1766834a45eb79825e1a341003add60a388d05 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:07:16 +0200 Subject: [PATCH 4/9] refactor(items): strengthen RoomChest types, guard receive_item, expose get_spawned_item --- scripts/objects/room_chest.gd | 14 +++++++++++--- test/unit/test_room_chest.gd | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/objects/room_chest.gd b/scripts/objects/room_chest.gd index 324a702..5cce863 100644 --- a/scripts/objects/room_chest.gd +++ b/scripts/objects/room_chest.gd @@ -9,7 +9,7 @@ const SPAWN_TWEEN_DURATION: float = 0.3 @export var chest_id: String = "" -var _spawned_items: Array = [] +var _spawned_items: Array[HoldableItem] = [] var _item_configs: Array[ChestItemData] = [] @@ -41,6 +41,8 @@ func spawn_items() -> void: func receive_item(item: HoldableItem) -> void: + if not _spawned_items.has(item): + return _spawned_items.erase(item) if GameState.has_method("set_chest_state"): if _spawned_items.is_empty(): @@ -63,6 +65,12 @@ func get_item_config_count() -> int: return _item_configs.size() +func get_spawned_item(index: int) -> HoldableItem: + if index < 0 or index >= _spawned_items.size(): + return null + return _spawned_items[index] + + func _create_item(config: ChestItemData) -> HoldableItem: var item: HoldableItem if config.item_type == ChestItemData.ItemType.OUTFIT: @@ -75,8 +83,8 @@ func _create_item(config: ChestItemData) -> HoldableItem: return item -func _get_spawned_ids() -> Array: - var ids: Array = [] +func _get_spawned_ids() -> Array[String]: + var ids: Array[String] = [] for item: HoldableItem in _spawned_items: ids.append(item.item_id) return ids diff --git a/test/unit/test_room_chest.gd b/test/unit/test_room_chest.gd index 69ca74a..107d3fc 100644 --- a/test/unit/test_room_chest.gd +++ b/test/unit/test_room_chest.gd @@ -80,6 +80,6 @@ func test_receive_item_decrements_spawned_count() -> void: chest.chest_id = "reception_desk" add_child_autofree(chest) chest.spawn_items() - var item: HoldableItem = chest._spawned_items[0] as HoldableItem + var item: HoldableItem = chest.get_spawned_item(0) chest.receive_item(item) assert_eq(chest.get_spawned_count(), 2) From 96ec05333176e545183dbae8b933a074d10481fb Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:09:47 +0200 Subject: [PATCH 5/9] feat(items): add chest-return priority to HoldableItem and GameState v3 chest states - HoldableItem._try_return_to_chest() snaps item back if within CHEST_RETURN_RADIUS (80px) - _on_drag_released checks chest return before hand-slot fallback - OutfitItem._on_drag_released checks chest return before outfit/hand-slot logic - GameState: _chest_states dict + get/set/clear_chest_state methods - GameState.get_save_data() bumped to version 3, includes chest_states - GameState.apply_save_data() restores chest_states from save data --- scripts/autoload/GameState.gd | 22 +++++++++++++++++- scripts/objects/holdable_item.gd | 12 ++++++++++ scripts/objects/outfit_item.gd | 2 ++ test/unit/test_game_state.gd | 38 +++++++++++++++++++++++++++++++- test/unit/test_holdable_item.gd | 30 +++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/scripts/autoload/GameState.gd b/scripts/autoload/GameState.gd index 8f7d4eb..e7c82ee 100644 --- a/scripts/autoload/GameState.gd +++ b/scripts/autoload/GameState.gd @@ -8,6 +8,7 @@ var _character_positions: Dictionary = {} var _character_outfits: Dictionary = {} var _character_held_items: Dictionary = {} var _object_states: Dictionary = {} +var _chest_states: Dictionary = {} var current_room: String = "reception" var music_volume: float = 0.6 var sfx_volume: float = 1.0 @@ -58,17 +59,32 @@ func set_object_state(id: String, state: String) -> void: state_changed.emit() +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() + + func get_save_data() -> Dictionary: var positions: Dictionary = {} for key: String in _character_positions: var pos: Vector2 = _character_positions[key] positions[key] = [pos.x, pos.y] return { - "version": 2, + "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, @@ -98,3 +114,7 @@ func apply_save_data(data: Dictionary) -> void: music_volume = data["music_volume"] if data.has("sfx_volume"): sfx_volume = data["sfx_volume"] + if data.has("chest_states"): + _chest_states = data["chest_states"].duplicate(true) + else: + _chest_states = {} diff --git a/scripts/objects/holdable_item.gd b/scripts/objects/holdable_item.gd index be93faa..a2f248d 100644 --- a/scripts/objects/holdable_item.gd +++ b/scripts/objects/holdable_item.gd @@ -7,6 +7,7 @@ signal item_picked_up(item: HoldableItem) signal item_placed(item: HoldableItem) const HAND_SLOT_RADIUS: float = 60.0 +const CHEST_RETURN_RADIUS: float = 80.0 @export var item_id: String = "" @@ -27,6 +28,8 @@ func _on_drag_picked_up(_pos: Vector2) -> void: 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 @@ -35,6 +38,15 @@ func _on_drag_released(_pos: Vector2) -> void: item_placed.emit(self) +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 as RoomChest).receive_item(self) + return true + + func is_in_hand_slot() -> bool: var p: Node = get_parent() if p == null: diff --git a/scripts/objects/outfit_item.gd b/scripts/objects/outfit_item.gd index d993c7d..b4c04e8 100644 --- a/scripts/objects/outfit_item.gd +++ b/scripts/objects/outfit_item.gd @@ -10,6 +10,8 @@ const OUTFIT_APPLY_RADIUS: float = 80.0 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) diff --git a/test/unit/test_game_state.gd b/test/unit/test_game_state.gd index a8a8f9a..7ad6187 100644 --- a/test/unit/test_game_state.gd +++ b/test/unit/test_game_state.gd @@ -144,4 +144,40 @@ func test_apply_save_data_restores_held_items() -> void: func test_save_data_has_version_two() -> void: var data: Dictionary = _state.get_save_data() - assert_eq(data.get("version", 0), 2) + assert_eq(data.get("version", 0), 3) + + +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"]) diff --git a/test/unit/test_holdable_item.gd b/test/unit/test_holdable_item.gd index 5511cdc..97f9d33 100644 --- a/test/unit/test_holdable_item.gd +++ b/test/unit/test_holdable_item.gd @@ -69,3 +69,33 @@ func test_holdable_item_detach_preserves_global_position() -> void: var hand_pos: Vector2 = character.get_node("HandLeft").global_position item._on_drag_picked_up(hand_pos) assert_eq(item.global_position, hand_pos) + + +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()) From 87db92955acc868f39eeec6f565caf2e8754f287 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:12:29 +0200 Subject: [PATCH 6/9] fix(items): safe cast in _try_return_to_chest, typed chest state param, object_states reset - Replace unsafe direct cast in HoldableItem._try_return_to_chest() with guarded as-cast - Type set_chest_state() parameter as Array[String] to match RoomChest._get_spawned_ids() - Add else-branch in apply_save_data() to reset _object_states when key absent - Rename test_save_data_has_version_two to test_save_data_has_version_three --- scripts/autoload/GameState.gd | 4 +++- scripts/objects/holdable_item.gd | 5 ++++- test/unit/test_game_state.gd | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/autoload/GameState.gd b/scripts/autoload/GameState.gd index e7c82ee..34c0ff2 100644 --- a/scripts/autoload/GameState.gd +++ b/scripts/autoload/GameState.gd @@ -63,7 +63,7 @@ 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: +func set_chest_state(chest_id: String, spawned_item_ids: Array[String]) -> void: _chest_states[chest_id] = spawned_item_ids state_changed.emit() @@ -108,6 +108,8 @@ func apply_save_data(data: Dictionary) -> void: _character_held_items = {} if data.has("object_states"): _object_states = data["object_states"] + else: + _object_states = {} if data.has("current_room"): current_room = data["current_room"] if data.has("music_volume"): diff --git a/scripts/objects/holdable_item.gd b/scripts/objects/holdable_item.gd index a2f248d..b768979 100644 --- a/scripts/objects/holdable_item.gd +++ b/scripts/objects/holdable_item.gd @@ -43,7 +43,10 @@ func _try_return_to_chest() -> bool: return false if global_position.distance_to(home_chest.global_position) >= CHEST_RETURN_RADIUS: return false - (home_chest as RoomChest).receive_item(self) + var chest: RoomChest = home_chest as RoomChest + if chest == null: + return false + chest.receive_item(self) return true diff --git a/test/unit/test_game_state.gd b/test/unit/test_game_state.gd index 7ad6187..5d0515a 100644 --- a/test/unit/test_game_state.gd +++ b/test/unit/test_game_state.gd @@ -142,7 +142,7 @@ func test_apply_save_data_restores_held_items() -> void: assert_eq(_state.get_character_held_item("kitten_f", "left"), "gel_tube") -func test_save_data_has_version_two() -> void: +func test_save_data_has_version_three() -> void: var data: Dictionary = _state.get_save_data() assert_eq(data.get("version", 0), 3) From a877d8f5fe8d16916fdc67142bb6e589efd9c67a Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:16:15 +0200 Subject: [PATCH 7/9] feat(rooms): add RoomChest nodes to Floor 0 rooms ReceptionDesk, GiftShopShelf, RestaurantCounter, EmergencyCabinet added to their respective tscn files. Fixes spawn_items deferred call to avoid add_child race during _ready tree setup. Co-Authored-By: Claude Sonnet 4.6 --- scenes/rooms/floor0/EmergencyRoom.tscn | 8 +++- scenes/rooms/floor0/GiftShop.tscn | 8 +++- scenes/rooms/floor0/Reception.tscn | 8 +++- scenes/rooms/floor0/Restaurant.tscn | 8 +++- scripts/objects/room_chest.gd | 2 +- test/unit/test_room_chests_floor0.gd | 54 ++++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 test/unit/test_room_chests_floor0.gd diff --git a/scenes/rooms/floor0/EmergencyRoom.tscn b/scenes/rooms/floor0/EmergencyRoom.tscn index b8d369f..c503b3d 100644 --- a/scenes/rooms/floor0/EmergencyRoom.tscn +++ b/scenes/rooms/floor0/EmergencyRoom.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://cozypaw_emergency"] +[gd_scene load_steps=5 format=3 uid="uid://cozypaw_emergency"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="PackedScene" path="res://scenes/objects/Ambulance.tscn" id="2_ambulance"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] [node name="EmergencyRoom" type="Node2D"] @@ -79,3 +80,8 @@ trigger_room = 3 position = Vector2(310, 480) script = ExtResource("3_snap") pose = "lying" + +[node name="EmergencyCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "emergency_cabinet" diff --git a/scenes/rooms/floor0/GiftShop.tscn b/scenes/rooms/floor0/GiftShop.tscn index 61a6301..ee9951d 100644 --- a/scenes/rooms/floor0/GiftShop.tscn +++ b/scenes/rooms/floor0/GiftShop.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_giftshop"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_giftshop"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="GiftShop" type="Node2D"] @@ -68,3 +69,8 @@ position = Vector2(640, 510) [node name="SnapCounter" type="Node2D" parent="."] position = Vector2(640, 528) script = ExtResource("2_snap") + +[node name="GiftShopShelf" type="Node2D" parent="."] +position = Vector2(120.0, 300.0) +script = ExtResource("3_chest") +chest_id = "giftshop_shelf" diff --git a/scenes/rooms/floor0/Reception.tscn b/scenes/rooms/floor0/Reception.tscn index 5ebf8d9..841318e 100644 --- a/scenes/rooms/floor0/Reception.tscn +++ b/scenes/rooms/floor0/Reception.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_reception"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_reception"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="Reception" type="Node2D"] @@ -102,3 +103,8 @@ script = ExtResource("2_snap") [node name="SnapBenchRight2" type="Node2D" parent="."] position = Vector2(1080, 555) script = ExtResource("2_snap") + +[node name="ReceptionDesk" type="Node2D" parent="."] +position = Vector2(120.0, 555.0) +script = ExtResource("3_chest") +chest_id = "reception_desk" diff --git a/scenes/rooms/floor0/Restaurant.tscn b/scenes/rooms/floor0/Restaurant.tscn index 19f8eef..06a2f4a 100644 --- a/scenes/rooms/floor0/Restaurant.tscn +++ b/scenes/rooms/floor0/Restaurant.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_restaurant"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_restaurant"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="Restaurant" type="Node2D"] @@ -118,3 +119,8 @@ script = ExtResource("2_snap") [node name="SnapTable3Right" type="Node2D" parent="."] position = Vector2(1120, 510) script = ExtResource("2_snap") + +[node name="RestaurantCounter" type="Node2D" parent="."] +position = Vector2(120.0, 555.0) +script = ExtResource("3_chest") +chest_id = "restaurant_counter" diff --git a/scripts/objects/room_chest.gd b/scripts/objects/room_chest.gd index 5cce863..3b87e00 100644 --- a/scripts/objects/room_chest.gd +++ b/scripts/objects/room_chest.gd @@ -18,7 +18,7 @@ func _ready() -> void: _item_configs = RoomChestConfig.get_items(chest_id) if not chest_id.is_empty() and GameState.has_method("get_chest_state"): if not GameState.get_chest_state(chest_id).is_empty(): - spawn_items() + call_deferred("spawn_items") func spawn_items() -> void: diff --git a/test/unit/test_room_chests_floor0.gd b/test/unit/test_room_chests_floor0.gd new file mode 100644 index 0000000..1ea7daa --- /dev/null +++ b/test/unit/test_room_chests_floor0.gd @@ -0,0 +1,54 @@ +## 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) From 9aded82dbb1090d74d42d3c70323725bfbf3ea97 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:20:36 +0200 Subject: [PATCH 8/9] feat(rooms): add RoomChest nodes to Floor 1 rooms XRayCabinet (3 items), PharmacyMedicine + PharmacyTools (2 each), LabBench (3 items), PatientCabinet (3 items). 10 new tests, all passing. Co-Authored-By: Claude Sonnet 4.6 --- scenes/rooms/floor1/Lab.tscn | 10 ++++- scenes/rooms/floor1/PatientRoom.tscn | 8 +++- scenes/rooms/floor1/Pharmacy.tscn | 13 +++++- scenes/rooms/floor1/XRay.tscn | 8 +++- test/unit/test_room_chests_floor1.gd | 67 ++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 test/unit/test_room_chests_floor1.gd diff --git a/scenes/rooms/floor1/Lab.tscn b/scenes/rooms/floor1/Lab.tscn index 3c7d001..e47b495 100644 --- a/scenes/rooms/floor1/Lab.tscn +++ b/scenes/rooms/floor1/Lab.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_lab"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_lab"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="Lab" type="Node2D"] @@ -25,7 +26,7 @@ color = Color(0.88, 0.92, 0.94, 1) size = Vector2(40, 620) position = Vector2(1240, 0) -[node name="LabBench" type="ColorRect" parent="."] +[node name="LabBenchSurface" type="ColorRect" parent="."] color = Color(0.88, 0.88, 0.92, 1) size = Vector2(800, 40) position = Vector2(240, 480) @@ -77,3 +78,8 @@ script = ExtResource("2_snap") [node name="SnapLabBench2" type="Node2D" parent="."] position = Vector2(750, 470) script = ExtResource("2_snap") + +[node name="LabBench" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("3_chest") +chest_id = "lab_bench" diff --git a/scenes/rooms/floor1/PatientRoom.tscn b/scenes/rooms/floor1/PatientRoom.tscn index 455bc1b..18afbdd 100644 --- a/scenes/rooms/floor1/PatientRoom.tscn +++ b/scenes/rooms/floor1/PatientRoom.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_patientroom"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_patientroom"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="PatientRoom" type="Node2D"] @@ -93,3 +94,8 @@ pose = "lying" position = Vector2(810, 465) script = ExtResource("2_snap") pose = "lying" + +[node name="PatientCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("3_chest") +chest_id = "patient_cabinet" diff --git a/scenes/rooms/floor1/Pharmacy.tscn b/scenes/rooms/floor1/Pharmacy.tscn index 59d4ea9..bb21821 100644 --- a/scenes/rooms/floor1/Pharmacy.tscn +++ b/scenes/rooms/floor1/Pharmacy.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cozypaw_pharmacy"] +[gd_scene load_steps=4 format=3 uid="uid://cozypaw_pharmacy"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="2_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="3_chest"] [node name="Pharmacy" type="Node2D"] @@ -73,3 +74,13 @@ position = Vector2(640, 430) [node name="SnapCounter" type="Node2D" parent="."] position = Vector2(640, 520) script = ExtResource("2_snap") + +[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" diff --git a/scenes/rooms/floor1/XRay.tscn b/scenes/rooms/floor1/XRay.tscn index a483fb8..3f07a7f 100644 --- a/scenes/rooms/floor1/XRay.tscn +++ b/scenes/rooms/floor1/XRay.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://cozypaw_xray"] +[gd_scene load_steps=5 format=3 uid="uid://cozypaw_xray"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="PackedScene" path="res://scenes/objects/XRayMachine.tscn" id="2_xraymachine"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] [node name="XRay" type="Node2D"] @@ -59,3 +60,8 @@ position = Vector2(900, 560) position = Vector2(640, 480) script = ExtResource("3_snap") pose = "lying" + +[node name="XRayCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "xray_cabinet" diff --git a/test/unit/test_room_chests_floor1.gd b/test/unit/test_room_chests_floor1.gd new file mode 100644 index 0000000..8017f0a --- /dev/null +++ b/test/unit/test_room_chests_floor1.gd @@ -0,0 +1,67 @@ +## 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) From cd3ce7bf6e081337431f76a9c0b2cb435ee1b7d5 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Sat, 9 May 2026 01:24:18 +0200 Subject: [PATCH 9/9] feat(rooms): add RoomChest nodes to Floor 2 and Home rooms Add UltrasoundCart, DeliveryCabinet, NurseryShelf, GardenTable, GardenStorage chest nodes to their respective scenes. All 10 new tests pass (196 total). Co-Authored-By: Claude Sonnet 4.6 --- scenes/rooms/floor2/DeliveryRoom.tscn | 8 ++- scenes/rooms/floor2/Nursery.tscn | 8 ++- scenes/rooms/floor2/Ultrasound.tscn | 8 ++- scenes/rooms/home/GardenParty.tscn | 13 ++++- test/unit/test_room_chests_floor2_home.gd | 67 +++++++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 test/unit/test_room_chests_floor2_home.gd diff --git a/scenes/rooms/floor2/DeliveryRoom.tscn b/scenes/rooms/floor2/DeliveryRoom.tscn index 9b49223..b2b8913 100644 --- a/scenes/rooms/floor2/DeliveryRoom.tscn +++ b/scenes/rooms/floor2/DeliveryRoom.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://cozypaw_deliveryroom"] +[gd_scene load_steps=5 format=3 uid="uid://cozypaw_deliveryroom"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="PackedScene" path="res://scenes/objects/DeliveryBed.tscn" id="2_deliverybed"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] [node name="DeliveryRoom" type="Node2D"] @@ -42,3 +43,8 @@ position = Vector2(880, 540) position = Vector2(540, 480) script = ExtResource("3_snap") pose = "lying" + +[node name="DeliveryCabinet" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "delivery_cabinet" diff --git a/scenes/rooms/floor2/Nursery.tscn b/scenes/rooms/floor2/Nursery.tscn index 74f0466..88f4874 100644 --- a/scenes/rooms/floor2/Nursery.tscn +++ b/scenes/rooms/floor2/Nursery.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://cozypaw_nursery"] +[gd_scene load_steps=5 format=3 uid="uid://cozypaw_nursery"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="PackedScene" path="res://scenes/objects/Cradle.tscn" id="2_cradle"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] [node name="Nursery" type="Node2D"] @@ -66,3 +67,8 @@ position = Vector2(860, 240) script = ExtResource("3_snap") pose = "lying" baby_only = true + +[node name="NurseryShelf" type="Node2D" parent="."] +position = Vector2(640.0, 260.0) +script = ExtResource("4_chest") +chest_id = "nursery_shelf" diff --git a/scenes/rooms/floor2/Ultrasound.tscn b/scenes/rooms/floor2/Ultrasound.tscn index 4a76d51..0cec7e5 100644 --- a/scenes/rooms/floor2/Ultrasound.tscn +++ b/scenes/rooms/floor2/Ultrasound.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://cozypaw_ultrasound"] +[gd_scene load_steps=5 format=3 uid="uid://cozypaw_ultrasound"] [ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"] [ext_resource type="PackedScene" path="res://scenes/objects/UltrasoundMachine.tscn" id="2_ultrasound"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="3_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="4_chest"] [node name="Ultrasound" type="Node2D"] @@ -57,3 +58,8 @@ position = Vector2(480, 450) position = Vector2(470, 480) script = ExtResource("3_snap") pose = "lying" + +[node name="UltrasoundCart" type="Node2D" parent="."] +position = Vector2(150.0, 400.0) +script = ExtResource("4_chest") +chest_id = "ultrasound_cart" diff --git a/scenes/rooms/home/GardenParty.tscn b/scenes/rooms/home/GardenParty.tscn index 8cf4e8d..a8856ac 100644 --- a/scenes/rooms/home/GardenParty.tscn +++ b/scenes/rooms/home/GardenParty.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=5 format=3 uid="uid://cozypaw_gardenparty"] +[gd_scene load_steps=6 format=3 uid="uid://cozypaw_gardenparty"] [ext_resource type="PackedScene" path="res://scenes/objects/GiftBox.tscn" id="1_giftbox"] [ext_resource type="PackedScene" path="res://scenes/objects/TeaPot.tscn" id="2_teapot"] [ext_resource type="PackedScene" path="res://scenes/objects/HomeButton.tscn" id="3_homebtn"] [ext_resource type="Script" path="res://scripts/objects/snap_point.gd" id="4_snap"] +[ext_resource type="Script" path="res://scripts/objects/room_chest.gd" id="5_chest"] [node name="GardenParty" type="Node2D"] @@ -93,3 +94,13 @@ script = ExtResource("4_snap") [node name="SnapTableRight" type="Node2D" parent="."] position = Vector2(750, 455) script = ExtResource("4_snap") + +[node name="GardenTable" type="Node2D" parent="."] +position = Vector2(200.0, 400.0) +script = ExtResource("5_chest") +chest_id = "garden_table" + +[node name="GardenStorage" type="Node2D" parent="."] +position = Vector2(900.0, 400.0) +script = ExtResource("5_chest") +chest_id = "garden_storage" diff --git a/test/unit/test_room_chests_floor2_home.gd b/test/unit/test_room_chests_floor2_home.gd new file mode 100644 index 0000000..cb4e38b --- /dev/null +++ b/test/unit/test_room_chests_floor2_home.gd @@ -0,0 +1,67 @@ +## 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)