feat: validate expanded resource discovery

This commit is contained in:
2026-07-07 16:08:40 +02:00
parent 4bfc09d5f6
commit b74573fe0e
6 changed files with 116 additions and 11 deletions
+6 -5
View File
@@ -652,11 +652,12 @@ Completed after the architecture gate:
The practical next sequence is: The practical next sequence is:
1. Expand resource discovery with many finite `ResourceNode` instances placed 1. Continue growing resource discovery with finite `ResourceNode` instances
in meaningful foliage, animal-camp, berry, and village-stockpile contexts. placed in meaningful foliage, animal-camp, berry, and village-stockpile
The first distance/safety/comfort scoring pass and small authored resource contexts. The first distance/safety/comfort scoring pass, twelve authored
spread exist; next, validate placement rules and grow the authored set resources, and placement validation exist; next additions should increase
carefully rather than reintroducing abstract resource zones. 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 2. Expand persistence only when schedules, player state, or a real save menu
creates a concrete requirement. creates a concrete requirement.
+6
View File
@@ -550,6 +550,12 @@ profession, and NPC comfort range. The first scoring pass stores
`safety_risk`, `comfort_distance`, and `discovery_priority` on resource state `safety_risk`, `comfort_distance`, and `discovery_priority` on resource state
and uses them when selecting NPC resource targets. 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 ## Test scenarios
At minimum, exercise: At minimum, exercise:
+4 -4
View File
@@ -36,10 +36,10 @@ func _run() -> void:
"Runtime should not retain duplicate flat-world objects" "Runtime should not retain duplicate flat-world objects"
) )
var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes") 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( _check(
simulation_manager.resource_states.size() == 8, simulation_manager.resource_states.size() == 12,
"Simulation authority should bind all eight Jajce resources" "Simulation authority should bind all twelve Jajce resources"
) )
var navigation_map: RID = main_scene.get_world_3d().navigation_map var navigation_map: RID = main_scene.get_world_3d().navigation_map
@@ -102,7 +102,7 @@ func _run() -> void:
) )
if failures.is_empty(): 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) quit(0)
return return
+52 -1
View File
@@ -116,19 +116,42 @@ func _run() -> void:
ids ids
== [ == [
"animal_camp_01", "animal_camp_01",
"animal_camp_river_01",
"berry_bush_01", "berry_bush_01",
"berry_bush_02", "berry_bush_02",
"berry_patch_river_01", "berry_patch_river_01",
"berry_patch_south_01",
"tree_01", "tree_01",
"tree_02", "tree_02",
"tree_north_outskirts_01", "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" "Jajce resources should preserve all stable and discoverable IDs"
) )
var metadata_valid := true 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: for resource in resources:
var node := resource as ResourceNode 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 = (
metadata_valid metadata_valid
and node.comfort_distance > 0.0 and node.comfort_distance > 0.0
@@ -136,6 +159,12 @@ func _run() -> void:
and node.safety_risk <= 1.0 and node.safety_risk <= 1.0
) )
_check(metadata_valid, "Jajce resources should define discovery metadata") _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") _check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode") _check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
@@ -190,6 +219,24 @@ func _run() -> void:
var adapter := ActiveWorldAdapter.new() var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry adapter.pantry_storage = pantry
root.add_child(adapter) 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() var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.debug_logs = false manager.debug_logs = false
manager.active_world_adapter = adapter manager.active_world_adapter = adapter
@@ -202,6 +249,10 @@ func _run() -> void:
manager.resolve_npc_target(npc.id, Vector3.ZERO), manager.resolve_npc_target(npc.id, Vector3.ZERO),
"Target resolver should find Jajce resources without a parent path" "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) var selected := ResourceNode.get_by_id(npc.target_id)
if selected != null: if selected != null:
var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id) var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id)
+8 -1
View File
@@ -10,7 +10,14 @@ func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
if node.action_id != action_id or node.interaction_point == null: if node.action_id != action_id or node.interaction_point == null:
continue continue
candidates.append( 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 return candidates
+40
View File
@@ -255,6 +255,24 @@ safety_risk = 0.35
comfort_distance = 24.0 comfort_distance = 24.0
discovery_priority = -0.5 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")] [node name="Tree_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(6, 0, -8) position = Vector3(6, 0, -8)
node_id = &"tree_01" node_id = &"tree_01"
@@ -296,6 +314,28 @@ yield_per_action = 2.0
safety_risk = 0.24 safety_risk = 0.24
comfort_distance = 26.0 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="StorageSites" type="Node3D" parent="WorldObjects"]
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")] [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]