diff --git a/data/scenarios/002_sishui_gate.json b/data/scenarios/002_sishui_gate.json index f66d649..22fa410 100644 --- a/data/scenarios/002_sishui_gate.json +++ b/data/scenarios/002_sishui_gate.json @@ -2,14 +2,27 @@ "id": "002_sishui_gate", "name": "사수관 선봉전", "objectives": { - "victory": "화웅의 선봉대를 격파하라.", - "defeat": "조조가 퇴각하거나 제9턴에 이르면 패한다." + "victory": "사수관 입구를 장악하고 화웅의 원군까지 격파하라.", + "defeat": "조조가 퇴각하거나 제17턴이 시작되면 패한다." }, "conditions": { - "victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_2_warning" }, + "victory": { + "type": "all", + "conditions": [ + { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_8_gate_reserve" }, + { + "type": "unit_reaches_tile", + "label": "관문 장악", + "team": "player", + "unit_ids": ["cao_cao_ch2", "xiahou_dun_ch2", "xiahou_yuan_ch2", "cao_ren_ch2"], + "officer_ids": ["cao_cao", "xiahou_dun", "xiahou_yuan", "cao_ren"], + "cells": [[10, 3], [11, 3], [10, 4], [11, 4], [10, 5], [11, 5]] + } + ] + }, "defeat": [ { "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }, - { "type": "turn_reached", "turn": 9, "team": "player" } + { "type": "turn_reached", "turn": 17, "team": "player" } ] }, "briefing": { @@ -17,7 +30,8 @@ "location": "사수관, 초평 원년", "lines": [ "동탁을 토벌하기 위해 제후의 군기가 사수관 아래 모였다.", - "조조는 작은 선봉을 이끌고 화웅의 진을 먼저 시험하려 한다." + "조조는 작은 선봉을 이끌고 화웅의 진을 먼저 시험하려 한다.", + "서남쪽 마을과 관문 안쪽 성채를 차지하면 병력을 고칠 수 있으나, 화웅의 후속 원군이 뒤따른다." ], "camp_conversations": [ { @@ -85,7 +99,7 @@ } }, "roster": { - "max_units": 3, + "max_units": 4, "required_officers": ["cao_cao"] }, "formation": { @@ -99,11 +113,11 @@ "GGGGGRRGGGGG", "GGGFFRRGHHGG", "GGGFFRRGHHGG", - "GGGGGRRGGGGG", - "RRRRRRRRRRRR", - "GGGGGRRGGGGG", - "GGHGGFFGGGGG", - "GGHGGFFGGWWG", + "GGGGGRRGGGCC", + "RRRRRRRRRRCC", + "GGGGGRRGGGCC", + "GGHTTFFGGGGG", + "GGHTTFFGGWWG", "GGGGGGGGGWWG" ] }, @@ -169,6 +183,42 @@ "level": 2, "pos": [10, 2], "base": { "hp": 28, "atk": 10, "def": 4 } + }, + { + "unit_id": "gate_guard_1", + "name": "관문 수비병", + "class_id": "infantry", + "team": "enemy", + "level": 2, + "pos": [10, 3], + "base": { "hp": 36, "atk": 11, "def": 8 } + }, + { + "unit_id": "gate_guard_2", + "name": "관문 궁병", + "class_id": "archer", + "team": "enemy", + "level": 2, + "pos": [10, 5], + "base": { "hp": 28, "atk": 10, "def": 5 } + }, + { + "unit_id": "liang_outrider", + "name": "양주 기마척후", + "class_id": "cavalry", + "team": "enemy", + "level": 2, + "pos": [7, 6], + "base": { "hp": 34, "atk": 12, "def": 6 } + }, + { + "unit_id": "liang_gate_sentinel", + "name": "관문 창병", + "class_id": "infantry", + "team": "enemy", + "level": 2, + "pos": [9, 5], + "base": { "hp": 34, "atk": 11, "def": 7 } } ], "events": [ @@ -202,8 +252,8 @@ { "type": "log", "text": "화웅의 병사들이 진형을 조여 왔다." }, { "type": "set_objective", - "victory": "화웅의 선봉대와 원군을 모두 격파하라.", - "defeat": "조조가 퇴각하거나 제9턴에 이르면 패한다." + "victory": "관문 입구를 장악하고 화웅의 선봉과 원군을 격파하라.", + "defeat": "조조가 퇴각하거나 제17턴이 시작되면 패한다." }, { "type": "spawn_deployments", @@ -229,6 +279,98 @@ ] } ] + }, + { + "id": "turn_5_ridge_patrol", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 5 }, + "actions": [ + { "type": "log", "text": "북쪽 능선에서 화웅의 정찰대가 전장으로 내려온다." }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "liang_ridge_archer", + "name": "능선 궁병", + "class_id": "archer", + "team": "enemy", + "level": 2, + "pos": [9, 1], + "base": { "hp": 28, "atk": 10, "def": 4 } + }, + { + "unit_id": "liang_ridge_spearman", + "name": "능선 창병", + "class_id": "infantry", + "team": "enemy", + "level": 2, + "pos": [8, 1], + "base": { "hp": 34, "atk": 11, "def": 6 } + }, + { + "unit_id": "liang_ridge_cavalry", + "name": "능선 기병", + "class_id": "cavalry", + "team": "enemy", + "level": 2, + "pos": [9, 2], + "base": { "hp": 34, "atk": 12, "def": 6 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "xiahou_dun_ch2", + "priority": 8, + "text": "능선 정찰대가 하후돈의 선봉을 노린다." + } + ] + }, + { + "id": "turn_8_gate_reserve", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 8 }, + "actions": [ + { "type": "log", "text": "관문 뒤편에서 화웅의 예비대가 마지막으로 밀려 나온다." }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "liang_gate_reserve_cavalry", + "name": "관문 기병", + "class_id": "cavalry", + "team": "enemy", + "level": 2, + "pos": [11, 2], + "base": { "hp": 36, "atk": 13, "def": 6 } + }, + { + "unit_id": "liang_gate_reserve_guard", + "name": "관문 호위병", + "class_id": "infantry", + "team": "enemy", + "level": 2, + "pos": [11, 6], + "base": { "hp": 36, "atk": 11, "def": 7 } + }, + { + "unit_id": "liang_gate_reserve_archer", + "name": "관문 궁병", + "class_id": "archer", + "team": "enemy", + "level": 2, + "pos": [11, 7], + "base": { "hp": 28, "atk": 10, "def": 4 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "cao_cao_ch2", + "priority": 11, + "text": "화웅의 예비대가 조조의 선봉 깃발을 노린다." + } + ] } ], "post_battle_choices": [ diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 8805bf4..2cd3207 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -41,7 +41,7 @@ func _init() -> void: failures, "002 gated victory progress", _progress_text("res://data/scenarios/002_sishui_gate.json", false), - "제2군령에 전공 갱신" + "제8군령에 전공 갱신" ) _check_contains( failures, @@ -49,6 +49,12 @@ func _init() -> void: _progress_text("res://data/scenarios/002_sishui_gate.json", false), "적군 격파 0/" ) + _check_contains( + failures, + "002 gate capture progress", + _progress_text("res://data/scenarios/002_sishui_gate.json", false), + "관문 장악" + ) _check_contains( failures, "007 destination progress", @@ -71,6 +77,7 @@ func _init() -> void: _check_event_gated_multi_cell_objective_markers(failures) _check_turn_gated_objective_does_not_create_marker(failures) _check_opening_battle_requires_castle_capture(failures) + _check_sishui_gate_extended_pressure(failures) _check_objective_update(failures) _check_objective_notice(failures) _check_terrain_recovery(failures) @@ -304,6 +311,55 @@ func _check_opening_battle_extended_pressure(failures: Array[String]) -> void: failures.append("opening battle should field 19 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) +func _check_sishui_gate_extended_pressure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): + failures.append("Sishui Gate pressure smoke could not load scenario") + return + if state.get_turn_limit() != 16: + failures.append("Sishui Gate should allow play through turn 16, got limit %d" % state.get_turn_limit()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 8: + failures.append("Sishui Gate should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): + failures.append("Sishui Gate final defeat-all condition should be gated until turn 8 reserve") + var marker_cells := state.get_objective_cells() + for gate_cell in [Vector2i(10, 3), Vector2i(11, 3), Vector2i(10, 4), Vector2i(11, 4), Vector2i(10, 5), Vector2i(11, 5)]: + if not marker_cells.has(gate_cell): + failures.append("Sishui Gate should mark gate capture cell %s: %s" % [str(gate_cell), str(marker_cells)]) + if state.get_objective_cell_label(gate_cell) != "관문 장악": + failures.append("Sishui Gate gate marker should expose label at %s" % str(gate_cell)) + var recovery_summaries := state.get_recovery_terrain_summaries() + var has_gate_recovery := false + var has_village_recovery := false + for summary in recovery_summaries: + if str(summary.get("name", "")) == "성채" and int(summary.get("count", 0)) >= 6 and int(summary.get("heal", 0)) == 8: + has_gate_recovery = true + 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_gate_recovery: + failures.append("Sishui Gate should expose castle recovery terrain: %s" % str(recovery_summaries)) + if not has_village_recovery: + failures.append("Sishui Gate should expose village recovery terrain: %s" % str(recovery_summaries)) + 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 not state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): + failures.append("Sishui Gate final gate should open after turn 8 reserve event") + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16: + failures.append("Sishui Gate should field 16 enemies after gate reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/002_sishui_gate.json"): + failures.append("Sishui Gate briefing smoke could not load scenario") + scene.free() + return + var overview := scene._format_briefing_battlefield_overview_text() + _check_contains(failures, "002 briefing map size", overview, "12칸 x 9칸") + _check_contains(failures, "002 briefing enemy count", overview, "적세 8명") + _check_contains(failures, "002 briefing village recovery", overview, "마을 4칸 +6") + _check_contains(failures, "002 briefing castle recovery", overview, "성채 6칸 +8") + 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"):