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: