From b74573fe0e64062c427fa572d40e3470068ae471 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Tue, 7 Jul 2026 16:08:40 +0200 Subject: [PATCH] feat: validate expanded resource discovery --- docs/LEARNING_ROADMAP.md | 11 ++--- docs/RESOURCE_NODE_MIGRATION.md | 6 +++ tests/jajce_runtime_integration_test.gd | 8 ++-- tests/jajce_world_scaffold_test.gd | 53 ++++++++++++++++++++++++- world/active_world_adapter.gd | 9 ++++- world/jajce/JajceWorld.tscn | 40 +++++++++++++++++++ 6 files changed, 116 insertions(+), 11 deletions(-) diff --git a/docs/LEARNING_ROADMAP.md b/docs/LEARNING_ROADMAP.md index d826973..bf52664 100644 --- a/docs/LEARNING_ROADMAP.md +++ b/docs/LEARNING_ROADMAP.md @@ -652,11 +652,12 @@ Completed after the architecture gate: The practical next sequence is: -1. Expand resource discovery with many finite `ResourceNode` instances placed - in meaningful foliage, animal-camp, berry, and village-stockpile contexts. - The first distance/safety/comfort scoring pass and small authored resource - spread exist; next, validate placement rules and grow the authored set - carefully rather than reintroducing abstract resource zones. +1. Continue growing resource discovery with finite `ResourceNode` instances + placed in meaningful foliage, animal-camp, berry, and village-stockpile + contexts. The first distance/safety/comfort scoring pass, twelve authored + resources, and placement validation exist; next additions should increase + authored variety only when they preserve reachability, stable IDs, scoring + metadata, and clear player interaction ranges. 2. Expand persistence only when schedules, player state, or a real save menu creates a concrete requirement. diff --git a/docs/RESOURCE_NODE_MIGRATION.md b/docs/RESOURCE_NODE_MIGRATION.md index 9559b48..a60e1e9 100644 --- a/docs/RESOURCE_NODE_MIGRATION.md +++ b/docs/RESOURCE_NODE_MIGRATION.md @@ -550,6 +550,12 @@ profession, and NPC comfort range. The first scoring pass stores `safety_risk`, `comfort_distance`, and `discovery_priority` on resource state and uses them when selecting NPC resource targets. +The first validated spread contains twelve finite resources: berry bushes and +patches, animal camps, trees, and one village woodpile. Placement is guarded by +headless tests for stable IDs, food/wood coverage, semantic contexts, +discovery metadata, navigation reachability, and non-overlapping pantry/player +interaction range. + ## Test scenarios At minimum, exercise: diff --git a/tests/jajce_runtime_integration_test.gd b/tests/jajce_runtime_integration_test.gd index ce01833..5c6070e 100644 --- a/tests/jajce_runtime_integration_test.gd +++ b/tests/jajce_runtime_integration_test.gd @@ -36,10 +36,10 @@ func _run() -> void: "Runtime should not retain duplicate flat-world objects" ) var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes") - _check(resource_root.get_child_count() == 8, "Runtime should contain eight Jajce ResourceNodes") + _check(resource_root.get_child_count() == 12, "Runtime should contain twelve Jajce ResourceNodes") _check( - simulation_manager.resource_states.size() == 8, - "Simulation authority should bind all eight Jajce resources" + simulation_manager.resource_states.size() == 12, + "Simulation authority should bind all twelve Jajce resources" ) var navigation_map: RID = main_scene.get_world_3d().navigation_map @@ -102,7 +102,7 @@ func _run() -> void: ) if failures.is_empty(): - print("[TEST] Jajce runtime passed: 6 NPCs, 8 resources, typed sites") + print("[TEST] Jajce runtime passed: 6 NPCs, 12 resources, typed sites") quit(0) return diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index f70fc73..d305f00 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -116,19 +116,42 @@ func _run() -> void: ids == [ "animal_camp_01", + "animal_camp_river_01", "berry_bush_01", "berry_bush_02", "berry_patch_river_01", + "berry_patch_south_01", "tree_01", "tree_02", "tree_north_outskirts_01", - "tree_south_edge_01" + "tree_river_resource_01", + "tree_south_edge_01", + "wood_pile_village_01" ], "Jajce resources should preserve all stable and discoverable IDs" ) var metadata_valid := true + var food_count := 0 + var wood_count := 0 + var animal_context_count := 0 + var berry_context_count := 0 + var village_context_count := 0 + var outskirt_context_count := 0 for resource in resources: var node := resource as ResourceNode + var id := String(node.node_id) + if node.resource_id == SimulationIds.RESOURCE_FOOD: + food_count += 1 + if node.resource_id == SimulationIds.RESOURCE_WOOD: + wood_count += 1 + if id.begins_with("animal_camp"): + animal_context_count += 1 + if id.begins_with("berry_"): + berry_context_count += 1 + if id.contains("_village_"): + village_context_count += 1 + if id.contains("_outskirts_") or id.contains("_edge_") or id.contains("_river_"): + outskirt_context_count += 1 metadata_valid = ( metadata_valid and node.comfort_distance > 0.0 @@ -136,6 +159,12 @@ func _run() -> void: and node.safety_risk <= 1.0 ) _check(metadata_valid, "Jajce resources should define discovery metadata") + _check(food_count >= 6, "Jajce should expose at least six finite food resources") + _check(wood_count >= 6, "Jajce should expose at least six finite wood resources") + _check(animal_context_count >= 2, "Jajce food discovery should include animal-camp contexts") + _check(berry_context_count >= 4, "Jajce food discovery should include berry contexts") + _check(village_context_count >= 1, "Jajce discovery should include at least one village stockpile") + _check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts") _check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites") var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode _check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode") @@ -190,6 +219,24 @@ func _run() -> void: var adapter := ActiveWorldAdapter.new() adapter.pantry_storage = pantry root.add_child(adapter) + var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD) + _check(food_candidates.size() >= 6, "Adapter should expose all authored food candidates") + var candidate_metadata_valid := true + for candidate in food_candidates: + candidate_metadata_valid = ( + candidate_metadata_valid + and candidate.has_all( + [ + "target_id", + "position", + "resource_id", + "safety_risk", + "comfort_distance", + "discovery_priority" + ] + ) + ) + _check(candidate_metadata_valid, "Adapter resource facts should include discovery metadata") var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.debug_logs = false manager.active_world_adapter = adapter @@ -202,6 +249,10 @@ func _run() -> void: manager.resolve_npc_target(npc.id, Vector3.ZERO), "Target resolver should find Jajce resources without a parent path" ) + _check( + npc.target_id != &"animal_camp_river_01", + "Target resolver should not choose the far risky animal camp while safer food exists" + ) var selected := ResourceNode.get_by_id(npc.target_id) if selected != null: var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id) diff --git a/world/active_world_adapter.gd b/world/active_world_adapter.gd index b46500b..c938de4 100644 --- a/world/active_world_adapter.gd +++ b/world/active_world_adapter.gd @@ -10,7 +10,14 @@ func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: if node.action_id != action_id or node.interaction_point == null: continue candidates.append( - {"target_id": String(node.node_id), "position": node.interaction_point.global_position} + { + "target_id": String(node.node_id), + "position": node.interaction_point.global_position, + "resource_id": String(node.resource_id), + "safety_risk": node.safety_risk, + "comfort_distance": node.comfort_distance, + "discovery_priority": node.discovery_priority + } ) return candidates diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index a4877e8..3ffaecd 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -255,6 +255,24 @@ safety_risk = 0.35 comfort_distance = 24.0 discovery_priority = -0.5 +[node name="BerryPatch_South_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")] +position = Vector3(-18, 0, -6) +node_id = &"berry_patch_south_01" +initial_amount = 7.0 +yield_per_action = 1.5 +safety_risk = 0.12 +comfort_distance = 24.0 +discovery_priority = 0.1 + +[node name="AnimalCamp_River_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")] +position = Vector3(20, 0, 16) +node_id = &"animal_camp_river_01" +initial_amount = 4.0 +yield_per_action = 1.0 +safety_risk = 0.45 +comfort_distance = 30.0 +discovery_priority = -0.75 + [node name="Tree_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")] position = Vector3(6, 0, -8) node_id = &"tree_01" @@ -296,6 +314,28 @@ yield_per_action = 2.0 safety_risk = 0.24 comfort_distance = 26.0 +[node name="Tree_River_Resource_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")] +position = Vector3(20, 0, 18) +node_id = &"tree_river_resource_01" +action_id = &"gather_wood" +resource_id = &"wood" +initial_amount = 8.0 +yield_per_action = 2.0 +safety_risk = 0.22 +comfort_distance = 30.0 +discovery_priority = 0.1 + +[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")] +position = Vector3(5, 0, -13) +node_id = &"wood_pile_village_01" +action_id = &"gather_wood" +resource_id = &"wood" +initial_amount = 6.0 +yield_per_action = 1.5 +safety_risk = 0.02 +comfort_distance = 16.0 +discovery_priority = 0.75 + [node name="StorageSites" type="Node3D" parent="WorldObjects"] [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]