1298 lines
74 KiB
GDScript
1298 lines
74 KiB
GDScript
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),
|
|
"동도 호송로 (13, 5), (14, 5) · 미도달"
|
|
)
|
|
_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),
|
|
"서쪽 탈출로 (2, 5), (1, 5) · 미도달"
|
|
)
|
|
_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),
|
|
"전장 표식 (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_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 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(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(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_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, "22칸 x 14칸")
|
|
_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, "마을 8칸 +6")
|
|
_check_contains(failures, "001 briefing castle recovery", overview, "성채 9칸 +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_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_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_contains(failures, "006 briefing map size", overview, "14칸 x 10칸")
|
|
_check_contains(failures, "006 briefing enemy count", overview, "적세 8명")
|
|
_check_contains(failures, "006 briefing village recovery", overview, "마을 4칸 +6")
|
|
_check_contains(failures, "006 briefing center recovery", overview, "성채 4칸 +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_contains(failures, "007 briefing map size", overview, "15칸 x 10칸")
|
|
_check_contains(failures, "007 briefing enemy count", overview, "적세 8명")
|
|
_check_contains(failures, "007 briefing village recovery", overview, "마을 4칸 +6")
|
|
_check_contains(failures, "007 briefing road recovery", overview, "성채 4칸 +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_contains(failures, "008 briefing map size", overview, "15칸 x 10칸")
|
|
_check_contains(failures, "008 briefing enemy count", overview, "적세 9명")
|
|
_check_contains(failures, "008 briefing village recovery", overview, "마을 4칸 +6")
|
|
_check_contains(failures, "008 briefing gate recovery", overview, "성채 4칸 +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_contains(failures, "009 briefing map size", overview, "15칸 x 10칸")
|
|
_check_contains(failures, "009 briefing enemy count", overview, "적세 10명")
|
|
_check_contains(failures, "009 briefing village recovery", overview, "마을 4칸 +6")
|
|
_check_contains(failures, "009 briefing gate recovery", overview, "성채 4칸 +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_contains(failures, "010 briefing map size", overview, "15칸 x 10칸")
|
|
_check_contains(failures, "010 briefing enemy count", overview, "적세 9명")
|
|
_check_contains(failures, "010 briefing village recovery", overview, "마을 4칸 +6")
|
|
_check_contains(failures, "010 briefing ferry recovery", overview, "성채 4칸 +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])
|