extends Node ## Opt-in QA entry point. Runs inside the actual release PCK with ordinary budgets. const AI = preload("res://scripts/ai/simple_ai.gd") const RELEASE_MODELS: Dictionary = { "w04": ["res://assets/phase11_manifest.json", "res://assets/characters/ringborke.glb", 27], "worker": ["res://assets/phase12_manifest.json", "res://assets/characters/arbeiter.glb", 27], "g04": ["res://assets/phase15_manifest.json", "res://assets/characters/basaltbastion.glb", 17], "w10": ["res://assets/phase16_manifest.json", "res://assets/characters/quellgeflecht.glb", 17], "g05": ["res://assets/phase18_manifest.json", "res://assets/characters/funkenlanze.glb", 26], "g06": ["res://assets/phase19_manifest.json", "res://assets/characters/glasballiste.glb", 20], } const FIGHTER_CLIPS: Array[String] = ["idle", "start", "walk", "stop", "attack_a", "attack_b", "hit", "ability", "death"] const WORKER_CLIPS: Array[String] = ["idle", "start", "walk", "stop", "strike", "pickup", "deposit", "return", "hit", "death"] var checks: int = 0 var failures: Array[String] = [] func check(value: bool, message: String) -> void: checks += 1 if not value: failures.append(message) printerr("SMOKE_FAIL: ", message) func triangles(mesh: Mesh) -> int: var total: int = 0 for index: int in range(mesh.get_surface_count()): var arrays: Array = mesh.surface_get_arrays(index) var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX] var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] total += (indices.size() if not indices.is_empty() else vertices.size()) / 3 return total func key(code: Key) -> void: var event := InputEventKey.new() event.keycode = code event.pressed = true get_viewport().push_input(event, true) event = event.duplicate() event.pressed = false get_viewport().push_input(event, true) func capture(label: String) -> void: if DisplayServer.get_name() == "headless": return await get_tree().process_frame await get_tree().create_timer(.22).timeout await RenderingServer.frame_post_draw var directory: String = "user://phase8-smoke" DirAccess.make_dir_recursive_absolute(directory) check(get_viewport().get_texture().get_image().save_png(directory + "/" + label + ".png") == OK, "Capture " + label) func check_release_assets() -> void: # Branching on has_method("update_combat") also lets the rigid fallback pass. # Require the release's exact manifest selection and the instantiated skin/clips. for path: String in ["res://assets/phase3_manifest.json", "res://assets/phase4_manifest.json", "res://assets/phase11_manifest.json", "res://assets/phase12_manifest.json", "res://assets/phase15_manifest.json", "res://assets/phase16_manifest.json", "res://assets/phase18_manifest.json", "res://assets/phase19_manifest.json", "res://assets/menu_stage_manifest.json", "res://assets/audio/manifest.json", "res://assets/icons/manifest.json"]: check(FileAccess.file_exists(path), "Packaged runtime manifest " + path) if FileAccess.file_exists(path): check(JSON.parse_string(FileAccess.get_file_as_string(path)) is Dictionary, "Valid runtime manifest " + path) # Phase 20A menu: emblems, control icons, fonts and the diorama must ship in the package. for path: String in ["res://assets/ui/emblems/saumwacht.svg", "res://assets/ui/emblems/glutpakt.svg", "res://assets/ui/emblems/wurzelbund.svg", "res://assets/ui/controls/toggle_on.svg", "res://assets/ui/controls/toggle_off.svg", "res://assets/ui/controls/choice_on.svg", "res://assets/ui/controls/choice_off.svg"]: check(load(path) is Texture2D, "Packaged menu graphic " + path.get_file()) for path: String in ["res://assets/fonts/Cinzel-VF.ttf", "res://assets/fonts/Alegreya-VF.ttf", "res://assets/fonts/Alegreya-Italic-VF.ttf"]: check(load(path) is Font, "Packaged menu font " + path.get_file()) check(load("res://assets/environment/menu-stage.glb") is PackedScene, "Packaged menu diorama") for id: String in RELEASE_MODELS: var expected: Array = RELEASE_MODELS[id] var record: Dictionary = AssetLibrary._models.get(id, {}) var selected: bool = record.get("skeletal", false) and record.get("resource_path", "") == expected[1] check(selected, id + " selects the current skeletal release asset") check(ResourceLoader.exists(expected[1]), id + " skeletal resource is packaged") if not selected or not ResourceLoader.exists(expected[1]): continue var a: Node3D = AssetLibrary.instantiate_fighter(id) var b: Node3D = AssetLibrary.instantiate_fighter(id) check(a is AssetLibrary.SkeletalFigure and b is AssetLibrary.SkeletalFigure, id + " instantiates the skeletal runtime") if not (a is AssetLibrary.SkeletalFigure and b is AssetLibrary.SkeletalFigure): if a != null: a.free() if b != null: b.free() continue add_child(a) add_child(b) check(a.skeleton != null and a.player != null, id + " imports skeleton and animation player") if a.skeleton == null or a.player == null or b.skeleton == null or b.player == null: a.free() b.free() continue check(a.skeleton.get_bone_count() == expected[2], id + " has the current authored bone count") check(a.skeleton != b.skeleton and a.player != b.player, id + " skeleton and playback are per instance") var weighted_meshes: int = 0 for mesh: MeshInstance3D in a.find_children("*", "MeshInstance3D", true, false): if mesh.skin != null and mesh.get_node_or_null(mesh.skeleton) == a.skeleton: weighted_meshes += 1 check(weighted_meshes > 0, id + " mesh binds to the imported skeleton") var required: Array[String] = WORKER_CLIPS if id == "worker" else FIGHTER_CLIPS for clip: String in required: check(a.clips.has(clip), id + " imports authored clip " + clip) if a.clips.has(clip): var animation: Animation = a.player.get_animation(a.clips[clip]) check(animation.length > .1 and animation.get_track_count() > 0, id + " has playable tracks for " + clip) if a.clips.has("walk"): var own_rest: Array[Transform3D] = [] var other_rest: Array[Transform3D] = [] for bone: int in range(a.skeleton.get_bone_count()): own_rest.append(a.skeleton.get_bone_pose(bone)) other_rest.append(b.skeleton.get_bone_pose(bone)) a.preview("walk", a.clip_length("walk") * .25) var changed: bool = false var independent: bool = true for bone: int in range(a.skeleton.get_bone_count()): changed = changed or not a.skeleton.get_bone_pose(bone).is_equal_approx(own_rest[bone]) independent = independent and b.skeleton.get_bone_pose(bone).is_equal_approx(other_rest[bone]) check(changed, id + " clip drives actual bone poses") check(independent, id + " playback leaves the other instance unchanged") if id == "worker": check(a is AssetLibrary.WorkerFigure and a.has_method("update_work"), "Worker uses the current work-cycle runtime") if a is AssetLibrary.WorkerFigure and required.all(func(clip: String) -> bool: return a.clips.has(clip)): var seen: Dictionary = {} var contacts: int = 0 for step: int in range(ceili(a.cycle_length() * 60.0)): a.update_work(1.0 / 60.0) seen[a.current_clip] = true if a.consume_strike_contact(): contacts += 1 for clip: String in ["strike", "pickup", "deposit", "return", "idle"]: check(seen.has(clip), "Worker work cycle plays " + clip) check(contacts == 1, "Worker emits one tool contact per complete work cycle") a.free() b.free() func run(main: Node3D) -> void: GameState.reset_match() GameState.factions = ["glutpakt", "glutpakt"] GameState.timed_build = true GameState.select_mode(GameState.MODE_SHORT) WaveManager.set_physics_process(false) Soundscape.enabled = false await get_tree().process_frame check_release_assets() if not failures.is_empty(): printerr("EXPORT_SMOKE_FAIL: %d release asset failures" % failures.size()) await Soundscape.quit_game(1) return for id: String in DataCatalog.UNITS: var a: Node3D = AssetLibrary.instantiate_fighter(id) var b: Node3D = AssetLibrary.instantiate_fighter(id) var static_model := AssetLibrary.instantiate_model(id) as MeshInstance3D check(a != null and a.has_method("pose"), id + " has articulated model") if a != null and a.has_method("pose"): var mesh: ArrayMesh = AssetLibrary.bake_mesh(a) check(triangles(mesh) == triangles(static_model.mesh), id + " rig retains every triangle") # Skeletal remakes are judged by form and motion, not a raw poly floor. var poly_floor: int = 14000 if a.has_method("update_combat") else 20000 check(triangles(mesh) > poly_floor, id + " has high-detail geometry") check(mesh.get_aabb().position.distance_to(static_model.get_aabb().position) < .001 and mesh.get_aabb().end.distance_to(static_model.get_aabb().end) < .001, id + " neutral pose retains original bounds") if a.has_method("update_combat"): check(a.skeleton != b.skeleton, id + " skeleton is per instance") check(a.clips.size() >= 9, id + " contains authored animation clips") a.free() b.free() static_model.free() continue check(a.parts.has("LegL") and a.parts.has("LegR"), id + " has two separate leg joints") var other_leg: Transform3D = b.parts.LegL.transform a.pose(.25, true, .14, .4, .12) check(absf(a.parts.LegL.rotation.x) > .005 and a.parts.LegL.rotation.x * a.parts.LegR.rotation.x < 0, id + " legs step in opposition") check(b.parts.LegL.transform == other_leg, id + " animation does not move another unit") check(a.parts.LegL.mesh == b.parts.LegL.mesh, id + " immutable joint geometry is shared") check(a.parts.LegL.material_overlay != b.parts.LegL.material_overlay, id + " hit tint is per instance") a.free() b.free() static_model.free() for id: String in ["e01","e02","e03","e04","e07","e08"]: var figure: Node3D = AssetLibrary.instantiate_fighter(id) var still: Node3D = AssetLibrary.instantiate_model(id) var baked: ArrayMesh = AssetLibrary.bake_mesh(figure) check(figure.has_method("pose") and triangles(baked) > 10000, id + " articulated high-detail enemy") check(triangles(baked) == triangles(still.mesh), id + " enemy rig retains every triangle") check(baked.get_aabb().end.distance_to(still.get_aabb().end) < .001, id + " enemy neutral bounds") figure.free() still.free() for id: String in Soundscape.CLIPS: var stream := load("res://assets/audio/" + id + ".wav") as AudioStreamWAV check(stream != null and stream.get_length() > .1, "Packaged audio " + id) for id: String in DataCatalog.UNITS.keys() + DataCatalog.SENDS.keys() + ["worker", "res_gold", "res_sparks", "res_income", "res_core"]: check(AssetLibrary.icon(id) != null, "Packaged icon " + id) check(AssetLibrary.missing.is_empty(), "No missing models") await capture("menu") main.hud._overlay_action.pressed.emit() main.hud._starter_button.pressed.emit() check(GameState.phase == GameState.Phase.BUILD and EconomyManager.get_team(0).gold == 40, "Menu and paid starter work in package") main.hud._ready_button.grab_focus() key(KEY_V) check(main.hud.model_inspector.visible and GameState.paused, "Model inspector pauses the match") var inspect_remaining: float = WaveManager.remaining WaveManager.advance(2) key(KEY_SPACE) check(main.hud.model_inspector.visible and WaveManager.remaining == inspect_remaining and GameState.phase == GameState.Phase.BUILD, "Inspector blocks ready shortcut and simulation") main.hud.model_inspector._selector.item_selected.emit(3) check(main.hud.model_inspector.model_id == "g04", "Inspector switches to selected upgrade") main.hud.model_inspector._rotate_button.button_pressed = false await capture("model-g04") main.hud.model_inspector._select_model("g10") await capture("model-g10") main.hud.model_inspector._select_model("w10") await capture("model-w10") main.hud.model_inspector._select_model("worker") await capture("model-worker") key(KEY_ESCAPE) check(not main.hud.model_inspector.visible and not GameState.paused and main.hud._ready_button.has_focus(), "Inspector restores running state and focus") key(KEY_P) key(KEY_V) key(KEY_V) check(GameState.paused, "Inspector preserves a pre-existing pause") key(KEY_P) key(KEY_F1) check(main.hud.settings_panel.visible and GameState.paused, "Options pause the match") var remaining: float = WaveManager.remaining WaveManager.advance(2) key(KEY_SPACE) check(WaveManager.remaining == remaining and GameState.phase == GameState.Phase.BUILD, "Options block simulation and ready shortcut") await capture("options") key(KEY_ESCAPE) check(not main.hud.settings_panel.visible and not GameState.paused, "Closing options restores running state") key(KEY_P) key(KEY_F1) key(KEY_ESCAPE) check(GameState.paused, "Options preserve a pre-existing pause") key(KEY_P) var basis: Basis = main.camera.basis key(KEY_E) check(main.camera.basis != basis, "Orbit key rotates camera") key(KEY_R) check(main.camera.basis.is_equal_approx(basis), "Reset restores default angle") # Round trip in an isolated config file; restore the user's actual settings. var path: String = Preferences.storage_path var volume: float = Preferences.volume var muted: bool = Preferences.muted Preferences.storage_path = "user://phase8-smoke-preferences.cfg" Preferences.volume = .37 Preferences.muted = true check(Preferences.save_settings() == OK, "Settings save") Preferences.volume = .9 Preferences.muted = false Preferences.read_settings() check(is_equal_approx(Preferences.volume, .37) and Preferences.muted, "Settings survive reload") DirAccess.remove_absolute(Preferences.storage_path) Preferences.storage_path = path Preferences.volume = volume Preferences.muted = muted Preferences.apply() await capture("build") key(KEY_SPACE) check(GameState.phase == GameState.Phase.COMBAT, "Ready starts combat") for step: int in range(200): WaveManager.advance(1.0/60.0) await capture("combat") # One full acceptance match, using normal production rules and resources. GameState.ai_level = GameState.AI_LEARNING WaveManager.start_match() var player := AI.new() for number: int in range(1, 9): check(GameState.wave == number and GameState.phase == GameState.Phase.BUILD, "Wave %d build boundary" % number) player.plan(0, number, WaveManager.rival_snapshot(), GameState.last_reports[0], GameState.AI_STANDARD) WaveManager.ready_for_battle() for step: int in range(5401): if GameState.phase != GameState.Phase.COMBAT: break WaveManager.advance(1.0/60.0) if number == 8 and step == 330: await capture("boss") check(GameState.phase in [GameState.Phase.RESULTS, GameState.Phase.GAME_OVER], "Wave %d settles" % number) if GameState.phase == GameState.Phase.GAME_OVER: break WaveManager.ready_for_battle() check(GameState.wave == 8 and GameState.outcome == 0, "Actual eight-wave package match is winnable") await capture("victory") print("%s: %d checks; %d failures; engine %s; cores %s" % ["EXPORT_SMOKE_PASS" if failures.is_empty() else "EXPORT_SMOKE_FAIL", checks, failures.size(), Engine.get_version_info().string, str(GameState.core_hp)]) print("SMOKE_CAPTURES: ", ProjectSettings.globalize_path("user://phase8-smoke")) await Soundscape.quit_game(0 if failures.is_empty() else 1)