feat: author discoverable forest edge
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqt2i8xhxa20l
|
||||
Reference in New Issue
Block a user