extends RefCounted ## Headless tests for the generated prop models (tools/model_pipeline.py) and ## their placement in the procedural world. const WorldBuilder := preload("res://src/game/world_builder.gd") const MODEL_DIR := "res://assets/models/generated/" func test_prop_glb_files_exist() -> bool: var missing: Array[String] = [] for prop in WorldBuilder.MODEL_PROPS: if not FileAccess.file_exists(MODEL_DIR + str(prop) + ".glb"): missing.append(str(prop)) return TestUtils.check(missing.is_empty(), "all prop models built (missing: %s)" % str(missing)) func test_prop_glb_files_are_imported_scenes() -> bool: ## `load()` only returns a PackedScene once Godot has imported the `.glb`; ## a null here means the checkout is missing the import or the model is invalid. var problems: Array[String] = [] for prop in WorldBuilder.MODEL_PROPS: var path := MODEL_DIR + str(prop) + ".glb" var packed := ResourceLoader.load(path) as PackedScene if packed == null: problems.append("%s not imported as a scene" % prop) elif not FileAccess.file_exists(path + ".import"): problems.append("%s has no .import file" % prop) return TestUtils.check(problems.is_empty(), "prop models import as scenes (%s)" % str(problems)) func test_prop_scenes_instantiate_with_meshes() -> bool: var problems: Array[String] = [] for prop in WorldBuilder.MODEL_PROPS: var packed := ResourceLoader.load(MODEL_DIR + str(prop) + ".glb") as PackedScene if packed == null: problems.append("%s: no scene" % prop) continue var instance := packed.instantiate() var meshes := instance.find_children("*", "MeshInstance3D", true, false) if meshes.is_empty(): problems.append("%s: no mesh instances" % prop) instance.free() return TestUtils.check(problems.is_empty(), "every prop scene has geometry (%s)" % str(problems)) func test_prop_scenes_share_one_atlas_material() -> bool: ## All parts of a prop share one material backed by the colour atlas, so a ## prop is a single draw material instead of one per part. var problems: Array[String] = [] for prop in WorldBuilder.MODEL_PROPS: var packed := ResourceLoader.load(MODEL_DIR + str(prop) + ".glb") as PackedScene if packed == null: problems.append("%s: no scene" % prop) continue var instance := packed.instantiate() var meshes := instance.find_children("*", "MeshInstance3D", true, false) var materials := {} var surfaces := 0 for node in meshes: var mesh: Mesh = (node as MeshInstance3D).mesh if mesh == null: continue for index in mesh.get_surface_count(): surfaces += 1 var material := mesh.surface_get_material(index) if material != null: materials[material.get_instance_id()] = true if surfaces == 0: problems.append("%s: no surfaces" % prop) elif materials.size() != 1: problems.append("%s: %d materials (expected 1)" % [prop, materials.size()]) instance.free() return TestUtils.check(problems.is_empty(), "one atlas material per prop (%s)" % str(problems)) func test_prop_instances_share_the_imported_material() -> bool: ## Instancing a prop must not duplicate its atlas material; every copy uses ## the same resource, so many props stay cheap to draw. var problems: Array[String] = [] for prop in WorldBuilder.MODEL_PROPS: var packed := ResourceLoader.load(MODEL_DIR + str(prop) + ".glb") as PackedScene if packed == null: problems.append("%s: no scene" % prop) continue var first := packed.instantiate() var second := packed.instantiate() var a := _first_material(first) var b := _first_material(second) if a == null or b == null: problems.append("%s: missing material" % prop) elif a.get_instance_id() != b.get_instance_id(): problems.append("%s: material is duplicated per instance" % prop) first.free() second.free() return TestUtils.check(problems.is_empty(), "prop instances share materials (%s)" % str(problems)) func _first_material(instance: Node) -> Material: for node in instance.find_children("*", "MeshInstance3D", true, false): var mesh: Mesh = (node as MeshInstance3D).mesh if mesh != null and mesh.get_surface_count() > 0: return mesh.surface_get_material(0) return null func test_world_places_generated_props() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var detail := root.get_node_or_null("StreetDetail") var a := TestUtils.check(detail != null, "StreetDetail exists") var placed: int = detail.get_child_count() if detail else 0 var b := TestUtils.check(placed >= 10, "street furniture placed (%d)" % placed) var c := TestUtils.check(builder.loaded_models.size() >= WorldBuilder.MODEL_PROPS.size(), "generated models loaded instead of fallbacks") var d := TestUtils.check(builder.fallback_props.is_empty(), "no procedural fallback used (fallbacks: %s)" % str(builder.fallback_props)) root.free() return a and b and c and d func test_prop_placements_are_valid() -> bool: var problems := WorldBuilder.placement_problems() return TestUtils.check(problems.is_empty(), "prop placements keep the door, steps and approach lane clear (%s)" % str(problems)) func test_placement_validator_catches_problems() -> bool: var bad: Array = WorldBuilder.PROP_PLACEMENTS.duplicate(true) bad.append({"zone": "test", "prop": "bench", "x": 0.0, "z": 2.0}) bad.append({"zone": "test", "prop": "bench", "x": 0.0, "z": 2.0}) var problems := WorldBuilder.placement_problems(bad) var a := TestUtils.check(not problems.is_empty(), "validator rejects bad placements") var joined := " ".join(problems) var b := TestUtils.check(joined.contains("steps and door"), "validator reports the door/steps exclusion") var c := TestUtils.check(joined.contains("duplicate"), "validator reports a duplicate") return a and b and c func test_prop_variants_are_deterministic() -> bool: var first := WorldBuilder.variant_for(3, 180.0) var second := WorldBuilder.variant_for(3, 180.0) var other := WorldBuilder.variant_for(4, 180.0) var a := TestUtils.check(is_equal_approx(first["rotation"], second["rotation"]) and is_equal_approx(first["scale"], second["scale"]), "the same placement always gets the same variant") var b := TestUtils.check(not is_equal_approx(first["rotation"], other["rotation"]), "different placements get different rotations") var c := TestUtils.check(first["scale"] >= 0.95 and first["scale"] <= 1.05, "variant scale stays subtle") var d := TestUtils.check(absf(first["rotation"] - 180.0) <= 2.01, "variant rotation stays near the authored angle") return a and b and c and d func test_every_prop_has_a_collider() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var detail := root.get_node_or_null("StreetDetail") var problems: Array[String] = [] if detail == null: problems.append("no StreetDetail") else: for holder in detail.get_children(): var body := holder.get_node_or_null("Collision") if body == null or not (body is StaticBody3D): problems.append("%s has no StaticBody3D" % holder.name) continue var collider := body.get_node_or_null("Shape") as CollisionShape3D if collider == null or collider.shape == null: problems.append("%s has no collision shape" % holder.name) continue var size: Vector3 if collider.shape is BoxShape3D: size = (collider.shape as BoxShape3D).size elif collider.shape is CylinderShape3D: var cylinder := collider.shape as CylinderShape3D size = Vector3(cylinder.radius * 2.0, cylinder.height, cylinder.radius * 2.0) else: problems.append("%s uses an unexpected shape" % holder.name) continue if size.x <= 0.05 or size.y <= 0.05 or size.z <= 0.05: problems.append("%s has a degenerate collider" % holder.name) if collider.position.y + size.y * 0.5 <= 0.05: problems.append("%s collider sits below the ground" % holder.name) if holder.get_node_or_null("Avoidance") == null: problems.append("%s has no navigation obstacle" % holder.name) root.free() return TestUtils.check(problems.is_empty(), "every prop has a static collider (%s)" % str(problems)) func test_fallback_props_get_the_same_collider() -> bool: ## A missing `.glb` must not silently remove collision: fallback primitives go ## through the same bounds fit as the imported models. var root := Node3D.new() var builder := WorldBuilder.new(root) var holder := Node3D.new() var fallback := builder._fallback_prop("bench") holder.add_child(fallback) builder._add_prop_collision(holder, "bench", fallback) var body := holder.get_node_or_null("Collision") as StaticBody3D var a := TestUtils.check(body != null, "fallback bench gets a static body") var collider := body.get_node_or_null("Shape") as CollisionShape3D if body else null var b := TestUtils.check(collider != null and collider.shape is BoxShape3D, "fallback bench gets a box shape") var width: float = (collider.shape as BoxShape3D).size.x if b else 0.0 var c := TestUtils.check(width > 1.0, "fallback collider follows the bench width (%.2f)" % width) var d := TestUtils.check(holder.get_node_or_null("Avoidance") is NavigationObstacle3D, "fallback bench publishes a navigation obstacle") holder.free() root.free() return a and b and c and d func test_world_loads_the_blender_library_props() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var a := TestUtils.check(builder.fallback_props.is_empty(), "no primitive prop fallback (fallbacks: %s)" % str(builder.fallback_props)) var b := TestUtils.check(builder.library_props.has("street_lamp") and builder.library_props.has("bollard") and builder.library_props.has("palm_tree"), "replacement and new props come from the library (%d)" % builder.library_props.size()) var c := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null and root.get_node_or_null("OvationHollywood/OvationSign") != null and root.get_node_or_null("WalkOfFame") != null and root.get_node_or_null("Building/FriezeInscription") != null, "the landmark neighbours, opposite block, Walk of Fame and frieze are placed") var d := TestUtils.check(not builder._lamp_bulb_mats.is_empty() and not builder._marquee_emissive.is_empty(), "library lamps/marquee expose emissive materials") builder.set_night_mode(true) var lit := (builder._lamp_bulb_mats[0].emission_energy_multiplier > 0.0 and builder._marquee_emissive[0].emission_energy_multiplier > 0.0) builder.set_night_mode(false) var dark := builder._lamp_bulb_mats[0].emission_energy_multiplier <= 0.0 var e := TestUtils.check(lit and dark, "night mode drives library emissives") var ao_ok := false var detail := root.get_node_or_null("StreetDetail") if detail != null: for child in detail.get_children(): if str(child.name).begins_with("Prop_bench"): for mi in (child as Node3D).find_children("*", "MeshInstance3D", true, false): for index in (mi as MeshInstance3D).mesh.get_surface_count(): var mat := (mi as MeshInstance3D).get_surface_override_material(index) as StandardMaterial3D if mat != null and mat.ao_enabled and mat.ao_texture != null: ao_ok = true var ao := TestUtils.check(ao_ok, "library prop materials carry the baked AO map") var triangles := builder.library_triangle_count() var f := TestUtils.check(triangles > 0 and triangles <= WorldBuilder.LIBRARY_TRIANGLE_BUDGET, "library prop triangles stay inside the budget (%d)" % triangles) root.free() return a and b and c and d and e and f and ao func test_world_facade_windows_light_at_night() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) builder.build() var a := TestUtils.check(builder._window_mats.size() >= 3, "door transom panes built") builder.set_night_mode(true) var lit: bool = builder._window_mats[0].emission_energy_multiplier > 0.0 builder.set_night_mode(false) var dark: bool = is_zero_approx(builder._window_mats[0].emission_energy_multiplier) root.free() return a and TestUtils.check(lit, "windows light up at night") and TestUtils.check(dark, "windows are dark by day") func test_fallback_prop_is_usable() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) var fallback := builder._fallback_prop("bench") var meshes := fallback.find_children("*", "MeshInstance3D", true, false) var ok := TestUtils.check(meshes.size() >= 3, "fallback bench has geometry") fallback.free() root.free() return ok