diff --git a/data/scenarios/002_sishui_gate.json b/data/scenarios/002_sishui_gate.json index 22fa410..2e3ee76 100644 --- a/data/scenarios/002_sishui_gate.json +++ b/data/scenarios/002_sishui_gate.json @@ -238,8 +238,9 @@ { "type": "dialogue", "lines": [ - { "speaker": "Cao Cao", "display_speaker": "조조", "text": "화웅의 선봉은 제후 연합의 담력을 시험하고 있다. 한 치도 땅을 내줄 수 없다." }, - { "speaker": "Xiahou Yuan", "display_speaker": "하후연", "text": "적 궁병이 능선을 잡았습니다. 제가 옆구리를 눌러 두겠습니다." } + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "연합의 큰 군기는 뒤에 서 있다. 오늘 앞에 선 깃발은 우리 것이다. 관문 입구를 먼저 열어라." }, + { "speaker": "Hua Xiong", "display_speaker": "화웅", "side": "right", "text": "제후들이 사람을 고르다 겨우 조조의 작은 선봉을 내보냈나? 사수관 앞에서 담력을 꺾어 주마." }, + { "speaker": "Xiahou Yuan", "display_speaker": "하후연", "side": "right", "text": "적 궁병이 능선을 잡았습니다. 제가 옆구리를 눌러 두겠습니다." } ] } ] @@ -250,6 +251,13 @@ "when": { "type": "turn_start", "team": "enemy", "turn": 2 }, "actions": [ { "type": "log", "text": "화웅의 병사들이 진형을 조여 왔다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Scout", "display_speaker": "척후", "side": "right", "text": "관문 안쪽 북소리가 빨라졌습니다. 창병과 기병이 입구를 좁히고 있습니다." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "저들이 좁힌 길은 우리에게도 칼끝을 모을 곳이다. 관문 입구를 빼앗아 사기를 눌러라." } + ] + }, { "type": "set_objective", "victory": "관문 입구를 장악하고 화웅의 선봉과 원군을 격파하라.", @@ -286,6 +294,14 @@ "when": { "type": "turn_start", "team": "enemy", "turn": 5 }, "actions": [ { "type": "log", "text": "북쪽 능선에서 화웅의 정찰대가 전장으로 내려온다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Xiahou Yuan", "display_speaker": "하후연", "side": "right", "text": "능선 뒤에서 말발굽과 활시위 소리가 겹칩니다. 저들이 높은 길을 타고 내려옵니다." }, + { "speaker": "Hua Xiong", "display_speaker": "화웅", "side": "right", "text": "위에서 누르면 좁은 길의 선봉은 몸을 돌릴 틈도 없다. 조조의 깃발을 몰아붙여라!" }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "하후돈은 중앙을 붙들고, 하후연은 능선의 눈을 흐려라. 어느 쪽도 관문 앞에서 물러서지 않는다." } + ] + }, { "type": "spawn_deployments", "deployments": [ @@ -332,6 +348,18 @@ "when": { "type": "turn_start", "team": "enemy", "turn": 8 }, "actions": [ { "type": "log", "text": "관문 뒤편에서 화웅의 예비대가 마지막으로 밀려 나온다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Hua Xiong", "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": "관문 입구를 장악한 채 화웅의 마지막 예비대까지 모두 격파하라." + }, { "type": "spawn_deployments", "deployments": [ @@ -371,6 +399,24 @@ "text": "화웅의 예비대가 조조의 선봉 깃발을 노린다." } ] + }, + { + "id": "hua_xiong_vanguard_falls", + "once": true, + "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["hua_xiong_vanguard"] }, + "actions": [ + { + "type": "dialogue", + "lines": [ + { "speaker": "Hua Xiong", "display_speaker": "화웅", "side": "right", "text": "내 선봉이 조조의 작은 군에 밀렸다고? 관문 뒤 병력을 앞으로 세워라!" }, + { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "선봉의 기세가 꺾였다. 그러나 화웅은 아직 문 안에서 병력을 밀어 낼 것이다. 진형을 흐트리지 마라." } + ] + }, + { + "type": "set_objective", + "victory": "화웅의 선봉을 꺾었다. 관문 입구를 장악한 채 남은 예비대까지 격파하라." + } + ] } ], "post_battle_choices": [ diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index d18be48..c4fbe62 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -125,6 +125,7 @@ const SPEAKER_DISPLAY_NAMES := { "Cao Ang": "조앙", "Xu Rong": "서영", "Li Jue": "이각", + "Hua Xiong": "화웅", "Zhang Mancheng": "장만성", "Chen Gong": "진궁", "Gao Shun": "고순", @@ -174,6 +175,7 @@ const SPEAKER_CANONICAL_NAMES := { "조앙": "Cao Ang", "서영": "Xu Rong", "이각": "Li Jue", + "화웅": "Hua Xiong", "장만성": "Zhang Mancheng", "진궁": "Chen Gong", "고순": "Gao Shun", @@ -213,6 +215,7 @@ const DIALOGUE_RIGHT_SIDE_SPEAKERS := { "Yuan Scout": true, "Imperial Envoy": true, "Imperial Guard": true, + "Hua Xiong": true, "Zhang Mancheng": true, "Xu Rong": true, "Li Jue": true, diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index af174a4..614fd02 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -24,7 +24,9 @@ func _init() -> void: _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) _check_opening_battle_event_dialogue_structure(failures) + _check_sishui_gate_event_dialogue_structure(failures) _check_opening_battle_story_beats(failures) + _check_sishui_gate_story_beats(failures) _check_pixel_unit_helpers(failures) if failures.is_empty(): @@ -109,9 +111,10 @@ func _check_dialogue_localization_and_side(failures: Array[String]) -> void: {"speaker": "Cao Cao", "text": "전열을 세워라."}, {"speaker": "Lu Bu", "text": "조조를 짓밟아라."}, {"speaker": "Qingzhou Chief", "text": "마을 곡식을 내놓아라."}, - {"speaker": "Camp Merchant", "text": "군막에 물자가 남았습니다."} + {"speaker": "Camp Merchant", "text": "군막에 물자가 남았습니다."}, + {"speaker": "Hua Xiong", "text": "사수관을 지켜라."} ]) - if lines.size() != 4: + if lines.size() != 5: failures.append("dialogue normalization should keep all valid chapter one lines") scene.free() return @@ -123,6 +126,8 @@ func _check_dialogue_localization_and_side(failures: Array[String]) -> void: failures.append("chapter one named enemy dialogue should be localized and right-sided") if lines[3].get("speaker", "") != "군막 상인" or lines[3].get("side", "") != "right": failures.append("camp merchant dialogue should be localized and right-sided by default") + if lines[4].get("speaker", "") != "화웅" or lines[4].get("side", "") != "right": + failures.append("Sishui Gate enemy commander dialogue should localize to Korean on the right") scene.free() @@ -178,6 +183,34 @@ func _check_opening_battle_event_dialogue_structure(failures: Array[String]) -> failures.append("Zhang Mancheng defeat event should watch yellow_turban_1") +func _check_sishui_gate_event_dialogue_structure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): + failures.append("could not load Sishui Gate for event dialogue structure") + return + for event_id in [ + "opening_dialogue", + "turn_2_warning", + "turn_5_ridge_patrol", + "turn_8_gate_reserve", + "hua_xiong_vanguard_falls" + ]: + var event := _event_by_id(state, event_id) + if event.is_empty(): + failures.append("Sishui Gate missing event: %s" % event_id) + continue + if not _event_has_dialogue_action(event): + failures.append("Sishui Gate event should include contextual dialogue: %s" % event_id) + var vanguard_event := _event_by_id(state, "hua_xiong_vanguard_falls") + if not vanguard_event.is_empty(): + var when: Dictionary = vanguard_event.get("when", {}) + var unit_ids: Array = when.get("unit_ids", []) + if not unit_ids.has("hua_xiong_vanguard"): + failures.append("Hua Xiong vanguard defeat event should watch hua_xiong_vanguard") + if not _event_has_set_objective_action(vanguard_event): + failures.append("Hua Xiong vanguard defeat event should clarify the next 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"): @@ -203,6 +236,30 @@ func _check_opening_battle_story_beats(failures: Array[String]) -> void: failures.append("opening battle final objective gate should open after the turn 9 story beat") +func _check_sishui_gate_story_beats(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): + failures.append("could not load Sishui Gate for story beat checks") + return + + var dialogue_batches := [] + state.dialogue_requested.connect(func(lines: Array) -> void: + dialogue_batches.append(lines) + ) + + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2) + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 5) + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 8) + + if dialogue_batches.size() < 3: + failures.append("Sishui Gate should add dialogue beats for each major reserve phase") + var victory_text := str(state.objectives.get("victory", "")) + if not victory_text.contains("마지막 예비대"): + failures.append("Sishui Gate turn 8 event should update the victory objective after the final reserve arrives") + if not state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): + failures.append("Sishui Gate final objective gate should open after the turn 8 story beat") + + 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: @@ -218,6 +275,14 @@ func _event_has_dialogue_action(event: Dictionary) -> bool: return false +func _event_has_set_objective_action(event: Dictionary) -> bool: + var actions: Array = event.get("actions", []) + for action in actions: + if typeof(action) == TYPE_DICTIONARY and str(action.get("type", "")) == "set_objective": + return true + return false + + func _check_pixel_unit_helpers(failures: Array[String]) -> void: var scene = BattleSceneScript.new() for method_name in [