Files
heros/tools/smoke_objective_progress.gd

1314 lines
76 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"),
"잔여 군령 21"
)
_check_contains(
failures,
"001 late reserve gate",
_progress_text("res://data/scenarios/001_yellow_turbans.json", false),
"제11군령에 전공 갱신"
)
_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,
"006 gated victory progress",
_progress_text("res://data/scenarios/006_dingtao_counterattack.json", false),
"제9군령에 전공 갱신"
)
_check_contains(
failures,
"006 center line progress",
_progress_text("res://data/scenarios/006_dingtao_counterattack.json", false),
"중앙 방진"
)
_check_contains(
failures,
"007 gated victory progress",
_progress_text("res://data/scenarios/007_xian_emperor_escort.json", false),
"제9군령에 전공 갱신"
)
_check_contains(
failures,
"007 destination progress",
_progress_text("res://data/scenarios/007_xian_emperor_escort.json", false),
"동도 호송로 · 미도달"
)
_check_contains(
failures,
"007 protected-unit risk progress",
_defeat_text("res://data/scenarios/007_xian_emperor_escort.json"),
"천자 사자 건재"
)
_check_contains(
failures,
"008 gated victory progress",
_progress_text("res://data/scenarios/008_wan_castle_escape.json", false),
"제9군령에 전공 갱신"
)
_check_contains(
failures,
"008 destination progress",
_progress_text("res://data/scenarios/008_wan_castle_escape.json", false),
"서쪽 탈출로 · 미도달"
)
_check_contains(
failures,
"008 protected-unit risk progress",
_defeat_text("res://data/scenarios/008_wan_castle_escape.json"),
"조앙 건재"
)
_check_contains(
failures,
"009 gated victory progress",
_progress_text("res://data/scenarios/009_xiapi_siege.json", false),
"제9군령에 전공 갱신"
)
_check_contains(
failures,
"009 siege line progress",
_progress_text("res://data/scenarios/009_xiapi_siege.json", false),
"하비 포위선"
)
_check_contains(
failures,
"010 gated victory progress",
_progress_text("res://data/scenarios/010_white_horse_relief.json", false),
"제9군령에 전공 갱신"
)
_check_contains(
failures,
"010 decoy line progress",
_progress_text("res://data/scenarios/010_white_horse_relief.json", false),
"백마 유인선"
)
_check_contains(
failures,
"045 gated destination progress",
_progress_text("res://data/scenarios/045_fancheng_relief.json", false),
"전장 표식 · 미도달"
)
_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_side_event_markers(failures)
_check_opening_battle_requires_castle_capture(failures)
_check_opening_battle_capture_events(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_dingtao_counterattack_extended_pressure(failures)
_check_xian_emperor_escort_extended_pressure(failures)
_check_wan_castle_escape_extended_pressure(failures)
_check_xiapi_siege_extended_pressure(failures)
_check_white_horse_relief_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 "<load failed: %s>" % 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 "<load failed: %s>" % 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_side_event_markers(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("opening side event marker smoke could not load scenario")
return
var cache_cell := Vector2i(4, 2)
var lure_cell := Vector2i(4, 6)
var village_cell := Vector2i(6, 5)
var initial_cells := state.get_event_marker_cells()
if not initial_cells.has(cache_cell):
failures.append("opening battle should mark the northern woods supply cache: %s" % str(initial_cells))
if not initial_cells.has(lure_cell):
failures.append("opening battle should mark the first tactical lure line: %s" % str(initial_cells))
if not initial_cells.has(village_cell):
failures.append("opening battle should mark village supply side event cells: %s" % str(initial_cells))
if state.get_event_marker_label(cache_cell) != "북숲 보급고":
failures.append("northern woods cache marker should expose Korean label: %s" % state.get_event_marker_label(cache_cell))
if state.get_event_marker_kind(cache_cell) != "supply":
failures.append("northern woods cache marker should be a supply marker")
if state.get_event_marker_label(lure_cell) != "유인선":
failures.append("lure line marker should expose Korean label: %s" % state.get_event_marker_label(lure_cell))
if state.get_event_marker_kind(lure_cell) != "tactic":
failures.append("lure line marker should be a tactic marker")
if state.get_event_marker_label(village_cell) != "마을 보급":
failures.append("village supply marker should expose Korean label: %s" % state.get_event_marker_label(village_cell))
if state.get_event_marker_kind(village_cell) != "supply":
failures.append("village supply marker should be a supply marker")
var cao_cao: Dictionary = state.get_unit("cao_cao")
cao_cao["pos"] = cache_cell
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, cao_cao)
if state.get_event_marker_cells().has(cache_cell):
failures.append("northern woods supply marker should clear after pickup")
if int(state.get_inventory_snapshot().get("bean", 0)) < 1:
failures.append("northern woods supply marker event should grant a Bean")
cao_cao["pos"] = village_cell
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 2, cao_cao)
if state.get_event_marker_cells().has(village_cell):
failures.append("village supply marker should clear after securing the village")
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(19, 0), Vector2i(20, 0), Vector2i(21, 0), Vector2i(19, 1), Vector2i(20, 1), Vector2i(21, 1), Vector2i(19, 2), Vector2i(20, 2), Vector2i(21, 2)]:
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(20, 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_ACTIVE:
failures.append("opening battle should wait for the turn 11 last rally before victory, got %s" % str(state.battle_status))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 11)
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 the turn 11 last rally falls and castle is captured, got %s" % str(state.battle_status))
func _check_opening_battle_capture_events(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("opening capture event smoke could not load scenario")
return
var dialogue_batches := []
state.dialogue_requested.connect(func(lines: Array) -> void:
dialogue_batches.append(lines)
)
var cao_cao: Dictionary = state.get_unit("cao_cao")
cao_cao["pos"] = Vector2i(6, 5)
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 3, cao_cao)
if int(state.get_inventory_snapshot().get("bean", 0)) != 1:
failures.append("opening village capture should grant one Bean")
if state.get_battle_gold_reward() != 80:
failures.append("opening village capture should grant 80 battle gold, got %d" % state.get_battle_gold_reward())
var village_objective := str(state.objectives.get("victory", ""))
if not village_objective.contains("마을 보급") or not village_objective.contains("동쪽 성채"):
failures.append("opening village capture should update the active objective: %s" % village_objective)
if dialogue_batches.size() < 1:
failures.append("opening village capture should show contextual dialogue")
cao_cao["pos"] = Vector2i(20, 1)
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 4, cao_cao)
var castle_objective := str(state.objectives.get("victory", ""))
if not castle_objective.contains("성채를 장악") or not castle_objective.contains("마지막 황건 잔당"):
failures.append("opening castle capture should update the active objective: %s" % castle_objective)
if dialogue_batches.size() < 2:
failures.append("opening castle capture should show contextual dialogue")
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_briefing_overview_hides_raw_map_size(failures: Array[String], context: String, overview: String) -> void:
if overview.contains("칸 x") or overview.contains("×") or overview.contains("칸x"):
failures.append("%s should describe battlefield scale without raw grid dimensions: %s" % [context, overview])
if overview.contains("칸 +"):
failures.append("%s should keep recovery cell counts out of visible briefing text: %s" % [context, overview])
if not overview.contains("전장"):
failures.append("%s should keep a readable battlefield scale label: %s" % [context, overview])
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_briefing_overview_hides_raw_map_size(failures, "001 briefing battlefield overview", overview)
_check_contains(failures, "001 briefing enemy count", overview, "적세 16명")
_check_contains(failures, "001 briefing enemy classes", overview, "보병")
_check_contains(failures, "001 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "001 briefing castle recovery", overview, "성채 회복 +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() != 21:
failures.append("opening battle should allow play through turn 21, got limit %d" % state.get_turn_limit())
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16:
failures.append("opening battle should start with 16 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if state._condition_gate_open({"after_event": "turn_11_last_rally"}):
failures.append("opening battle final defeat-all condition should be gated until turn 11 last rally")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4)
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19:
failures.append("opening battle should field 19 enemies after turn 4 reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("xiahou_dun").get("ai_target_priority", 0)) < 9:
failures.append("opening battle turn 4 reserve should pressure Xiahou Dun's vanguard")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 7)
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 22:
failures.append("opening battle should field 22 enemies after turn 7 raiders, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 11:
failures.append("opening battle turn 7 raiders should pressure Cao Cao")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 26:
failures.append("opening battle should field 26 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 12:
failures.append("opening battle turn 9 reserve should keep pressure on Cao Cao")
if state._condition_gate_open({"after_event": "turn_11_last_rally"}):
failures.append("opening battle should stay open-ended until the turn 11 last rally arrives")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 11)
if not state._condition_gate_open({"after_event": "turn_11_last_rally"}):
failures.append("opening battle final gate should open after turn 11 last rally event")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 30:
failures.append("opening battle should field 30 enemies after final rally, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 13 or int(state.get_unit("xiahou_dun").get("ai_target_priority", 0)) < 13:
failures.append("opening battle turn 11 last rally should pressure both Cao Cao and Xiahou Dun")
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_briefing_overview_hides_raw_map_size(failures, "002 briefing battlefield overview", overview)
_check_contains(failures, "002 briefing enemy count", overview, "적세 8명")
_check_contains(failures, "002 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "002 briefing castle recovery", overview, "성채 회복 +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_briefing_overview_hides_raw_map_size(failures, "003 briefing battlefield overview", overview)
_check_contains(failures, "003 briefing enemy count", overview, "적세 6명")
_check_contains(failures, "003 briefing village recovery", overview, "마을 회복 +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_briefing_overview_hides_raw_map_size(failures, "004 briefing battlefield overview", overview)
_check_contains(failures, "004 briefing enemy count", overview, "적세 8명")
_check_contains(failures, "004 briefing village recovery", overview, "마을 회복 +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_briefing_overview_hides_raw_map_size(failures, "005 briefing battlefield overview", overview)
_check_contains(failures, "005 briefing enemy count", overview, "적세 8명")
_check_contains(failures, "005 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "005 briefing camp recovery", overview, "성채 회복 +8")
scene.free()
func _check_dingtao_counterattack_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"):
failures.append("Dingtao Counterattack pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("Dingtao Counterattack 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("Dingtao Counterattack 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_second_charge"}):
failures.append("Dingtao Counterattack final defeat-all condition should be gated until turn 9 second charge")
var marker_cells := state.get_objective_cells()
for center_cell in [Vector2i(8, 3), Vector2i(9, 3), Vector2i(8, 5), Vector2i(9, 5)]:
if not marker_cells.has(center_cell):
failures.append("Dingtao Counterattack should mark center line cell %s: %s" % [str(center_cell), str(marker_cells)])
if state.get_objective_cell_label(center_cell) != "중앙 방진":
failures.append("Dingtao Counterattack center marker should expose label at %s" % str(center_cell))
var recovery_summaries := state.get_recovery_terrain_summaries()
var has_center_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_center_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_center_recovery:
failures.append("Dingtao Counterattack should expose center recovery terrain: %s" % str(recovery_summaries))
if not has_supply_recovery:
failures.append("Dingtao Counterattack 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_second_charge"}):
failures.append("Dingtao Counterattack final gate should open after turn 9 second charge event")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 14:
failures.append("Dingtao Counterattack base branch should field 14 enemies after second charge, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("xiahou_dun_ch6").get("ai_target_priority", 0)) != 8:
failures.append("Dingtao Counterattack turn 6 flank pressure should target Xiahou Dun")
if int(state.get_unit("cao_cao_ch6").get("ai_target_priority", 0)) != 12:
failures.append("Dingtao Counterattack turn 9 second charge should target Cao Cao")
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("Dingtao Counterattack should not end before center line is held, got %s" % str(state.battle_status))
var cao_cao: Dictionary = state.get_unit("cao_cao_ch6")
cao_cao["pos"] = Vector2i(8, 3)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("Dingtao Counterattack should end after second charge falls and center line is held, got %s" % str(state.battle_status))
var fortified_state = BattleStateScript.new()
if not fortified_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"fortified_yan_province": true}):
failures.append("Dingtao Counterattack fortified branch smoke could not load scenario")
return
fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3)
fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if fortified_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16:
failures.append("Dingtao Counterattack fortified branch should field 16 enemies after second charge, got %d" % fortified_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var pressed_state = BattleStateScript.new()
if not pressed_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"pressed_lu_bu": true}):
failures.append("Dingtao Counterattack pressed branch smoke could not load scenario")
return
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17:
failures.append("Dingtao Counterattack pressed branch should field 17 enemies after second charge, got %d" % pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"):
failures.append("Dingtao Counterattack briefing smoke could not load scenario")
scene.free()
return
var overview := scene._format_briefing_battlefield_overview_text()
_check_briefing_overview_hides_raw_map_size(failures, "006 briefing battlefield overview", overview)
_check_contains(failures, "006 briefing enemy count", overview, "적세 8명")
_check_contains(failures, "006 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "006 briefing center recovery", overview, "성채 회복 +8")
scene.free()
func _check_xian_emperor_escort_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"):
failures.append("Xian Emperor Escort pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("Xian Emperor Escort 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("Xian Emperor Escort should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if state._condition_gate_open({"after_event": "turn_9_final_escort_pressure"}):
failures.append("Xian Emperor Escort final defeat-all condition should be gated until turn 9 final pressure")
var marker_cells := state.get_objective_cells()
for road_cell in [Vector2i(12, 4), Vector2i(13, 4)]:
if not marker_cells.has(road_cell):
failures.append("Xian Emperor Escort should mark east road cell %s: %s" % [str(road_cell), str(marker_cells)])
if state.get_objective_cell_label(road_cell) != "동도 호송로":
failures.append("Xian Emperor Escort road marker should expose label at %s" % str(road_cell))
var recovery_summaries := state.get_recovery_terrain_summaries()
var has_road_recovery := false
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)) == 8:
has_road_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_road_recovery:
failures.append("Xian Emperor Escort should expose road recovery terrain: %s" % str(recovery_summaries))
if not has_village_recovery:
failures.append("Xian Emperor Escort should expose village recovery terrain: %s" % str(recovery_summaries))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3)
var cao_cao: Dictionary = state.get_unit("cao_cao_ch7")
cao_cao["pos"] = Vector2i(7, 4)
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, cao_cao)
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
if int(state.get_unit("imperial_envoy_ch7").get("ai_target_priority", 0)) != 10:
failures.append("Xian Emperor Escort turn 6 pressure should target the imperial envoy")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if not state._condition_gate_open({"after_event": "turn_9_final_escort_pressure"}):
failures.append("Xian Emperor Escort final gate should open after turn 9 pressure event")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 18:
failures.append("Xian Emperor Escort should field 18 enemies after ambush and final pressure, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao_ch7").get("ai_target_priority", 0)) != 12:
failures.append("Xian Emperor Escort turn 9 final pressure should target Cao Cao")
if int(state.get_unit("imperial_envoy_ch7").get("ai_target_priority", 0)) != 12:
failures.append("Xian Emperor Escort turn 9 final pressure should keep targeting the imperial envoy")
for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
cao_cao["pos"] = Vector2i(11, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("Xian Emperor Escort should not end before the east road is secured, got %s" % str(state.battle_status))
cao_cao["pos"] = Vector2i(12, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("Xian Emperor Escort should end after final pressure falls and the east road is secured, got %s" % str(state.battle_status))
var no_ambush_state = BattleStateScript.new()
if not no_ambush_state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"):
failures.append("Xian Emperor Escort no-ambush smoke could not load scenario")
return
no_ambush_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3)
no_ambush_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
no_ambush_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if no_ambush_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16:
failures.append("Xian Emperor Escort should field 16 enemies without the road ambush, got %d" % no_ambush_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"):
failures.append("Xian Emperor Escort briefing smoke could not load scenario")
scene.free()
return
var overview := scene._format_briefing_battlefield_overview_text()
_check_briefing_overview_hides_raw_map_size(failures, "007 briefing battlefield overview", overview)
_check_contains(failures, "007 briefing enemy count", overview, "적세 8명")
_check_contains(failures, "007 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "007 briefing road recovery", overview, "성채 회복 +8")
scene.free()
func _check_wan_castle_escape_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("Wan Castle Escape pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("Wan Castle Escape should allow play through turn 17, got limit %d" % state.get_turn_limit())
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 9:
failures.append("Wan Castle Escape should start with 9 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if state._condition_gate_open({"after_event": "turn_9_west_gate_hunt"}):
failures.append("Wan Castle Escape final defeat-all condition should be gated until turn 9 west gate hunt")
var early_cao_cao: Dictionary = state.get_unit("cao_cao_ch8")
early_cao_cao["pos"] = Vector2i(1, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("Wan Castle Escape should not end before turn 9 west gate hunt, got %s" % str(state.battle_status))
early_cao_cao["pos"] = Vector2i(12, 4)
var marker_cells := state.get_objective_cells()
for escape_cell in [Vector2i(1, 4), Vector2i(0, 4)]:
if not marker_cells.has(escape_cell):
failures.append("Wan Castle Escape should mark escape road cell %s: %s" % [str(escape_cell), str(marker_cells)])
if state.get_objective_cell_label(escape_cell) != "서쪽 탈출로":
failures.append("Wan Castle Escape escape marker should expose label at %s" % str(escape_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)) == 4 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("Wan Castle Escape should expose west gate recovery terrain: %s" % str(recovery_summaries))
if not has_village_recovery:
failures.append("Wan Castle Escape should expose inner village recovery terrain: %s" % str(recovery_summaries))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
if int(state.get_unit("cao_ang_ch8").get("ai_target_priority", 0)) != 10:
failures.append("Wan Castle Escape turn 2 gate close should target Cao Ang")
_assert_unit_at(failures, state, "east_gate_rider_north", Vector2i(13, 2), "Wan Castle Escape turn 2 north rider")
_assert_unit_at(failures, state, "east_gate_rider_south", Vector2i(13, 6), "Wan Castle Escape turn 2 south rider")
var cao_cao: Dictionary = state.get_unit("cao_cao_ch8")
cao_cao["pos"] = Vector2i(8, 4)
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, cao_cao)
_assert_unit_at(failures, state, "midroad_wan_archer_north", Vector2i(8, 2), "Wan Castle Escape midroad north archer")
_assert_unit_at(failures, state, "midroad_wan_archer_south", Vector2i(8, 6), "Wan Castle Escape midroad south archer")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
if int(state.get_unit("cao_ang_ch8").get("ai_target_priority", 0)) != 12:
failures.append("Wan Castle Escape turn 6 crossfire should target Cao Ang")
_assert_unit_at(failures, state, "wan_crossfire_archer", Vector2i(9, 2), "Wan Castle Escape turn 6 archer")
_assert_unit_at(failures, state, "wan_crossfire_rider", Vector2i(9, 6), "Wan Castle Escape turn 6 rider")
_assert_unit_at(failures, state, "wan_crossfire_bandit", Vector2i(4, 8), "Wan Castle Escape turn 6 bandit")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if not state._condition_gate_open({"after_event": "turn_9_west_gate_hunt"}):
failures.append("Wan Castle Escape final gate should open after turn 9 west gate hunt")
_assert_unit_at(failures, state, "west_gate_hunt_rider", Vector2i(1, 2), "Wan Castle Escape turn 9 rider")
_assert_unit_at(failures, state, "west_gate_hunt_guard", Vector2i(3, 4), "Wan Castle Escape turn 9 guard")
_assert_unit_at(failures, state, "west_gate_hunt_archer", Vector2i(1, 6), "Wan Castle Escape turn 9 archer")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19:
failures.append("Wan Castle Escape should field 19 enemies after midroad trap and west hunt, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao_ch8").get("ai_target_priority", 0)) != 14:
failures.append("Wan Castle Escape turn 9 west gate hunt should target Cao Cao")
if int(state.get_unit("cao_ang_ch8").get("ai_target_priority", 0)) != 14:
failures.append("Wan Castle Escape turn 9 west gate hunt should keep targeting Cao Ang")
for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
cao_cao["pos"] = Vector2i(2, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("Wan Castle Escape should not end before the escape road is reached, got %s" % str(state.battle_status))
cao_cao["pos"] = Vector2i(1, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("Wan Castle Escape should end after west hunt falls and escape road is reached, got %s" % str(state.battle_status))
var no_trap_state = BattleStateScript.new()
if not no_trap_state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("Wan Castle Escape no-trap smoke could not load scenario")
return
no_trap_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
no_trap_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
no_trap_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if no_trap_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17:
failures.append("Wan Castle Escape should field 17 enemies without the midroad trap, got %d" % no_trap_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var pursued_state = BattleStateScript.new()
if not pursued_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"pursued_li_jue_remnants": true}):
failures.append("Wan Castle Escape pursued branch smoke could not load scenario")
return
pursued_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
pursued_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3)
var pursued_cao_cao: Dictionary = pursued_state.get_unit("cao_cao_ch8")
pursued_cao_cao["pos"] = Vector2i(8, 4)
pursued_state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, pursued_cao_cao)
pursued_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
pursued_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if pursued_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 21:
failures.append("Wan Castle Escape pursued branch should field 21 enemies after west hunt, got %d" % pursued_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var court_state = BattleStateScript.new()
if not court_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"secured_imperial_court": true}):
failures.append("Wan Castle Escape court branch smoke could not load scenario")
return
court_state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 3)
if court_state.get_unit("imperial_guard_support_ch8").is_empty():
failures.append("Wan Castle Escape court branch should add imperial guard support")
else:
_assert_unit_at(failures, court_state, "imperial_guard_support_ch8", Vector2i(9, 4), "Wan Castle Escape court support")
var defeat_state = BattleStateScript.new()
if not defeat_state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("Wan Castle Escape defeat smoke could not load scenario")
return
var cao_ang: Dictionary = defeat_state.get_unit("cao_ang_ch8")
cao_ang["alive"] = false
defeat_state._check_battle_status()
if str(defeat_state.battle_status) != BattleStateScript.STATUS_DEFEAT:
failures.append("Wan Castle Escape should fail when Cao Ang is defeated, got %s" % str(defeat_state.battle_status))
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("Wan Castle Escape briefing smoke could not load scenario")
scene.free()
return
var overview := scene._format_briefing_battlefield_overview_text()
_check_briefing_overview_hides_raw_map_size(failures, "008 briefing battlefield overview", overview)
_check_contains(failures, "008 briefing enemy count", overview, "적세 9명")
_check_contains(failures, "008 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "008 briefing gate recovery", overview, "성채 회복 +8")
scene.free()
func _check_xiapi_siege_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/009_xiapi_siege.json"):
failures.append("Xiapi Siege pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("Xiapi Siege should allow play through turn 17, got limit %d" % state.get_turn_limit())
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 10:
failures.append("Xiapi Siege should start with 10 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if state._condition_gate_open({"after_event": "turn_9_lu_bu_final_breakout"}):
failures.append("Xiapi Siege final defeat-all condition should be gated until turn 9 final breakout")
var early_state = BattleStateScript.new()
if not early_state.load_battle("res://data/scenarios/009_xiapi_siege.json"):
failures.append("Xiapi Siege early victory smoke could not load scenario")
return
var early_cao_cao: Dictionary = early_state.get_unit("cao_cao_ch9")
early_cao_cao["pos"] = Vector2i(8, 4)
for enemy in early_state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
early_state._check_battle_status()
if str(early_state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("Xiapi Siege should not end before turn 9 final breakout, got %s" % str(early_state.battle_status))
var cao_cao: Dictionary = state.get_unit("cao_cao_ch9")
var marker_cells := state.get_objective_cells()
for siege_cell in [Vector2i(8, 4), Vector2i(9, 4), Vector2i(8, 5), Vector2i(9, 5)]:
if not marker_cells.has(siege_cell):
failures.append("Xiapi Siege should mark siege line cell %s: %s" % [str(siege_cell), str(marker_cells)])
if state.get_objective_cell_label(siege_cell) != "하비 포위선":
failures.append("Xiapi Siege marker should expose siege line label at %s" % str(siege_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)) == 4 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("Xiapi Siege should expose flooded gate recovery terrain: %s" % str(recovery_summaries))
if not has_village_recovery:
failures.append("Xiapi Siege should expose siege camp recovery terrain: %s" % str(recovery_summaries))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
_assert_unit_at(failures, state, "lu_bu_flank_rider_north", Vector2i(6, 1), "Xiapi Siege turn 2 north rider")
_assert_unit_at(failures, state, "lu_bu_flank_rider_south", Vector2i(6, 8), "Xiapi Siege turn 2 south rider")
state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 3)
if not state._condition_gate_open({"after_event": "turn_3_flood_gates"}):
failures.append("Xiapi Siege flood gate event should fire on turn 3")
if bool(state.get_unit("lu_bu_flank_rider_north").get("deployed", true)):
failures.append("Xiapi Siege turn 3 flood should withdraw north rider")
if bool(state.get_unit("lu_bu_flank_rider_south").get("deployed", true)):
failures.append("Xiapi Siege turn 3 flood should withdraw south rider")
_assert_unit_at(failures, state, "flooded_gate_guard", Vector2i(7, 4), "Xiapi Siege turn 3 gate guard")
_assert_unit_at(failures, state, "flooded_wall_archer", Vector2i(8, 6), "Xiapi Siege turn 3 wall archer")
if state._condition_gate_open({"after_event": "turn_9_lu_bu_final_breakout"}):
failures.append("Xiapi Siege final gate should still be closed after flood gate event")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
if int(state.get_unit("xiahou_dun_ch9").get("ai_target_priority", 0)) != 10:
failures.append("Xiapi Siege turn 6 counter should target Xiahou Dun")
_assert_unit_at(failures, state, "siege_line_counter_rider", Vector2i(6, 1), "Xiapi Siege turn 6 rider")
_assert_unit_at(failures, state, "siege_line_counter_guard", Vector2i(6, 8), "Xiapi Siege turn 6 guard")
_assert_unit_at(failures, state, "siege_line_counter_archer", Vector2i(9, 8), "Xiapi Siege turn 6 archer")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if not state._condition_gate_open({"after_event": "turn_9_lu_bu_final_breakout"}):
failures.append("Xiapi Siege final gate should open after turn 9 final breakout")
_assert_unit_at(failures, state, "lu_bu_final_breakout_rider", Vector2i(13, 3), "Xiapi Siege turn 9 rider")
_assert_unit_at(failures, state, "lu_bu_final_breakout_guard", Vector2i(13, 5), "Xiapi Siege turn 9 guard")
_assert_unit_at(failures, state, "lu_bu_final_breakout_archer", Vector2i(13, 7), "Xiapi Siege turn 9 archer")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 18:
failures.append("Xiapi Siege should field 18 enemies after final breakout, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao_ch9").get("ai_target_priority", 0)) != 13:
failures.append("Xiapi Siege turn 9 final breakout should target Cao Cao")
if int(state.get_unit("xiahou_dun_ch9").get("ai_target_priority", 0)) != 13:
failures.append("Xiapi Siege turn 9 final breakout should target Xiahou Dun")
for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
cao_cao["pos"] = Vector2i(7, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("Xiapi Siege should not end before the siege line is held, got %s" % str(state.battle_status))
cao_cao["pos"] = Vector2i(8, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("Xiapi Siege should end after final breakout falls and the siege line is held, got %s" % str(state.battle_status))
var swift_state = BattleStateScript.new()
if not swift_state.load_battle("res://data/scenarios/009_xiapi_siege.json", {}, {}, {"swift_wan_escape": true}):
failures.append("Xiapi Siege swift branch smoke could not load scenario")
return
swift_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
swift_state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 3)
swift_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4)
swift_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
swift_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if swift_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19:
failures.append("Xiapi Siege swift branch should field 19 enemies after final breakout, got %d" % swift_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/009_xiapi_siege.json"):
failures.append("Xiapi Siege briefing smoke could not load scenario")
scene.free()
return
var overview := scene._format_briefing_battlefield_overview_text()
_check_briefing_overview_hides_raw_map_size(failures, "009 briefing battlefield overview", overview)
_check_contains(failures, "009 briefing enemy count", overview, "적세 10명")
_check_contains(failures, "009 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "009 briefing gate recovery", overview, "성채 회복 +8")
scene.free()
func _check_white_horse_relief_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/010_white_horse_relief.json"):
failures.append("White Horse Relief pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("White Horse Relief should allow play through turn 17, got limit %d" % state.get_turn_limit())
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 9:
failures.append("White Horse Relief should start with 9 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
_assert_required_unit(failures, state, "guo_jia_ch10", "White Horse Relief Guo Jia")
_assert_required_unit(failures, state, "xiahou_dun_ch10", "White Horse Relief Xiahou Dun")
if state._condition_gate_open({"after_event": "turn_9_yuan_main_pressure"}):
failures.append("White Horse Relief final defeat-all condition should be gated until turn 9 Yuan main pressure")
var early_state = BattleStateScript.new()
if not early_state.load_battle("res://data/scenarios/010_white_horse_relief.json"):
failures.append("White Horse Relief early victory smoke could not load scenario")
return
var early_cao_cao: Dictionary = early_state.get_unit("cao_cao_ch10")
early_cao_cao["pos"] = Vector2i(8, 4)
for enemy in early_state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
early_state._check_battle_status()
if str(early_state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("White Horse Relief should not end before turn 9 Yuan main pressure, got %s" % str(early_state.battle_status))
var marker_cells := state.get_objective_cells()
for decoy_cell in [Vector2i(8, 4), Vector2i(9, 4), Vector2i(8, 5), Vector2i(9, 5)]:
if not marker_cells.has(decoy_cell):
failures.append("White Horse Relief should mark decoy line cell %s: %s" % [str(decoy_cell), str(marker_cells)])
if state.get_objective_cell_label(decoy_cell) != "백마 유인선":
failures.append("White Horse Relief marker should expose decoy line label at %s" % str(decoy_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)) == 4 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("White Horse Relief should expose ferry recovery terrain: %s" % str(recovery_summaries))
if not has_village_recovery:
failures.append("White Horse Relief should expose reserve camp recovery terrain: %s" % str(recovery_summaries))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
_assert_unit_at(failures, state, "yan_liang_rider_north", Vector2i(8, 2), "White Horse Relief turn 2 north rider")
_assert_unit_at(failures, state, "yan_liang_rider_south", Vector2i(8, 6), "White Horse Relief turn 2 south rider")
state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 3)
if not state._condition_gate_open({"after_event": "turn_3_decoy_line"}):
failures.append("White Horse Relief decoy line event should fire on turn 3")
_assert_unit_at(failures, state, "decoy_cutoff_guard", Vector2i(7, 4), "White Horse Relief turn 3 cutoff guard")
_assert_unit_at(failures, state, "decoy_cutoff_archer", Vector2i(7, 5), "White Horse Relief turn 3 cutoff archer")
if state._condition_gate_open({"after_event": "turn_9_yuan_main_pressure"}):
failures.append("White Horse Relief final gate should still be closed after decoy line event")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
if int(state.get_unit("xiahou_dun_ch10").get("ai_target_priority", 0)) != 10:
failures.append("White Horse Relief turn 6 pressure should target Xiahou Dun")
_assert_unit_at(failures, state, "yuan_wing_rider", Vector2i(6, 1), "White Horse Relief turn 6 rider")
_assert_unit_at(failures, state, "yuan_wing_guard", Vector2i(6, 8), "White Horse Relief turn 6 guard")
_assert_unit_at(failures, state, "yuan_wing_archer", Vector2i(9, 8), "White Horse Relief turn 6 archer")
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if not state._condition_gate_open({"after_event": "turn_9_yuan_main_pressure"}):
failures.append("White Horse Relief final gate should open after turn 9 Yuan main pressure")
_assert_unit_at(failures, state, "yuan_main_rider", Vector2i(13, 3), "White Horse Relief turn 9 rider")
_assert_unit_at(failures, state, "yuan_main_guard", Vector2i(13, 5), "White Horse Relief turn 9 guard")
_assert_unit_at(failures, state, "yuan_main_archer", Vector2i(13, 7), "White Horse Relief turn 9 archer")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19:
failures.append("White Horse Relief should field 19 enemies after Yuan main pressure, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if int(state.get_unit("cao_cao_ch10").get("ai_target_priority", 0)) != 13:
failures.append("White Horse Relief turn 9 Yuan main pressure should target Cao Cao")
if int(state.get_unit("xiahou_dun_ch10").get("ai_target_priority", 0)) != 13:
failures.append("White Horse Relief turn 9 Yuan main pressure should target Xiahou Dun")
for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
var cao_cao: Dictionary = state.get_unit("cao_cao_ch10")
cao_cao["pos"] = Vector2i(7, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("White Horse Relief should not end before the decoy line is held, got %s" % str(state.battle_status))
cao_cao["pos"] = Vector2i(8, 4)
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("White Horse Relief should end after Yuan pressure falls and decoy line is held, got %s" % str(state.battle_status))
var pressed_state = BattleStateScript.new()
if not pressed_state.load_battle("res://data/scenarios/010_white_horse_relief.json", {}, {}, {"pressed_northern_campaign": true}):
failures.append("White Horse Relief pressed branch smoke could not load scenario")
return
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 3)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 20:
failures.append("White Horse Relief pressed branch should field 20 enemies after Yuan main pressure, got %d" % pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/010_white_horse_relief.json"):
failures.append("White Horse Relief briefing smoke could not load scenario")
scene.free()
return
var overview := scene._format_briefing_battlefield_overview_text()
_check_briefing_overview_hides_raw_map_size(failures, "010 briefing battlefield overview", overview)
_check_contains(failures, "010 briefing enemy count", overview, "적세 9명")
_check_contains(failures, "010 briefing village recovery", overview, "마을 회복 +6")
_check_contains(failures, "010 briefing ferry recovery", overview, "성채 회복 +8")
scene.free()
func _assert_unit_at(failures: Array[String], state, unit_id: String, expected_cell: Vector2i, label: String) -> void:
var unit: Dictionary = state.get_unit(unit_id)
if unit.is_empty() or not bool(unit.get("alive", false)) or not bool(unit.get("deployed", true)):
failures.append("%s should deploy %s" % [label, unit_id])
return
if unit.get("pos", Vector2i(-999, -999)) != expected_cell:
failures.append("%s should be at %s, got %s" % [label, str(expected_cell), str(unit.get("pos", Vector2i(-999, -999)))])
func _assert_required_unit(failures: Array[String], state, unit_id: String, label: String) -> void:
var unit: Dictionary = state.get_unit(unit_id)
if unit.is_empty() or not bool(unit.get("alive", false)) or not bool(unit.get("deployed", true)):
failures.append("%s should deploy %s" % [label, unit_id])
return
if not bool(unit.get("required_deployment", false)):
failures.append("%s should be required deployment: %s" % [label, unit_id])
func _check_opening_board_avoids_side_panel(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("opening board layout smoke could not load scenario")
scene.free()
return
var board_rect := scene._board_rect()
var view_rect := scene._map_view_rect()
var side_rect := Rect2(BattleSceneScript.SIDE_PANEL_POSITION, BattleSceneScript.SIDE_PANEL_SIZE)
if view_rect.intersects(side_rect):
failures.append("opening map view should not overlap side HUD: view %s side %s" % [str(view_rect), str(side_rect)])
if board_rect.size.x <= view_rect.size.x or board_rect.size.y <= view_rect.size.y:
failures.append("opening board should be larger than the visible map view to exercise scrolling: board %s view %s" % [str(board_rect), str(view_rect)])
var far_offset := scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0))
if far_offset.x >= -0.01 or far_offset.y >= -0.01:
failures.append("opening board should allow both horizontal and vertical scrolling: %s" % str(far_offset))
scene.free()
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])