feat: author discoverable forest edge

This commit is contained in:
Rijad Zuzo
2026-07-18 19:09:04 +02:00
parent 2719b0398b
commit 1f09ebe6b6
21 changed files with 911 additions and 180 deletions
+268
View File
@@ -0,0 +1,268 @@
extends SceneTree
const GROVE_TREE_01 := &"tree_forest_grove_01"
const GROVE_TREE_02 := &"tree_forest_grove_02"
const GROVE_IDS := [GROVE_TREE_01, GROVE_TREE_02]
const CLUSTER_PATH := "JajceWorld/WorldObjects/ResourceClusters/ForestEdgeResourceCluster"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
for _frame in 10:
await physics_frame
var manager = main_scene.get_node("SimulationManager")
manager.set_process(false)
var terrain := main_scene.get_node("JajceWorld/TerrainRoot/Terrain3D") as Terrain3D
var cluster := main_scene.get_node(CLUSTER_PATH) as ForestEdgeResourceCluster
_check(cluster != null, "Jajce should expose the authored forest-edge resource cluster")
if cluster == null:
_finish()
return
var stats := cluster.get_presentation_stats()
_check(
int(stats["understory_instance_count"]) == 24,
"Forest understory should keep its reviewed 24-instance budget"
)
_check(
int(stats["multimesh_batch_count"]) == 3,
"Forest understory should use exactly three intentional MultiMesh batches"
)
_check(
int(stats["decorative_tree_count"]) == 2,
"Forest cluster should reuse two non-authoritative silhouette trees"
)
_check(
int(stats["resource_anchor_count"]) == 2,
"Forest density should contain only the two existing finite tree anchors"
)
_check(
_collect_resource_nodes(cluster.get_node("DecorativeInstances")).is_empty(),
"Decorative forest foliage 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 anchor_height := terrain.data.get_height(anchor.global_position)
var interaction_position := anchor.interaction_point.global_position
var interaction_height := terrain.data.get_height(interaction_position)
_check(
absf(anchor.global_position.y - anchor_height) < 0.05,
"Forest anchor %s should snap to Terrain3D" % anchor.node_id
)
_check(
absf(interaction_position.y - interaction_height) < 0.1,
"Forest interaction %s should follow Terrain3D" % anchor.node_id
)
_check(
interaction_position.distance_to(Vector3.ZERO) > 24.0,
"Forest interaction %s should require expanded discovery" % anchor.node_id
)
var visual := anchor.get_node("Visual/TreePresentation") as HarvestableTreePresentation
_check(
(
visual != null
and visual.get_amount_tier() == ResourceAmountVisual.AmountTier.FULL
and visual.get_visible_canopy_count() == 4
),
"Forest anchor %s should reuse the full harvestable-tree visual" % anchor.node_id
)
_check(
anchor_ids.size() == 2 and anchor_ids.has(GROVE_TREE_01) and anchor_ids.has(GROVE_TREE_02),
"Forest cluster should preserve both existing stable tree IDs"
)
var resources := _collect_resource_nodes(main_scene.get_node("JajceWorld/WorldObjects"))
_check(resources.size() == 18, "Forest clustering should preserve all eighteen resources")
_check(
manager.resource_states.size() == 18,
"Simulation authority should still bind eighteen resource records"
)
var adapter := main_scene.get_node("ActiveWorldAdapter") as ActiveWorldAdapter
var farther_wood := adapter.get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_WOOD, Vector3.ZERO, 64.0, 24.0
)
var farther_ids := _candidate_ids(farther_wood)
_check(
farther_ids.has(GROVE_TREE_01) and farther_ids.has(GROVE_TREE_02),
"Both nested forest trees should occupy the farther discovery range"
)
await _check_navigation(main_scene, terrain, anchors)
_check_expanding_resolution(manager, resources)
_check_depletion_restore(manager, cluster)
_finish()
func _check_expanding_resolution(manager, resources: Array[ResourceNode]) -> void:
for resource in resources:
if resource.resource_id != SimulationIds.RESOURCE_WOOD:
continue
var state: ResourceStateRecord = manager.get_resource_state(resource.node_id)
state.set_enabled(resource.node_id == GROVE_TREE_01)
var npc: SimNPC = manager.npcs[0]
npc.set_task(SimulationIds.ACTION_GATHER_WOOD)
_check(
manager.resolve_npc_target(npc.id, Vector3.ZERO),
"Resolver should expand from the village to the authored forest edge"
)
_check(
npc.target_id == GROVE_TREE_01,
"Expanded discovery should return the forced nested forest tree by stable ID"
)
_check(
int(manager.target_resolver.last_resource_query_stats["range_pass_count"]) > 1,
"Forest discovery should expand beyond the initial 24 m range"
)
manager.release_npc_reservation(npc.id)
for resource in resources:
if resource.resource_id == SimulationIds.RESOURCE_WOOD:
manager.get_resource_state(resource.node_id).set_enabled(resource.initial_enabled)
func _check_depletion_restore(manager, cluster: ForestEdgeResourceCluster) -> void:
var tree := cluster.get_node("ResourceAnchors/Tree_Forest_Grove_01") as ResourceNode
var visual := tree.get_node("Visual/TreePresentation") as HarvestableTreePresentation
var state: ResourceStateRecord = manager.get_resource_state(GROVE_TREE_01)
state.set_amount_remaining(tree.yield_per_action)
_check(
(
visual.get_amount_tier() == ResourceAmountVisual.AmountTier.LOW
and visual.get_visible_canopy_count() == 2
),
"A nearly spent forest tree should derive the shared low crown from amount"
)
var low_snapshot: String = manager.serialize_state()
var npc: SimNPC = manager.npcs[0]
var inventory_before := npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
npc.target_id = GROVE_TREE_01
_check(
manager.reserve_resource(GROVE_TREE_01, npc.id),
"NPC should reserve the nested forest tree before gathering"
)
manager.call("_complete_resource_gather", npc, SimulationIds.ACTION_GATHER_WOOD)
_check(
(
state.is_depleted()
and is_equal_approx(
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
inventory_before + tree.yield_per_action
)
),
"Real NPC extraction should conserve the final forest-tree yield"
)
_check(
(
visual.get_amount_tier() == ResourceAmountVisual.AmountTier.DEPLETED
and visual.is_stump_visible()
and not visual.is_standing_visible()
),
"Real NPC depletion should leave the shared readable stump"
)
_check(
manager.restore_state_from_json(low_snapshot), "Low forest-tree authority should restore"
)
state = manager.get_resource_state(GROVE_TREE_01)
_check(
(
is_equal_approx(state.get_amount_remaining(), tree.yield_per_action)
and visual.get_amount_tier() == ResourceAmountVisual.AmountTier.LOW
and visual.is_standing_visible()
and not visual.is_stump_visible()
and visual.get_visible_canopy_count() == 2
),
"Restore should rebuild the sparse standing tree from authoritative amount alone"
)
func _check_navigation(main_scene: Node, terrain: Terrain3D, anchors: Array[ResourceNode]) -> void:
var navigation_map: RID = main_scene.get_world_3d().navigation_map
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
for anchor in anchors:
var destination := anchor.interaction_point.global_position
var path := NavigationServer3D.map_get_path(navigation_map, Vector3.ZERO, destination, true)
_check(
not path.is_empty(), "Navigation should reach forest interaction %s" % anchor.node_id
)
_check_path_tracks_terrain(terrain, path, anchor.node_id)
if not path.is_empty():
var final_point := path[path.size() - 1]
_check(
(
Vector2(final_point.x, final_point.z).distance_to(
Vector2(destination.x, destination.z)
)
<= 1.5
),
"Navigation should finish beside forest interaction %s" % anchor.node_id
)
func _wait_for_navigation_map(navigation_map: RID) -> void:
for _attempt in 30:
if (
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
):
await physics_frame
return
await physics_frame
_check(false, "Forest-edge navigation map did not synchronize within 30 physics frames")
func _check_path_tracks_terrain(
terrain: Terrain3D, path: PackedVector3Array, node_id: StringName
) -> void:
for point in path:
var terrain_height := terrain.data.get_height(point)
if is_nan(terrain_height) or absf(point.y - terrain_height) > 0.85:
_check(false, "Forest route to %s should track Terrain3D height" % node_id)
return
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 forest edge passed: bounded grove -> far discovery -> restore")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)