Files
gamedev-the-steward/tests/jajce_runtime_integration_test.gd
T
2026-08-12 10:02:45 +02:00

757 lines
28 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
var day_night_cycle := main_scene.get_node("JajceWorld/DayNightCycle")
day_night_cycle.call("_process", 0.11)
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 runtime_player := main_scene.get_node("Player") as Node3D
_check(
runtime_player.is_in_group("grass_interactors"),
"The real player transform should drive nearby grass response"
)
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 all_npc_visuals_drive_grass := true
for npc_visual in world_view.active_npc_visuals.values():
all_npc_visuals_drive_grass = (
all_npc_visuals_drive_grass and (npc_visual as Node3D).is_in_group("grass_interactors")
)
_check(
all_npc_visuals_drive_grass,
"Every active villager visual should drive local grass response"
)
var grass_material := (
main_scene.get_node("JajceWorld/TerrainRoot/Terrain3D/CozyGrassField").get(
"mesh_material_override"
)
as ShaderMaterial
)
var grass_interactor_count := int(grass_material.get_shader_parameter("interactor_count"))
_check(
grass_interactor_count in range(1, 9),
"Grass shader should receive a bounded set of real nearby actor transforms"
)
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")
var village_whisper := (
main_scene.get_node("VillageWhisperLayer/VillageWhisper") as VillageWhisperHud
)
var whisper_kicker := village_whisper.get_node("Copy/Kicker") as Label
var whisper_message := village_whisper.get_node("Copy/Message") as Label
_check(
not village_whisper.visible,
"The player-facing village whisper should begin quiet instead of replaying history"
)
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"
)
var field_note := (
main_scene.get_node("VillagerInspectionLayer/VillagerFieldNote") as VillagerFieldNoteHud
)
contributor_visual.global_position = Vector3(-30.0, 0.0, -30.0)
var note_player := main_scene.get_node("Player") as Node3D
note_player.global_position = contributor_visual.global_position + Vector3(1.0, 0.0, 0.0)
var note_context: VillagerInspectionResult = note_player.get_nearby_villager_inspection()
_check(
(
note_context != null
and note_context.npc_id == contributor.id
and note_context.has_open_need
and note_context.need_type == SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY
and note_context.need_progress == "Food 0 / 1"
and not note_context.need_response.strip_edges().is_empty()
),
"Field note should derive the inspected villager's exact open need and response"
)
field_note.refresh_note(true)
var need_label := field_note.get_node("Copy/Need") as Label
_check(
field_note.visible and need_label.visible and "Food 0 / 1" in need_label.text,
"Field note HUD should render the surfaced need line for its interested villager"
)
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-v11 restore"
)
await process_frame
contributor = simulation_manager.npcs[contributor.id]
witness = simulation_manager.npcs[witness.id]
listener = simulation_manager.npcs[listener.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
and not village_whisper.visible
),
"Restore should rebuild stable cues without replaying refill or player-HUD feedback"
)
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")
_check(
(
simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
>= 1.0
),
"Player gathering should first carry the finite yield"
)
var player_pantry := (
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
)
player.global_position = player_pantry.get_interaction_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
and whisper_kicker.text == "NEED MET"
and "The player restocked the pantry" in whisper_message.text
),
"Player gathering should resolve the restored need in debug detail and transient HUD"
)
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)
var woodpile_node := (
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode
)
contributor.position = woodpile_node.get_interaction_position()
for npc in simulation_manager.npcs:
if npc.id != contributor.id:
npc.position = contributor.position + Vector3(20.0 + npc.id * 2.0, 0.0, 0.0)
witness.profession = SimulationIds.PROFESSION_WOODCUTTER
contributor.set_task(SimulationIds.ACTION_STUDY, 1.0)
contributor.target_id = SimulationIds.STORAGE_VILLAGE_WOODPILE
contributor.travel_target_position = contributor.position
contributor.start_working()
simulation_manager.simulate_tick()
village_ui.selected_npc_index = contributor.id
village_ui.call("_refresh_npc_inspector")
var runtime_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
var runtime_trigger_id := (
runtime_opportunity.get_trigger_event_id() if runtime_opportunity != null else -1
)
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
var runtime_player_response: OpportunityPlayerResponseResult = (
simulation_manager.get_active_opportunity_player_response()
)
_check(
(
runtime_opportunity != null
and runtime_helper == null
and runtime_player_response != null
and runtime_player_response.action_id == SimulationIds.ACTION_GATHER_WOOD
and runtime_player_response.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE
and runtime_player_response.available_source_count > 0
and not simulation_manager.npc_knows_event(witness.id, runtime_trigger_id)
and whisper_kicker.text == "YOU CAN HELP"
and (
"Harvest a tree; wood goes straight to the village woodpile."
in whisper_message.text
)
),
"An unassisted blocked-work need should surface its real player harvest route"
)
_check(
(
"◆ Supply wood for blocked work" in village_stats_label.text
and "could not finish Study" in village_stats_label.text
and (
"Possible helper: none informed, trusted, and able to supply"
in village_stats_label.text
)
and "Player route: Gather Wood → Village Woodpile" in village_stats_label.text
and "◆ Find wood for Study" in inspector_label.text
and not contributor_visual.get_node("OpportunityConcernRoot").visible
),
"The missing-wood UI should honestly show that no capable informed helper exists yet"
)
contributor.set_task(SimulationIds.ACTION_PATROL)
contributor.target_id = guard_site.site_id
contributor.position = guard_site.get_interaction_position()
contributor.start_working()
witness.set_task(SimulationIds.ACTION_PATROL)
witness.target_id = guard_site.site_id
witness.position = contributor.position + Vector3(1.0, 0.0, 0.0)
simulation_manager.notify_npc_arrived(witness.id)
var communicated_trigger: KnownEventStateRecord = simulation_manager.get_known_event_record(
witness.id, runtime_trigger_id
)
runtime_helper = simulation_manager.get_active_opportunity_helper()
runtime_player_response = simulation_manager.get_active_opportunity_player_response()
_check(
(
communicated_trigger != null
and (
communicated_trigger.get_acquisition_method()
== SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
)
and communicated_trigger.get_source_npc_id() == contributor.id
and runtime_helper != null
and runtime_player_response == null
and runtime_helper.helper_npc_id == witness.id
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
and whisper_kicker.text == "NEWS TRAVELS"
and contributor.npc_name in whisper_message.text
and witness.npc_name in whisper_message.text
),
"A real shared activity should report the exact need and reveal the trusted helper"
)
var runtime_helper_reason := (
"Knows the need; trust %.2f toward %s" % [runtime_helper.trust, contributor.npc_name]
if runtime_helper != null
else "missing helper"
)
_check(
(
"◆ Supply wood for blocked work" in village_stats_label.text
and "could not finish Study" in village_stats_label.text
and "Possible helper: %s — Gather Wood" % witness.npc_name in village_stats_label.text
and runtime_helper_reason in village_stats_label.text
and "◆ Find wood for Study" in inspector_label.text
and not contributor_visual.get_node("OpportunityConcernRoot").visible
),
"The compact summary should update from the newly communicated simulation fact"
)
witness.current_task = SimulationIds.ACTION_IDLE
witness.task_state = SimNPC.TASK_STATE_IDLE
witness.task_complete = true
witness.target_id = &""
witness.has_travel_target = false
witness.hunger = 20.0
witness.energy = 80.0
witness.mourning_ticks = 0
simulation_manager.simulate_tick()
var runtime_decision: ActionSelectionResult = simulation_manager.get_latest_decision(witness.id)
var helper_resource: ResourceStateRecord = simulation_manager.get_resource_state(
witness.target_id
)
_check(
(
runtime_decision != null
and runtime_decision.action_id == SimulationIds.ACTION_GATHER_WOOD
and "Responding to village need" in runtime_decision.reason
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
and helper_resource != null
and helper_resource.get_reserved_by() == witness.id
and whisper_kicker.text == "HELP IS ON THE WAY"
and witness.npc_name in whisper_message.text
and "gather wood" in whisper_message.text
),
"The informed helper and cozy HUD should reflect ordinary target resolution and reservation"
)
var demo_controller := main_scene.get_node("DemoController")
demo_controller.set_debug_overlay_visible(false)
_check(
not village_ui.visible and village_whisper.visible,
"The transient player HUD should remain legible when development overlays are hidden"
)
demo_controller.set_debug_overlay_visible(true)
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
and whisper_kicker.text == "NEED MET"
and "supplied the woodpile" in whisper_message.text
),
"Resolution should name its supplier in both debug detail and the transient player HUD"
)
_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 runtime_resources := _collect_resource_nodes(main_scene.get_node("JajceWorld/WorldObjects"))
_check(
runtime_resources.size() == 18,
"Runtime should contain eighteen Jajce ResourceNodes across authored clusters"
)
_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 runtime_resources:
destinations.append(resource.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 _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_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]
)