From 2d4a3a4a516e65de3ecc5497331a5ebaf4fd274a Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 16:40:22 +0900 Subject: [PATCH] Add story beats to Xian Emperor Escort --- data/scenarios/007_xian_emperor_escort.json | 47 +++++++++ tools/smoke_chapter_one_polish.gd | 107 ++++++++++++++++++++ tools/smoke_event_cells.gd | 4 +- 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/data/scenarios/007_xian_emperor_escort.json b/data/scenarios/007_xian_emperor_escort.json index fb550a4..b40f9f2 100644 --- a/data/scenarios/007_xian_emperor_escort.json +++ b/data/scenarios/007_xian_emperor_escort.json @@ -313,6 +313,11 @@ { "speaker": "Cao Cao", "text": "그렇다면 사자를 지체시킬 수 없다. 앞으로 나아가라." } ] }, + { + "type": "set_objective", + "victory": "천자 사자를 지키며 동도 호송로까지 조조를 전진시켜라.", + "defeat": "조조나 천자 사자가 퇴각하거나 제18군령이 시작되면 패한다." + }, { "type": "spawn_deployments", "deployments": [ @@ -350,6 +355,10 @@ { "speaker": "Cao Cao", "text": "길 자체가 미끼였군. 사자를 감싸고 그대로 돌파하라." } ] }, + { + "type": "set_objective", + "victory": "중간 매복을 뚫고 천자 사자를 보호하며 동도 호송로를 장악하라." + }, { "type": "spawn_deployments", "deployments": [ @@ -499,6 +508,44 @@ "text": "마지막 잔당이 조조와 천자 사자를 함께 노린다." } ] + }, + { + "id": "li_jue_vanguard_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["li_jue_vanguard"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Li Jue", "display_speaker": "이각", "side": "right", "text": "선봉이 밀린다 해도 동도 길목은 아직 쉽게 열리지 않는다." }, + { "speaker": "Imperial Envoy", "display_speaker": "천자 사자", "side": "right", "text": "길 앞의 먼지가 조금 걷혔습니다. 거가가 멈추지 않게 해 주십시오." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "이각의 선봉을 꺾었다. 사자를 가운데 두고 동도 호송로까지 밀어라." } + ] + }, + { + "type": "set_objective", + "victory": "이각 선봉을 꺾었다. 천자 사자를 지키며 동도 호송로를 장악하라." + } + ] + }, + { + "id": "final_escort_rider_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["final_escort_rider"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Li Jue", "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": "마지막 추격기를 꺾었다. 동도 호송로를 지키며 남은 잔당을 격파하라." + } + ] } ], "rewards": { diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 69222af..4e64ae8 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -29,12 +29,14 @@ func _init() -> void: _check_qingzhou_campaign_event_dialogue_structure(failures) _check_puyang_raid_event_dialogue_structure(failures) _check_dingtao_counterattack_event_dialogue_structure(failures) + _check_xian_emperor_escort_event_dialogue_structure(failures) _check_opening_battle_story_beats(failures) _check_sishui_gate_story_beats(failures) _check_xingyang_ambush_story_beats(failures) _check_qingzhou_campaign_story_beats(failures) _check_puyang_raid_story_beats(failures) _check_dingtao_counterattack_story_beats(failures) + _check_xian_emperor_escort_story_beats(failures) _check_pixel_unit_helpers(failures) if failures.is_empty(): @@ -507,6 +509,57 @@ func _check_dingtao_counterattack_event_dialogue_structure(failures: Array[Strin failures.append("Dingtao Counterattack second charge event should clarify the final objective") +func _check_xian_emperor_escort_event_dialogue_structure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"): + failures.append("could not load Xian Emperor Escort for event dialogue structure") + return + for event_id in [ + "opening_dialogue", + "turn_3_rear_pressure", + "midroad_ambush", + "turn_5_road_marker", + "turn_6_envoy_pincher", + "turn_9_final_escort_pressure", + "li_jue_vanguard_falls", + "final_escort_rider_falls" + ]: + var event := _event_by_id(state, event_id) + if event.is_empty(): + failures.append("Xian Emperor Escort missing event: %s" % event_id) + continue + if not _event_has_dialogue_action(event): + failures.append("Xian Emperor Escort event should include contextual dialogue: %s" % event_id) + var li_jue_event := _event_by_id(state, "li_jue_vanguard_falls") + if not li_jue_event.is_empty(): + var when: Dictionary = li_jue_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Li Jue vanguard defeat event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("li_jue_vanguard"): + failures.append("Li Jue vanguard defeat event should watch li_jue_vanguard") + if not _event_has_set_objective_action(li_jue_event): + failures.append("Li Jue vanguard defeat event should clarify the escort objective") + var final_rider_event := _event_by_id(state, "final_escort_rider_falls") + if not final_rider_event.is_empty(): + var when: Dictionary = final_rider_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Final escort rider defeat event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("final_escort_rider"): + failures.append("Final escort rider defeat event should watch final_escort_rider") + if not _event_has_set_objective_action(final_rider_event): + failures.append("Final escort rider defeat event should clarify the final escort objective") + for objective_event_id in [ + "turn_3_rear_pressure", + "midroad_ambush", + "turn_9_final_escort_pressure" + ]: + 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("Xian Emperor Escort 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"): @@ -758,6 +811,60 @@ func _check_dingtao_counterattack_story_beats(failures: Array[String]) -> void: failures.append("Lu Bu second charge break objective should still point to the center and remaining charge") +func _check_xian_emperor_escort_story_beats(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"): + failures.append("could not load Xian Emperor Escort 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, 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, 3, cao_cao) + state._run_events("turn_start", BattleStateScript.TEAM_PLAYER, 5) + 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("Xian Emperor Escort should add dialogue beats for each major escort pressure phase") + var victory_text := str(state.objectives.get("victory", "")) + if not victory_text.contains("동도 호송로") or not victory_text.contains("마지막 잔당"): + failures.append("Xian Emperor Escort turn 9 event should update the victory objective for final escort pressure") + if not state._condition_gate_open({"after_event": "turn_9_final_escort_pressure"}): + failures.append("Xian Emperor Escort final objective gate should open after the turn 9 final pressure") + if int(state.get_unit("imperial_envoy_ch7").get("ai_target_priority", 0)) != 12: + failures.append("Xian Emperor Escort final pressure should keep the imperial envoy under threat") + + var li_jue: Dictionary = state.get_unit("li_jue_vanguard") + if li_jue.is_empty(): + failures.append("Xian Emperor Escort should include Li Jue vanguard for the road-opening story beat") + else: + li_jue["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, li_jue) + if dialogue_batches.size() < 6: + failures.append("Xian Emperor Escort should show dialogue when Li Jue vanguard falls") + var li_jue_objective_text := str(state.objectives.get("victory", "")) + if not li_jue_objective_text.contains("천자 사자") or not li_jue_objective_text.contains("동도 호송로"): + failures.append("Li Jue vanguard defeat objective should still point to the envoy and east road") + + var final_rider: Dictionary = state.get_unit("final_escort_rider") + if final_rider.is_empty(): + failures.append("Xian Emperor Escort should include final escort rider for the last pressure story beat") + else: + final_rider["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, final_rider) + if dialogue_batches.size() < 7: + failures.append("Xian Emperor Escort should show dialogue when final escort rider falls") + var final_objective_text := str(state.objectives.get("victory", "")) + if not final_objective_text.contains("동도 호송로") or not final_objective_text.contains("잔당"): + failures.append("Final escort rider defeat objective should still point to the east road and remaining enemies") + + 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: diff --git a/tools/smoke_event_cells.gd b/tools/smoke_event_cells.gd index 73968bb..4f701c7 100644 --- a/tools/smoke_event_cells.gd +++ b/tools/smoke_event_cells.gd @@ -32,12 +32,12 @@ func _check_condition_cells(failures: Array[String]) -> void: if not objective_cells.has(Vector2i(2, 3)) or not objective_cells.has(Vector2i(2, 4)): failures.append("objective cells missing multi-cell destination: %s" % str(objective_cells)) var progress: String = state.get_objective_progress_text(false) - if not progress.contains("(3, 4), (3, 5): not reached"): + if not progress.contains("전장 표식 (3, 4), (3, 5) · 미도달"): failures.append("multi-cell progress text mismatch before reach: %s" % progress) var cao_cao: Dictionary = state.get_unit("cao_cao") cao_cao["pos"] = Vector2i(2, 4) progress = state.get_objective_progress_text(false) - if not progress.contains("(3, 4), (3, 5): reached"): + if not progress.contains("전장 표식 (3, 4), (3, 5) · 도달"): failures.append("multi-cell progress text mismatch after reach: %s" % progress) if not state._is_condition_met(state.battle_conditions["victory"]): failures.append("multi-cell unit_reaches_tile condition should be met")