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)