c61d286005
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.
225 lines
8.4 KiB
GDScript
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]
|
|
)
|