extends RefCounted ## Headless tests for the procedural world, navigation mesh, and phase lighting. const WorldBuilder := preload("res://src/game/world_builder.gd") const Rules := preload("res://src/core/rules.gd") const GameManager := preload("res://src/game/game_manager.gd") func test_navmesh_built() -> bool: var root := Node3D.new() WorldBuilder.new(root).build() var region: NavigationRegion3D = root.find_child("NavRegion", true, false) as NavigationRegion3D if region == null: return TestUtils.fail("no NavRegion in world") var mesh: NavigationMesh = region.navigation_mesh if mesh == null: return TestUtils.fail("NavRegion has no navigation mesh") var a := TestUtils.check(mesh.get_polygon_count() > 0, "navmesh has polygons") var b := TestUtils.check(mesh.get_vertices().size() >= 4, "navmesh has vertices") root.free() return a and b func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool: var hollywood := WorldBuilder.new(Node3D.new()) var joke := WorldBuilder.new(Node3D.new(), "joke_shop") var studio := WorldBuilder.new(Node3D.new(), "tv_studio") var unknown := WorldBuilder.new(Node3D.new(), "not_a_venue") var a := TestUtils.check(hollywood.venue_id == "hollywood_building", "the default venue is Hollywood") var b := TestUtils.check(float(hollywood.spec["facade_width"]) > float(joke.spec["facade_width"]), "the Joke Shop frontage is slightly narrower than the boulevard") var c := TestUtils.check(not bool(joke.spec["features"]["skyline"]) and not bool(joke.spec["features"]["marquee"]), "the Joke Shop disables the skyline and marquee") var d := TestUtils.check(bool(studio.spec["features"]["bulb_canopy"]) and not bool(studio.spec["features"]["marquee"]), "the studio uses a bulb canopy instead of the marquee") var e := TestUtils.check(unknown.venue_id == "hollywood_building", "an unknown venue falls back to Hollywood") var root := Node3D.new() var builder := WorldBuilder.new(root, "joke_shop") builder.build() var body := root.find_child("Building", true, false) var facade := body.get_child(0) as MeshInstance3D if body != null else null var f := TestUtils.check(facade != null and str(builder.spec.get("architecture", "")) == "comedy_store" and (facade.mesh as BoxMesh).size.x > 24.0 and (facade.mesh as BoxMesh).size.x < 30.0, "the Joke Shop builds its 27 m Sunset frontage") var g := TestUtils.check(root.find_child("SkylineBackdrop", true, false) == null, "the Joke Shop builds no skyline ring") hollywood.root.free() joke.root.free() studio.root.free() unknown.root.free() root.free() return a and b and c and d and e and f and g func test_joke_shop_uses_the_real_sunset_strip_layout() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root, "joke_shop") builder.build() var a := TestUtils.check(root.find_child("SignatureCanopy", true, false) != null, "the signature-covered canopy is built") var sign := root.find_child("ComedySignLabel", true, false) as Label3D var b := TestUtils.check(sign != null and sign.text.contains("JOKE") and sign.text.contains("SHOP"), "the circular blade sign carries the club wordmark") var c := TestUtils.check(root.get_node_or_null("SunsetHillside/DryHillside") != null, "the dry hillside rises behind the club") var d := TestUtils.check(root.get_node_or_null("SunsetNeighbours/WestMediterraneanBlock") != null and root.get_node_or_null("SunsetNeighbours/EastCommercialBlock") != null, "the west and east Sunset neighbours are built") var e := TestUtils.check(root.find_child("SunsetAcrossStreet", true, false) != null, "the opposite side of Sunset is built as background context") var f := TestUtils.check(root.find_child("EntranceStep", true, false) != null, "the low entrance step is built") var g := TestUtils.check(root.find_child("AlleyWall", true, false) == null, "the old alley walls are gone") var placements: Array = builder.spec.get("prop_placements", []) var h := TestUtils.check(not placements.is_empty() and WorldBuilder.placement_problems(placements).is_empty(), "the Joke Shop prop placements stay on the sidewalk and clear the lane") root.free() return a and b and c and d and e and f and g and h func test_joke_shop_geometry_stays_inside_the_map_budget() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root, "joke_shop") builder.build() var triangles := builder.world_triangle_count() var meshes := builder.world_mesh_count() print("joke shop map: %d triangles across %d meshes" % [triangles, meshes]) var a := TestUtils.check(triangles < 150000, "Joke Shop triangle budget stays under 150k (got %d)" % triangles) var b := TestUtils.check(meshes < 700, "Joke Shop draw-call count stays under 700 meshes (got %d)" % meshes) root.free() return a and b func test_hollywood_uses_the_real_landmark_layout() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var columns := root.find_child("IonicColonnade", true, false) var steps := root.find_child("Steps", true, false) var a := TestUtils.check(columns != null and columns.get_child_count() >= 12, "six Ionic columns with bases and capitals are built") var b := TestUtils.check(steps != null and steps.get_child_count() >= 8, "eight stone steps are built") var frieze := root.find_child("FriezeInscription", true, false) as Label3D var c := TestUtils.check(frieze != null and frieze.text.contains("FREEMASONRY") and frieze.text.contains("HEARTS OF MEN"), "the Masonic parapet motto is present") var banners := root.find_child("EventBanners", true, false) var d := TestUtils.check(banners != null and banners.get_child_count() >= 9, "three red event banners with their trim are present") # The event banners stay readable at night: the cloth carries a warm # self-light that the day/night cycle brightens (not a flat dead panel). var banner_cloth: Material = banners.get_child(0).material_override \ if banners != null and banners.get_child_count() > 0 else null var d2 := TestUtils.check(banner_cloth is StandardMaterial3D and (banner_cloth as StandardMaterial3D).emission_enabled, "the event banner cloth is emissive for the night balance") var walk := root.find_child("WalkOfFame", true, false) var walk_band := walk.get_child(0) as MeshInstance3D if walk != null else null var walk_material: Material = walk_band.mesh.surface_get_material(0) if walk_band != null else null var e := TestUtils.check(walk != null and walk_material != null and walk_material is StandardMaterial3D and (walk_material as StandardMaterial3D).albedo_texture != null, "the Hollywood Walk of Fame strip is built with the terrazzo texture") var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null, "the El Capitan Theatre and east retail neighbours are built") var g := TestUtils.check(root.find_child("OvationHollywood", true, false) != null, "the opposite Ovation Hollywood block is built") var placements: Array = builder.spec.get("prop_placements", []) var h := TestUtils.check(not placements.is_empty() and WorldBuilder.placement_problems(placements).is_empty(), "the landmark prop placements keep the wide steps clear") root.free() return a and b and c and d and d2 and e and f and g and h func test_hollywood_geometry_stays_inside_the_map_budget() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var triangles := builder.world_triangle_count() var meshes := builder.world_mesh_count() print("hollywood map: %d triangles across %d meshes" % [triangles, meshes]) var a := TestUtils.check(triangles < 150000, "landmark triangle budget stays under 150k (got %d)" % triangles) var b := TestUtils.check(meshes < 600, "landmark draw-call count stays under 600 meshes (got %d)" % meshes) root.free() return a and b func test_navigation_mesh_matches_each_venue_bounds() -> bool: var problems: Array[String] = [] for id in ["hollywood_building", "joke_shop", "tv_studio"]: var root := Node3D.new() var builder := WorldBuilder.new(root, id) builder.build() var region := root.find_child("NavRegion", true, false) as NavigationRegion3D if region == null or region.navigation_mesh == null: problems.append("%s has no navmesh" % id) root.free() continue var verts := region.navigation_mesh.get_vertices() var nav_min: Vector2 = builder.spec["nav_min"] var nav_max: Vector2 = builder.spec["nav_max"] var min_x := 999.0 var max_x := -999.0 var min_z := 999.0 var max_z := -999.0 for v in verts: min_x = minf(min_x, v.x) max_x = maxf(max_x, v.x) min_z = minf(min_z, v.z) max_z = maxf(max_z, v.z) if absf(min_x - nav_min.x) > 0.01 or absf(max_x - nav_max.x) > 0.01 \ or absf(min_z - nav_min.y) > 0.01 or absf(max_z - nav_max.y) > 0.01: problems.append("%s navmesh %s does not match spec %s" % [ id, str(Vector4(min_x, max_x, min_z, max_z)), str(Vector4(nav_min.x, nav_max.x, nav_min.y, nav_max.y))]) root.free() return TestUtils.check(problems.is_empty(), "each venue builds its own navigation bounds (%s)" % str(problems)) func test_phase_lighting_config_covered() -> bool: var ok := true for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]: var cfg: Variant = GameManager.PHASE_LIGHTING.get(phase, null) if not (cfg is Dictionary): ok = false continue for key in ["sun_energy", "sun_angle", "sun_color", "sky_top", "sky_horizon", "ambient", "night"]: if not (cfg as Dictionary).has(key): ok = false return TestUtils.check(ok, "all four phases have a full lighting config") func test_street_lamps_and_marquee_built() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var a := TestUtils.check(builder._street_lamps.size() >= 2, "street lamps built") var b := TestUtils.check(builder.marquee_label != null, "marquee label built") # Night mode raises bulb emission; day mode drops it. builder.set_night_mode(true) var night_on := builder._lamp_bulb_mats[0].emission_energy_multiplier > 1.0 builder.set_night_mode(false) var day_off := builder._lamp_bulb_mats[0].emission_energy_multiplier <= 0.5 root.free() return a and b and TestUtils.check(night_on, "lamps brighten at night") and TestUtils.check(day_off, "lamps dim in daylight") func test_marquee_emissive_balanced_at_night() -> bool: ## The marquee sign face and bulbs are registered with the day/night cycle: ## they brighten after dark and fall back to a faint glow in daylight, so the ## marquee reads as practical lighting instead of a black box. var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var emissive := builder._marquee_emissive var ok := emissive.size() >= 2 builder.set_night_mode(true) var night_energy := 0.0 for mat in emissive: night_energy += mat.emission_energy_multiplier builder.set_night_mode(false) var day_energy := 0.0 for mat in emissive: day_energy += mat.emission_energy_multiplier root.free() return ok and TestUtils.check(night_energy > day_energy, "marquee emissive surfaces brighten at night") func test_asset_textures_present_and_pow2() -> bool: var required := [ "res://assets/textures/brick_facade.png", "res://assets/textures/concrete.png", "res://assets/textures/asphalt.png", "res://assets/textures/awning.png", "res://assets/textures/skyline.png", "res://assets/textures/vignette.png", "res://assets/textures/icons/flashlight_on.png", "res://assets/textures/icons/door_locked.png", "res://assets/textures/icons/wand.png", "res://assets/textures/icons/rec_dot.png", "res://assets/textures/cards/id_card.png", "res://assets/textures/cards/wristband.png", "res://assets/textures/cards/ticket.png", "res://assets/textures/cards/credential.png", "res://assets/textures/cards/guest_list.png", ] for path in required: if not FileAccess.file_exists(path): return TestUtils.fail("missing generated asset: %s — run tools/generate_textures.py" % path) var img := Image.new() for path in required: if img.load(path) != OK: return TestUtils.fail("cannot decode asset: %s" % path) var w := img.get_width() var h := img.get_height() if w > 1024 or h > 1024: return TestUtils.fail("asset too large: %s (%dx%d)" % [path, w, h]) if (w & (w - 1)) != 0 or (h & (h - 1)) != 0: return TestUtils.fail("asset not power-of-two: %s (%dx%d)" % [path, w, h]) return true func test_steps_have_colliders() -> bool: ## Steps were visual-only and the guard walked through them (TODO). var root := Node3D.new() WorldBuilder.new(root).build() var steps := root.find_child("Steps", true, false) var a := TestUtils.check(steps != null, "Steps node exists") var b := TestUtils.check(steps is StaticBody3D, "Steps are a physics body") var shape_count := 0 if steps is StaticBody3D: var body := steps as StaticBody3D shape_count = body.get_shape_owners().size() var c := TestUtils.check(shape_count >= 5, "four treads + landing have colliders (got %d)" % shape_count) root.free() return a and b and c func test_skyline_backdrop_uses_alpha() -> bool: ## The old opaque skyline texture made the plane render as a black slab, and ## the scene fog bleached the panorama into a white band. The backdrop is an ## alpha-scissored, fog-free shader with a panorama per phase. var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var backdrop := root.find_child("SkylineBackdrop", true, false) var a := TestUtils.check(backdrop != null, "SkylineBackdrop exists") var mat: ShaderMaterial = builder._skyline_mat var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured") var code := mat.shader.code if mat != null and mat.shader != null else "" var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled") and code.contains("haze_strength"), "backdrop shader is alpha-scissored, fog-free and haze-blended") var haze := float(mat.get_shader_parameter("haze_strength")) if mat != null else 0.0 var c2 := TestUtils.check(haze > 0.0, "skyline haze is enabled") c = c and c2 var day: Texture2D = mat.get_shader_parameter("sky_texture") builder.set_phase(Rules.Phase.LATE) var night: Texture2D = mat.get_shader_parameter("sky_texture") var d := TestUtils.check(day != null and night != null and day != night, "the skyline panorama follows the shift phase") var e := TestUtils.check(builder._street_mat != null and builder._street_mat.roughness < 0.5, "the night street turns wet") root.free() return a and b and c and d and e func test_door_indicator_built() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var a := TestUtils.check(builder.door_indicator != null, "door indicator mesh exists") if builder.door_indicator != null: builder.set_door_locked(true) var mat: StandardMaterial3D = builder.door_indicator.material_override as StandardMaterial3D var b := TestUtils.check(mat != null and mat.albedo_color.r > 0.5, "indicator turns red when locked") builder.set_door_locked(false) var c := TestUtils.check(mat != null and mat.albedo_color.g > 0.5, "indicator turns green when open") root.free() return a and b and c root.free() return a ## 3D review gate: a placed prop that people can walk into must publish both a ## static body and a navigation-avoidance footprint. A prop whose bounds are ## empty (broken GLB/fallback) used to lose its collider silently. func test_every_placed_prop_has_a_collider_and_avoidance() -> bool: var problems: Array[String] = [] for venue in ["hollywood_building", "joke_shop", "tv_studio"]: var root := Node3D.new() var builder := WorldBuilder.new(root, venue) builder.build() var holders := root.find_children("Prop_*", "Node3D", true, false) if holders.size() < 8: problems.append("%s places only %d props" % [venue, holders.size()]) for holder in holders: var body := holder.get_node_or_null("Collision") as StaticBody3D var shape := body.get_node_or_null("Shape") as CollisionShape3D if body != null else null var obstacle := holder.get_node_or_null("Avoidance") as NavigationObstacle3D if body == null or shape == null or shape.shape == null: problems.append("%s/%s has no collision shape" % [venue, holder.name]) if obstacle == null or obstacle.radius <= 0.0: problems.append("%s/%s has no avoidance radius" % [venue, holder.name]) root.free() return TestUtils.check(problems.is_empty(), "placed props are covered: %s" % ", ".join(problems.slice(0, 6))) ## Library dressing must either publish a walk-blocking body or be on the ## documented mounted/decorative list. The check also proves the metadata that ## ties a node to its catalogue asset is present. func test_library_dressing_collision_matches_the_documented_exemptions() -> bool: var problems: Array[String] = [] for venue in ["hollywood_building", "joke_shop", "tv_studio"]: var root := Node3D.new() var builder := WorldBuilder.new(root, venue) builder.build() if builder.library_nodes.is_empty(): problems.append("%s placed no library dressing" % venue) for node in builder.library_nodes: if not is_instance_valid(node): continue var prop := str(node.get_meta("library_prop", "")) if prop == "": problems.append("%s/%s has no library_prop metadata" % [venue, node.name]) continue if WorldBuilder.NON_BLOCKING_LIBRARY.has(prop): continue if _walk_blocking_body(node) == null: problems.append("%s/%s (%s) blocks walking without a body" % [venue, node.name, prop]) root.free() return TestUtils.check(problems.is_empty(), "library dressing is covered: %s" % ", ".join(problems.slice(0, 6))) func _walk_blocking_body(node: Node) -> StaticBody3D: for child in node.find_children("*", "StaticBody3D", true, false): var body := child as StaticBody3D for shape in body.find_children("*", "CollisionShape3D", true, false): if (shape as CollisionShape3D).shape != null: return body return null