extends Node const DIALOGUE := preload("res://scripts/core/dialogue_overlay.gd") const PORTRAIT := preload("res://scripts/core/story_portrait.gd") const PROMPTS := preload("res://scripts/core/input_prompts.gd") const IMPACT := preload("res://scripts/core/impact_fx_3d.gd") const ENTRANCE := preload("res://scripts/core/entrance_fx_3d.gd") var is_probe := false var failures := 0 var captures_dir := "" var captured: Dictionary = {} func _ready() -> void: if is_probe: return GameState.save_locked = true var probe := Node.new() probe.set_script(get_script()) probe.set("is_probe", true) probe.process_mode = Node.PROCESS_MODE_ALWAYS get_tree().root.add_child.call_deferred(probe) probe.call_deferred("_run") func check(condition: bool, message: String) -> void: if not condition: failures += 1 push_error("COMBAT PRESENTATION: " + message) func _frames(count: int = 3) -> void: for frame: int in count: await get_tree().process_frame func _run() -> void: for argument: String in OS.get_cmdline_user_args(): if argument.begins_with("--combat-captures="): captures_dir = argument.trim_prefix("--combat-captures=") if not captures_dir.is_empty(): Loc.set_language("fr") await _frames() check(DataCatalog.loaded_from_master, "master failed: " + DataCatalog.last_load_error) var event := {"id": "win", "type": "victory", "enemy": "ti_gras", "result": "defeated", "color": "#dfac4b", "duration": 2.0} check(DataCatalog._normalize_story_event(event, [], [], {}, {"enemies": DataCatalog.ENEMIES}) is Dictionary, "the authored victory event is rejected") for broken: Dictionary in [{"enemy": "missing"}, {"result": "unknown"}, {"color": "oops"}, {"duration": -1.0}]: var invalid := event.duplicate() invalid.merge(broken, true) check(DataCatalog._normalize_story_event(invalid, [], [], {}, {"enemies": DataCatalog.ENEMIES}) == null, "invalid victory event accepted: %s" % broken) DataCatalog.CHARACTERS["probe_primitive"] = {"display_name": "Probe", "portrait_3d": { "source": "enemies:big_egg_one", "span": 1.2, "aim_height": 0.6}} var portrait := PORTRAIT.new() add_child(portrait) check(portrait.show_character("probe_primitive") and portrait.has_model, "a boss without a GLB has no portrait of its actual primitive") check(portrait.show_character("mld", "enemies:mld_two") and portrait.config.get("source") == "enemies:mld_two", "a shared character shows the wrong combat variant") check(portrait.show_character("mld") and portrait.config.get("source") == "enemies:mld_one", "a combat variant leaked into the next ordinary dialogue") portrait.queue_free() var covered := 0 for room_id: String in DataCatalog.ROOMS: var room: Dictionary = DataCatalog.ROOMS[room_id] for wave: Dictionary in room.get("waves", []): for enemy_id: String in wave.get("enemies", {}): if not bool(DataCatalog.ENEMIES.get(enemy_id, {}).get("boss_bar", false)): continue var found := false for entry: Dictionary in room.get("combat_scenes", []): if entry.get("enemy", "") == enemy_id: found = true covered += 1 check(found, "boss encounter has no combat presentation: %s/%s" % [room_id, enemy_id]) check(covered >= 30, "boss coverage is incomplete") if not DataCatalog.ROOMS.get("rat_cellar", {}).get("combat_scenes", []).is_empty(): await _two_real_fights() await _final_phase() GameState.story_triggers.enabled = false GameState.story_triggers.reset() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.set_custom_mouse_cursor(null) get_tree().paused = false get_tree().change_scene_to_file("res://scenes/main/main_menu.tscn") await _frames(8) AudioDirector.shutdown() # GPU resources kept by static caches must die while RenderingServer lives. VisualHolder.clear_cache() Shape3DFactory._grain = null PROMPTS._icons.clear() IMPACT._textures.clear() ENTRANCE._textures.clear() await get_tree().create_timer(0.5).timeout print("COMBAT PRESENTATION CHECK: ", "OK" if failures == 0 else "FAILED (%d)" % failures) get_tree().quit(0 if failures == 0 else 1) func _read_all(hero: Player, expect_banner: bool = false, enemy: String = "ti_gras", outcome: String = "defeated") -> Array[String]: var lines: Array[String] = [] var held := hero.global_position var observed := "" var seen_banner := false if DIALOGUE.current != null: Input.action_press("move_right") await _frames(3) Input.action_release("move_right") check(hero.global_position.is_equal_approx(held), "movement input crossed the scene's pause") for step: int in 2000: var overlay: Node = DIALOGUE.current if overlay == null: break var story: Node = overlay.get("story") var key := str(story.get("current_scene_id")) + "/" + str(story.get("current_event_id")) var text: Dictionary = story.get("_current_event") if text.get("type", "") == "text" and key != observed: observed = key lines.append(str(text.get("text", ""))) var speaker := str(text.get("speaker", "")) if speaker != "none" and not str(DataCatalog.CHARACTERS.get(speaker, {}).get("portrait_3d", {}).get("source", "")).is_empty(): check(story.portrait().visible, "a combat line has no portrait") if text.get("type", "") == "text" and text.get("speaker", "") != "none" and bool(story.get("_waiting_continue")): await _capture("portrait") var banner: Control = story.get("_victory") if banner != null and banner.visible: seen_banner = true check(banner.get("enemy_id") == enemy and banner.get("result") == Loc.ui("ui.combat.victory." + outcome).to_upper(), "the victory stamp names the wrong opponent or result") if float(banner.get("progress")) > 0.35: await _capture("victoire") check(hero.global_position.is_equal_approx(held), "hero moved under a combat scene") story.press_a() await _frames(2) if expect_banner: check(seen_banner, "the victory event never appeared after the defeat line") return lines func _capture(label: String) -> void: if captures_dir.is_empty() or DisplayServer.get_name() == "headless" or captured.has(label): return captured[label] = true DirAccess.make_dir_recursive_absolute(captures_dir) await RenderingServer.frame_post_draw var picture := get_viewport().get_texture().get_image() check(picture != null and picture.save_png(captures_dir.path_join(label + ".png")) == OK, "could not capture " + label) func _two_real_fights() -> void: GameState._reset_progress_state() GameState.clear_announcements() var texts: Array = [] for visit: int in 2: GameState.story_triggers.enabled = false GameState.story_triggers.reset() GameState.set_scoped_value("run", "debug_arrival_room", "rat_cellar") get_tree().change_scene_to_file("res://scenes/dungeon/dungeon.tscn") await _frames(8) var dungeon: Node = get_tree().current_scene var hero: Player = dungeon.get("player") var director: Node = dungeon.get("combat_presentation") check(director != null, "dungeon has no combat presentation director") if director == null: return director.set("enabled", true) # Advance the authored six-second reinforcement delay, then exercise its # ordinary spawn path. The fight itself still uses the player's weapon. dungeon.call("_advance_waves", 6.2) dungeon.call("_advance_waves", 6.2) await _frames(4) var boss: Enemy = null for node: Node in GameState.scoped_nodes(dungeon, "enemy"): if node is Enemy: node.set_physics_process(false) if node.enemy_id == "ti_gras": boss = node check(boss != null, "the authored cellar has no Ti-Gras") if boss == null: return await _frames(4) check(DIALOGUE.current != null and get_tree().paused, "the introduction did not precede combat") var introduction := await _read_all(hero) check(introduction.size() >= 2, "the full introduction was shortened") await _frames(4) check(DIALOGUE.current == null and not get_tree().paused, "introduction did not release combat") boss.health = 1.0 boss.encounter_defense = {} boss.attack_invulnerable = false hero.global_position = boss.global_position - Vector2(boss.body_radius() + hero.body_radius() + 8.0, 0.0) hero.facing = Vector2.RIGHT hero.mouse_aim_enabled = false check(hero.attack(), "the actual player's final attack could not start") for frame: int in 180: await get_tree().physics_frame if not is_instance_valid(boss) or not boss.active: break check(not is_instance_valid(boss) or not boss.active, "the player's weapon did not defeat Ti-Gras") await _frames(4) check(DIALOGUE.current != null and get_tree().paused, "the defeat line did not follow the fatal blow") var defeat := await _read_all(hero, true) check(not defeat.is_empty(), "the defeated boss was silent") texts.append([introduction, defeat]) await _frames(4) check(DIALOGUE.current == null and not get_tree().paused, "victory did not return control") check(GameState.key_item_amount("metro_pass") == 1, "replaying text duplicated the quest reward") get_tree().change_scene_to_file("res://scenes/main/main_menu.tscn") await _frames(8) check(texts.size() == 2 and texts[0] == texts[1], "the second fight lost or shortened its full texts") func _final_phase() -> void: GameState.story_triggers.enabled = false GameState.story_triggers.reset() GameState._reset_progress_state() GameState.clear_announcements() GameState.set_scoped_value("meta", "dimension_index", 3) GameState.set_fact_value("true_end_ready", true) GameState.set_fact_value("world_is_corrupt", false) GameState.set_scoped_value("run", "debug_arrival_room", "d4_l46_executive_floors") get_tree().change_scene_to_file("res://scenes/dungeon/dungeon.tscn") await _frames(8) var dungeon: Node = get_tree().current_scene var hero: Player = dungeon.get("player") var director: Node = dungeon.get("combat_presentation") director.set("enabled", true) var shell: Enemy = null for node: Node in GameState.scoped_nodes(dungeon, "enemy"): if node is Enemy and node.enemy_id == "big_big_egg": shell = node check(shell != null, "the true-end roof did not spawn its shell") if shell == null: return shell.set_physics_process(false) # A graphical run enables presentations from the first spawn, unlike the # headless fixture. Finish its full feast before testing the fatal transition. if DIALOGUE.current != null: await _read_all(hero) await _frames(3) shell.die() await _frames(4) check(DIALOGUE.current != null and str(DIALOGUE.current.get("scene_id")) == "roof_entity_emerges", "the shell announces final victory before the Maloeuf phase") var creature: Enemy = null for node: Node in GameState.scoped_nodes(dungeon, "enemy"): if node is Enemy and node.enemy_id == "maloeuf": creature = node check(creature != null, "the real death ability did not spawn Maloeuf") if creature == null: if DIALOGUE.current != null: DIALOGUE.current.get("story").skip() return creature.set_physics_process(false) await _read_all(hero) await _frames(4) check(not dungeon.get("run_finished"), "the battle finished between its two phases") creature.die() await _frames(4) check(DIALOGUE.current != null and str(DIALOGUE.current.get("scene_id")) == "maloeuf_combat_defeat", "the final opponent died without its own reply") await _read_all(hero, true, "maloeuf", "destroyed") await _frames(8) get_tree().change_scene_to_file("res://scenes/main/main_menu.tscn") await _frames(8)