extends SceneTree const BattleStateScript := preload("res://scripts/core/battle_state.gd") const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") func _init() -> void: var failures: Array[String] = [] _check_contains( failures, "001 HUD progress", _progress_text("res://data/scenarios/001_yellow_turbans.json", false), "적군 격파 0/" ) _check_contains( failures, "001 castle capture progress", _progress_text("res://data/scenarios/001_yellow_turbans.json", false), "성채 장악" ) _check_contains( failures, "001 commander defeat risk", _defeat_text("res://data/scenarios/001_yellow_turbans.json"), "조조 건재" ) _check_contains( failures, "001 turn pressure risk", _defeat_text("res://data/scenarios/001_yellow_turbans.json"), "잔여 군령 17" ) _check_contains( failures, "001 late reserve gate", _progress_text("res://data/scenarios/001_yellow_turbans.json", false), "제9군령에 전공 갱신" ) _check_contains( failures, "002 gated victory progress", _progress_text("res://data/scenarios/002_sishui_gate.json", false), "제8군령에 전공 갱신" ) _check_contains( failures, "002 pending enemy progress", _progress_text("res://data/scenarios/002_sishui_gate.json", false), "적군 격파 0/" ) _check_contains( failures, "002 gate capture progress", _progress_text("res://data/scenarios/002_sishui_gate.json", false), "관문 장악" ) _check_contains( failures, "003 gated victory progress", _progress_text("res://data/scenarios/003_xingyang_ambush.json", false), "제9군령에 전공 갱신" ) _check_contains( failures, "003 road capture progress", _progress_text("res://data/scenarios/003_xingyang_ambush.json", false), "동쪽 길목" ) _check_contains( failures, "004 gated victory progress", _progress_text("res://data/scenarios/004_qingzhou_campaign.json", false), "제9군령에 전공 갱신" ) _check_contains( failures, "004 village protection progress", _progress_text("res://data/scenarios/004_qingzhou_campaign.json", false), "촌락 보호" ) _check_contains( failures, "005 gated victory progress", _progress_text("res://data/scenarios/005_puyang_raid.json", false), "제9군령에 전공 갱신" ) _check_contains( failures, "005 camp capture progress", _progress_text("res://data/scenarios/005_puyang_raid.json", false), "군영 장악" ) _check_contains( failures, "007 destination progress", _progress_text("res://data/scenarios/007_xian_emperor_escort.json", false), "전장 표식 (14, 5) · 미도달" ) _check_contains( failures, "007 protected-unit risk progress", _defeat_text("res://data/scenarios/007_xian_emperor_escort.json"), "천자 사자 건재" ) _check_contains( failures, "045 gated destination progress", _progress_text("res://data/scenarios/045_fancheng_relief.json", false), "전장 표식 (10, 5) · 미도달" ) _check_event_gated_objective_markers(failures) _check_event_gated_multi_cell_objective_markers(failures) _check_turn_gated_objective_does_not_create_marker(failures) _check_opening_battle_requires_castle_capture(failures) _check_sishui_gate_extended_pressure(failures) _check_xingyang_ambush_extended_pressure(failures) _check_qingzhou_campaign_extended_pressure(failures) _check_puyang_raid_extended_pressure(failures) _check_objective_update(failures) _check_objective_notice(failures) _check_terrain_recovery(failures) _check_briefing_battlefield_overview(failures) _check_opening_battle_extended_pressure(failures) _check_opening_board_avoids_side_panel(failures) if failures.is_empty(): print("objective progress smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _progress_text(path: String, include_defeat_risks: bool) -> String: var state = BattleStateScript.new() if not state.load_battle(path): return "" % path return state.get_objective_progress_text(include_defeat_risks) func _defeat_text(path: String) -> String: var state = BattleStateScript.new() if not state.load_battle(path): return "" % path return state.get_defeat_progress_text() func _check_objective_update(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("objective update smoke could not load scenario") return var signal_values := [] var logs := [] state.objective_updated.connect(func(victory: String, defeat: String) -> void: signal_values.append({"victory": victory, "defeat": defeat}) ) state.log_added.connect(func(message: String) -> void: logs.append(message) ) state._apply_objective_event({ "victory": "Defeat the new vanguard.", "defeat": "Cao Cao is defeated." }) if signal_values.size() != 1: failures.append("objective update signal expected 1 emission, got %d" % signal_values.size()) else: var signal_value: Dictionary = signal_values[0] if str(signal_value.get("victory", "")) != "Defeat the new vanguard.": failures.append("objective update signal victory mismatch: %s" % str(signal_value.get("victory", ""))) if str(signal_value.get("defeat", "")) != "Cao Cao is defeated.": failures.append("objective update signal defeat mismatch: %s" % str(signal_value.get("defeat", ""))) _check_log_contains(failures, logs, "군령 갱신: Defeat the new vanguard.") _check_log_contains(failures, logs, "패배 조건 갱신: Cao Cao is defeated.") signal_values.clear() logs.clear() state._apply_objective_event({"defeat": "Supply train is lost."}) if signal_values.size() != 1: failures.append("defeat-only objective signal expected 1 emission, got %d" % signal_values.size()) else: var defeat_signal_value: Dictionary = signal_values[0] if not str(defeat_signal_value.get("victory", "")).is_empty(): failures.append("defeat-only objective signal should not include victory: %s" % str(defeat_signal_value.get("victory", ""))) if str(defeat_signal_value.get("defeat", "")) != "Supply train is lost.": failures.append("defeat-only objective signal mismatch: %s" % str(defeat_signal_value.get("defeat", ""))) _check_log_contains(failures, logs, "패배 조건 갱신: Supply train is lost.") func _check_event_gated_objective_markers(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/013_wuchao_raid.json"): failures.append("event-gated objective marker smoke could not load Wuchao raid") return var depot_cell := Vector2i(14, 4) var initial_cells := state.get_objective_cells() if not initial_cells.has(depot_cell): failures.append("Wuchao depot marker missing before event opens victory condition: %s" % str(initial_cells)) var cao_cao: Dictionary = state.get_unit("cao_cao_ch13") if cao_cao.is_empty(): failures.append("event-gated objective marker smoke missing Cao Cao") return cao_cao["pos"] = depot_cell state._run_events("unit_reaches_tile", "player", 1, cao_cao) var opened_cells := state.get_objective_cells() if opened_cells.has(depot_cell): failures.append("Wuchao depot marker should clear after event opens defeat-all objective: %s" % str(opened_cells)) func _check_event_gated_multi_cell_objective_markers(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("multi-cell event-gated objective marker smoke could not load scenario") return state.battle_conditions["victory"] = { "type": "all_units_defeated", "team": "enemy", "after_event": "multi_cell_gate" } state.battle_events.clear() state.battle_events.append({ "id": "multi_cell_gate", "once": true, "when": {"type": "unit_reaches_tile", "team": "player", "cells": [[3, 2], [3, 3]]}, "actions": [{"type": "log", "text": "Gate opened."}] }) var marker_cells := state.get_objective_cells() if not marker_cells.has(Vector2i(3, 2)) or not marker_cells.has(Vector2i(3, 3)): failures.append("multi-cell event-gated marker should include every event cell: %s" % str(marker_cells)) func _check_turn_gated_objective_does_not_create_marker(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("turn-gated objective marker smoke could not load scenario") return state.battle_conditions["victory"] = { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_gate" } state.battle_events.clear() state.battle_events.append({ "id": "turn_gate", "once": true, "when": {"type": "turn_start", "team": "player", "turn": 2}, "actions": [{"type": "log", "text": "Turn gate opened."}] }) var marker_cells := state.get_objective_cells() if not marker_cells.is_empty(): failures.append("turn-gated objective should not create map markers: %s" % str(marker_cells)) func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("opening castle objective smoke could not load scenario") return var marker_cells := state.get_objective_cells() for castle_cell in [Vector2i(12, 0), Vector2i(13, 0), Vector2i(12, 1), Vector2i(13, 1)]: if not marker_cells.has(castle_cell): failures.append("opening battle should mark castle capture cell %s: %s" % [str(castle_cell), str(marker_cells)]) if state.get_objective_cell_label(castle_cell) != "성채 장악": failures.append("opening battle castle marker should expose label at %s" % str(castle_cell)) for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): enemy["alive"] = false state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("opening battle should not end before castle capture, got %s" % str(state.battle_status)) var cao_cao: Dictionary = state.get_unit("cao_cao") cao_cao["pos"] = Vector2i(12, 1) state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("opening battle should wait for late reserve before victory, got %s" % str(state.battle_status)) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): enemy["alive"] = false state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: failures.append("opening battle should end after late reserve falls and castle is captured, got %s" % str(state.battle_status)) func _check_log_contains(failures: Array[String], logs: Array, expected: String) -> void: for log_entry in logs: if str(log_entry) == expected: return failures.append("expected log `%s` in `%s`" % [expected, logs]) func _check_terrain_recovery(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("terrain recovery smoke could not load scenario") return var cao_cao: Dictionary = state.get_unit("cao_cao") cao_cao["pos"] = Vector2i(6, 5) cao_cao["hp"] = 10 state._reset_team_actions(BattleStateScript.TEAM_PLAYER) if int(cao_cao.get("hp", 0)) != 16: failures.append("village terrain should recover 6 hp at phase start, got %d" % int(cao_cao.get("hp", 0))) var boss: Dictionary = state.get_unit("yellow_turban_1") boss["hp"] = 20 state._reset_team_actions(BattleStateScript.TEAM_ENEMY) if int(boss.get("hp", 0)) != 28: failures.append("castle terrain should recover 8 hp at phase start, got %d" % int(boss.get("hp", 0))) func _check_briefing_battlefield_overview(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("briefing battlefield overview smoke could not load scenario") scene.free() return var overview := scene._format_briefing_battlefield_overview_text() _check_contains(failures, "001 briefing map size", overview, "14칸 x 10칸") _check_contains(failures, "001 briefing enemy count", overview, "적세 12명") _check_contains(failures, "001 briefing enemy classes", overview, "보병") _check_contains(failures, "001 briefing village recovery", overview, "마을 4칸 +6") _check_contains(failures, "001 briefing castle recovery", overview, "성채 4칸 +8") scene.free() func _check_opening_battle_extended_pressure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("opening battle pressure smoke could not load scenario") return if state.get_turn_limit() != 17: failures.append("opening battle should allow play through turn 17, got limit %d" % state.get_turn_limit()) if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 12: failures.append("opening battle should start with 12 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_9_western_reserve"}): failures.append("opening battle final defeat-all condition should be gated until turn 9 reserve") state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 7) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if not state._condition_gate_open({"after_event": "turn_9_western_reserve"}): failures.append("opening battle final gate should open after turn 9 reserve event") if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19: failures.append("opening battle should field 19 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) func _check_sishui_gate_extended_pressure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("Sishui Gate pressure smoke could not load scenario") return if state.get_turn_limit() != 16: failures.append("Sishui Gate should allow play through turn 16, got limit %d" % state.get_turn_limit()) if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 8: failures.append("Sishui Gate should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): failures.append("Sishui Gate final defeat-all condition should be gated until turn 8 reserve") var marker_cells := state.get_objective_cells() for gate_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 4), Vector2i(11, 4), Vector2i(10, 5), Vector2i(11, 5)]: if not marker_cells.has(gate_cell): failures.append("Sishui Gate should mark gate capture cell %s: %s" % [str(gate_cell), str(marker_cells)]) if state.get_objective_cell_label(gate_cell) != "관문 장악": failures.append("Sishui Gate gate marker should expose label at %s" % str(gate_cell)) var recovery_summaries := state.get_recovery_terrain_summaries() var has_gate_recovery := false var has_village_recovery := false for summary in recovery_summaries: if str(summary.get("name", "")) == "성채" and int(summary.get("count", 0)) >= 6 and int(summary.get("heal", 0)) == 8: has_gate_recovery = true if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: has_village_recovery = true if not has_gate_recovery: failures.append("Sishui Gate should expose castle recovery terrain: %s" % str(recovery_summaries)) if not has_village_recovery: failures.append("Sishui Gate should expose village recovery terrain: %s" % str(recovery_summaries)) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 5) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 8) if not state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): failures.append("Sishui Gate final gate should open after turn 8 reserve event") if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16: failures.append("Sishui Gate should field 16 enemies after gate reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("Sishui Gate briefing smoke could not load scenario") scene.free() return var overview := scene._format_briefing_battlefield_overview_text() _check_contains(failures, "002 briefing map size", overview, "12칸 x 9칸") _check_contains(failures, "002 briefing enemy count", overview, "적세 8명") _check_contains(failures, "002 briefing village recovery", overview, "마을 4칸 +6") _check_contains(failures, "002 briefing castle recovery", overview, "성채 6칸 +8") scene.free() func _check_xingyang_ambush_extended_pressure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/003_xingyang_ambush.json", {}, {}, {"pursued_dong_zhuo": true}): failures.append("Xingyang Ambush pressure smoke could not load scenario") return if state.get_turn_limit() != 16: failures.append("Xingyang Ambush should allow play through turn 16, got limit %d" % state.get_turn_limit()) if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 6: failures.append("Xingyang Ambush should start with 6 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_9_forest_blockade"}): failures.append("Xingyang Ambush final defeat-all condition should be gated until turn 9 blockade") var marker_cells := state.get_objective_cells() for road_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 4), Vector2i(11, 4)]: if not marker_cells.has(road_cell): failures.append("Xingyang Ambush should mark road capture cell %s: %s" % [str(road_cell), str(marker_cells)]) if state.get_objective_cell_label(road_cell) != "동쪽 길목": failures.append("Xingyang Ambush road marker should expose label at %s" % str(road_cell)) var recovery_summaries := state.get_recovery_terrain_summaries() var has_waystation_recovery := false for summary in recovery_summaries: if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: has_waystation_recovery = true if not has_waystation_recovery: failures.append("Xingyang Ambush should expose roadside recovery terrain: %s" % str(recovery_summaries)) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if not state._condition_gate_open({"after_event": "turn_9_forest_blockade"}): failures.append("Xingyang Ambush final gate should open after turn 9 blockade event") if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 15: failures.append("Xingyang Ambush pursuit branch should field 15 enemies after blockade, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): enemy["alive"] = false state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("Xingyang Ambush should not end before east road capture, got %s" % str(state.battle_status)) var cao_cao: Dictionary = state.get_unit("cao_cao_ch3") cao_cao["pos"] = Vector2i(10, 3) state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: failures.append("Xingyang Ambush should end after blockade falls and east road is captured, got %s" % str(state.battle_status)) var regroup_state = BattleStateScript.new() if not regroup_state.load_battle("res://data/scenarios/003_xingyang_ambush.json", {}, {}, {"regrouped_after_sishui": true}): failures.append("Xingyang Ambush regroup pressure smoke could not load scenario") return regroup_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) regroup_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) regroup_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if regroup_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 14: failures.append("Xingyang Ambush regroup branch should field 14 enemies after blockade, got %d" % regroup_state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/003_xingyang_ambush.json"): failures.append("Xingyang Ambush briefing smoke could not load scenario") scene.free() return var overview := scene._format_briefing_battlefield_overview_text() _check_contains(failures, "003 briefing map size", overview, "12칸 x 9칸") _check_contains(failures, "003 briefing enemy count", overview, "적세 6명") _check_contains(failures, "003 briefing village recovery", overview, "마을 4칸 +6") scene.free() func _check_qingzhou_campaign_extended_pressure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): failures.append("Qingzhou Campaign pressure smoke could not load scenario") return if state.get_turn_limit() != 17: failures.append("Qingzhou Campaign should allow play through turn 17, got limit %d" % state.get_turn_limit()) if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 8: failures.append("Qingzhou Campaign should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_9_village_last_stand"}): failures.append("Qingzhou Campaign final defeat-all condition should be gated until turn 9 last stand") var marker_cells := state.get_objective_cells() for village_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 5), Vector2i(11, 5)]: if not marker_cells.has(village_cell): failures.append("Qingzhou Campaign should mark village protection cell %s: %s" % [str(village_cell), str(marker_cells)]) if state.get_objective_cell_label(village_cell) != "촌락 보호": failures.append("Qingzhou Campaign village marker should expose label at %s" % str(village_cell)) var recovery_summaries := state.get_recovery_terrain_summaries() var has_village_recovery := false for summary in recovery_summaries: if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: has_village_recovery = true if not has_village_recovery: failures.append("Qingzhou Campaign should expose village recovery terrain: %s" % str(recovery_summaries)) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if not state._condition_gate_open({"after_event": "turn_9_village_last_stand"}): failures.append("Qingzhou Campaign final gate should open after turn 9 last stand event") if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17: failures.append("Qingzhou Campaign should field 17 enemies after last stand, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): enemy["alive"] = false state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("Qingzhou Campaign should not end before village protection, got %s" % str(state.battle_status)) var cao_cao: Dictionary = state.get_unit("cao_cao_ch4") cao_cao["pos"] = Vector2i(10, 3) state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: failures.append("Qingzhou Campaign should end after last stand falls and village is protected, got %s" % str(state.battle_status)) var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): failures.append("Qingzhou Campaign briefing smoke could not load scenario") scene.free() return var overview := scene._format_briefing_battlefield_overview_text() _check_contains(failures, "004 briefing map size", overview, "14칸 x 10칸") _check_contains(failures, "004 briefing enemy count", overview, "적세 8명") _check_contains(failures, "004 briefing village recovery", overview, "마을 4칸 +6") scene.free() func _check_puyang_raid_extended_pressure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/005_puyang_raid.json"): failures.append("Puyang Raid pressure smoke could not load scenario") return if state.get_turn_limit() != 17: failures.append("Puyang Raid should allow play through turn 17, got limit %d" % state.get_turn_limit()) if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 8: failures.append("Puyang Raid should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_9_lu_bu_encirclement"}): failures.append("Puyang Raid final defeat-all condition should be gated until turn 9 encirclement") var marker_cells := state.get_objective_cells() for camp_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 5), Vector2i(11, 5)]: if not marker_cells.has(camp_cell): failures.append("Puyang Raid should mark camp capture cell %s: %s" % [str(camp_cell), str(marker_cells)]) if state.get_objective_cell_label(camp_cell) != "군영 장악": failures.append("Puyang Raid camp marker should expose label at %s" % str(camp_cell)) var recovery_summaries := state.get_recovery_terrain_summaries() var has_camp_recovery := false var has_supply_recovery := false for summary in recovery_summaries: if str(summary.get("name", "")) == "성채" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 8: has_camp_recovery = true if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: has_supply_recovery = true if not has_camp_recovery: failures.append("Puyang Raid should expose camp recovery terrain: %s" % str(recovery_summaries)) if not has_supply_recovery: failures.append("Puyang Raid should expose supply recovery terrain: %s" % str(recovery_summaries)) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if not state._condition_gate_open({"after_event": "turn_9_lu_bu_encirclement"}): failures.append("Puyang Raid final gate should open after turn 9 encirclement event") if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17: failures.append("Puyang Raid should field 17 enemies after encirclement, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): enemy["alive"] = false state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("Puyang Raid should not end before camp capture, got %s" % str(state.battle_status)) var cao_cao: Dictionary = state.get_unit("cao_cao_ch5") cao_cao["pos"] = Vector2i(10, 3) state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: failures.append("Puyang Raid should end after encirclement falls and camp is captured, got %s" % str(state.battle_status)) var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/005_puyang_raid.json"): failures.append("Puyang Raid briefing smoke could not load scenario") scene.free() return var overview := scene._format_briefing_battlefield_overview_text() _check_contains(failures, "005 briefing map size", overview, "14칸 x 10칸") _check_contains(failures, "005 briefing enemy count", overview, "적세 8명") _check_contains(failures, "005 briefing village recovery", overview, "마을 4칸 +6") _check_contains(failures, "005 briefing camp recovery", overview, "성채 4칸 +8") scene.free() func _check_opening_board_avoids_side_panel(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("opening board layout smoke could not load scenario") return var board_rect := Rect2( BattleSceneScript.BOARD_OFFSET, Vector2(state.get_map_size()) * BattleSceneScript.TILE_SIZE ) var side_rect := Rect2(BattleSceneScript.SIDE_PANEL_POSITION, BattleSceneScript.SIDE_PANEL_SIZE) if board_rect.intersects(side_rect): failures.append("opening board should not overlap side HUD: board %s side %s" % [str(board_rect), str(side_rect)]) if board_rect.end.y > 720.0: failures.append("opening board should fit within the default viewport height: %s" % str(board_rect)) func _check_objective_notice(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() scene._on_objective_updated("Defeat the new vanguard.", "") if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: failures.append("objective notice panel should be visible after objective update") elif not scene.objective_notice_label.text.contains("군령 갱신") or not scene.objective_notice_label.text.contains("목표: Defeat the new vanguard."): failures.append("objective notice label should use Korean order wording: %s" % scene.objective_notice_label.text) scene._on_objective_updated("", "Supply train is lost.") if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: failures.append("objective notice panel should be visible after defeat update") elif not scene.objective_notice_label.text.contains("군령 갱신") or not scene.objective_notice_label.text.contains("주의: Supply train is lost."): failures.append("objective notice label should use Korean risk wording: %s" % scene.objective_notice_label.text) if scene._format_log_entry_text("Objective updated: Defeat the new vanguard.") != "군령 갱신: Defeat the new vanguard.": failures.append("objective log display should use Korean order wording") if scene._format_log_entry_text("Defeat condition updated: Supply train is lost.") != "패배 조건 갱신: Supply train is lost.": failures.append("defeat log display should use Korean risk wording") if scene._format_log_entry_text("군령 갱신: Defeat the new vanguard.") != "군령 갱신: Defeat the new vanguard.": failures.append("localized objective log should pass through") if scene._format_log_entry_text("패배 조건 갱신: Supply train is lost.") != "패배 조건 갱신: Supply train is lost.": failures.append("localized defeat log should pass through") if scene.objective_notice_timer <= 0.0: failures.append("objective notice timer should be active") scene.free() func _check_contains(failures: Array[String], label: String, text: String, expected: String) -> void: if text.contains(expected): return failures.append("%s expected `%s` in `%s`" % [label, expected, text])