From 04c3f9d61566a46cb93220c8824a59a213fe862b Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 16:33:57 +0900 Subject: [PATCH] Add story beats to Dingtao Counterattack --- data/scenarios/006_dingtao_counterattack.json | 65 +++++++++ tools/smoke_chapter_one_polish.gd | 124 ++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/data/scenarios/006_dingtao_counterattack.json b/data/scenarios/006_dingtao_counterattack.json index 0124829..487a4d3 100644 --- a/data/scenarios/006_dingtao_counterattack.json +++ b/data/scenarios/006_dingtao_counterattack.json @@ -376,6 +376,14 @@ "when": { "type": "turn_start", "team": "enemy", "turn": 3 }, "actions": [ { "type": "log", "text": "여포가 후속 부대를 전장에 밀어 넣는다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Lu Bu", "display_speaker": "여포", "side": "right", "text": "방진을 세웠느냐. 그렇다면 말발굽으로 그 가운데를 갈라 보겠다." }, + { "speaker": "Cao Ren", "display_speaker": "조인", "side": "right", "text": "중앙이 흔들리면 양쪽 길도 무너집니다. 예비대를 뒤에 붙들겠습니다." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "좋다. 저들의 속도를 빼앗고, 중앙 방진에서 반격의 숨을 고르라." } + ] + }, { "type": "set_objective", "victory": "중앙 방진을 붙들고 여포의 후속 부대를 격파하라.", @@ -494,6 +502,63 @@ } ] }, + { + "id": "gao_shun_line_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["gao_shun_line"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Gao Shun", "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": "lu_bu_counterattack_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["lu_bu_counterattack"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Lu Bu", "display_speaker": "여포", "side": "right", "text": "조조의 방진이 아직 버틴다고? 말머리를 물리지 마라. 다시 들이쳐라!" }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "여포의 첫 반격이 꺾였다. 승세에 취하지 말고 중앙을 지켜 다음 돌격을 끊어라." }, + { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "중앙을 비우지 않겠습니다. 저들이 다시 달려오면 이번에는 더 깊이 베겠습니다." } + ] + }, + { + "type": "set_objective", + "victory": "여포 반격대를 꺾었다. 중앙 방진을 지키며 남은 재돌격 병력을 격파하라." + } + ] + }, + { + "id": "lu_bu_second_charge_breaks", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["lu_bu_second_rider"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Lu Bu", "display_speaker": "여포", "side": "right", "text": "두 번째 돌격마저 막는다고? 아직 내 창끝은 물러서지 않았다!" }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "재돌격의 앞줄이 무너졌다. 중앙 방진을 지키며 남은 병력을 거두어라." }, + { "speaker": "Dian Wei", "display_speaker": "전위", "side": "right", "text": "저들의 말발굽이 멎었습니다. 이제 우리가 한 걸음씩 밀 차례입니다." } + ] + }, + { + "type": "set_objective", + "victory": "여포의 재돌격 선봉을 꺾었다. 중앙 방진을 지키며 남은 돌격대를 정리하라." + } + ] + }, { "id": "turn_5_hold_center", "once": true, diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 54aa44e..69222af 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -28,11 +28,13 @@ func _init() -> void: _check_xingyang_ambush_event_dialogue_structure(failures) _check_qingzhou_campaign_event_dialogue_structure(failures) _check_puyang_raid_event_dialogue_structure(failures) + _check_dingtao_counterattack_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_pixel_unit_helpers(failures) if failures.is_empty(): @@ -444,6 +446,67 @@ func _check_puyang_raid_event_dialogue_structure(failures: Array[String]) -> voi failures.append("Puyang Raid encirclement event should clarify the final objective") +func _check_dingtao_counterattack_event_dialogue_structure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): + failures.append("could not load Dingtao Counterattack for event dialogue structure") + return + for event_id in [ + "opening_dialogue", + "turn_3_fortified_reserve", + "turn_3_pressed_lu_bu_charge", + "turn_3_counterattack", + "turn_5_hold_center", + "turn_6_flank_pressure", + "turn_9_lu_bu_second_charge", + "gao_shun_line_falls", + "lu_bu_counterattack_falls", + "lu_bu_second_charge_breaks" + ]: + var event := _event_by_id(state, event_id) + if event.is_empty(): + failures.append("Dingtao Counterattack missing event: %s" % event_id) + continue + if not _event_has_dialogue_action(event): + failures.append("Dingtao Counterattack event should include contextual dialogue: %s" % event_id) + var gao_shun_event := _event_by_id(state, "gao_shun_line_falls") + if not gao_shun_event.is_empty(): + var when: Dictionary = gao_shun_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Gao Shun line defeat event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("gao_shun_line"): + failures.append("Gao Shun line defeat event should watch gao_shun_line") + if not _event_has_set_objective_action(gao_shun_event): + failures.append("Gao Shun line defeat event should clarify the next objective") + var lu_bu_event := _event_by_id(state, "lu_bu_counterattack_falls") + if not lu_bu_event.is_empty(): + var when: Dictionary = lu_bu_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Lu Bu counterattack defeat event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("lu_bu_counterattack"): + failures.append("Lu Bu counterattack defeat event should watch lu_bu_counterattack") + if not _event_has_set_objective_action(lu_bu_event): + failures.append("Lu Bu counterattack defeat event should clarify the next objective") + var second_charge_break_event := _event_by_id(state, "lu_bu_second_charge_breaks") + if not second_charge_break_event.is_empty(): + var when: Dictionary = second_charge_break_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Lu Bu second charge break event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("lu_bu_second_rider"): + failures.append("Lu Bu second charge break event should watch lu_bu_second_rider") + if not _event_has_set_objective_action(second_charge_break_event): + failures.append("Lu Bu second charge break event should clarify the next objective") + var counterattack_event := _event_by_id(state, "turn_3_counterattack") + if not counterattack_event.is_empty() and not _event_has_set_objective_action(counterattack_event): + failures.append("Dingtao Counterattack turn 3 event should clarify the pressure objective") + var second_charge_event := _event_by_id(state, "turn_9_lu_bu_second_charge") + if not second_charge_event.is_empty() and not _event_has_set_objective_action(second_charge_event): + failures.append("Dingtao Counterattack second charge event should clarify the final objective") + + 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"): @@ -634,6 +697,67 @@ func _check_puyang_raid_story_beats(failures: Array[String]) -> void: failures.append("Lu Bu banner defeat objective should still point to camp control and the encirclement") +func _check_dingtao_counterattack_story_beats(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): + failures.append("could not load Dingtao Counterattack 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) + 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() < 4: + failures.append("Dingtao Counterattack should add dialogue beats for each major countercharge phase") + var victory_text := str(state.objectives.get("victory", "")) + if not victory_text.contains("두 번째 돌격") or not victory_text.contains("중앙 방진"): + failures.append("Dingtao Counterattack turn 9 event should update the victory objective for the second charge") + if not state._condition_gate_open({"after_event": "turn_9_lu_bu_second_charge"}): + failures.append("Dingtao Counterattack final objective gate should open after the turn 9 second charge") + + var gao_shun: Dictionary = state.get_unit("gao_shun_line") + if gao_shun.is_empty(): + failures.append("Dingtao Counterattack should include Gao Shun line for the center story beat") + else: + gao_shun["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, gao_shun) + if dialogue_batches.size() < 5: + failures.append("Dingtao Counterattack should show dialogue when Gao Shun line falls") + var gao_shun_objective_text := str(state.objectives.get("victory", "")) + if not gao_shun_objective_text.contains("중앙 방진") or not gao_shun_objective_text.contains("재돌격"): + failures.append("Gao Shun line defeat objective should still point to the center and second charge") + + var lu_bu_counterattack: Dictionary = state.get_unit("lu_bu_counterattack") + if lu_bu_counterattack.is_empty(): + failures.append("Dingtao Counterattack should include Lu Bu counterattack for the major pressure story beat") + else: + lu_bu_counterattack["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, lu_bu_counterattack) + if dialogue_batches.size() < 6: + failures.append("Dingtao Counterattack should show dialogue when Lu Bu counterattack falls") + var lu_bu_objective_text := str(state.objectives.get("victory", "")) + if not lu_bu_objective_text.contains("중앙 방진") or not lu_bu_objective_text.contains("재돌격"): + failures.append("Lu Bu counterattack defeat objective should still point to the center and second charge") + + var second_charge_rider: Dictionary = state.get_unit("lu_bu_second_rider") + if second_charge_rider.is_empty(): + failures.append("Dingtao Counterattack should include Lu Bu second charge rider for the final charge story beat") + else: + second_charge_rider["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, second_charge_rider) + if dialogue_batches.size() < 7: + failures.append("Dingtao Counterattack should show dialogue when Lu Bu second charge breaks") + var second_charge_objective_text := str(state.objectives.get("victory", "")) + if not second_charge_objective_text.contains("중앙 방진") or not second_charge_objective_text.contains("남은"): + failures.append("Lu Bu second charge break objective should still point to the center and remaining charge") + + 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: