Improve Qingzhou campaign battle pacing

This commit is contained in:
2026-06-19 13:56:46 +09:00
parent 0451550234
commit 37d087e1c6
2 changed files with 227 additions and 9 deletions

View File

@@ -2,14 +2,27 @@
"id": "004_qingzhou_campaign",
"name": "청주 평정전",
"objectives": {
"victory": "황건 잔당의 우두머리를 격파하라.",
"defeat": "조조가 퇴각하거나 제12턴에 이르면 패한다."
"victory": "청주 촌락을 지키고 황건 잔당을 평정하라.",
"defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다."
},
"conditions": {
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_bandit_push" },
"victory": {
"type": "all",
"conditions": [
{ "type": "all_units_defeated", "team": "enemy", "after_event": "turn_9_village_last_stand" },
{
"type": "unit_reaches_tile",
"label": "촌락 보호",
"team": "player",
"unit_ids": ["cao_cao_ch4", "xiahou_dun_ch4", "xiahou_yuan_ch4", "cao_ren_ch4"],
"officer_ids": ["cao_cao", "xiahou_dun", "xiahou_yuan", "cao_ren"],
"cells": [[10, 3], [11, 3], [10, 5], [11, 5]]
}
]
},
"defeat": [
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
{ "type": "turn_reached", "turn": 12, "team": "player" }
{ "type": "turn_reached", "turn": 18, "team": "player" }
]
},
"briefing": {
@@ -18,7 +31,8 @@
"lines": [
"연합군이 흩어진 뒤 조조는 청주로 눈을 돌렸다.",
"황건 잔당이 마을과 상로를 어지럽히며 백성을 겁박한다.",
"길목을 장악하고 적장을 베어, 흩어진 장정을 군율 아래 모아라."
"길목을 장악하고 적장을 베어, 흩어진 장정을 군율 아래 모아라.",
"동쪽 촌락을 지키며 잔당의 마지막 반격까지 꺾으면, 백성을 지키던 무사 전위가 조조군을 지켜볼 것이다."
],
"conditional_lines": [
{
@@ -127,9 +141,9 @@
"GGGGGGGGGGGGGG",
"GGGFFFGGGHHGGG",
"GGGFFFGRRHHGGG",
"GGGGGGRRRGGGGG",
"GGGGGGRRRGTTGG",
"RRRRRRRRRRRRRR",
"GGGGGGRRRGGGGG",
"GGGGGGRRRGTTGG",
"GGHGGGFFFGGGGG",
"GGHHGGFFFGGWWG",
"GGGGGGGFFGGWWG",
@@ -207,6 +221,33 @@
"level": 4,
"pos": [11, 6],
"base": { "hp": 32, "mp": 16, "atk": 7, "def": 4, "int": 11 }
},
{
"unit_id": "qingzhou_village_guard",
"name": "촌락 약탈병",
"class_id": "bandit",
"team": "enemy",
"level": 4,
"pos": [10, 3],
"base": { "hp": 36, "atk": 13, "def": 5 }
},
{
"unit_id": "qingzhou_road_spearman",
"name": "상로 창병",
"class_id": "infantry",
"team": "enemy",
"level": 4,
"pos": [8, 4],
"base": { "hp": 40, "atk": 13, "def": 7 }
},
{
"unit_id": "qingzhou_hill_archer",
"name": "언덕 궁병",
"class_id": "archer",
"team": "enemy",
"level": 4,
"pos": [11, 2],
"base": { "hp": 30, "atk": 12, "def": 5 }
}
],
"events": [
@@ -248,8 +289,8 @@
},
{
"type": "set_objective",
"victory": "청주 두령과 모든 약탈병을 격파하라.",
"defeat": "조조가 퇴각하거나 제12턴에 이르면 패한다."
"victory": "촌락 길목을 지키며 청주 두령과 약탈병을 격파하라.",
"defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다."
},
{
"type": "spawn_deployments",
@@ -298,6 +339,117 @@
]
}
]
},
{
"id": "turn_6_village_raiders",
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 6 },
"actions": [
{ "type": "log", "text": "마을 뒤편에서 잔당의 별동대가 다시 약탈길을 연다." },
{
"type": "dialogue",
"lines": [
{ "speaker": "Cao Ren", "display_speaker": "조인", "text": "마을 뒤쪽에 또 다른 약탈병입니다. 길목을 비우면 백성들이 먼저 베입니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "text": "적을 쫓는 칼보다 백성을 등지는 발이 더 무겁다. 촌락을 붙들어라." }
]
},
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "qingzhou_village_raider_west",
"name": "촌락 약탈병",
"class_id": "bandit",
"team": "enemy",
"level": 4,
"pos": [5, 6],
"base": { "hp": 34, "atk": 13, "def": 5 }
},
{
"unit_id": "qingzhou_village_raider_spear",
"name": "촌락 창병",
"class_id": "infantry",
"team": "enemy",
"level": 4,
"pos": [8, 6],
"base": { "hp": 38, "atk": 13, "def": 7 }
},
{
"unit_id": "qingzhou_village_raider_archer",
"name": "촌락 궁병",
"class_id": "archer",
"team": "enemy",
"level": 4,
"pos": [12, 6],
"base": { "hp": 30, "atk": 12, "def": 4 }
}
]
},
{
"type": "set_ai_target_priority",
"unit_id": "xiahou_dun_ch4",
"priority": 8,
"text": "촌락 별동대가 하후돈의 엄호선을 노린다."
}
]
},
{
"id": "turn_9_village_last_stand",
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 9 },
"actions": [
{ "type": "log", "text": "청주 두령의 마지막 무리가 촌락 입구로 몰려든다." },
{
"type": "set_objective",
"victory": "마지막 잔당을 격파하고 촌락 길목을 지켜라.",
"defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다."
},
{
"type": "dialogue",
"lines": [
{ "speaker": "Dian Wei", "display_speaker": "전위", "text": "백성을 등지고 달아나는 군대가 아니라면, 그 길목을 끝까지 지켜 보십시오." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "text": "듣던 무사가 저 자인가. 좋다. 청주의 길목에서 우리 군율을 보이겠다." }
]
},
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "qingzhou_last_guard",
"name": "잔당 호위병",
"class_id": "infantry",
"team": "enemy",
"level": 4,
"pos": [12, 3],
"base": { "hp": 42, "atk": 14, "def": 8 }
},
{
"unit_id": "qingzhou_last_rider",
"name": "잔당 기병",
"class_id": "cavalry",
"team": "enemy",
"level": 4,
"pos": [13, 4],
"base": { "hp": 42, "atk": 15, "def": 7 }
},
{
"unit_id": "qingzhou_last_archer",
"name": "잔당 궁병",
"class_id": "archer",
"team": "enemy",
"level": 4,
"pos": [12, 5],
"base": { "hp": 30, "atk": 12, "def": 4 }
}
]
},
{
"type": "set_ai_target_priority",
"unit_id": "cao_cao_ch4",
"priority": 11,
"text": "마지막 잔당이 조조의 깃발을 향해 달려든다."
}
]
}
],
"rewards": {

View File

@@ -67,6 +67,18 @@ func _init() -> void:
_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,
"007 destination progress",
@@ -91,6 +103,7 @@ func _init() -> void:
_check_opening_battle_requires_castle_capture(failures)
_check_sishui_gate_extended_pressure(failures)
_check_xingyang_ambush_extended_pressure(failures)
_check_qingzhou_campaign_extended_pressure(failures)
_check_objective_update(failures)
_check_objective_notice(failures)
_check_terrain_recovery(failures)
@@ -435,6 +448,59 @@ func _check_xingyang_ambush_extended_pressure(failures: Array[String]) -> void:
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_opening_board_avoids_side_panel(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):