From 585c2e76a03f1057b46a39ac6fa23a3625ba3546 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 14:16:47 +0900 Subject: [PATCH] Improve Dingtao counterattack battle pacing --- data/scenarios/006_dingtao_counterattack.json | 176 ++++++++++++++++-- tools/smoke_objective_progress.gd | 94 ++++++++++ 2 files changed, 258 insertions(+), 12 deletions(-) diff --git a/data/scenarios/006_dingtao_counterattack.json b/data/scenarios/006_dingtao_counterattack.json index 53da7e8..0124829 100644 --- a/data/scenarios/006_dingtao_counterattack.json +++ b/data/scenarios/006_dingtao_counterattack.json @@ -2,14 +2,27 @@ "id": "006_dingtao_counterattack", "name": "정도 반격전", "objectives": { - "victory": "여포의 반격군을 격파하라.", - "defeat": "조조가 퇴각하거나 제12군령에 이르면 패한다." + "victory": "중앙 방진을 붙들고 여포의 재돌격을 격파하라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." }, "conditions": { - "victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_counterattack" }, + "victory": { + "type": "all", + "conditions": [ + { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_9_lu_bu_second_charge" }, + { + "type": "unit_reaches_tile", + "label": "중앙 방진", + "team": "player", + "unit_ids": ["cao_cao_ch6", "dian_wei_ch6", "xiahou_dun_ch6", "xiahou_yuan_ch6", "cao_ren_ch6"], + "officer_ids": ["cao_cao", "dian_wei", "xiahou_dun", "xiahou_yuan", "cao_ren"], + "cells": [[8, 3], [9, 3], [8, 5], [9, 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": { @@ -17,7 +30,8 @@ "location": "정도, 흥평 원년", "lines": [ "복양 야습 뒤 여포는 분노를 거두지 않고 정도 들판으로 병마를 몰아온다.", - "언덕과 길목이 전장을 가른다. 전열을 좁혀 기병의 첫 돌격을 받아내라." + "언덕과 길목이 전장을 가른다. 전열을 좁혀 기병의 첫 돌격을 받아내라.", + "중앙 방진을 빼앗기지 않고 여포의 두 번째 돌격까지 버티면, 연주의 들판이 다시 조조군 편에 설 것이다." ], "conditional_lines": [ { @@ -133,11 +147,11 @@ "GGGGGGGGGGGGGG", "GGGFFGGGHHGGGG", "GGGFFRRRHHGGGG", - "GGGGGRRRGGGGGG", + "GGGGGRRRCCGGGG", "RRRRRRRRRRRRRR", - "GGGGGRRRGGGGGG", - "GGHHGFFFGRRGGG", - "GGHHGFFFGGGWWG", + "GGGGGRRRCCGGGG", + "GGHHTTFFGRRGGG", + "GGHHTTFFGGGWWG", "GGGGGGFFGGGWWG", "GGGGGGGGGGGGGG" ] @@ -220,6 +234,33 @@ "level": 6, "pos": [11, 6], "base": { "hp": 34, "mp": 18, "atk": 7, "def": 5, "int": 12 } + }, + { + "unit_id": "dingtao_road_spearman", + "name": "정도 길목창병", + "class_id": "infantry", + "team": "enemy", + "level": 6, + "pos": [8, 4], + "base": { "hp": 44, "atk": 15, "def": 9 } + }, + { + "unit_id": "dingtao_hill_rider", + "name": "정도 언덕기병", + "class_id": "cavalry", + "team": "enemy", + "level": 6, + "pos": [10, 6], + "base": { "hp": 42, "atk": 16, "def": 8, "agi": 11 } + }, + { + "unit_id": "dingtao_hill_archer", + "name": "정도 언덕궁병", + "class_id": "archer", + "team": "enemy", + "level": 6, + "pos": [8, 2], + "base": { "hp": 32, "atk": 12, "def": 5 } } ], "events": [ @@ -276,7 +317,7 @@ "class_id": "archer", "team": "enemy", "level": 6, - "pos": [12, 6], + "pos": [10, 8], "base": { "hp": 30, "atk": 12, "def": 5 } } ] @@ -337,8 +378,119 @@ { "type": "log", "text": "여포가 후속 부대를 전장에 밀어 넣는다." }, { "type": "set_objective", - "victory": "여포의 반격군과 후속 부대를 모두 격파하라.", - "defeat": "조조가 퇴각하거나 제12군령에 이르면 패한다." + "victory": "중앙 방진을 붙들고 여포의 후속 부대를 격파하라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." + } + ] + }, + { + "id": "turn_6_flank_pressure", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 6 }, + "actions": [ + { "type": "log", "text": "여포의 측면대가 언덕 사이로 돌아 중앙 방진을 흔든다." }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Gao Shun", "display_speaker": "고순", "text": "중앙을 지키는 자가 무너지면 조조의 군기는 갈 곳이 없다. 양쪽에서 눌러라." }, + { "speaker": "Dian Wei", "display_speaker": "전위", "text": "중앙이 좁을수록 좋습니다. 제 도끼가 닿을 곳도 좁아지니까요." } + ] + }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "dingtao_flank_rider", + "name": "측면 기병", + "class_id": "cavalry", + "team": "enemy", + "level": 6, + "pos": [6, 1], + "base": { "hp": 40, "atk": 15, "def": 7, "agi": 11 } + }, + { + "unit_id": "dingtao_flank_guard", + "name": "측면 창병", + "class_id": "infantry", + "team": "enemy", + "level": 6, + "pos": [6, 8], + "base": { "hp": 42, "atk": 14, "def": 9 } + }, + { + "unit_id": "dingtao_flank_archer", + "name": "측면 궁병", + "class_id": "archer", + "team": "enemy", + "level": 6, + "pos": [9, 8], + "base": { "hp": 32, "atk": 12, "def": 5 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "xiahou_dun_ch6", + "priority": 8, + "text": "측면대가 하후돈의 중앙 돌파선을 노린다." + } + ] + }, + { + "id": "turn_9_lu_bu_second_charge", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 9 }, + "actions": [ + { "type": "log", "text": "여포가 두 번째 돌격을 명하며 정도 들판의 먼지가 다시 솟는다." }, + { + "type": "set_objective", + "victory": "여포의 두 번째 돌격을 꺾고 중앙 방진을 지켜라.", + "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." + }, + { + "type": "dialogue", + "lines": [ + { "speaker": "Lu Bu", "display_speaker": "여포", "text": "한 번 버텼다고 전장이 네 것이 되느냐. 이번에는 중앙을 짓밟아 주마." }, + { "speaker": "Cao Cao", "display_speaker": "조조", "text": "여포의 기세가 돌아왔다. 중앙 방진을 지키며 저 돌격의 숨을 끊어라." } + ] + }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "lu_bu_second_rider", + "name": "재돌격 기병", + "class_id": "elite_cavalry", + "team": "enemy", + "level": 7, + "pos": [13, 3], + "base": { "hp": 52, "atk": 18, "def": 9, "agi": 12 } + }, + { + "unit_id": "lu_bu_second_guard", + "name": "재돌격 창병", + "class_id": "infantry", + "team": "enemy", + "level": 6, + "pos": [13, 5], + "base": { "hp": 44, "atk": 15, "def": 9 } + }, + { + "unit_id": "lu_bu_second_archer", + "name": "재돌격 궁병", + "class_id": "archer", + "team": "enemy", + "level": 6, + "pos": [13, 6], + "base": { "hp": 34, "atk": 13, "def": 5 } + } + ] + }, + { + "type": "set_ai_target_priority", + "unit_id": "cao_cao_ch6", + "priority": 12, + "text": "여포의 재돌격이 조조의 군기를 노린다." } ] }, diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 0dea831..f7a4bfd 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -91,6 +91,18 @@ func _init() -> void: _progress_text("res://data/scenarios/005_puyang_raid.json", false), "군영 장악" ) + _check_contains( + failures, + "006 gated victory progress", + _progress_text("res://data/scenarios/006_dingtao_counterattack.json", false), + "제9군령에 전공 갱신" + ) + _check_contains( + failures, + "006 center line progress", + _progress_text("res://data/scenarios/006_dingtao_counterattack.json", false), + "중앙 방진" + ) _check_contains( failures, "007 destination progress", @@ -117,6 +129,7 @@ func _init() -> void: _check_xingyang_ambush_extended_pressure(failures) _check_qingzhou_campaign_extended_pressure(failures) _check_puyang_raid_extended_pressure(failures) + _check_dingtao_counterattack_extended_pressure(failures) _check_objective_update(failures) _check_objective_notice(failures) _check_terrain_recovery(failures) @@ -573,6 +586,87 @@ func _check_puyang_raid_extended_pressure(failures: Array[String]) -> void: scene.free() +func _check_dingtao_counterattack_extended_pressure(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): + failures.append("Dingtao Counterattack pressure smoke could not load scenario") + return + if state.get_turn_limit() != 17: + failures.append("Dingtao Counterattack 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("Dingtao Counterattack should start with 8 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state._condition_gate_open({"after_event": "turn_9_lu_bu_second_charge"}): + failures.append("Dingtao Counterattack final defeat-all condition should be gated until turn 9 second charge") + var marker_cells := state.get_objective_cells() + for center_cell in [Vector2i(8, 3), Vector2i(9, 3), Vector2i(8, 5), Vector2i(9, 5)]: + if not marker_cells.has(center_cell): + failures.append("Dingtao Counterattack should mark center line cell %s: %s" % [str(center_cell), str(marker_cells)]) + if state.get_objective_cell_label(center_cell) != "중앙 방진": + failures.append("Dingtao Counterattack center marker should expose label at %s" % str(center_cell)) + var recovery_summaries := state.get_recovery_terrain_summaries() + var has_center_recovery := false + var has_supply_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)) == 8: + has_center_recovery = true + if str(summary.get("name", "")) == "마을" and int(summary.get("count", 0)) == 4 and int(summary.get("heal", 0)) == 6: + has_supply_recovery = true + if not has_center_recovery: + failures.append("Dingtao Counterattack should expose center recovery terrain: %s" % str(recovery_summaries)) + if not has_supply_recovery: + failures.append("Dingtao Counterattack should expose supply 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_lu_bu_second_charge"}): + failures.append("Dingtao Counterattack final gate should open after turn 9 second charge event") + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 14: + failures.append("Dingtao Counterattack base branch should field 14 enemies after second charge, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if int(state.get_unit("xiahou_dun_ch6").get("ai_target_priority", 0)) != 8: + failures.append("Dingtao Counterattack turn 6 flank pressure should target Xiahou Dun") + if int(state.get_unit("cao_cao_ch6").get("ai_target_priority", 0)) != 12: + failures.append("Dingtao Counterattack turn 9 second charge should target Cao Cao") + 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("Dingtao Counterattack should not end before center line is held, got %s" % str(state.battle_status)) + var cao_cao: Dictionary = state.get_unit("cao_cao_ch6") + cao_cao["pos"] = Vector2i(8, 3) + state._check_battle_status() + if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: + failures.append("Dingtao Counterattack should end after second charge falls and center line is held, got %s" % str(state.battle_status)) + var fortified_state = BattleStateScript.new() + if not fortified_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"fortified_yan_province": true}): + failures.append("Dingtao Counterattack fortified branch smoke could not load scenario") + return + fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) + fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) + fortified_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) + if fortified_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16: + failures.append("Dingtao Counterattack fortified branch should field 16 enemies after second charge, got %d" % fortified_state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + var pressed_state = BattleStateScript.new() + if not pressed_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"pressed_lu_bu": true}): + failures.append("Dingtao Counterattack pressed branch smoke could not load scenario") + return + pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 3) + pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 6) + pressed_state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) + if pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 17: + failures.append("Dingtao Counterattack pressed branch should field 17 enemies after second charge, got %d" % pressed_state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): + failures.append("Dingtao Counterattack briefing smoke could not load scenario") + scene.free() + return + var overview := scene._format_briefing_battlefield_overview_text() + _check_contains(failures, "006 briefing map size", overview, "14칸 x 10칸") + _check_contains(failures, "006 briefing enemy count", overview, "적세 8명") + _check_contains(failures, "006 briefing village recovery", overview, "마을 4칸 +6") + _check_contains(failures, "006 briefing center recovery", overview, "성채 4칸 +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"):