From c61d2860053871568965ca9fbca40626a956dc0f Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Wed, 8 Jul 2026 18:55:51 +0200 Subject: [PATCH] feat: wood inventory, carrying, and woodpile storage Wood now follows the same gather-carry-deposit pattern as food. NPCs put harvested wood into inventory, walk to the VillageWoodpile StorageNode, and deposit it. Village wood syncs from storage. Player harvesting also routes wood to the woodpile. ActiveWorldAdapter exposes woodpile routing for DEPOSIT_WOOD action targeting. WorldViewManager recognizes wood inventory changes. Tests verify woodpile node existence, storage ID, navigation reachability, and adapter routing. --- main.tscn | 3 +- simulation/SimulationManager.gd | 74 +++++++++++++++++-- simulation/actions/ActionSelectionSystem.gd | 4 + .../definitions/SimulationDefinitions.gd | 3 +- simulation/definitions/SimulationIds.gd | 2 + .../definitions/actions/deposit_wood.tres | 10 +++ tests/jajce_runtime_integration_test.gd | 11 +++ tests/jajce_world_scaffold_test.gd | 13 ++++ tests/simulation_definitions_test.gd | 2 +- world/active_world_adapter.gd | 3 + world/jajce/JajceWorld.tscn | 4 + world/world_view_manager.gd | 12 +-- 12 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 simulation/definitions/actions/deposit_wood.tres diff --git a/main.tscn b/main.tscn index 92f3644..bf83800 100644 --- a/main.tscn +++ b/main.tscn @@ -59,9 +59,10 @@ max_zoom_offset = Vector3(0, 20, 15) current = true fov = 65.0 -[node name="ActiveWorldAdapter" type="Node" parent="." unique_id=1773902505 node_paths=PackedStringArray("pantry_storage")] +[node name="ActiveWorldAdapter" type="Node" parent="." unique_id=1773902505 node_paths=PackedStringArray("pantry_storage", "woodpile_storage")] script = ExtResource("9_adapter") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry") +woodpile_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillageWoodpile") [node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")] script = ExtResource("3_lquwl") diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 4e8d1da..5a22cfc 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -162,6 +162,13 @@ func simulate_tick() -> void: SimulationIds.RESOURCE_FOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) ) + elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD: + npc.add_inventory(SimulationIds.RESOURCE_WOOD, extracted) + npc_inventory_changed.emit( + npc, + SimulationIds.RESOURCE_WOOD, + npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) + ) else: village.apply_resource_delta( resource_state.get_resource_id(), extracted @@ -172,7 +179,9 @@ func simulate_tick() -> void: resource_state.get_node_id(), ( _npc_inventory_id(npc.id) - if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD + if resource_state.get_resource_id() in [ + SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD + ] else &"village" ), resource_state.get_resource_id(), @@ -200,6 +209,8 @@ func simulate_tick() -> void: release_npc_reservation(npc.id) elif completed_task == SimulationIds.ACTION_DEPOSIT_FOOD: deposit_npc_inventory(npc, SimulationIds.RESOURCE_FOOD) + elif completed_task == SimulationIds.ACTION_DEPOSIT_WOOD: + deposit_npc_wood(npc) elif completed_task == SimulationIds.ACTION_WITHDRAW_FOOD: withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0) elif completed_task == SimulationIds.ACTION_EAT: @@ -450,19 +461,28 @@ func _record_economic_event( func _initialize_storage() -> void: - if storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY): - _sync_village_food() - return - storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create( - SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_FOOD): village.food} - )) + if not storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY): + storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create( + SimulationIds.STORAGE_VILLAGE_PANTRY, + {String(SimulationIds.RESOURCE_FOOD): village.food} + )) + if not storage_states.has(SimulationIds.STORAGE_VILLAGE_WOODPILE): + storage_states[SimulationIds.STORAGE_VILLAGE_WOODPILE] = (StorageStateRecord.create( + SimulationIds.STORAGE_VILLAGE_WOODPILE, + {String(SimulationIds.RESOURCE_WOOD): village.wood} + )) _sync_village_food() + _sync_village_wood() func get_pantry() -> StorageStateRecord: return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord +func get_woodpile() -> StorageStateRecord: + return storage_states.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord + + func register_loaded_storage_nodes() -> void: for candidate in StorageNode.get_all(): var node := candidate as StorageNode @@ -490,6 +510,14 @@ func _sync_village_food() -> void: village.update_priorities() +func _sync_village_wood() -> void: + var woodpile := get_woodpile() + if woodpile != null: + village.wood = woodpile.get_amount(SimulationIds.RESOURCE_WOOD) + village.update_modifiers() + village.update_priorities() + + func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float: var available := npc.get_inventory_amount(item_id) var deposited := get_pantry().deposit(item_id, available) @@ -508,6 +536,29 @@ func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float: return deposited +func deposit_npc_wood(npc: SimNPC) -> float: + var available := npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) + var woodpile := get_woodpile() + if woodpile == null: + return 0.0 + var deposited := woodpile.deposit(SimulationIds.RESOURCE_WOOD, available) + npc.remove_inventory(SimulationIds.RESOURCE_WOOD, deposited) + _sync_village_wood() + if deposited > 0.0: + npc_inventory_changed.emit( + npc, SimulationIds.RESOURCE_WOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) + ) + _record_economic_event( + SimulationIds.EVENT_STORAGE_DEPOSITED, + npc.id, + _npc_inventory_id(npc.id), + SimulationIds.STORAGE_VILLAGE_WOODPILE, + SimulationIds.RESOURCE_WOOD, + deposited + ) + return deposited + + func withdraw_to_npc(npc: SimNPC, item_id: StringName, amount: float) -> float: var withdrawn := get_pantry().withdraw(item_id, amount) npc.add_inventory(item_id, withdrawn) @@ -620,6 +671,9 @@ func harvest_resource_node(node: ResourceNode) -> float: if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD: get_pantry().deposit(SimulationIds.RESOURCE_FOOD, extracted) _sync_village_food() + elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD: + get_woodpile().deposit(SimulationIds.RESOURCE_WOOD, extracted) + _sync_village_wood() else: village.apply_resource_delta(resource_state.get_resource_id(), extracted) _record_economic_event( @@ -629,7 +683,11 @@ func harvest_resource_node(node: ResourceNode) -> float: ( SimulationIds.STORAGE_VILLAGE_PANTRY if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD - else &"village" + else ( + SimulationIds.STORAGE_VILLAGE_WOODPILE + if resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD + else &"village" + ) ), resource_state.get_resource_id(), extracted diff --git a/simulation/actions/ActionSelectionSystem.gd b/simulation/actions/ActionSelectionSystem.gd index dc59365..564c292 100644 --- a/simulation/actions/ActionSelectionSystem.gd +++ b/simulation/actions/ActionSelectionSystem.gd @@ -104,6 +104,10 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) - return ActionSelectionResult.new( SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage" ) + if npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) > 0.0: + return ActionSelectionResult.new( + SimulationIds.ACTION_DEPOSIT_WOOD, -1.0, "Carrying wood for storage" + ) if npc.energy < 25.0: return ActionSelectionResult.new( SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold" diff --git a/simulation/definitions/SimulationDefinitions.gd b/simulation/definitions/SimulationDefinitions.gd index 1774356..167637a 100644 --- a/simulation/definitions/SimulationDefinitions.gd +++ b/simulation/definitions/SimulationDefinitions.gd @@ -11,7 +11,8 @@ const ACTION_PATHS := [ "res://simulation/definitions/actions/wander.tres", "res://simulation/definitions/actions/deposit_food.tres", "res://simulation/definitions/actions/withdraw_food.tres", - "res://simulation/definitions/actions/sleep.tres" + "res://simulation/definitions/actions/sleep.tres", + "res://simulation/definitions/actions/deposit_wood.tres" ] const PROFESSION_PATHS := [ diff --git a/simulation/definitions/SimulationIds.gd b/simulation/definitions/SimulationIds.gd index a6e7b13..e3d3b4f 100644 --- a/simulation/definitions/SimulationIds.gd +++ b/simulation/definitions/SimulationIds.gd @@ -13,6 +13,7 @@ const ACTION_REST := &"rest" const ACTION_WANDER := &"wander" const ACTION_DEPOSIT_FOOD := &"deposit_food" const ACTION_WITHDRAW_FOOD := &"withdraw_food" +const ACTION_DEPOSIT_WOOD := &"deposit_wood" const PROFESSION_FARMER := &"farmer" const PROFESSION_WOODCUTTER := &"woodcutter" @@ -28,6 +29,7 @@ const RESOURCE_FOOD := &"food" const RESOURCE_WOOD := &"wood" const STORAGE_VILLAGE_PANTRY := &"village_pantry" +const STORAGE_VILLAGE_WOODPILE := &"village_woodpile" const EVENT_RESOURCE_EXTRACTED := &"resource_extracted" const EVENT_STORAGE_DEPOSITED := &"storage_deposited" diff --git a/simulation/definitions/actions/deposit_wood.tres b/simulation/definitions/actions/deposit_wood.tres new file mode 100644 index 0000000..9f2fbf2 --- /dev/null +++ b/simulation/definitions/actions/deposit_wood.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +action_id = &"deposit_wood" +display_name = "Deposit Wood" +default_duration = 1.0 +target_type = &"activity" diff --git a/tests/jajce_runtime_integration_test.gd b/tests/jajce_runtime_integration_test.gd index e1bfa29..276d91d 100644 --- a/tests/jajce_runtime_integration_test.gd +++ b/tests/jajce_runtime_integration_test.gd @@ -106,6 +106,12 @@ func _run() -> void: ) var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode destinations.append(pantry.get_interaction_position()) + _check( + main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile"), + "Runtime should expose a typed VillageWoodpile StorageNode" + ) + var woodpile := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode + destinations.append(woodpile.get_interaction_position()) var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites") _check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites") for site in activity_root.get_children(): @@ -149,6 +155,11 @@ func _run() -> void: storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY), "Runtime adapter should route storage actions to village_pantry" ) + var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD) + _check( + wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE), + "Runtime adapter should route wood deposit to village_woodpile" + ) var study_target := adapter.get_activity_target(SimulationIds.ACTION_STUDY) _check( study_target.get("target_id", "") == "study_desk", diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index 8a84e82..19ed620 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -187,6 +187,12 @@ func _run() -> void: pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY, "VillagePantry should preserve the stable storage ID" ) + var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode + _check(woodpile != null, "JajceWorld should include a typed VillageWoodpile StorageNode") + _check( + woodpile.storage_id == SimulationIds.STORAGE_VILLAGE_WOODPILE, + "VillageWoodpile should preserve the stable storage ID" + ) _check( not world.has_node("LegacyActivityMarkers/PantryMarker"), "Pantry should no longer be a legacy activity marker" @@ -224,6 +230,7 @@ func _run() -> void: for resource in resources: destinations.append((resource as ResourceNode).interaction_point.global_position) destinations.append(pantry.get_interaction_position()) + destinations.append(woodpile.get_interaction_position()) for site in activity_root.get_children(): destinations.append((site as ActivitySite).get_interaction_position()) @@ -243,6 +250,7 @@ func _run() -> void: var adapter := ActiveWorldAdapter.new() adapter.pantry_storage = pantry + adapter.woodpile_storage = woodpile root.add_child(adapter) var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD) _check(food_candidates.size() >= 9, "Adapter should expose all authored food candidates") @@ -296,6 +304,11 @@ func _run() -> void: storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01, "Storage actions should use the pantry StorageNode interaction point" ) + var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD) + _check( + wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE), + "Wood deposit should target the typed woodpile's stable ID" + ) var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST) _check( rest_target.get("target_id", "") == "rest_bench", diff --git a/tests/simulation_definitions_test.gd b/tests/simulation_definitions_test.gd index 73fae30..c30a6dc 100644 --- a/tests/simulation_definitions_test.gd +++ b/tests/simulation_definitions_test.gd @@ -34,7 +34,7 @@ func _test_registry_integrity() -> void: SimulationDefinitions.get_action(definition.action_id) == definition, "Action lookup should return '%s'" % definition.action_id ) - _check(action_ids.size() == 10, "Registry should contain all ten executable prototype actions") + _check(action_ids.size() == 11, "Registry should contain all eleven executable prototype actions") var profession_ids := SimulationDefinitions.get_profession_ids() _check(profession_ids.size() == 5, "Registry should contain all five prototype professions") diff --git a/world/active_world_adapter.gd b/world/active_world_adapter.gd index 5aeae70..0857d19 100644 --- a/world/active_world_adapter.gd +++ b/world/active_world_adapter.gd @@ -2,6 +2,7 @@ class_name ActiveWorldAdapter extends Node @export var pantry_storage: StorageNode +@export var woodpile_storage: StorageNode func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: @@ -38,6 +39,8 @@ func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO) return _get_storage_target(pantry_storage) SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD: return _get_storage_target(pantry_storage) + SimulationIds.ACTION_DEPOSIT_WOOD: + return _get_storage_target(woodpile_storage) return {} diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index 586e404..8a1a6ee 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -473,6 +473,10 @@ discovery_priority = 0.45 [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")] position = Vector3(0, 0, -8) +[node name="VillageWoodpile" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")] +position = Vector3(8, 0, -12) +storage_id = &"village_woodpile" + [node name="PantrySignPole" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"] position = Vector3(-1.05, 1.1, 1.25) mesh = SubResource("Mesh_site_pole") diff --git a/world/world_view_manager.gd b/world/world_view_manager.gd index c0dc1db..1389743 100644 --- a/world/world_view_manager.gd +++ b/world/world_view_manager.gd @@ -161,15 +161,15 @@ func _on_npc_died(npc: SimNPC) -> void: func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void: - if item_id != SimulationIds.RESOURCE_FOOD: - return var visual = active_npc_visuals.get(npc.id) if visual == null: return - if visual.has_method("set_carried_food_visible"): - visual.set_carried_food_visible(amount > 0.0) - else: - push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method") + if item_id == SimulationIds.RESOURCE_FOOD: + if visual.has_method("set_carried_food_visible"): + visual.set_carried_food_visible(amount > 0.0) + elif item_id == SimulationIds.RESOURCE_WOOD: + if visual.has_method("set_carried_wood_visible"): + visual.set_carried_wood_visible(amount > 0.0) func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void: