Improve Xian emperor escort battle pacing

This commit is contained in:
2026-06-19 14:26:17 +09:00
parent 585c2e76a0
commit b25743a4a7
4 changed files with 252 additions and 21 deletions

View File

@@ -2,20 +2,28 @@
"id": "007_xian_emperor_escort",
"name": "헌제 호송전",
"objectives": {
"victory": "조조를 동쪽 행로 표식까지 이동시켜라.",
"defeat": "조조 또는 천자 사자가 퇴각하거나 제11군령에 이르면 패한다."
"victory": "동도 호송로를 장악한 뒤 잔당을 격파하라.",
"defeat": "조조 천자 사자가 퇴각하거나 제18군령이 시작되면 패한다."
},
"conditions": {
"victory": {
"type": "unit_reaches_tile",
"team": "player",
"officer_ids": ["cao_cao"],
"pos": [13, 4]
"type": "all",
"conditions": [
{ "type": "all_units_defeated", "team": "enemy", "after_event": "turn_9_final_escort_pressure" },
{
"type": "unit_reaches_tile",
"label": "동도 호송로",
"team": "player",
"unit_ids": ["cao_cao_ch7"],
"officer_ids": ["cao_cao"],
"cells": [[12, 4], [13, 4]]
}
]
},
"defeat": [
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
{ "type": "any_unit_defeated", "unit_ids": ["imperial_envoy_ch7"] },
{ "type": "turn_reached", "turn": 11, "team": "player" }
{ "type": "turn_reached", "turn": 18, "team": "player" }
]
},
"briefing": {
@@ -23,7 +31,8 @@
"location": "낙양 동도, 건안 원년",
"lines": [
"폐허가 된 낙양에서 천자의 거가가 제후들의 압박을 피해 동쪽 길로 나섰다.",
"잔당이 길목을 죄기 전에 조조를 동도 표식까지 이끌어 호송로를 장악하라."
"잔당이 길목을 죄기 전에 조조를 동도 표식까지 이끌어 호송로를 장악하라.",
"후미의 사자를 지키며 제9군령의 마지막 압박까지 견디면, 조정의 행렬이 안전하게 빠져나갈 수 있다."
],
"conditional_lines": [
{
@@ -140,11 +149,11 @@
"GGGGGGGGGGGGGGG",
"GGGFFGGGHHGGGGG",
"GGGFFRRRHHGGGGG",
"GGGGGRRRGGFFFGG",
"GGGGGRRRGGCCFGG",
"RRRRRRRRRRRRRRR",
"GGGGGRRRGGFFFGG",
"GGHHGFFFGGGRRGG",
"GGHHGFFFGGGWWGG",
"GGGGGRRRGGCCFGG",
"GGHHTTFFGGGRRGG",
"GGHHTTFFGGGWWGG",
"GGGGGGFFGGGWWGG",
"GGGGGGGGGGGGGGG"
]
@@ -239,6 +248,33 @@
"level": 7,
"pos": [12, 6],
"base": { "hp": 36, "mp": 20, "atk": 7, "def": 6, "int": 13 }
},
{
"unit_id": "escort_gate_spearman",
"name": "동도 창병",
"class_id": "infantry",
"team": "enemy",
"level": 7,
"pos": [10, 3],
"base": { "hp": 44, "atk": 15, "def": 9 }
},
{
"unit_id": "escort_south_guard",
"name": "남쪽 방패병",
"class_id": "infantry",
"team": "enemy",
"level": 7,
"pos": [11, 5],
"base": { "hp": 44, "atk": 14, "def": 10 }
},
{
"unit_id": "escort_woods_rider",
"name": "숲길 기병",
"class_id": "cavalry",
"team": "enemy",
"level": 7,
"pos": [9, 6],
"base": { "hp": 42, "atk": 16, "def": 8, "agi": 11 }
}
],
"events": [
@@ -305,7 +341,7 @@
{
"id": "midroad_ambush",
"once": true,
"when": { "type": "unit_reaches_tile", "team": "player", "pos": [7, 4] },
"when": { "type": "unit_reaches_tile", "team": "player", "unit_ids": ["cao_cao_ch7"], "officer_ids": ["cao_cao"], "pos": [7, 4] },
"actions": [
{
"type": "dialogue",
@@ -352,6 +388,117 @@
]
}
]
},
{
"id": "turn_6_envoy_pincher",
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 6 },
"actions": [
{ "type": "log", "text": "잔당이 후미 사자를 노리고 숲길 양쪽에서 밀려든다." },
{
"type": "dialogue",
"lines": [
{ "speaker": "Scout", "text": "남쪽 숲과 북쪽 언덕에서 동시에 움직임이 있습니다!" },
{ "speaker": "Dian Wei", "text": "사자 곁을 비우지 마십시오. 적이 찾는 것은 조조가 아니라 행렬의 약한 고리입니다." }
]
},
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "envoy_pincher_archer",
"name": "북쪽 궁병",
"class_id": "archer",
"team": "enemy",
"level": 7,
"pos": [6, 1],
"base": { "hp": 34, "atk": 13, "def": 6 }
},
{
"unit_id": "envoy_pincher_bandit",
"name": "남쪽 도적",
"class_id": "bandit",
"team": "enemy",
"level": 7,
"pos": [6, 8],
"base": { "hp": 42, "atk": 14, "def": 7 }
},
{
"unit_id": "envoy_pincher_rider",
"name": "숲길 추격기",
"class_id": "cavalry",
"team": "enemy",
"level": 7,
"pos": [9, 8],
"base": { "hp": 42, "atk": 16, "def": 8, "agi": 11 }
}
]
},
{
"type": "set_ai_target_priority",
"unit_id": "imperial_envoy_ch7",
"priority": 10,
"text": "후미 압박대가 천자 사자를 노린다."
}
]
},
{
"id": "turn_9_final_escort_pressure",
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 9 },
"actions": [
{
"type": "set_objective",
"victory": "동도 호송로를 지키며 마지막 잔당을 격파하라.",
"defeat": "조조나 천자 사자가 퇴각하거나 제18군령이 시작되면 패한다."
},
{ "type": "log", "text": "이각과 곽사의 잔당이 마지막 길목을 막아선다." },
{
"type": "dialogue",
"lines": [
{ "speaker": "Li Jue", "display_speaker": "이각", "text": "천자의 행렬이 여기까지 온다 해도, 동쪽 길은 아직 우리 창끝 아래 있다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "text": "길은 황제가 지나가면 길이 되고, 막는 자는 잔당이 된다. 동도 호송로를 열어라." }
]
},
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "final_escort_rider",
"name": "마지막 추격기",
"class_id": "cavalry",
"team": "enemy",
"level": 8,
"pos": [13, 3],
"base": { "hp": 48, "atk": 17, "def": 8, "agi": 12 }
},
{
"unit_id": "final_escort_guard",
"name": "마지막 방패병",
"class_id": "infantry",
"team": "enemy",
"level": 8,
"pos": [13, 5],
"base": { "hp": 48, "atk": 15, "def": 10 }
},
{
"unit_id": "final_escort_archer",
"name": "마지막 궁병",
"class_id": "archer",
"team": "enemy",
"level": 8,
"pos": [12, 2],
"base": { "hp": 36, "atk": 14, "def": 6 }
}
]
},
{
"type": "set_ai_target_priority",
"unit_ids": ["cao_cao_ch7", "imperial_envoy_ch7"],
"priority": 12,
"text": "마지막 잔당이 조조와 천자 사자를 함께 노린다."
}
]
}
],
"rewards": {

View File

@@ -1515,8 +1515,8 @@ func _create_hud() -> void:
briefing_objective_header.add_child(_make_section_caption("승리와 패배", 548))
briefing_objective_toggle_button = Button.new()
briefing_objective_toggle_button.text = "조건 접"
briefing_objective_toggle_button.tooltip_text = "승리와 패배 조건을 거나 펼칩니다."
briefing_objective_toggle_button.text = ""
briefing_objective_toggle_button.tooltip_text = "승리와 패배 조건을 거나 펼칩니다."
briefing_objective_toggle_button.custom_minimum_size = Vector2(124, 28)
briefing_objective_toggle_button.pressed.connect(_on_briefing_objective_toggle_pressed)
briefing_objective_header.add_child(briefing_objective_toggle_button)
@@ -5798,7 +5798,7 @@ func _update_briefing_objective_visibility() -> void:
briefing_objective_panel.visible = has_text and not briefing_objective_collapsed
if briefing_objective_toggle_button != null:
briefing_objective_toggle_button.visible = has_text
briefing_objective_toggle_button.text = "조건 보" if briefing_objective_collapsed else "조건 접"
briefing_objective_toggle_button.text = "펼치" if briefing_objective_collapsed else ""
func _on_begin_battle_pressed() -> void:

View File

@@ -103,11 +103,17 @@ func _init() -> void:
_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),
"전장 표식 (14, 5) · 미도달"
"동도 호송로 (13, 5), (14, 5) · 미도달"
)
_check_contains(
failures,
@@ -130,6 +136,7 @@ func _init() -> void:
_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_objective_update(failures)
_check_objective_notice(failures)
_check_terrain_recovery(failures)
@@ -667,6 +674,83 @@ func _check_dingtao_counterattack_extended_pressure(failures: Array[String]) ->
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_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"):

View File

@@ -3199,14 +3199,14 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
failures.append("mission objective panel should reopen cleanly")
if scene.briefing_start_button == null or scene.briefing_start_button.text != "전투 시작":
failures.append("briefing start button should clearly start after briefing")
if scene.briefing_objective_toggle_button == null or scene.briefing_objective_toggle_button.text != "조건 접":
failures.append("briefing objective panel should expose a collapse button")
if scene.briefing_objective_toggle_button == null or scene.briefing_objective_toggle_button.text != "":
failures.append("briefing objective panel should expose a close button")
else:
scene._set_briefing_objective_collapsed(true)
if scene.briefing_objective_panel == null or scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "조건 보":
if scene.briefing_objective_panel == null or scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "펼치":
failures.append("briefing objective panel should collapse cleanly")
scene._set_briefing_objective_collapsed(false)
if scene.briefing_objective_panel == null or not scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "조건 접":
if scene.briefing_objective_panel == null or not scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "":
failures.append("briefing objective panel should reopen cleanly")
scene.briefing_panel.visible = true
scene.battle_started = false