Files
gamedev-the-steward/tests/jajce_runtime_integration_test.gd
admin c61d286005 feat: wood inventory, carrying, and woodpile storage
Wood now follows the same gather-carry-deposit pattern as food. NPCs put harvested wood into inventory, walk to the VillageWoodpile StorageNode, and deposit it. Village wood syncs from storage. Player harvesting also routes wood to the woodpile. ActiveWorldAdapter exposes woodpile routing for DEPOSIT_WOOD action targeting. WorldViewManager recognizes wood inventory changes. Tests verify woodpile node existence, storage ID, navigation reachability, and adapter routing.
2026-07-08 18:55:51 +02:00

225 lines
8.4 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)
_check(simulation_manager.npcs.size() == 6, "Baseline should create six NPCs")
_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(
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]
)