Files
gamedev-the-steward/tests/jajce_world_scaffold_test.gd
T

333 lines
12 KiB
GDScript

extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var camera := Camera3D.new()
camera.current = true
root.add_child(camera)
var configured_assets: Terrain3DAssets = load("res://terrain/jajce/assets.tres")
var configured_texture_count := configured_assets.get_texture_count()
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
root.add_child(world)
await process_frame
for frame in 10:
await physics_frame
var terrain = world.get_node("TerrainRoot/Terrain3D")
_check(terrain is Terrain3D, "JajceWorld should own a Terrain3D node")
_check(
terrain.data_directory == "res://terrain/jajce/data",
"Terrain3D should use dedicated Jajce data"
)
_check(terrain.collision_mode != 0, "Terrain3D collision should be enabled for gameplay")
var greybox_ground := world.get_node("NavigationRegion3D/GreyboxGround") as StaticBody3D
_check(
greybox_ground.collision_layer == 0 and greybox_ground.collision_mask == 0,
"GreyboxGround should not provide runtime physics collision"
)
_check(
configured_texture_count == 6,
(
"Jajce terrain should define the bounded six-material palette (found %s)"
% configured_texture_count
)
)
var texture_contract_valid := true
for texture_index in configured_texture_count:
var texture_asset: Terrain3DTextureAsset = configured_assets.get_texture(texture_index)
texture_contract_valid = (
texture_contract_valid
and texture_asset.albedo_texture != null
and texture_asset.normal_texture != null
and texture_asset.albedo_texture.get_image().has_mipmaps()
and texture_asset.normal_texture.get_image().has_mipmaps()
)
_check(
texture_contract_valid,
"All Terrain3D texture assets should provide matching mipmapped maps"
)
_check(
absf(terrain.data.get_height(Vector3.ZERO)) < 0.1,
"Central gameplay area should remain level for current navigation"
)
_check(
terrain.data.get_height(Vector3(-72, 0, 18)) > 10.0,
"Authored terrain should raise the western fortress ridge"
)
_check(world.has_node("WaterRoot"), "JajceWorld should expose WaterRoot")
_check(world.has_node("VillageRoot"), "JajceWorld should expose VillageRoot")
_check(world.has_node("FoliageRoot"), "JajceWorld should expose FoliageRoot")
_check(
world.get_node("FoliageRoot").get_child_count() >= 10,
"JajceWorld should include restrained authored foliage clusters"
)
_check(world.has_node("FortressBlockout"), "JajceWorld should include the fortress landmark")
_check(world.has_node("WaterRoot/WaterfallBody"), "WaterRoot should include a waterfall drop")
_check(
world.has_node("WaterRoot/WaterfallMist"),
"WaterRoot should include waterfall mist particles"
)
var waterfall_mist: GPUParticles3D = world.get_node("WaterRoot/WaterfallMist")
_check(
waterfall_mist.draw_pass_1 != null,
"Waterfall mist particles should have visible billboard geometry"
)
_check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface")
var environment: Environment = (
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
)
_check(
environment.fog_enabled and environment.sky != null,
"Jajce environment should provide sky and restrained valley fog"
)
_check(
environment.glow_enabled
and environment.volumetric_fog_enabled
and environment.adjustment_enabled,
"WorldEnvironment should provide bounded glow, depth haze, and color grading"
)
_check(
world.has_node("VillageRoot/Mill") and world.has_node("VillageRoot/Bridge"),
"VillageRoot should include the mill and bridge blockouts"
)
var smoke_count := 0
for suffix in ["House_01", "House_02", "House_03"]:
if world.has_node("VillageRoot/%s/Smoke" % suffix):
var smoke: GPUParticles3D = world.get_node("VillageRoot/%s/Smoke" % suffix)
if smoke.draw_pass_1 == null:
continue
smoke_count += 1
_check(
smoke_count >= 2,
"At least two houses should have chimney smoke particles (found %s)" % smoke_count
)
var lookdev: Node3D = load("res://world/jajce/JajceLookdev.tscn").instantiate()
_check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld")
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
lookdev.free()
var resources := world.get_node("WorldObjects/ResourceNodes").get_children()
var ids: Array[String] = []
for resource in resources:
ids.append(String((resource as ResourceNode).node_id))
ids.sort()
_check(
ids
== [
"animal_camp_01",
"animal_camp_river_01",
"berry_bush_01",
"berry_bush_02",
"berry_patch_river_01",
"berry_patch_south_01",
"tree_01",
"tree_02",
"tree_north_outskirts_01",
"tree_river_resource_01",
"tree_south_edge_01",
"wood_pile_village_01"
],
"Jajce resources should preserve all stable and discoverable IDs"
)
var metadata_valid := true
var food_count := 0
var wood_count := 0
var animal_context_count := 0
var berry_context_count := 0
var village_context_count := 0
var outskirt_context_count := 0
for resource in resources:
var node := resource as ResourceNode
var id := String(node.node_id)
if node.resource_id == SimulationIds.RESOURCE_FOOD:
food_count += 1
if node.resource_id == SimulationIds.RESOURCE_WOOD:
wood_count += 1
if id.begins_with("animal_camp"):
animal_context_count += 1
if id.begins_with("berry_"):
berry_context_count += 1
if id.contains("_village_"):
village_context_count += 1
if id.contains("_outskirts_") or id.contains("_edge_") or id.contains("_river_"):
outskirt_context_count += 1
metadata_valid = (
metadata_valid
and node.comfort_distance > 0.0
and node.safety_risk >= 0.0
and node.safety_risk <= 1.0
)
_check(metadata_valid, "Jajce resources should define discovery metadata")
_check(food_count >= 6, "Jajce should expose at least six finite food resources")
_check(wood_count >= 6, "Jajce should expose at least six finite wood resources")
_check(animal_context_count >= 2, "Jajce food discovery should include animal-camp contexts")
_check(berry_context_count >= 4, "Jajce food discovery should include berry contexts")
_check(village_context_count >= 1, "Jajce discovery should include at least one village stockpile")
_check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts")
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
_check(
pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY,
"VillagePantry should preserve the stable storage ID"
)
_check(
not world.has_node("LegacyActivityMarkers/PantryMarker"),
"Pantry should no longer be a legacy activity marker"
)
_check(
not world.has_node("LegacyActivityMarkers"),
"JajceWorld should no longer expose legacy activity markers"
)
var activity_root := world.get_node("WorldObjects/ActivitySites")
var activity_ids: Array[String] = []
for site in activity_root.get_children():
activity_ids.append(String((site as ActivitySite).site_id))
activity_ids.sort()
_check(
activity_ids == ["guard_post", "rest_bench", "study_desk"],
"Jajce activity sites should preserve stable semantic IDs"
)
var navigation_region := world.get_node("NavigationRegion3D") as NavigationRegion3D
var navigation_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid())
_check(navigation_map.is_valid(), "Jajce navigation region should be assigned to a map")
_check(
navigation_region.navigation_mesh.get_polygon_count() > 0,
"Jajce navigation mesh should contain baked polygons"
)
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
var origins := [Vector3(-6, 0, -4), Vector3(0, 0, 0), Vector3(6, 0, 4)]
var destinations: Array[Vector3] = []
for resource in resources:
destinations.append((resource as ResourceNode).interaction_point.global_position)
destinations.append(pantry.get_interaction_position())
for site in activity_root.get_children():
destinations.append((site as ActivitySite).get_interaction_position())
for origin in origins:
for destination in destinations:
var path := NavigationServer3D.map_get_path(navigation_map, origin, destination, true)
_check(
not path.is_empty(),
"Jajce navigation path should exist from %s to %s" % [origin, destination]
)
_check_path_tracks_terrain(
terrain, path, "Jajce navigation path should track Terrain3D height"
)
var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry
root.add_child(adapter)
var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD)
_check(food_candidates.size() >= 6, "Adapter should expose all authored food candidates")
var candidate_metadata_valid := true
for candidate in food_candidates:
candidate_metadata_valid = (
candidate_metadata_valid
and candidate.has_all(
[
"target_id",
"position",
"resource_id",
"safety_risk",
"comfort_distance",
"discovery_priority"
]
)
)
_check(candidate_metadata_valid, "Adapter resource facts should include discovery metadata")
var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.debug_logs = false
manager.active_world_adapter = adapter
root.add_child(manager)
manager.set_process(false)
await process_frame
var npc: SimNPC = manager.npcs[0]
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
_check(
manager.resolve_npc_target(npc.id, Vector3.ZERO),
"Target resolver should find Jajce resources without a parent path"
)
_check(
npc.target_id != &"animal_camp_river_01",
"Target resolver should not choose the far risky animal camp while safer food exists"
)
var selected := ResourceNode.get_by_id(npc.target_id)
if selected != null:
var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id)
_check(
selected_state.get_reserved_by() == npc.id,
"Selected Jajce resource should be authoritatively reserved"
)
manager.release_resource(selected.node_id, npc.id)
var storage_target := adapter.get_activity_target(SimulationIds.ACTION_WITHDRAW_FOOD)
_check(
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
"Storage actions should target the typed pantry's stable ID"
)
var storage_target_position: Vector3 = storage_target.get("position", Vector3(9999, 9999, 9999))
_check(
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
"Storage actions should use the pantry StorageNode interaction point"
)
var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST)
_check(
rest_target.get("target_id", "") == "rest_bench",
"Rest should target the typed RestBench ActivitySite"
)
if failures.is_empty():
print(
"[TEST] Jajce scaffold passed: Terrain3D, scattered 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, "Jajce 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