From 37d087e1c664631afbc754d9d8da3f4ecdd134f7 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 13:56:46 +0900 Subject: [PATCH] Improve Qingzhou campaign battle pacing --- data/scenarios/004_qingzhou_campaign.json | 170 ++++++++++++++++++++-- tools/smoke_objective_progress.gd | 66 +++++++++ 2 files changed, 227 insertions(+), 9 deletions(-) diff --git a/data/scenarios/004_qingzhou_campaign.json b/data/scenarios/004_qingzhou_campaign.json index f0f6d17..d9354ef 100644 --- a/data/scenarios/004_qingzhou_campaign.json +++ b/data/scenarios/004_qingzhou_campaign.json @@ -2,14 +2,27 @@ "id": "004_qingzhou_campaign", "name": "청주 평정전", "objectives": { - "victory": "황건 잔당의 우두머리를 격파하라.", - "defeat": "조조가 퇴각하거나 제12턴에 이르면 패한다." + "victory": "청주 촌락을 지키고 황건 잔당을 평정하라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." }, "conditions": { - "victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_bandit_push" }, + "victory": { + "type": "all", + "conditions": [ + { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_9_village_last_stand" }, + { + "type": "unit_reaches_tile", + "label": "촌락 보호", + "team": "player", + "unit_ids": ["cao_cao_ch4", "xiahou_dun_ch4", "xiahou_yuan_ch4", "cao_ren_ch4"], + "officer_ids": ["cao_cao", "xiahou_dun", "xiahou_yuan", "cao_ren"], + "cells": [[10, 3], [11, 3], [10, 5], [11, 5]] + } + ] + }, "defeat": [ { "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }, - { "type": "turn_reached", "turn": 12, "team": "player" } + { "type": "turn_reached", "turn": 18, "team": "player" } ] }, "briefing": { @@ -18,7 +31,8 @@ "lines": [ "연합군이 흩어진 뒤 조조는 청주로 눈을 돌렸다.", "황건 잔당이 마을과 상로를 어지럽히며 백성을 겁박한다.", - "길목을 장악하고 적장을 베어, 흩어진 장정을 군율 아래 모아라." + "길목을 장악하고 적장을 베어, 흩어진 장정을 군율 아래 모아라.", + "동쪽 촌락을 지키며 잔당의 마지막 반격까지 꺾으면, 백성을 지키던 무사 전위가 조조군을 지켜볼 것이다." ], "conditional_lines": [ { @@ -127,9 +141,9 @@ "GGGGGGGGGGGGGG", "GGGFFFGGGHHGGG", "GGGFFFGRRHHGGG", - "GGGGGGRRRGGGGG", + "GGGGGGRRRGTTGG", "RRRRRRRRRRRRRR", - "GGGGGGRRRGGGGG", + "GGGGGGRRRGTTGG", "GGHGGGFFFGGGGG", "GGHHGGFFFGGWWG", "GGGGGGGFFGGWWG", @@ -207,6 +221,33 @@ "level": 4, "pos": [11, 6], "base": { "hp": 32, "mp": 16, "atk": 7, "def": 4, "int": 11 } + }, + { + "unit_id": "qingzhou_village_guard", + "name": "촌락 약탈병", + "class_id": "bandit", + "team": "enemy", + "level": 4, + "pos": [10, 3], + "base": { "hp": 36, "atk": 13, "def": 5 } + }, + { + "unit_id": "qingzhou_road_spearman", + "name": "상로 창병", + "class_id": "infantry", + "team": "enemy", + "level": 4, + "pos": [8, 4], + "base": { "hp": 40, "atk": 13, "def": 7 } + }, + { + "unit_id": "qingzhou_hill_archer", + "name": "언덕 궁병", + "class_id": "archer", + "team": "enemy", + "level": 4, + "pos": [11, 2], + "base": { "hp": 30, "atk": 12, "def": 5 } } ], "events": [ @@ -248,8 +289,8 @@ }, { "type": "set_objective", - "victory": "청주 두령과 모든 약탈병을 격파하라.", - "defeat": "조조가 퇴각하거나 제12턴에 이르면 패한다." + "victory": "촌락 길목을 지키며 청주 두령과 약탈병을 격파하라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." }, { "type": "spawn_deployments", @@ -298,6 +339,117 @@ ] } ] + }, + { + "id": "turn_6_village_raiders", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 6 }, + "actions": [ + { "type": "log", "text": "마을 뒤편에서 잔당의 별동대가 다시 약탈길을 연다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Cao Ren", "display_speaker": "조인", "text": "마을 뒤쪽에 또 다른 약탈병입니다. 길목을 비우면 백성들이 먼저 베입니다." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "text": "적을 쫓는 칼보다 백성을 등지는 발이 더 무겁다. 촌락을 붙들어라." } + ] + }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "qingzhou_village_raider_west", + "name": "촌락 약탈병", + "class_id": "bandit", + "team": "enemy", + "level": 4, + "pos": [5, 6], + "base": { "hp": 34, "atk": 13, "def": 5 } + }, + { + "unit_id": "qingzhou_village_raider_spear", + "name": "촌락 창병", + "class_id": "infantry", + "team": "enemy", + "level": 4, + "pos": [8, 6], + "base": { "hp": 38, "atk": 13, "def": 7 } + }, + { + "unit_id": "qingzhou_village_raider_archer", + "name": "촌락 궁병", + "class_id": "archer", + "team": "enemy", + "level": 4, + "pos": [12, 6], + "base": { "hp": 30, "atk": 12, "def": 4 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "xiahou_dun_ch4", + "priority": 8, + "text": "촌락 별동대가 하후돈의 엄호선을 노린다." + } + ] + }, + { + "id": "turn_9_village_last_stand", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 9 }, + "actions": [ + { "type": "log", "text": "청주 두령의 마지막 무리가 촌락 입구로 몰려든다." }, + { + "type": "set_objective", + "victory": "마지막 잔당을 격파하고 촌락 길목을 지켜라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." + }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Dian Wei", "display_speaker": "전위", "text": "백성을 등지고 달아나는 군대가 아니라면, 그 길목을 끝까지 지켜 보십시오." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "text": "듣던 무사가 저 자인가. 좋다. 청주의 길목에서 우리 군율을 보이겠다." } + ] + }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "qingzhou_last_guard", + "name": "잔당 호위병", + "class_id": "infantry", + "team": "enemy", + "level": 4, + "pos": [12, 3], + "base": { "hp": 42, "atk": 14, "def": 8 } + }, + { + "unit_id": "qingzhou_last_rider", + "name": "잔당 기병", + "class_id": "cavalry", + "team": "enemy", + "level": 4, + "pos": [13, 4], + "base": { "hp": 42, "atk": 15, "def": 7 } + }, + { + "unit_id": "qingzhou_last_archer", + "name": "잔당 궁병", + "class_id": "archer", + "team": "enemy", + "level": 4, + "pos": [12, 5], + "base": { "hp": 30, "atk": 12, "def": 4 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "cao_cao_ch4", + "priority": 11, + "text": "마지막 잔당이 조조의 깃발을 향해 달려든다." + } + ] } ], "rewards": { diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 4a867f6..6496201 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -67,6 +67,18 @@ func _init() -> void: _progress_text("res://data/scenarios/003_xingyang_ambush.json", false), "동쪽 길목" ) + _check_contains( + failures, + "004 gated victory progress", + _progress_text("res://data/scenarios/004_qingzhou_campaign.json", false), + "제9군령에 전공 갱신" + ) + _check_contains( + failures, + "004 village protection progress", + _progress_text("res://data/scenarios/004_qingzhou_campaign.json", false), + "촌락 보호" + ) _check_contains( failures, "007 destination progress", @@ -91,6 +103,7 @@ func _init() -> void: _check_opening_battle_requires_castle_capture(failures) _check_sishui_gate_extended_pressure(failures) _check_xingyang_ambush_extended_pressure(failures) + _check_qingzhou_campaign_extended_pressure(failures) _check_objective_update(failures) _check_objective_notice(failures) _check_terrain_recovery(failures) @@ -435,6 +448,59 @@ func _check_xingyang_ambush_extended_pressure(failures: Array[String]) -> void: scene.free() +func _check_qingzhou_campaign_extended_pressure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): + failures.append("Qingzhou Campaign pressure smoke could not load scenario") + return + if state.get_turn_limit() != 17: + failures.append("Qingzhou Campaign should allow play through turn 17, got limit %d" % state.get_turn_limit()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 8: + failures.append("Qingzhou Campaign should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state._condition_gate_open({"after_event": "turn_9_village_last_stand"}): + failures.append("Qingzhou Campaign final defeat-all condition should be gated until turn 9 last stand") + var marker_cells := state.get_objective_cells() + for village_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 5), Vector2i(11, 5)]: + if not marker_cells.has(village_cell): + failures.append("Qingzhou Campaign should mark village protection cell %s: %s" % [str(village_cell), str(marker_cells)]) + if state.get_objective_cell_label(village_cell) != "촌락 보호": + failures.append("Qingzhou Campaign village marker should expose label at %s" % str(village_cell)) + var recovery_summaries := state.get_recovery_terrain_summaries() + var has_village_recovery := false + for summary in recovery_summaries: + if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: + has_village_recovery = true + if not has_village_recovery: + failures.append("Qingzhou Campaign should expose village recovery terrain: %s" % str(recovery_summaries)) + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) + state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) + if not state._condition_gate_open({"after_event": "turn_9_village_last_stand"}): + failures.append("Qingzhou Campaign final gate should open after turn 9 last stand event") + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17: + failures.append("Qingzhou Campaign should field 17 enemies after last stand, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY): + enemy["alive"] = false + state._check_battle_status() + if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: + failures.append("Qingzhou Campaign should not end before village protection, got %s" % str(state.battle_status)) + var cao_cao: Dictionary = state.get_unit("cao_cao_ch4") + cao_cao["pos"] = Vector2i(10, 3) + state._check_battle_status() + if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: + failures.append("Qingzhou Campaign should end after last stand falls and village is protected, got %s" % str(state.battle_status)) + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): + failures.append("Qingzhou Campaign briefing smoke could not load scenario") + scene.free() + return + var overview := scene._format_briefing_battlefield_overview_text() + _check_contains(failures, "004 briefing map size", overview, "14칸 x 10칸") + _check_contains(failures, "004 briefing enemy count", overview, "적세 8명") + _check_contains(failures, "004 briefing village recovery", overview, "마을 4칸 +6") + scene.free() + + func _check_opening_board_avoids_side_panel(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):