diff --git a/data/scenarios/004_qingzhou_campaign.json b/data/scenarios/004_qingzhou_campaign.json index d9354ef..50c7800 100644 --- a/data/scenarios/004_qingzhou_campaign.json +++ b/data/scenarios/004_qingzhou_campaign.json @@ -450,6 +450,25 @@ "text": "마지막 잔당이 조조의 깃발을 향해 달려든다." } ] + }, + { + "id": "qingzhou_chief_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["qingzhou_chief"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Qingzhou Chief", "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 d810393..1c0ba30 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -26,9 +26,11 @@ func _init() -> void: _check_opening_battle_event_dialogue_structure(failures) _check_sishui_gate_event_dialogue_structure(failures) _check_xingyang_ambush_event_dialogue_structure(failures) + _check_qingzhou_campaign_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_pixel_unit_helpers(failures) if failures.is_empty(): @@ -354,6 +356,43 @@ func _check_xingyang_ambush_event_dialogue_structure(failures: Array[String]) -> failures.append("Xingyang Ambush blockade event should clarify the final objective") +func _check_qingzhou_campaign_event_dialogue_structure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): + failures.append("could not load Qingzhou Campaign for event dialogue structure") + return + for event_id in [ + "opening_dialogue", + "turn_3_bandit_push", + "turn_5_dian_wei_rumor", + "turn_6_village_raiders", + "turn_9_village_last_stand", + "qingzhou_chief_falls" + ]: + var event := _event_by_id(state, event_id) + if event.is_empty(): + failures.append("Qingzhou Campaign missing event: %s" % event_id) + continue + if not _event_has_dialogue_action(event): + failures.append("Qingzhou Campaign event should include contextual dialogue: %s" % event_id) + var chief_event := _event_by_id(state, "qingzhou_chief_falls") + if not chief_event.is_empty(): + var when: Dictionary = chief_event.get("when", {}) + if str(when.get("type", "")) != "unit_defeated": + failures.append("Qingzhou chief defeat event should trigger when the unit is defeated") + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("qingzhou_chief"): + failures.append("Qingzhou chief defeat event should watch qingzhou_chief") + if not _event_has_set_objective_action(chief_event): + failures.append("Qingzhou chief defeat event should clarify the next objective") + var first_push_event := _event_by_id(state, "turn_3_bandit_push") + if not first_push_event.is_empty() and not _event_has_set_objective_action(first_push_event): + failures.append("Qingzhou Campaign first raider push should clarify the pressure objective") + var last_stand_event := _event_by_id(state, "turn_9_village_last_stand") + if not last_stand_event.is_empty() and not _event_has_set_objective_action(last_stand_event): + failures.append("Qingzhou Campaign last stand 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"): @@ -457,6 +496,43 @@ func _check_xingyang_ambush_story_beats(failures: Array[String]) -> void: failures.append("Xingyang Ambush regroup branch should keep dialogue beats for scouted ambush pressure") +func _check_qingzhou_campaign_story_beats(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): + failures.append("could not load Qingzhou Campaign 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("Qingzhou Campaign should add dialogue beats for each major village pressure phase") + var victory_text := str(state.objectives.get("victory", "")) + if not victory_text.contains("마지막 잔당") or not victory_text.contains("촌락 길목"): + failures.append("Qingzhou Campaign turn 9 event should update the victory objective for the village last stand") + if not state._condition_gate_open({"after_event": "turn_9_village_last_stand"}): + failures.append("Qingzhou Campaign final objective gate should open after the turn 9 last stand") + + var qingzhou_chief: Dictionary = state.get_unit("qingzhou_chief") + if qingzhou_chief.is_empty(): + failures.append("Qingzhou Campaign should include the Qingzhou chief for the defeat story beat") + else: + qingzhou_chief["alive"] = false + state._run_events("unit_defeated", BattleStateScript.TEAM_ENEMY, 9, qingzhou_chief) + if dialogue_batches.size() < 5: + failures.append("Qingzhou Campaign should show dialogue when the Qingzhou chief falls") + var chief_objective_text := str(state.objectives.get("victory", "")) + if not chief_objective_text.contains("촌락 길목") or not chief_objective_text.contains("잔당"): + failures.append("Qingzhou chief defeat objective should still point to the village 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: