547 lines
20 KiB
GDScript
547 lines
20 KiB
GDScript
extends SceneTree
|
|
|
|
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 simulation_manager: Node = main_scene.get_node("SimulationManager")
|
|
simulation_manager.set_process(false)
|
|
var saved_clock_ticks: int = simulation_manager.clock.elapsed_ticks
|
|
simulation_manager.clock.elapsed_ticks = 0
|
|
await process_frame
|
|
await process_frame
|
|
var environment: Environment = (
|
|
(main_scene.get_node("JajceWorld/WorldEnvironment") as WorldEnvironment).environment
|
|
)
|
|
_check(
|
|
is_equal_approx(environment.ambient_light_energy, 0.25),
|
|
"Day/night presentation should follow the authoritative simulation clock"
|
|
)
|
|
simulation_manager.clock.elapsed_ticks = saved_clock_ticks
|
|
await process_frame
|
|
|
|
_check(simulation_manager.npcs.size() == 6, "Baseline should create six NPCs")
|
|
var inspector_label := (
|
|
main_scene.get_node("UI/NpcInspectorPanel/MarginContainer/NpcInspectorLabel") as Label
|
|
)
|
|
_check(
|
|
(
|
|
"Relationship:" in inspector_label.text
|
|
and "familiar 50%" in inspector_label.text
|
|
and "trust 50%" in inspector_label.text
|
|
),
|
|
"Runtime inspector should render authoritative directed relationship state"
|
|
)
|
|
var contributor: SimNPC = simulation_manager.npcs[0]
|
|
var witness: SimNPC = simulation_manager.npcs[1]
|
|
var listener: SimNPC = simulation_manager.npcs[2]
|
|
for npc in simulation_manager.npcs:
|
|
npc.position = Vector3(20.0 + npc.id * 2.0, 0.0, 0.0)
|
|
contributor.position = Vector3.ZERO
|
|
witness.position = Vector3(4.0, 0.0, 0.0)
|
|
witness.hunger = 85.0
|
|
listener.hunger = 20.0
|
|
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
|
_check(
|
|
is_equal_approx(
|
|
simulation_manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD),
|
|
1.0
|
|
),
|
|
"Runtime knowledge UI check should use a completed simulation transaction"
|
|
)
|
|
var world_view := main_scene.get_node("WorldViewManager")
|
|
var contributor_visual: Node3D = world_view.active_npc_visuals[contributor.id]
|
|
var witness_visual: Node3D = world_view.active_npc_visuals[witness.id]
|
|
var listener_visual: Node3D = world_view.active_npc_visuals[listener.id]
|
|
_check(
|
|
(
|
|
witness_visual.get_node("TrustReactionRoot").visible
|
|
and not contributor_visual.get_node("TrustReactionRoot").visible
|
|
),
|
|
"A real trust consequence should blossom only above the reacting observer"
|
|
)
|
|
var reaction_checksum: String = simulation_manager.get_state_checksum()
|
|
witness_visual.play_trust_gain_reaction()
|
|
_check(
|
|
simulation_manager.get_state_checksum() == reaction_checksum,
|
|
"Restarting the transient trust blossom must not mutate simulation state"
|
|
)
|
|
var village_ui := main_scene.get_node("UI")
|
|
village_ui.selected_npc_index = witness.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"Memories 1 lasting · 0 recent" in inspector_label.text
|
|
and (
|
|
"◆ lasting · %s stocked 1 food · witnessed" % contributor.npc_name
|
|
in inspector_label.text
|
|
)
|
|
and "Relationship: %s" % contributor.npc_name in inspector_label.text
|
|
and "Because:" in inspector_label.text
|
|
and "Own history" in inspector_label.text
|
|
),
|
|
"NPC history should show the lasting witnessed fact and its relationship consequence"
|
|
)
|
|
var guard_site := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
|
|
)
|
|
witness.set_task(SimulationIds.ACTION_PATROL)
|
|
witness.target_id = guard_site.site_id
|
|
witness.position = guard_site.get_interaction_position()
|
|
witness.start_working()
|
|
listener.set_task(SimulationIds.ACTION_PATROL)
|
|
listener.target_id = guard_site.site_id
|
|
listener.position = witness.position + Vector3(1.0, 0.0, 0.0)
|
|
simulation_manager.notify_npc_arrived(listener.id)
|
|
village_ui.selected_npc_index = listener.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"Memories 0 lasting · 1 recent" in inspector_label.text
|
|
and (
|
|
"• recent · %s stocked 1 food · heard %s" % [contributor.npc_name, witness.npc_name]
|
|
in inspector_label.text
|
|
)
|
|
),
|
|
"NPC history should identify who communicated a retained fact"
|
|
)
|
|
_check(
|
|
not listener_visual.get_node("TrustReactionRoot").visible,
|
|
"Learning a fact without a relationship consequence should not show a trust blossom"
|
|
)
|
|
|
|
var village_stats_label := (
|
|
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
|
)
|
|
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
|
|
var pantry_visual := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
|
as PantryStockVisual
|
|
)
|
|
_check(
|
|
(
|
|
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.STOCKED
|
|
and pantry_visual.get_node("LowStock").visible
|
|
and pantry_visual.get_node("FullStock").visible
|
|
and not pantry_visual.get_node("EmptyStock").visible
|
|
),
|
|
"A stocked authoritative pantry should show its physical sacks and berry baskets"
|
|
)
|
|
contributor.hunger = 85.0
|
|
var withdrawn_for_need: float = simulation_manager.economy.withdraw_to_inventory(
|
|
contributor,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
)
|
|
_check(withdrawn_for_need > 0.0, "Runtime opportunity setup should empty the real pantry")
|
|
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
|
_check(opportunity != null, "Emptying the pantry should open one known shortage need")
|
|
if pantry_visual.transition_tween != null:
|
|
await pantry_visual.transition_tween.finished
|
|
_check(
|
|
(
|
|
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
|
and pantry_visual.get_node("EmptyStock").visible
|
|
and not pantry_visual.get_node("LowStock").visible
|
|
and contributor_visual.get_node("OpportunityConcernRoot").visible
|
|
and not listener_visual.get_node("OpportunityConcernRoot").visible
|
|
),
|
|
"The real shortage should empty the pantry prop and concern only its interested villager"
|
|
)
|
|
_check(
|
|
(
|
|
"Village need" in village_stats_label.text
|
|
and "◆ Restock the empty pantry" in village_stats_label.text
|
|
and (
|
|
"%s is worried about the shortage" % contributor.npc_name
|
|
in village_stats_label.text
|
|
)
|
|
and "Food 0 / 1" in village_stats_label.text
|
|
),
|
|
"An open opportunity should add one compact state-derived village need"
|
|
)
|
|
village_ui.selected_npc_index = contributor.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"Open village need" in inspector_label.text
|
|
and "◆ Restock the empty pantry" in inspector_label.text
|
|
and "Food 0 / 1" in inspector_label.text
|
|
),
|
|
"The interested villager should expose the open pantry need in their inspector"
|
|
)
|
|
village_ui.selected_npc_index = listener.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
"Open village need" not in inspector_label.text,
|
|
"An unrelated selected villager should not inherit someone else's open need"
|
|
)
|
|
|
|
_check(
|
|
(
|
|
simulation_manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD)
|
|
> 0.0
|
|
),
|
|
"Opportunity presentation should resolve from a real pantry deposit"
|
|
)
|
|
_check(
|
|
(
|
|
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.STOCKED
|
|
and pantry_visual.get_node("RefillResponse").visible
|
|
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
|
),
|
|
"Real supply should restore visible stock, play one refill response, and clear concern"
|
|
)
|
|
_check(
|
|
"Village need" not in village_stats_label.text,
|
|
"Resolved opportunities should leave the compact village summary"
|
|
)
|
|
village_ui.selected_npc_index = contributor.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"Resolved village need" in inspector_label.text
|
|
and "◆ %s restocked the pantry" % contributor.npc_name in inspector_label.text
|
|
and "Food target 1" in inspector_label.text
|
|
),
|
|
"The interested villager should retain a resolved detail naming the real actor"
|
|
)
|
|
village_ui.selected_npc_index = listener.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
"Resolved village need" not in inspector_label.text,
|
|
"Resolved opportunity detail should remain scoped to its interested villager"
|
|
)
|
|
|
|
var player_need_withdrawal: float = simulation_manager.economy.withdraw_to_inventory(
|
|
contributor,
|
|
SimulationIds.RESOURCE_FOOD,
|
|
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
)
|
|
_check(player_need_withdrawal > 0.0, "Player resolution setup should reopen a real need")
|
|
var player_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
|
_check(player_opportunity != null, "A later shortage should open a new opportunity record")
|
|
var active_need_json: String = simulation_manager.serialize_state()
|
|
var old_contributor_visual := contributor_visual
|
|
_check(
|
|
simulation_manager.restore_state_from_json(active_need_json),
|
|
"The visible shortage should survive a valid schema-v9 restore"
|
|
)
|
|
await process_frame
|
|
contributor = simulation_manager.npcs[contributor.id]
|
|
contributor_visual = world_view.active_npc_visuals[contributor.id]
|
|
player_opportunity = simulation_manager.get_active_opportunity()
|
|
_check(
|
|
(
|
|
contributor_visual != old_contributor_visual
|
|
and contributor_visual.get_node("OpportunityConcernRoot").visible
|
|
and pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
|
and pantry_visual.get_node("EmptyStock").visible
|
|
and not pantry_visual.get_node("RefillResponse").visible
|
|
),
|
|
"Restoration should rebuild concern and empty stock without replaying the refill response"
|
|
)
|
|
var player := main_scene.get_node("Player") as Node3D
|
|
var opportunity_bush := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
|
)
|
|
var opportunity_bush_state: ResourceStateRecord = simulation_manager.get_resource_state(
|
|
opportunity_bush.node_id
|
|
)
|
|
opportunity_bush_state.set_amount_remaining(1.0)
|
|
player.global_position = opportunity_bush.interaction_point.global_position
|
|
player.call("try_interact")
|
|
village_ui.selected_npc_index = contributor.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
|
and "Resolved village need" in inspector_label.text
|
|
and "◆ Player restocked the pantry" in inspector_label.text
|
|
),
|
|
"Player gathering should resolve the same need and be named from its real event"
|
|
)
|
|
|
|
var woodpile_state: StorageStateRecord = simulation_manager.get_woodpile()
|
|
woodpile_state.withdraw(
|
|
SimulationIds.RESOURCE_WOOD, woodpile_state.get_amount(SimulationIds.RESOURCE_WOOD)
|
|
)
|
|
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD)
|
|
contributor.set_task(SimulationIds.ACTION_STUDY, 1.0)
|
|
contributor.start_working()
|
|
simulation_manager.simulate_tick()
|
|
village_ui.selected_npc_index = contributor.id
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"◆ Supply wood for blocked work" in village_stats_label.text
|
|
and "could not finish Study" in village_stats_label.text
|
|
and "◆ Find wood for Study" in inspector_label.text
|
|
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
|
),
|
|
"A real missing-wood fact should surface through wood-specific UI without a food cue"
|
|
)
|
|
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
|
|
_check(
|
|
is_equal_approx(
|
|
simulation_manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_WOOD),
|
|
1.0
|
|
),
|
|
"Runtime opportunity presentation should resolve from a real wood deposit"
|
|
)
|
|
village_ui.call("_refresh_npc_inspector")
|
|
_check(
|
|
(
|
|
"Village need" not in village_stats_label.text
|
|
and "◆ %s supplied the woodpile" % contributor.npc_name in inspector_label.text
|
|
),
|
|
"The resolved wood need should name its exact supplier and leave the active summary"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
|
"Playable runtime should instance the Jajce Terrain3D world"
|
|
)
|
|
var terrain: Terrain3D = main_scene.get_node("JajceWorld/TerrainRoot/Terrain3D")
|
|
_check(
|
|
terrain.get_camera() == main_scene.get_node("CameraRig/Camera3D"),
|
|
"Runtime Terrain3D should bind to the gameplay camera"
|
|
)
|
|
var camera_rig := main_scene.get_node("CameraRig")
|
|
_check(
|
|
camera_rig.has_method("apply_presentation_preset"),
|
|
"Runtime camera should expose a presentation staging preset"
|
|
)
|
|
var presentation_focus := Vector3(-2.0, 1.2, -0.5)
|
|
camera_rig.apply_presentation_preset(presentation_focus, 128.0, 0.95)
|
|
_check(
|
|
camera_rig.global_position.distance_to(presentation_focus) > 10.0,
|
|
"Presentation preset should pull the camera back from the village focus"
|
|
)
|
|
_check(terrain.collision_mode != 0, "Runtime Terrain3D collision should be enabled")
|
|
_check(
|
|
(
|
|
main_scene.has_node("Player/Visual/Body")
|
|
and main_scene.has_node("Player/Visual/Head")
|
|
and main_scene.has_node("Player/Visual/Scarf")
|
|
),
|
|
"Player should use the same readable multi-part visual language as villagers"
|
|
)
|
|
_check(
|
|
(
|
|
not main_scene.has_node("Player/MeshInstance3D")
|
|
and not main_scene.has_node("Player/FaceMarker")
|
|
),
|
|
"Runtime should not retain the placeholder player capsule presentation"
|
|
)
|
|
_check(
|
|
not main_scene.has_node("JajceWorld/NavigationRegion3D/GreyboxGround"),
|
|
"Runtime should not keep the temporary greybox navigation ground"
|
|
)
|
|
_check(
|
|
(
|
|
not main_scene.has_node("World")
|
|
and not main_scene.has_node("ResourceNodes")
|
|
and not main_scene.has_node("ActivityMarkers")
|
|
and not main_scene.has_node("JajceWorld/LegacyActivityMarkers")
|
|
),
|
|
"Runtime should not retain duplicate flat-world objects"
|
|
)
|
|
var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes")
|
|
_check(
|
|
resource_root.get_child_count() == 18, "Runtime should contain eighteen Jajce ResourceNodes"
|
|
)
|
|
_check(
|
|
simulation_manager.resource_states.size() == 18,
|
|
"Simulation authority should bind all eighteen Jajce resources"
|
|
)
|
|
var village_root := main_scene.get_node("JajceWorld/VillageRoot")
|
|
var path_strips := 0
|
|
for child in village_root.get_children():
|
|
if child.name.begins_with("Path_"):
|
|
path_strips += 1
|
|
_check(
|
|
path_strips >= 4, "Runtime should include authored path strips for first-read composition"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/FortressBlockout/Keep/RidgeBanner"),
|
|
"Runtime should include a readable ridge landmark banner"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantrySign"),
|
|
"Pantry storage should have a readable work-site silhouette"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/GuardPost/GuardPennant"),
|
|
"Guard site should have a readable work-site silhouette"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/StudyDesk/OpenBook_A"),
|
|
"Study site should have readable study props"
|
|
)
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/RestBench/RestCanopy"),
|
|
"Rest site should have a readable rest silhouette"
|
|
)
|
|
|
|
var navigation_map: RID = main_scene.get_world_3d().navigation_map
|
|
var navigation_region := (
|
|
main_scene.get_node("JajceWorld/NavigationRegion3D") as NavigationRegion3D
|
|
)
|
|
_check(
|
|
(
|
|
navigation_region.navigation_mesh.resource_path
|
|
== "res://world/jajce/JajceNavigationMesh.tres"
|
|
),
|
|
"Runtime navigation should use the Terrain3D-derived baked mesh resource"
|
|
)
|
|
await _wait_for_navigation_map(navigation_map)
|
|
NavigationServer3D.map_force_update(navigation_map)
|
|
|
|
var fixed_origins := [Vector3(-6.0, 0.0, -4.0), Vector3(0.0, 0.0, 0.0), Vector3(6.0, 0.0, 4.0)]
|
|
var destinations: Array[Vector3] = []
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillagePantry"),
|
|
"Runtime should expose a typed VillagePantry StorageNode"
|
|
)
|
|
_check(
|
|
not main_scene.has_node("JajceWorld/LegacyActivityMarkers/PantryMarker"),
|
|
"Runtime should not keep the pantry as a legacy activity marker"
|
|
)
|
|
var pantry := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
|
)
|
|
destinations.append(pantry.get_interaction_position())
|
|
_check(
|
|
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile"),
|
|
"Runtime should expose a typed VillageWoodpile StorageNode"
|
|
)
|
|
var woodpile := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
|
)
|
|
destinations.append(woodpile.get_interaction_position())
|
|
var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites")
|
|
_check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites")
|
|
for site in activity_root.get_children():
|
|
destinations.append((site as ActivitySite).get_interaction_position())
|
|
|
|
for resource in resource_root.get_children():
|
|
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
|
|
|
for origin in fixed_origins:
|
|
for destination in destinations:
|
|
var path := NavigationServer3D.map_get_path(navigation_map, origin, destination, true)
|
|
_check(
|
|
not path.is_empty(),
|
|
"Navigation path should exist from %s to %s" % [origin, destination]
|
|
)
|
|
_check_path_tracks_terrain(
|
|
terrain, path, "Runtime navigation path should track Terrain3D height"
|
|
)
|
|
_check_path_reaches_destination(
|
|
path, destination, "Runtime navigation path should reach its requested target"
|
|
)
|
|
|
|
var village := SimVillage.new()
|
|
var worker := SimNPC.new(100, "BaselineWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0)
|
|
worker.set_task(SimulationIds.ACTION_STUDY, 2.0)
|
|
worker.start_working()
|
|
var executor := ActionExecutionSystem.new()
|
|
executor.advance_npc(worker, village)
|
|
executor.advance_npc(worker, village)
|
|
_check(worker.task_complete, "A two-tick working task should complete")
|
|
|
|
var starving := SimNPC.new(101, "BaselineStarving", SimulationIds.PROFESSION_WANDERER, 5.0, 5.0)
|
|
starving.hunger = 100.0
|
|
starving.starvation_death_threshold = 1
|
|
executor.advance_npc(starving, village)
|
|
_check(starving.is_dead, "Starvation threshold should still kill an NPC")
|
|
|
|
var adapter := main_scene.get_node("ActiveWorldAdapter") as ActiveWorldAdapter
|
|
var storage_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_FOOD)
|
|
_check(
|
|
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
|
|
"Runtime adapter should route storage actions to village_pantry"
|
|
)
|
|
var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD)
|
|
_check(
|
|
wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE),
|
|
"Runtime adapter should route wood deposit to village_woodpile"
|
|
)
|
|
var study_target := adapter.get_activity_target(SimulationIds.ACTION_STUDY)
|
|
_check(
|
|
study_target.get("target_id", "") == "study_desk",
|
|
"Runtime adapter should route study to the typed study desk"
|
|
)
|
|
|
|
if failures.is_empty():
|
|
print("[TEST] Jajce runtime passed: 6 NPCs, 18 resources, typed sites")
|
|
quit(0)
|
|
return
|
|
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|
|
|
|
|
|
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, "Navigation map did not synchronize within 30 physics frames")
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _check_path_tracks_terrain(
|
|
terrain: Terrain3D, path: PackedVector3Array, message: String
|
|
) -> void:
|
|
if path.is_empty():
|
|
return
|
|
|
|
for point in path:
|
|
var terrain_height: float = terrain.data.get_height(point)
|
|
if is_nan(terrain_height):
|
|
failures.append("%s: missing terrain height at %s" % [message, point])
|
|
return
|
|
if absf(point.y - terrain_height) > 0.85:
|
|
failures.append(
|
|
(
|
|
"%s: path point %s is too far from terrain height %.2f"
|
|
% [message, point, terrain_height]
|
|
)
|
|
)
|
|
return
|
|
|
|
|
|
func _check_path_reaches_destination(
|
|
path: PackedVector3Array, destination: Vector3, message: String
|
|
) -> void:
|
|
if path.is_empty():
|
|
return
|
|
|
|
var final_point := path[path.size() - 1]
|
|
var final_xz := Vector2(final_point.x, final_point.z)
|
|
var destination_xz := Vector2(destination.x, destination.z)
|
|
if final_xz.distance_to(destination_xz) > 1.5:
|
|
failures.append(
|
|
"%s: final path point %s is too far from %s" % [message, final_point, destination]
|
|
)
|