Add story beats to Wan Castle Escape

This commit is contained in:
2026-06-19 16:44:47 +09:00
parent 2d4a3a4a51
commit fd9eceee0e
2 changed files with 199 additions and 0 deletions

View File

@@ -478,6 +478,10 @@
{ "speaker": "Dian Wei", "text": "방패 뒤에 계십시오. 저들의 눈을 제 쪽으로 돌리겠습니다." } { "speaker": "Dian Wei", "text": "방패 뒤에 계십시오. 저들의 눈을 제 쪽으로 돌리겠습니다." }
] ]
}, },
{
"type": "set_objective",
"victory": "중간 매복을 뚫고 조앙을 지키며 서쪽 탈출로를 열어라."
},
{ {
"type": "spawn_deployments", "type": "spawn_deployments",
"deployments": [ "deployments": [
@@ -530,6 +534,11 @@
{ "speaker": "Cao Ang", "display_speaker": "조앙", "text": "제 걱정으로 발을 늦추지 마십시오. 서쪽 길을 먼저 여십시오." } { "speaker": "Cao Ang", "display_speaker": "조앙", "text": "제 걱정으로 발을 늦추지 마십시오. 서쪽 길을 먼저 여십시오." }
] ]
}, },
{
"type": "set_objective",
"victory": "완성 협공을 버티며 조앙을 보호하고 서쪽 탈출로를 장악하라.",
"defeat": "조조나 조앙이 퇴각하거나 제18군령이 시작되면 패한다."
},
{ {
"type": "spawn_deployments", "type": "spawn_deployments",
"deployments": [ "deployments": [
@@ -627,6 +636,63 @@
"text": "마지막 추격대가 조조와 조앙을 함께 노린다." "text": "마지막 추격대가 조조와 조앙을 함께 노린다."
} }
] ]
},
{
"id": "zhang_xiu_vanguard_falls",
"once": true,
"when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["zhang_xiu_vanguard"] },
"actions": [
{
"type": "dialogue",
"lines": [
{ "speaker": "Zhang Xiu", "display_speaker": "장수", "side": "right", "text": "선봉이 무너져도 성 안의 길은 아직 우리 손에 있다. 서문을 막아라!" },
{ "speaker": "Cao Ang", "display_speaker": "조앙", "side": "right", "text": "아버지, 앞길이 열립니다. 제가 뒤를 늦추겠습니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "장수의 선봉을 꺾었다. 조앙을 잃지 말고 서쪽 탈출로까지 밀어라." }
]
},
{
"type": "set_objective",
"victory": "장수 선봉을 꺾었다. 조앙을 지키며 서쪽 탈출로를 열어라."
}
]
},
{
"id": "jia_xu_strategist_falls",
"once": true,
"when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["jia_xu_strategist"] },
"actions": [
{
"type": "dialogue",
"lines": [
{ "speaker": "Jia Xu", "display_speaker": "가후", "side": "right", "text": "계책의 앞줄이 흐트러졌습니다. 그렇다면 미련을 더 깊이 찌르십시오." },
{ "speaker": "Dian Wei", "display_speaker": "전위", "side": "right", "text": "말로 묶는 자를 밀어냈습니다. 남은 것은 문 앞의 쇠뿐입니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "가후의 그물이 잠시 벌어졌다. 서쪽으로 움직이고, 조앙을 후미에 남겨 두지 마라." }
]
},
{
"type": "set_objective",
"victory": "가후의 매복 전열을 흔들었다. 조앙을 지키며 서쪽 탈출로를 장악하라."
}
]
},
{
"id": "west_gate_hunt_rider_falls",
"once": true,
"when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["west_gate_hunt_rider"] },
"actions": [
{
"type": "dialogue",
"lines": [
{ "speaker": "Zhang Xiu", "display_speaker": "장수", "side": "right", "text": "서문 추격기가 꺾였다고? 남은 병력은 문을 버리지 마라!" },
{ "speaker": "Cao Ang", "display_speaker": "조앙", "side": "right", "text": "서쪽 문이 흔들립니다. 지금 빠져나가야 합니다." },
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "마지막 추격의 앞줄을 끊었다. 서쪽 탈출로를 지키며 남은 적을 정리하라." }
]
},
{
"type": "set_objective",
"victory": "서문 추격기를 꺾었다. 서쪽 탈출로를 지키며 남은 완성 추격대를 격파하라."
}
]
} }
], ],
"rewards": { "rewards": {

View File

@@ -30,6 +30,7 @@ func _init() -> void:
_check_puyang_raid_event_dialogue_structure(failures) _check_puyang_raid_event_dialogue_structure(failures)
_check_dingtao_counterattack_event_dialogue_structure(failures) _check_dingtao_counterattack_event_dialogue_structure(failures)
_check_xian_emperor_escort_event_dialogue_structure(failures) _check_xian_emperor_escort_event_dialogue_structure(failures)
_check_wan_castle_escape_event_dialogue_structure(failures)
_check_opening_battle_story_beats(failures) _check_opening_battle_story_beats(failures)
_check_sishui_gate_story_beats(failures) _check_sishui_gate_story_beats(failures)
_check_xingyang_ambush_story_beats(failures) _check_xingyang_ambush_story_beats(failures)
@@ -37,6 +38,7 @@ func _init() -> void:
_check_puyang_raid_story_beats(failures) _check_puyang_raid_story_beats(failures)
_check_dingtao_counterattack_story_beats(failures) _check_dingtao_counterattack_story_beats(failures)
_check_xian_emperor_escort_story_beats(failures) _check_xian_emperor_escort_story_beats(failures)
_check_wan_castle_escape_story_beats(failures)
_check_pixel_unit_helpers(failures) _check_pixel_unit_helpers(failures)
if failures.is_empty(): if failures.is_empty():
@@ -560,6 +562,71 @@ func _check_xian_emperor_escort_event_dialogue_structure(failures: Array[String]
failures.append("Xian Emperor Escort event should update the active objective: %s" % objective_event_id) failures.append("Xian Emperor Escort event should update the active objective: %s" % objective_event_id)
func _check_wan_castle_escape_event_dialogue_structure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("could not load Wan Castle Escape for event dialogue structure")
return
for event_id in [
"opening_dialogue",
"turn_2_gate_closes",
"court_escape_lane",
"remnant_scout_pressure",
"midroad_trap",
"turn_6_last_push",
"turn_6_wan_crossfire",
"turn_9_west_gate_hunt",
"zhang_xiu_vanguard_falls",
"jia_xu_strategist_falls",
"west_gate_hunt_rider_falls"
]:
var event := _event_by_id(state, event_id)
if event.is_empty():
failures.append("Wan Castle Escape missing event: %s" % event_id)
continue
if not _event_has_dialogue_action(event):
failures.append("Wan Castle Escape event should include contextual dialogue: %s" % event_id)
var zhang_xiu_event := _event_by_id(state, "zhang_xiu_vanguard_falls")
if not zhang_xiu_event.is_empty():
var when: Dictionary = zhang_xiu_event.get("when", {})
if str(when.get("type", "")) != "unit_defeated":
failures.append("Zhang Xiu vanguard defeat event should trigger when the unit is defeated")
var unit_ids: Array = when.get("unit_ids", [])
if not unit_ids.has("zhang_xiu_vanguard"):
failures.append("Zhang Xiu vanguard defeat event should watch zhang_xiu_vanguard")
if not _event_has_set_objective_action(zhang_xiu_event):
failures.append("Zhang Xiu vanguard defeat event should clarify the escape objective")
var jia_xu_event := _event_by_id(state, "jia_xu_strategist_falls")
if not jia_xu_event.is_empty():
var when: Dictionary = jia_xu_event.get("when", {})
if str(when.get("type", "")) != "unit_defeated":
failures.append("Jia Xu strategist defeat event should trigger when the unit is defeated")
var unit_ids: Array = when.get("unit_ids", [])
if not unit_ids.has("jia_xu_strategist"):
failures.append("Jia Xu strategist defeat event should watch jia_xu_strategist")
if not _event_has_set_objective_action(jia_xu_event):
failures.append("Jia Xu strategist defeat event should clarify the escape objective")
var west_gate_event := _event_by_id(state, "west_gate_hunt_rider_falls")
if not west_gate_event.is_empty():
var when: Dictionary = west_gate_event.get("when", {})
if str(when.get("type", "")) != "unit_defeated":
failures.append("West gate hunt rider defeat event should trigger when the unit is defeated")
var unit_ids: Array = when.get("unit_ids", [])
if not unit_ids.has("west_gate_hunt_rider"):
failures.append("West gate hunt rider defeat event should watch west_gate_hunt_rider")
if not _event_has_set_objective_action(west_gate_event):
failures.append("West gate hunt rider defeat event should clarify the final escape objective")
for objective_event_id in [
"turn_2_gate_closes",
"midroad_trap",
"turn_6_wan_crossfire",
"turn_9_west_gate_hunt"
]:
var objective_event := _event_by_id(state, objective_event_id)
if not objective_event.is_empty() and not _event_has_set_objective_action(objective_event):
failures.append("Wan Castle Escape event should update the active objective: %s" % objective_event_id)
func _check_opening_battle_story_beats(failures: Array[String]) -> void: func _check_opening_battle_story_beats(failures: Array[String]) -> void:
var state = BattleStateScript.new() var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
@@ -865,6 +932,72 @@ func _check_xian_emperor_escort_story_beats(failures: Array[String]) -> void:
failures.append("Final escort rider defeat objective should still point to the east road and remaining enemies") failures.append("Final escort rider defeat objective should still point to the east road and remaining enemies")
func _check_wan_castle_escape_story_beats(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("could not load Wan Castle Escape for story beat checks")
return
var dialogue_batches: Array = []
state.dialogue_requested.connect(func(lines: Array) -> void:
dialogue_batches.append(lines)
)
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2)
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, 2, cao_cao)
state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 6)
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6)
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
if dialogue_batches.size() < 5:
failures.append("Wan Castle Escape should add dialogue beats for each major escape pressure phase")
var victory_text := str(state.objectives.get("victory", ""))
if not victory_text.contains("서쪽 탈출로") or not victory_text.contains("마지막 추격대"):
failures.append("Wan Castle Escape turn 9 event should update the victory objective for the west gate hunt")
if not state._condition_gate_open({"after_event": "turn_9_west_gate_hunt"}):
failures.append("Wan Castle Escape final objective gate should open after the turn 9 west gate hunt")
if int(state.get_unit("cao_ang_ch8").get("ai_target_priority", 0)) != 14:
failures.append("Wan Castle Escape final pressure should keep Cao Ang under threat")
var zhang_xiu: Dictionary = state.get_unit("zhang_xiu_vanguard")
if zhang_xiu.is_empty():
failures.append("Wan Castle Escape should include Zhang Xiu vanguard for the trap story beat")
else:
zhang_xiu["alive"] = false
state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, zhang_xiu)
if dialogue_batches.size() < 6:
failures.append("Wan Castle Escape should show dialogue when Zhang Xiu vanguard falls")
var zhang_xiu_objective_text := str(state.objectives.get("victory", ""))
if not zhang_xiu_objective_text.contains("조앙") or not zhang_xiu_objective_text.contains("서쪽 탈출로"):
failures.append("Zhang Xiu vanguard defeat objective should still point to Cao Ang and the escape road")
var jia_xu: Dictionary = state.get_unit("jia_xu_strategist")
if jia_xu.is_empty():
failures.append("Wan Castle Escape should include Jia Xu strategist for the ambush story beat")
else:
jia_xu["alive"] = false
state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, jia_xu)
if dialogue_batches.size() < 7:
failures.append("Wan Castle Escape should show dialogue when Jia Xu strategist falls")
var jia_xu_objective_text := str(state.objectives.get("victory", ""))
if not jia_xu_objective_text.contains("조앙") or not jia_xu_objective_text.contains("서쪽 탈출로"):
failures.append("Jia Xu strategist defeat objective should still point to Cao Ang and the escape road")
var west_gate_rider: Dictionary = state.get_unit("west_gate_hunt_rider")
if west_gate_rider.is_empty():
failures.append("Wan Castle Escape should include west gate hunt rider for the final escape story beat")
else:
west_gate_rider["alive"] = false
state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, west_gate_rider)
if dialogue_batches.size() < 8:
failures.append("Wan Castle Escape should show dialogue when west gate hunt rider falls")
var west_gate_objective_text := str(state.objectives.get("victory", ""))
if not west_gate_objective_text.contains("서쪽 탈출로") or not west_gate_objective_text.contains("완성 추격대"):
failures.append("West gate hunt rider defeat objective should still point to the escape road and remaining pursuers")
func _event_by_id(state, event_id: String) -> Dictionary: func _event_by_id(state, event_id: String) -> Dictionary:
for event in state.battle_events: for event in state.battle_events:
if typeof(event) == TYPE_DICTIONARY and str(event.get("id", "")) == event_id: if typeof(event) == TYPE_DICTIONARY and str(event.get("id", "")) == event_id: