feat: author resource-aware riverbank grove

This commit is contained in:
Rijad Zuzo
2026-07-17 13:57:57 +02:00
parent 145b90eb97
commit 04c5371b6b
23 changed files with 823 additions and 63 deletions
+184
View File
@@ -0,0 +1,184 @@
extends SceneTree
const FAR_BERRY_ID := &"berry_bush_river_02"
const CLUSTER_TREE_ID := &"tree_river_resource_01"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var camera := Camera3D.new()
camera.current = true
root.add_child(camera)
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
root.add_child(world)
await process_frame
for _frame in 5:
await physics_frame
var terrain := world.get_node("TerrainRoot/Terrain3D") as Terrain3D
var cluster := (
world.get_node("WorldObjects/ResourceClusters/RiverbankResourceCluster")
as RiverbankResourceCluster
)
_check(cluster != null, "Jajce should expose the authored riverbank resource cluster")
if cluster == null:
_finish()
return
var stats := cluster.get_presentation_stats()
_check(
int(stats["understory_instance_count"]) == 28,
"Riverbank understory should keep its reviewed 28-instance budget"
)
_check(
int(stats["multimesh_batch_count"]) == 3,
"Riverbank understory should use exactly three intentional MultiMesh batches"
)
_check(
int(stats["decorative_tree_count"]) == 2,
"Riverbank cluster should keep two non-authoritative silhouette trees"
)
_check(
int(stats["resource_anchor_count"]) == 2,
"Riverbank cluster should pair density with only two finite resource anchors"
)
_check(
_collect_resource_nodes(cluster.get_node("DecorativeInstances")).is_empty(),
"Decorative riverbank instances must not become simulation resources"
)
var anchors := cluster.get_resource_anchors()
var anchor_ids: Array[StringName] = []
for anchor in anchors:
anchor_ids.append(anchor.node_id)
var terrain_height := terrain.data.get_height(anchor.global_position)
_check(
absf(anchor.global_position.y - terrain_height) < 0.05,
"Resource anchor %s should snap to Terrain3D" % anchor.node_id
)
var channel_distance := absf(
(
anchor.global_position.x
- JajceWatercourse.downstream_center_x(anchor.global_position.z)
)
)
_check(
(
channel_distance
> JajceWatercourse.downstream_half_width(anchor.global_position.z) + 1.5
),
"Resource anchor %s should sit on the riverbank rather than in water" % anchor.node_id
)
_check(
anchor_ids.size() == 2 and anchor_ids.has(FAR_BERRY_ID) and anchor_ids.has(CLUSTER_TREE_ID),
"Authored cluster should preserve the existing river berry/tree stable IDs"
)
_check(
(
cluster.has_node("ResourceAnchors/BerryBush_River_02/Visual/BerryPatchPresentation")
and cluster.has_node("ResourceAnchors/Tree_River_Resource_01/Visual/TreePresentation")
),
"Finite cluster anchors should have resource-specific visuals instead of boxes"
)
var berry_presentation := (
cluster.get_node("ResourceAnchors/BerryBush_River_02/Visual/BerryPatchPresentation")
as StylizedBerryPatch
)
_check(
berry_presentation.get_presentation_instance_count() == 13,
"Berry resource visual should keep its bounded leaf-and-fruit instance count"
)
var resources := _collect_resource_nodes(world.get_node("WorldObjects"))
_check(resources.size() == 18, "Clustering should preserve all eighteen finite resources")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode
var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry
adapter.woodpile_storage = woodpile
root.add_child(adapter)
var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.debug_logs = false
manager.active_world_adapter = adapter
root.add_child(manager)
manager.set_process(false)
await process_frame
_check(
int(adapter.get_resource_index_stats()["candidate_count"]) == 18,
"ActiveWorldAdapter should index direct and nested resource anchors equally"
)
var nearby_food := adapter.get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, Vector3.ZERO, 24.0
)
var farther_food := adapter.get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, Vector3.ZERO, 64.0, 24.0
)
_check(not nearby_food.is_empty(), "The authored world should retain nearby food choices")
_check(
_candidate_ids(farther_food).has(FAR_BERRY_ID),
"The nested riverbank berry should occupy a discoverable farther range"
)
for resource in resources:
if resource.resource_id != SimulationIds.RESOURCE_FOOD:
continue
var state: ResourceStateRecord = manager.get_resource_state(resource.node_id)
state.set_enabled(resource.node_id == FAR_BERRY_ID)
var npc: SimNPC = manager.npcs[0]
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
_check(
manager.resolve_npc_target(npc.id, Vector3.ZERO),
"Resolver should expand from the village to the farther authored berry"
)
_check(
npc.target_id == FAR_BERRY_ID,
"Farther discovery should return the nested stable-ID berry anchor"
)
_check(
int(manager.target_resolver.last_resource_query_stats["range_pass_count"]) > 1,
"Farther authored discovery should expand beyond the initial 24 m range"
)
manager.release_resource(FAR_BERRY_ID, npc.id)
for resource in resources:
if resource.resource_id == SimulationIds.RESOURCE_FOOD:
manager.get_resource_state(resource.node_id).set_enabled(resource.initial_enabled)
_finish()
func _candidate_ids(candidates: Array[Dictionary]) -> Array[StringName]:
var ids: Array[StringName] = []
for candidate in candidates:
ids.append(StringName(candidate["target_id"]))
return ids
func _collect_resource_nodes(parent: Node) -> Array[ResourceNode]:
var resources: Array[ResourceNode] = []
for child in parent.get_children():
if child is ResourceNode:
resources.append(child)
else:
resources.append_array(_collect_resource_nodes(child))
return resources
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Jajce resource cluster passed: bounded density -> far stable-ID discovery")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)