From fd9eceee0e97ef2fedd829440d75d20a07027bf0 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 16:44:47 +0900 Subject: [PATCH] Add story beats to Wan Castle Escape --- data/scenarios/008_wan_castle_escape.json | 66 +++++++++++ tools/smoke_chapter_one_polish.gd | 133 ++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/data/scenarios/008_wan_castle_escape.json b/data/scenarios/008_wan_castle_escape.json index fe636d5..b08ba55 100644 --- a/data/scenarios/008_wan_castle_escape.json +++ b/data/scenarios/008_wan_castle_escape.json @@ -478,6 +478,10 @@ { "speaker": "Dian Wei", "text": "방패 뒤에 계십시오. 저들의 눈을 제 쪽으로 돌리겠습니다." } ] }, + { + "type": "set_objective", + "victory": "중간 매복을 뚫고 조앙을 지키며 서쪽 탈출로를 열어라." + }, { "type": "spawn_deployments", "deployments": [ @@ -530,6 +534,11 @@ { "speaker": "Cao Ang", "display_speaker": "조앙", "text": "제 걱정으로 발을 늦추지 마십시오. 서쪽 길을 먼저 여십시오." } ] }, + { + "type": "set_objective", + "victory": "완성 협공을 버티며 조앙을 보호하고 서쪽 탈출로를 장악하라.", + "defeat": "조조나 조앙이 퇴각하거나 제18군령이 시작되면 패한다." + }, { "type": "spawn_deployments", "deployments": [ @@ -627,6 +636,63 @@ "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": { diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 4e64ae8..cd35520 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -30,6 +30,7 @@ func _init() -> void: _check_puyang_raid_event_dialogue_structure(failures) _check_dingtao_counterattack_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_sishui_gate_story_beats(failures) _check_xingyang_ambush_story_beats(failures) @@ -37,6 +38,7 @@ func _init() -> void: _check_puyang_raid_story_beats(failures) _check_dingtao_counterattack_story_beats(failures) _check_xian_emperor_escort_story_beats(failures) + _check_wan_castle_escape_story_beats(failures) _check_pixel_unit_helpers(failures) 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) +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: var state = BattleStateScript.new() 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") +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: for event in state.battle_events: if typeof(event) == TYPE_DICTIONARY and str(event.get("id", "")) == event_id: