extends SceneTree const JajceWatercourse := preload("res://world/jajce/JajceWatercourse.gd") 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") _check( not world.has_node("NavigationRegion3D/GreyboxGround"), "JajceWorld should not keep the temporary greybox navigation ground" ) _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.has_node("WildlifeRoot"), "JajceWorld should expose ambient WildlifeRoot") _check( world.get_node("FoliageRoot").get_child_count() >= 10, "JajceWorld should include restrained authored foliage clusters" ) var conifer_count := 0 for foliage in world.get_node("FoliageRoot").get_children(): if foliage.name.begins_with("Conifer_"): conifer_count += 1 _check(conifer_count >= 6, "Citadel and river edges should include conifer silhouettes") _check( world.get_node("WildlifeRoot").get_child_count() >= 3, "Village center should include a few oversized ambient butterflies" ) var grass_field := world.get_node("TerrainRoot/Terrain3D/CozyGrassField") var grass_material := grass_field.get("mesh_material_override") as ShaderMaterial var grass_process := grass_field.get("process_material") as ShaderMaterial _check( int(grass_field.get("particle_count")) in range(9000, 15001), "Camera-local Terrain3D grass should stay inside its bounded particle budget" ) _check( ( grass_material != null and grass_material.shader == load("res://world/jajce/materials/cozy_grass.gdshader") ), "Terrain grass should use the cozy wind-and-interaction shader" ) var grass_max_scale := Vector3.ZERO var grass_clod_boost := INF if grass_process != null: grass_max_scale = grass_process.get_shader_parameter("max_scale") as Vector3 grass_clod_boost = float(grass_process.get_shader_parameter("clod_scale_boost")) _check( grass_process != null and grass_max_scale.y <= 0.5 and grass_clod_boost <= 0.2, "Grass placement should stay short enough to preserve villagers and paths" ) var grass_controller := world.get_node("TerrainRoot/GrassInteractionController") _check( grass_controller.get("grass_material") == grass_material, "Grass interaction should update the same material rendered by Terrain3D particles" ) _check( ( not world.has_node("WindStrokes_Valley") and not world.has_node("WindStrokes_Ridge") and not world.has_node("WindSpecks_Valley") and not world.has_node("WindSpecks_Ridge") ), "Legacy overlapping valley and ridge wind fields should remain removed" ) var wind_gust_field := world.get_node("AtmosphereRoot/WindGustField") as WindGustField _check(wind_gust_field != null, "Jajce should expose one bounded wind-gust controller") if wind_gust_field != null: _check( ( wind_gust_field.interval_min >= 5.0 and wind_gust_field.interval_max <= 12.0 and wind_gust_field.duration_max <= 2.5 and wind_gust_field.stroke_count_max <= 3 ), "Calligraphic wind should remain sporadic, brief, and sparse" ) _check( wind_gust_field.trigger_gust_at(Vector3.ZERO, true, 0.5), "Wind controller should support a deterministic staged gust" ) var active_stroke_count := wind_gust_field.get_active_stroke_count() _check( active_stroke_count in [2, 3], "One gust should contain only two or three related brush strokes" ) var gust_burst := wind_gust_field.get_node_or_null("WindGustBurst") as Node3D var gust_geometry_valid := gust_burst != null if gust_burst != null: for stroke in gust_burst.get_children(): var mesh_instance := stroke as MeshInstance3D if mesh_instance == null: gust_geometry_valid = false continue var material := mesh_instance.material_override as ShaderMaterial var tint: Color = ( material.get_shader_parameter("tint") if material != null else Color.WHITE ) var progress := ( float(material.get_shader_parameter("progress")) if material != null else 0.0 ) gust_geometry_valid = ( gust_geometry_valid and mesh_instance.mesh is ArrayMesh and material != null and material.shader == load("res://world/jajce/materials/wind_gust.gdshader") and tint.a >= 0.18 and tint.a <= 0.3 and progress >= 0.4 and progress <= 0.6 and (mesh_instance.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF) ) _check( gust_geometry_valid, "Gust strokes should use soft tapered meshes without bright alpha or shadows" ) _check( ( not wind_gust_field.trigger_gust_at(Vector3.ZERO) and wind_gust_field.get_active_stroke_count() == active_stroke_count ), "Repeated gust triggers should not stack visual fields" ) var tree_phases: Array[float] = [] var tree_wind_is_bounded := true for tree in world.get_node("FoliageRoot").get_children(): if not tree.has_node("Canopy"): continue var canopy := tree.get_node("Canopy") as MeshInstance3D var material := canopy.mesh.material as ShaderMaterial tree_wind_is_bounded = ( tree_wind_is_bounded and material != null and material.shader == load("res://world/jajce/materials/wind.gdshader") and float(material.get_shader_parameter("wind_strength")) <= 0.08 and float(material.get_shader_parameter("wind_speed")) <= 0.65 ) tree_phases.append(float(material.get_shader_parameter("wind_phase"))) _check(tree_phases.size() >= 2, "Decorative trees should expose named wind-reactive canopies") _check( tree_phases.size() >= 2 and not is_equal_approx(tree_phases[0], tree_phases[1]), "Tree wind should use stable per-tree phase instead of moving every canopy in lockstep" ) _check(tree_wind_is_bounded, "Tree wind should stay within the restrained cozy-motion budget") _check(world.has_node("FortressBlockout"), "JajceWorld should include the fortress landmark") _check( world.has_node("FortressBlockout/Keep/CitadelKeep"), "Fortress silhouette should expose a distinct summit citadel" ) _check(world.has_node("WaterRoot/WaterfallBody"), "WaterRoot should include a waterfall drop") _check( world.has_node("WaterRoot/WaterfallBody/RockFrame"), "Waterfall should be framed by the rocky river threshold" ) _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( waterfall_mist.position.distance_to(Vector3(25.0, 0.65, -21.5)) < 0.01, "Waterfall mist should stay at the waterfall base instead of obscuring the village" ) _check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface") var river_ribbon := world.get_node("WaterRoot/RiverSurface/RiverRibbon") as MeshInstance3D _check( river_ribbon.mesh is ArrayMesh, "River should build a lightweight ribbon from the current Terrain3D surface" ) var upper_river_ribbon := ( world.get_node("WaterRoot/RiverSurface/UpperRiverRibbon") as MeshInstance3D ) _check( upper_river_ribbon.mesh is ArrayMesh, "Waterfall shelf should expose a terrain-following upper stream" ) _check( world.has_node("WaterRoot/RiverSurface/PlungePool"), "River should widen into a readable plunge pool beneath the waterfall" ) var plunge_pool := world.get_node("WaterRoot/RiverSurface/PlungePool") as MeshInstance3D var expected_pool_height: float = terrain.data.get_height( Vector3(JajceWatercourse.PLUNGE_POOL_CENTER.x, 0.0, JajceWatercourse.PLUNGE_POOL_CENTER.y) ) _check( ( absf( ( plunge_pool.global_position.y - expected_pool_height - JajceWatercourse.WATER_SURFACE_OFFSET ) ) < 0.03 ), "Plunge-pool water should follow the carved basin height" ) 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 house_count := 0 for village_child in world.get_node("VillageRoot").get_children(): if village_child.name.begins_with("House_"): house_count += 1 _check(house_count >= 8, "Roofs should form a small residential amphitheater below the citadel") var smoke_count := 0 var smoke_is_translucent := true var smoke_follows_wind := true var smoke_direction := Vector2.ZERO 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 var smoke_material := (smoke.draw_pass_1 as QuadMesh).material as StandardMaterial3D var smoke_process := smoke.process_material as ParticleProcessMaterial if smoke_process != null and smoke_direction.is_zero_approx(): smoke_direction = ( Vector2(smoke_process.gravity.x, smoke_process.gravity.z).normalized() ) smoke_is_translucent = ( smoke_is_translucent and smoke_material != null and smoke_material.transparency != BaseMaterial3D.TRANSPARENCY_DISABLED and smoke_material.albedo_color.a < 0.5 ) smoke_follows_wind = ( smoke_follows_wind and smoke_process != null and smoke_process.gravity.x > 0.0 and smoke_process.gravity.z > 0.0 ) smoke_count += 1 _check( smoke_count >= 2, "At least two houses should have chimney smoke particles (found %s)" % smoke_count ) _check(smoke_is_translucent, "Chimney smoke should not render as opaque white quads") _check(smoke_follows_wind, "Chimney smoke should lean with the prevailing canopy wind") var gust_direction := Vector2.ZERO if wind_gust_field != null: gust_direction = ( Vector2(wind_gust_field.wind_direction.x, wind_gust_field.wind_direction.z).normalized() ) _check( gust_direction.dot(smoke_direction) >= 0.98, "Calligraphic gusts, canopy bend, and chimney smoke should share one wind direction" ) 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 := _collect_resource_nodes(world.get_node("WorldObjects")) 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_bush_river_02", "berry_patch_mill_01", "berry_patch_river_01", "berry_patch_south_01", "farm_crop_01", "tree_01", "tree_02", "tree_forest_grove_01", "tree_forest_grove_02", "tree_north_outskirts_01", "tree_river_resource_01", "tree_south_edge_01", "wood_pile_mill_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 var farming_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 if id.begins_with("farm_"): farming_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 >= 9, "Jajce should expose at least nine finite food resources") _check(wood_count >= 9, "Jajce should expose at least nine finite wood resources") _check(animal_context_count >= 2, "Jajce food discovery should include animal-camp contexts") _check(berry_context_count >= 6, "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(farming_context_count >= 1, "Jajce food discovery should include farming contexts") var animal_root := world.get_node("WorldObjects/Animals") var goat := animal_root.get_node("Dunja") as AnimalNode _check( animal_root.get_child_count() == 1 and goat != null, "Jajce should contain one bounded loaded-animal presentation" ) _check( ( goat.animal_id == SimulationIds.ANIMAL_DUNJA and goat.species_id == SimulationIds.SPECIES_GOAT and goat.display_name == "Dunja" ), "The authored goat should preserve its stable identity and readable name" ) _check( not goat.is_in_group("resource_nodes") and ResourceNode.get_by_id(goat.animal_id) == null, "The identity-backed goat should remain separate from finite resources" ) _check( goat.is_in_group("grass_interactors"), "Dunja should bend camera-local grass through the shared presentation group" ) var habitat_root := world.get_node("WorldObjects/AnimalHabitats") var habitat := habitat_root.get_node("DunjaHabitat") var routine_sites: Array[AnimalRoutineSite] = [] for site in AnimalRoutineSite.get_all(): routine_sites.append(site) routine_sites.sort_custom( func(first: AnimalRoutineSite, second: AnimalRoutineSite) -> bool: return String(first.site_id) < String(second.site_id) ) _check( habitat_root.get_child_count() == 1 and habitat != null and routine_sites.size() == 2, "Jajce should keep Dunja's two routine anchors inside one self-contained habitat scene" ) _check( ( routine_sites.size() == 2 and routine_sites[0].site_id == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE and routine_sites[1].site_id == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER ), "Dunja's pasture and shelter should expose deterministic stable IDs" ) _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" ) var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode _check(woodpile != null, "JajceWorld should include a typed VillageWoodpile StorageNode") _check( woodpile.storage_id == SimulationIds.STORAGE_VILLAGE_WOODPILE, "VillageWoodpile 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" ) _check( ( navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres" ), "Jajce navigation should use the Terrain3D-derived baked mesh resource" ) 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(goat.get_interaction_position()) for site in routine_sites: destinations.append(site.get_routine_position()) destinations.append(pantry.get_interaction_position()) destinations.append(woodpile.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" ) _check_path_reaches_destination( path, destination, "Jajce navigation path should reach its requested target" ) var adapter := ActiveWorldAdapter.new() adapter.pantry_storage = pantry adapter.woodpile_storage = woodpile root.add_child(adapter) var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD) _check(food_candidates.size() >= 9, "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 wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD) _check( wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE), "Wood deposit should target the typed woodpile's stable ID" ) 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 _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] )