Improve Sishui Gate battle pacing

This commit is contained in:
2026-06-19 13:40:57 +09:00
parent f69438e964
commit 1cdb59aa1e
2 changed files with 212 additions and 14 deletions

View File

@@ -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": [

View File

@@ -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"):