feat: author resource-aware riverbank grove
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user