diff --git a/data/scenarios/001_yellow_turbans.json b/data/scenarios/001_yellow_turbans.json index 0c2f024..64d7a59 100644 --- a/data/scenarios/001_yellow_turbans.json +++ b/data/scenarios/001_yellow_turbans.json @@ -1,31 +1,85 @@ { "id": "001_yellow_turbans", - "name": "영천 소전", + "name": "영천 황건 토벌전", "objectives": { - "victory": "성채를 장악하고 마지막 황건대까지 격파하라.", - "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다." + "victory": "동쪽 성채를 장악하고 제11군령 이후 나타나는 마지막 황건 잔당까지 격파하라.", + "defeat": "조조가 퇴각하거나 제22턴이 시작되면 패한다." }, "conditions": { "victory": { "type": "all", "conditions": [ - { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_11_last_rally" }, + { + "type": "all_units_defeated", + "team": "enemy", + "after_event": "turn_11_last_rally" + }, { "type": "unit_reaches_tile", "label": "성채 장악", "team": "player", - "unit_ids": ["cao_cao", "xiahou_dun"], - "cells": [[12, 0], [13, 0], [12, 1], [13, 1]] + "unit_ids": [ + "cao_cao", + "xiahou_dun" + ], + "cells": [ + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 21, + 1 + ], + [ + 19, + 2 + ], + [ + 20, + 2 + ], + [ + 21, + 2 + ] + ] } ] }, "defeat": [ - { "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }, - { "type": "turn_reached", "turn": 18, "team": "player" } + { + "type": "any_officer_defeated", + "team": "player", + "officer_ids": [ + "cao_cao" + ] + }, + { + "type": "turn_reached", + "turn": 22, + "team": "player" + } ] }, "briefing": { - "title": "영천 소전", + "title": "영천 황건 토벌전", "location": "영천, 중평 원년", "lines": [ "황건의 무리가 영천 근방에 깃발을 세웠다.", @@ -33,8 +87,18 @@ "적은 마을과 성채에서 병력을 고치며, 늦게 닿는 원군까지 기다리고 있다." ], "camp_dialogue": [ - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." }, - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "군사는 이미 창끝을 세웠습니다. 선봉을 맡겨 주시면 적의 앞줄을 묶어 두겠습니다." } + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." + }, + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "군사는 이미 창끝을 세웠습니다. 선봉을 맡겨 주시면 적의 앞줄을 묶어 두겠습니다." + } ], "camp_conversations": [ { @@ -46,8 +110,18 @@ "display_speaker": "조조", "summary": "첫 행군 전에 난세의 형세를 살핀다.", "lines": [ - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "적의 가운데가 허술하다. 앞줄을 깨고 깃발을 다시 모을 시간을 주지 마라." } + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "적의 가운데가 허술하다. 앞줄을 깨고 깃발을 다시 모을 시간을 주지 마라." + } ] }, { @@ -59,8 +133,18 @@ "display_speaker": "하후돈", "summary": "첫 접전에 앞서 선봉의 길을 정한다.", "lines": [ - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "군사는 이미 창끝을 세웠습니다. 선봉을 맡겨 주시면 적의 앞줄을 묶어 두겠습니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "붙잡되 깊이 쫓지는 마라. 숲 쪽에도 눈을 두어야 한다." } + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "군사는 이미 창끝을 세웠습니다. 선봉을 맡겨 주시면 적의 앞줄을 묶어 두겠습니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "붙잡되 깊이 쫓지는 마라. 숲 쪽에도 눈을 두어야 한다." + } ] }, { @@ -69,11 +153,26 @@ "label": "북쪽 숲 보급고", "summary": "상인이 흘린 보급 소문을 살핀다.", "lines": [ - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "상인의 말이 허황되지만은 않을 것이다. 전열이 버티면 북쪽 숲으로 척후를 보내라." }, - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "적지에 숨은 보급이라면, 저들이 기억해 내기 전에 우리가 거두겠습니다." } + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "상인의 말이 허황되지만은 않을 것이다. 전열이 버티면 북쪽 숲으로 척후를 보내라." + }, + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "적지에 숨은 보급이라면, 저들이 기억해 내기 전에 우리가 거두겠습니다." + } ], "effects": [ - { "type": "grant_item", "item_id": "bean", "count": 1, "text": "척후가 북쪽 숲에서 콩 한 줌을 거두어 군수에 보탰다." } + { + "type": "grant_item", + "item_id": "bean", + "count": 1, + "text": "척후가 북쪽 숲에서 콩 한 줌을 거두어 군수에 보탰다." + } ] } ] @@ -86,30 +185,65 @@ "북쪽 숲에는 묻어 둔 보급이 있다는 말도 도니, 척후가 닿으면 살펴보십시오." ] }, - "items": ["bean", "antidote"] + "items": [ + "bean", + "antidote" + ] }, "roster": { "max_units": 2, - "required_officers": ["cao_cao", "xiahou_dun"] + "required_officers": [ + "cao_cao", + "xiahou_dun" + ] }, "formation": { - "cells": [[1, 3], [1, 4], [2, 3], [2, 4]] + "cells": [ + [ + 1, + 5 + ], + [ + 2, + 5 + ], + [ + 1, + 6 + ], + [ + 2, + 6 + ], + [ + 1, + 7 + ], + [ + 2, + 7 + ] + ] }, "map": { - "width": 14, - "height": 10, + "width": 22, + "height": 14, "background": "res://art/backgrounds/battlefield_frontier.png", "terrain": [ - "GGGGGGGGGGGTCC", - "GGGFFFFGGGGTCC", - "GGGFFFFGGGGGGG", - "GGGRRRRRRRGGGG", - "GGGRGGGGGRGGHG", - "GGGRGGTTGRGGHG", - "GGGGGGGGGRGGGG", - "GGGWWGGGGRGGGG", - "GGGWWGGGGGGGGG", - "GGGGGGGGGGGGGG" + "GGGGFFFFGGGGGGGGHHHCCC", + "GGGGFFFFGGGGGGGGHHHCCC", + "GGGGFFFFGGGGGGGGHHHCCC", + "GGGFFFFFGGGGGGGGHHHRRG", + "GGGFFFFGGGGGGGGRRRRRGG", + "GGGGGGTTGGRRRRRRGHHHHG", + "GRRRRRTTRRRGGGGGGHHHHG", + "GGGGGGGGRRRRRRRRGGGGGG", + "GGGGGGGGGGGGGTTGGGGGGG", + "GGGGGGGGGGDDDTTGGGGGGG", + "GGGWWWGGGGDDDDGGGGGGGG", + "GGGWWWGGGGDDDDGGGGGGGG", + "GGGGWWWGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGGGGGGGG" ] }, "deployments": [ @@ -117,13 +251,19 @@ "unit_id": "cao_cao", "officer_id": "cao_cao", "team": "player", - "pos": [1, 3] + "pos": [ + 1, + 6 + ] }, { "unit_id": "xiahou_dun", "officer_id": "xiahou_dun", "team": "player", - "pos": [1, 4] + "pos": [ + 1, + 7 + ] }, { "unit_id": "yellow_turban_1", @@ -131,8 +271,15 @@ "class_id": "bandit", "team": "enemy", "level": 2, - "pos": [12, 1], - "base": { "hp": 46, "atk": 12, "def": 7 } + "pos": [ + 20, + 1 + ], + "base": { + "hp": 54, + "atk": 13, + "def": 8 + } }, { "unit_id": "yellow_turban_2", @@ -140,8 +287,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [9, 4], - "base": { "hp": 34, "atk": 10, "def": 6 } + "pos": [ + 12, + 5 + ], + "base": { + "hp": 36, + "atk": 10, + "def": 6 + } }, { "unit_id": "yellow_turban_3", @@ -149,8 +303,15 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [11, 2], - "base": { "hp": 26, "atk": 9, "def": 4 } + "pos": [ + 17, + 3 + ], + "base": { + "hp": 28, + "atk": 9, + "def": 4 + } }, { "unit_id": "yellow_turban_4", @@ -158,8 +319,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [6, 5], - "base": { "hp": 36, "atk": 9, "def": 7 } + "pos": [ + 6, + 5 + ], + "base": { + "hp": 38, + "atk": 9, + "def": 7 + } }, { "unit_id": "yellow_turban_5", @@ -167,8 +335,15 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [7, 3], - "base": { "hp": 32, "atk": 11, "def": 5 } + "pos": [ + 8, + 6 + ], + "base": { + "hp": 34, + "atk": 11, + "def": 5 + } }, { "unit_id": "yellow_turban_6", @@ -176,8 +351,15 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [11, 7], - "base": { "hp": 26, "atk": 9, "def": 4 } + "pos": [ + 15, + 7 + ], + "base": { + "hp": 28, + "atk": 9, + "def": 4 + } }, { "unit_id": "yellow_turban_7", @@ -185,8 +367,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [11, 1], - "base": { "hp": 38, "atk": 10, "def": 7 } + "pos": [ + 19, + 2 + ], + "base": { + "hp": 40, + "atk": 10, + "def": 8 + } }, { "unit_id": "yellow_turban_8", @@ -194,8 +383,15 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [10, 6], - "base": { "hp": 32, "atk": 11, "def": 5 } + "pos": [ + 14, + 8 + ], + "base": { + "hp": 34, + "atk": 11, + "def": 5 + } }, { "unit_id": "yellow_turban_9", @@ -203,8 +399,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [5, 4], - "base": { "hp": 34, "atk": 9, "def": 6 } + "pos": [ + 5, + 6 + ], + "base": { + "hp": 34, + "atk": 9, + "def": 6 + } }, { "unit_id": "yellow_turban_10", @@ -212,8 +415,15 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [8, 2], - "base": { "hp": 26, "atk": 9, "def": 4 } + "pos": [ + 9, + 4 + ], + "base": { + "hp": 28, + "atk": 9, + "def": 4 + } }, { "unit_id": "yellow_turban_11", @@ -221,8 +431,15 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [6, 6], - "base": { "hp": 32, "atk": 11, "def": 5 } + "pos": [ + 11, + 7 + ], + "base": { + "hp": 34, + "atk": 11, + "def": 5 + } }, { "unit_id": "yellow_turban_12", @@ -230,31 +447,127 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [4, 2], - "base": { "hp": 34, "atk": 9, "def": 6 } + "pos": [ + 5, + 2 + ], + "base": { + "hp": 34, + "atk": 9, + "def": 6 + } + }, + { + "unit_id": "yellow_turban_13", + "name": "마을 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [ + 7, + 6 + ], + "base": { + "hp": 36, + "atk": 10, + "def": 6 + } + }, + { + "unit_id": "yellow_turban_14", + "name": "동로 궁수", + "class_id": "archer", + "team": "enemy", + "level": 1, + "pos": [ + 18, + 5 + ], + "base": { + "hp": 28, + "atk": 9, + "def": 4 + } + }, + { + "unit_id": "yellow_turban_15", + "name": "언덕 유격병", + "class_id": "bandit", + "team": "enemy", + "level": 1, + "pos": [ + 16, + 4 + ], + "base": { + "hp": 34, + "atk": 11, + "def": 5 + } + }, + { + "unit_id": "yellow_turban_16", + "name": "성채 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [ + 21, + 2 + ], + "base": { + "hp": 38, + "atk": 10, + "def": 7 + } } ], "events": [ { "id": "opening", "once": true, - "when": { "type": "battle_start" }, + "when": { + "type": "battle_start" + }, "actions": [ - { "type": "log", "text": "조조군이 황건적과 맞붙었다." }, - { "type": "log", "text": "북쪽 숲 어딘가에 작은 보급고가 숨겨져 있다." } + { + "type": "log", + "text": "조조군이 황건적과 맞붙었다." + }, + { + "type": "log", + "text": "북쪽 숲 어딘가에 작은 보급고가 숨겨져 있다." + } ] }, { "id": "opening_dialogue", "once": true, - "when": { "type": "battle_begin" }, + "when": { + "type": "battle_begin" + }, "actions": [ { "type": "dialogue", "lines": [ - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "황건의 깃발은 마을 뒤 성채에 기대어 서 있다. 들판의 잔병을 먼저 끊고 성채로 길을 열어라." }, - { "speaker": "Zhang Mancheng", "display_speaker": "장만성", "side": "right", "text": "관군이 둘뿐이라니, 우습구나. 성채에 닿기 전에 들판에서 지치게 만들어라!" }, - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "그렇다면 지금 치겠습니다. 제가 앞줄을 열어 길을 내겠습니다." } + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "황건의 깃발은 마을 뒤 성채에 기대어 서 있다. 들판의 잔병을 먼저 끊고 성채로 길을 열어라." + }, + { + "speaker": "Zhang Mancheng", + "display_speaker": "장만성", + "side": "right", + "text": "관군이 둘뿐이라니, 우습구나. 성채에 닿기 전에 들판에서 지치게 만들어라!" + }, + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "그렇다면 지금 치겠습니다. 제가 앞줄을 열어 길을 내겠습니다." + } ] } ] @@ -262,15 +575,38 @@ { "id": "northern_woods_cache", "once": true, - "when": { "type": "unit_reaches_tile", "team": "player", "pos": [3, 2] }, + "when": { + "type": "unit_reaches_tile", + "team": "player", + "pos": [ + 4, + 2 + ] + }, "actions": [ - { "type": "log", "text": "북쪽 숲 보급고를 거두었다." }, - { "type": "grant_item", "item_id": "bean" }, + { + "type": "log", + "text": "북쪽 숲 보급고를 거두었다." + }, + { + "type": "grant_item", + "item_id": "bean" + }, { "type": "dialogue", "lines": [ - { "speaker": "Scout", "display_speaker": "척후", "side": "right", "text": "숲 속 낡은 수레에서 콩 자루를 찾았습니다. 황건병이 미처 옮기지 못한 물자입니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "좋다. 작은 보급도 오래 버티는 군에는 큰 힘이 된다. 전열에 나누어라." } + { + "speaker": "Scout", + "display_speaker": "척후", + "side": "right", + "text": "숲 속 낡은 수레에서 콩 자루를 찾았습니다. 황건병이 미처 옮기지 못한 물자입니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "좋다. 작은 보급도 오래 버티는 군에는 큰 힘이 된다. 전열에 나누어라." + } ] } ] @@ -278,14 +614,31 @@ { "id": "turn_2_warning", "once": true, - "when": { "type": "turn_start", "team": "enemy", "turn": 2 }, + "when": { + "type": "turn_start", + "team": "enemy", + "turn": 2 + }, "actions": [ - { "type": "log", "text": "황건적이 마을과 성채에 기대어 전열을 고친다." }, + { + "type": "log", + "text": "황건적이 마을과 성채에 기대어 전열을 고친다." + }, { "type": "dialogue", "lines": [ - { "speaker": "Scout", "display_speaker": "척후", "side": "right", "text": "마을 앞 수레가 움직입니다. 다친 적병이 집 뒤로 물러나 숨을 고릅니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "회복할 곳을 그냥 두면 싸움이 길어진다. 길목과 마을을 함께 눌러라." } + { + "speaker": "Scout", + "display_speaker": "척후", + "side": "right", + "text": "마을 앞 수레가 움직입니다. 다친 적병이 집 뒤로 물러나 숨을 고릅니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "회복할 곳을 그냥 두면 싸움이 길어진다. 길목과 마을을 함께 눌러라." + } ] } ] @@ -293,14 +646,31 @@ { "id": "turn_4_eastern_reserve", "once": true, - "when": { "type": "turn_start", "team": "enemy", "turn": 4 }, + "when": { + "type": "turn_start", + "team": "enemy", + "turn": 4 + }, "actions": [ - { "type": "log", "text": "동쪽 들판에서 황건 잔병이 합류했다." }, + { + "type": "log", + "text": "동쪽 들판에서 황건 잔병이 합류했다." + }, { "type": "dialogue", "lines": [ - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "동쪽 먼지가 일어납니다. 흩어진 잔병이 제 선봉 쪽으로 몰립니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "좋다. 네가 버티는 동안 중앙의 깃발을 끊겠다. 너무 깊이 끌려가지는 마라." } + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "동쪽 먼지가 일어납니다. 흩어진 잔병이 제 선봉 쪽으로 몰립니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "좋다. 네가 버티는 동안 중앙의 깃발을 끊겠다. 너무 깊이 끌려가지는 마라." + } ] }, { @@ -312,8 +682,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [13, 4], - "base": { "hp": 32, "atk": 10, "def": 5 } + "pos": [ + 21, + 5 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 5 + } }, { "unit_id": "yellow_turban_reinforcement_2", @@ -321,8 +698,31 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [13, 5], - "base": { "hp": 24, "atk": 9, "def": 4 } + "pos": [ + 20, + 6 + ], + "base": { + "hp": 26, + "atk": 9, + "def": 4 + } + }, + { + "unit_id": "yellow_turban_reinforcement_2b", + "name": "동문 유격병", + "class_id": "bandit", + "team": "enemy", + "level": 1, + "pos": [ + 18, + 6 + ], + "base": { + "hp": 32, + "atk": 11, + "def": 5 + } } ] }, @@ -337,14 +737,31 @@ { "id": "turn_7_southern_raiders", "once": true, - "when": { "type": "turn_start", "team": "enemy", "turn": 7 }, + "when": { + "type": "turn_start", + "team": "enemy", + "turn": 7 + }, "actions": [ - { "type": "log", "text": "남쪽 샛길에서 약탈대가 뒤늦게 모습을 드러냈다." }, + { + "type": "log", + "text": "남쪽 샛길에서 약탈대가 뒤늦게 모습을 드러냈다." + }, { "type": "dialogue", "lines": [ - { "speaker": "Scout", "display_speaker": "척후", "side": "right", "text": "남쪽 샛길입니다! 약탈대가 본진 쪽 깃발을 보고 돌아듭니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "뒤를 물어 시간을 벌려는 수작이다. 본진을 세워 두고 성채 압박을 늦추지 마라." } + { + "speaker": "Scout", + "display_speaker": "척후", + "side": "right", + "text": "남쪽 샛길입니다! 약탈대가 본진 쪽 깃발을 보고 돌아듭니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "뒤를 물어 시간을 벌려는 수작이다. 본진을 세워 두고 성채 압박을 늦추지 마라." + } ] }, { @@ -356,8 +773,15 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [8, 8], - "base": { "hp": 30, "atk": 10, "def": 5 } + "pos": [ + 13, + 12 + ], + "base": { + "hp": 30, + "atk": 10, + "def": 5 + } }, { "unit_id": "yellow_turban_reinforcement_4", @@ -365,8 +789,31 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [9, 8], - "base": { "hp": 24, "atk": 9, "def": 4 } + "pos": [ + 15, + 11 + ], + "base": { + "hp": 24, + "atk": 9, + "def": 4 + } + }, + { + "unit_id": "yellow_turban_reinforcement_4b", + "name": "남쪽 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [ + 16, + 10 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 6 + } } ] }, @@ -381,14 +828,31 @@ { "id": "turn_9_western_reserve", "once": true, - "when": { "type": "turn_start", "team": "enemy", "turn": 9 }, + "when": { + "type": "turn_start", + "team": "enemy", + "turn": 9 + }, "actions": [ - { "type": "log", "text": "서쪽 숲길에서 마지막 황건대가 전장에 닿았다." }, + { + "type": "log", + "text": "서쪽 숲길에서 마지막 황건대가 전장에 닿았다." + }, { "type": "dialogue", "lines": [ - { "speaker": "Zhang Mancheng", "display_speaker": "장만성", "side": "right", "text": "마지막 무리까지 불러들였다. 성채가 버티는 한 관군은 이 들판을 지나지 못한다!" }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "저들의 의지와 도망길이 한곳에 모였다. 성채를 붙들고 남은 깃발을 모두 꺾어라." } + { + "speaker": "Zhang Mancheng", + "display_speaker": "장만성", + "side": "right", + "text": "마지막 무리까지 불러들였다. 성채가 버티는 한 관군은 이 들판을 지나지 못한다!" + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "저들의 의지와 도망길이 한곳에 모였다. 성채를 붙들고 남은 깃발을 모두 꺾어라." + } ] }, { @@ -404,8 +868,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [5, 8], - "base": { "hp": 34, "atk": 10, "def": 6 } + "pos": [ + 7, + 11 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 6 + } }, { "unit_id": "yellow_turban_reinforcement_6", @@ -413,8 +884,15 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [6, 8], - "base": { "hp": 26, "atk": 9, "def": 4 } + "pos": [ + 9, + 10 + ], + "base": { + "hp": 26, + "atk": 9, + "def": 4 + } }, { "unit_id": "yellow_turban_reinforcement_7", @@ -422,8 +900,31 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [3, 6], - "base": { "hp": 32, "atk": 11, "def": 5 } + "pos": [ + 4, + 9 + ], + "base": { + "hp": 32, + "atk": 11, + "def": 5 + } + }, + { + "unit_id": "yellow_turban_reinforcement_8", + "name": "서쪽 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [ + 7, + 12 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 6 + } } ] }, @@ -438,15 +939,37 @@ { "id": "turn_11_last_rally", "once": true, - "when": { "type": "turn_start", "team": "enemy", "turn": 11 }, + "when": { + "type": "turn_start", + "team": "enemy", + "turn": 11 + }, "actions": [ - { "type": "log", "text": "흩어진 황건 잔당이 서쪽 둔덕에서 마지막 반격을 시도한다." }, + { + "type": "log", + "text": "흩어진 황건 잔당이 서쪽 둔덕에서 마지막 반격을 시도한다." + }, { "type": "dialogue", "lines": [ - { "speaker": "Scout", "display_speaker": "척후", "side": "right", "text": "서쪽 둔덕에 남은 깃발이 있습니다. 성채를 보고 마지막으로 모여듭니다!" }, - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "제 뒤를 물려는 놈들입니다. 성채 앞줄은 그대로 두고, 돌아드는 길만 끊겠습니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "좋다. 이제 도망길도, 기대어 설 깃발도 없다. 성채를 붙들고 남은 황건대를 모두 거두어라." } + { + "speaker": "Scout", + "display_speaker": "척후", + "side": "right", + "text": "서쪽 둔덕에 남은 깃발이 있습니다. 성채를 보고 마지막으로 모여듭니다!" + }, + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "제 뒤를 물려는 놈들입니다. 성채 앞줄은 그대로 두고, 돌아드는 길만 끊겠습니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "좋다. 이제 도망길도, 기대어 설 깃발도 없다. 성채를 붙들고 남은 황건대를 모두 거두어라." + } ] }, { @@ -462,8 +985,15 @@ "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [1, 8], - "base": { "hp": 34, "atk": 10, "def": 6 } + "pos": [ + 2, + 12 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 6 + } }, { "unit_id": "yellow_turban_last_rally_2", @@ -471,8 +1001,15 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [2, 8], - "base": { "hp": 26, "atk": 9, "def": 4 } + "pos": [ + 3, + 13 + ], + "base": { + "hp": 26, + "atk": 9, + "def": 4 + } }, { "unit_id": "yellow_turban_last_rally_3", @@ -480,14 +1017,40 @@ "class_id": "bandit", "team": "enemy", "level": 1, - "pos": [4, 9], - "base": { "hp": 32, "atk": 11, "def": 5 } + "pos": [ + 7, + 13 + ], + "base": { + "hp": 32, + "atk": 11, + "def": 5 + } + }, + { + "unit_id": "yellow_turban_last_rally_4", + "name": "마지막 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [ + 8, + 12 + ], + "base": { + "hp": 34, + "atk": 10, + "def": 6 + } } ] }, { "type": "set_ai_target_priority", - "unit_ids": ["cao_cao", "xiahou_dun"], + "unit_ids": [ + "cao_cao", + "xiahou_dun" + ], "priority": 13, "text": "마지막 황건 잔당이 조조와 하후돈의 후미를 노린다." } @@ -496,13 +1059,29 @@ { "id": "zhang_mancheng_falls", "once": true, - "when": { "type": "unit_defeated", "team": "enemy", "unit_ids": ["yellow_turban_1"] }, + "when": { + "type": "unit_defeated", + "team": "enemy", + "unit_ids": [ + "yellow_turban_1" + ] + }, "actions": [ { "type": "dialogue", "lines": [ - { "speaker": "Zhang Mancheng", "display_speaker": "장만성", "side": "right", "text": "성채의 깃발이... 이토록 빨리 꺾일 줄이야. 흩어져라, 산 자는 숲으로 빠져라!" }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "두령이 무너졌어도 잔병은 아직 들판에 남았다. 성채를 붙들고 남은 깃발을 거두어라." } + { + "speaker": "Zhang Mancheng", + "display_speaker": "장만성", + "side": "right", + "text": "성채의 깃발이... 이토록 빨리 꺾일 줄이야. 흩어져라, 산 자는 숲으로 빠져라!" + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "두령이 무너졌어도 잔병은 아직 들판에 남았다. 성채를 붙들고 남은 깃발을 거두어라." + } ] } ] @@ -510,13 +1089,38 @@ ], "rewards": { "gold": 300, - "items": ["bean"], - "join_officers": ["xiahou_yuan", "cao_ren"] + "items": [ + "bean" + ], + "join_officers": [ + "xiahou_yuan", + "cao_ren" + ] }, "post_battle_dialogue": [ - { "speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "적이 흩어졌습니다. 오늘 밤 영천은 한숨 돌릴 수 있겠습니다." }, - { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "이는 첫 불길일 뿐이다. 조정이 빨리 움직이지 못한다면 우리가 움직여야 한다." }, - { "speaker": "Xiahou Yuan", "display_speaker": "하후연", "side": "right", "text": "그렇다면 다음 출정에도 저희를 부르십시오. 함께 달리겠습니다." }, - { "speaker": "Cao Ren", "display_speaker": "조인", "side": "right", "text": "일족은 명을 기다리고 있습니다, 주공." } + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "적이 흩어졌습니다. 오늘 밤 영천은 한숨 돌릴 수 있겠습니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "이는 첫 불길일 뿐이다. 조정이 빨리 움직이지 못한다면 우리가 움직여야 한다." + }, + { + "speaker": "Xiahou Yuan", + "display_speaker": "하후연", + "side": "right", + "text": "그렇다면 다음 출정에도 저희를 부르십시오. 함께 달리겠습니다." + }, + { + "speaker": "Cao Ren", + "display_speaker": "조인", + "side": "right", + "text": "일족은 명을 기다리고 있습니다, 주공." + } ] } diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 51c9e0f..a337bc3 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -311,8 +311,8 @@ func _check_battle_presentation_sequence_contract(failures: Array[String]) -> vo failures.append("missing battle presentation helper: %s" % method_name) scene._begin_battle_presentation_sequence() - scene._on_unit_motion_requested("yellow_turban_1", Vector2i(12, 1), Vector2i(11, 1)) - scene._on_unit_action_motion_requested("yellow_turban_2", Vector2i(9, 4), Vector2i(8, 4), "attack") + scene._on_unit_motion_requested("yellow_turban_1", Vector2i(20, 1), Vector2i(19, 1)) + scene._on_unit_action_motion_requested("yellow_turban_2", Vector2i(12, 5), Vector2i(11, 5), "attack") scene._on_combat_feedback_requested("yellow_turban_2", "-10", "damage") scene._end_battle_presentation_sequence() diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 8018b59..2b3a335 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -29,7 +29,7 @@ func _init() -> void: failures, "001 turn pressure risk", _defeat_text("res://data/scenarios/001_yellow_turbans.json"), - "잔여 군령 17" + "잔여 군령 21" ) _check_contains( failures, @@ -332,7 +332,7 @@ func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> v failures.append("opening castle objective smoke could not load scenario") return var marker_cells := state.get_objective_cells() - for castle_cell in [Vector2i(12, 0), Vector2i(13, 0), Vector2i(12, 1), Vector2i(13, 1)]: + for castle_cell in [Vector2i(19, 0), Vector2i(20, 0), Vector2i(21, 0), Vector2i(19, 1), Vector2i(20, 1), Vector2i(21, 1), Vector2i(19, 2), Vector2i(20, 2), Vector2i(21, 2)]: if not marker_cells.has(castle_cell): failures.append("opening battle should mark castle capture cell %s: %s" % [str(castle_cell), str(marker_cells)]) if state.get_objective_cell_label(castle_cell) != "성채 장악": @@ -343,7 +343,7 @@ func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> v if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("opening battle should not end before castle capture, got %s" % str(state.battle_status)) var cao_cao: Dictionary = state.get_unit("cao_cao") - cao_cao["pos"] = Vector2i(12, 1) + cao_cao["pos"] = Vector2i(20, 1) state._check_battle_status() if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE: failures.append("opening battle should wait for late reserve before victory, got %s" % str(state.battle_status)) @@ -393,11 +393,11 @@ func _check_briefing_battlefield_overview(failures: Array[String]) -> void: scene.free() return var overview := scene._format_briefing_battlefield_overview_text() - _check_contains(failures, "001 briefing map size", overview, "14칸 x 10칸") - _check_contains(failures, "001 briefing enemy count", overview, "적세 12명") + _check_contains(failures, "001 briefing map size", overview, "22칸 x 14칸") + _check_contains(failures, "001 briefing enemy count", overview, "적세 16명") _check_contains(failures, "001 briefing enemy classes", overview, "보병") - _check_contains(failures, "001 briefing village recovery", overview, "마을 4칸 +6") - _check_contains(failures, "001 briefing castle recovery", overview, "성채 4칸 +8") + _check_contains(failures, "001 briefing village recovery", overview, "마을 8칸 +6") + _check_contains(failures, "001 briefing castle recovery", overview, "성채 9칸 +8") scene.free() @@ -406,25 +406,25 @@ func _check_opening_battle_extended_pressure(failures: Array[String]) -> void: if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("opening battle pressure smoke could not load scenario") return - if state.get_turn_limit() != 17: - failures.append("opening battle should allow play through turn 17, got limit %d" % state.get_turn_limit()) - if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 12: - failures.append("opening battle should start with 12 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state.get_turn_limit() != 21: + failures.append("opening battle should allow play through turn 21, got limit %d" % state.get_turn_limit()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16: + failures.append("opening battle should start with 16 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if state._condition_gate_open({"after_event": "turn_11_last_rally"}): failures.append("opening battle final defeat-all condition should be gated until turn 11 last rally") state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4) - if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 14: - failures.append("opening battle should field 14 enemies after turn 4 reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19: + failures.append("opening battle should field 19 enemies after turn 4 reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if int(state.get_unit("xiahou_dun").get("ai_target_priority", 0)) < 9: failures.append("opening battle turn 4 reserve should pressure Xiahou Dun's vanguard") state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 7) - if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 16: - failures.append("opening battle should field 16 enemies after turn 7 raiders, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 22: + failures.append("opening battle should field 22 enemies after turn 7 raiders, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 11: failures.append("opening battle turn 7 raiders should pressure Cao Cao") state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) - if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19: - failures.append("opening battle should field 19 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 26: + failures.append("opening battle should field 26 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 12: failures.append("opening battle turn 9 reserve should keep pressure on Cao Cao") if state._condition_gate_open({"after_event": "turn_11_last_rally"}): @@ -432,8 +432,8 @@ func _check_opening_battle_extended_pressure(failures: Array[String]) -> void: state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 11) if not state._condition_gate_open({"after_event": "turn_11_last_rally"}): failures.append("opening battle final gate should open after turn 11 last rally event") - if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 22: - failures.append("opening battle should field 22 enemies after final rally, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) + if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 30: + failures.append("opening battle should field 30 enemies after final rally, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size()) if int(state.get_unit("cao_cao").get("ai_target_priority", 0)) < 13 or int(state.get_unit("xiahou_dun").get("ai_target_priority", 0)) < 13: failures.append("opening battle turn 11 last rally should pressure both Cao Cao and Xiahou Dun") @@ -1176,19 +1176,22 @@ func _assert_required_unit(failures: Array[String], state, unit_id: String, labe 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"): + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("opening board layout smoke could not load scenario") + scene.free() return - var board_rect := Rect2( - BattleSceneScript.BOARD_OFFSET, - Vector2(state.get_map_size()) * BattleSceneScript.TILE_SIZE - ) + var board_rect := scene._board_rect() + var view_rect := scene._map_view_rect() var side_rect := Rect2(BattleSceneScript.SIDE_PANEL_POSITION, BattleSceneScript.SIDE_PANEL_SIZE) - if board_rect.intersects(side_rect): - failures.append("opening board should not overlap side HUD: board %s side %s" % [str(board_rect), str(side_rect)]) - if board_rect.end.y > 720.0: - failures.append("opening board should fit within the default viewport height: %s" % str(board_rect)) + if view_rect.intersects(side_rect): + failures.append("opening map view should not overlap side HUD: view %s side %s" % [str(view_rect), str(side_rect)]) + if board_rect.size.x <= view_rect.size.x or board_rect.size.y <= view_rect.size.y: + failures.append("opening board should be larger than the visible map view to exercise scrolling: board %s view %s" % [str(board_rect), str(view_rect)]) + var far_offset := scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0)) + if far_offset.x >= -0.01 or far_offset.y >= -0.01: + failures.append("opening board should allow both horizontal and vertical scrolling: %s" % str(far_offset)) + scene.free() func _check_objective_notice(failures: Array[String]) -> void: diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index 7bd0392..94b5d47 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -32,6 +32,7 @@ func _check_deferred_move_events(failures: Array[String]) -> void: var from_cell := Vector2i(1, 3) var to_cell := Vector2i(2, 3) + state.get_unit("cao_cao")["pos"] = from_cell state.battle_events.append({ "id": "pending_move_gold", "once": true, @@ -86,6 +87,7 @@ func _check_scene_post_move_menu_flow(failures: Array[String]) -> void: scene.briefing_panel.visible = false scene.result_panel.visible = false scene._clear_pending_move_state() + scene.state.get_unit("cao_cao")["pos"] = Vector2i(1, 3) if not scene.state.select_unit("cao_cao"): failures.append("could not select Cao Cao for scene flow") @@ -246,6 +248,7 @@ func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void: enemy["hp"] = 24 enemy["def"] = 1 var cao_cao: Dictionary = scene.state.get_unit("cao_cao") + cao_cao["pos"] = Vector2i(1, 3) cao_cao["agi"] = 999 cao_cao["atk"] = 60 scene.state.rng.seed = 1 @@ -383,6 +386,7 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void: enemy["hp"] = 28 enemy["def"] = 1 var cao_cao: Dictionary = scene.state.get_unit("cao_cao") + cao_cao["pos"] = Vector2i(1, 3) cao_cao["int"] = 80 cao_cao["mp"] = 10 @@ -491,6 +495,7 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: ally["pos"] = Vector2i(2, 4) ally["hp"] = 12 var cao_cao: Dictionary = scene.state.get_unit("cao_cao") + cao_cao["pos"] = Vector2i(1, 3) cao_cao["mp"] = 10 scene.state.battle_events.append({ diff --git a/tools/smoke_status_effect_summary.gd b/tools/smoke_status_effect_summary.gd index 004f9c7..2df5aa3 100644 --- a/tools/smoke_status_effect_summary.gd +++ b/tools/smoke_status_effect_summary.gd @@ -64,7 +64,7 @@ func _check_hud_reuses_status_summary(failures: Array[String]) -> void: } ] scene.state.selected_unit_id = "cao_cao" - scene.hover_cell = Vector2i(1, 3) + scene.hover_cell = Vector2i(1, 6) scene._update_hud() _check_contains(failures, "selected HUD status", scene.selected_label.text, "상태: 방비 -2 (2회)") _check_contains(failures, "hover tile status", scene.cell_info_label.text, "상태: 방비 -2 (2회)") diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 571873f..58d7cb3 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2967,7 +2967,7 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: if not merchant_notice.contains("군막 상인") or not merchant_notice.contains("길가의 물자"): failures.append("shop merchant notice should include name and flavor lines") var camp_overview := scene._format_briefing_camp_overview_text(scene.state.get_briefing(), false) - for expected in ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 4칸", "비축 물자"]: + for expected in ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 6칸", "비축 물자"]: if not camp_overview.contains(expected): failures.append("camp overview should include %s: %s" % [expected, camp_overview]) var bean: Dictionary = scene.state.get_item_def("bean") @@ -3034,14 +3034,14 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Cao Cao HUD focus should clarify move/attack affordance: %s" % cao_text) if not cao_text.contains("군령:") or not cao_text.contains("행군") or not cao_text.contains("타격") or not cao_text.contains("책략") or not cao_text.contains("병장") or not cao_text.contains("대기"): failures.append("Cao Cao HUD focus should summarize available actions: %s" % cao_text) - if not cao_text.contains("지형 2,4"): + if not cao_text.contains("지형 2,7"): failures.append("Cao Cao HUD focus should include current tile: %s" % cao_text) if not cao_text.contains("행군 1"): failures.append("Cao Cao HUD focus should include terrain move cost: %s" % cao_text) if not cao_text.contains("방비 +0"): failures.append("Cao Cao HUD focus should include terrain defense: %s" % cao_text) var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun") - xiahou_dun["pos"] = Vector2i(3, 1) + xiahou_dun["pos"] = Vector2i(4, 1) var forest_text := scene._unit_current_terrain_text(xiahou_dun) if not forest_text.contains("숲") or not forest_text.contains("행군 3"): failures.append("Cavalry movement should use mounted move_type cost on forest: %s" % forest_text) @@ -3053,8 +3053,8 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Archer HUD focus should clarify ranged attack affordance: %s" % archer_text) if not archer_text.contains("군령: 적군 군기"): failures.append("Enemy archer HUD focus should describe enemy control: %s" % archer_text) - if not scene._unit_current_terrain_text(archer).contains("평원"): - failures.append("Archer terrain text should reflect its current plain tile") + if not scene._unit_current_terrain_text(archer).contains("산지"): + failures.append("Archer terrain text should reflect its current hill tile") _check_unit_class_mark(failures, scene, "cao_cao", "tactic", "책") _check_unit_class_mark(failures, scene, "xiahou_dun", "cavalry", "기") _check_unit_class_mark(failures, scene, "yellow_turban_2", "infantry", "보") @@ -3545,21 +3545,21 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: return scene.battle_started = true var enemy: Dictionary = scene.state.get_unit("yellow_turban_1") - enemy["pos"] = Vector2i(1, 2) + enemy["pos"] = Vector2i(1, 5) scene.state.select_unit("cao_cao") scene._refresh_ranges() - scene.hover_cell = Vector2i(2, 3) + scene.hover_cell = Vector2i(2, 5) var move_badge := scene._target_preview_badge() if str(move_badge.get("text", "")) != "행군" or str(move_badge.get("kind", "")) != "move": failures.append("reachable empty tile should show MOVE badge: %s" % str(move_badge)) - scene.hover_cell = Vector2i(1, 4) + scene.hover_cell = Vector2i(1, 7) var select_badge := scene._target_preview_badge() if str(select_badge.get("text", "")) != "선택" or str(select_badge.get("kind", "")) != "select": failures.append("friendly selectable unit should show SELECT badge: %s" % str(select_badge)) - scene.hover_cell = Vector2i(1, 2) + scene.hover_cell = Vector2i(1, 5) var attack_badge := scene._target_preview_badge() if attack_badge.is_empty() or str(attack_badge.get("kind", "")) == "move" or str(attack_badge.get("text", "")) == "행군": failures.append("enemy hover should keep attack badge priority: %s" % str(attack_badge)) @@ -3571,7 +3571,7 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var cao_cao: Dictionary = scene.state.get_unit("cao_cao") cao_cao["moved"] = true scene._refresh_ranges() - scene.hover_cell = Vector2i(2, 3) + scene.hover_cell = Vector2i(2, 5) var moved_badge := scene._target_preview_badge() if str(moved_badge.get("kind", "")) == "move" or str(moved_badge.get("text", "")) == "행군": failures.append("moved unit should not show MOVE badge: %s" % str(moved_badge)) @@ -3613,11 +3613,11 @@ func _check_counter_attack_badge_variants(failures: Array[String]) -> void: scene.free() return - infantry["pos"] = Vector2i(2, 3) + infantry["pos"] = Vector2i(2, 6) infantry["hp"] = 99 infantry["max_hp"] = 99 archer["pos"] = Vector2i(10, 8) - scene.hover_cell = Vector2i(2, 3) + scene.hover_cell = Vector2i(2, 6) var counter_badge := scene._target_preview_badge() if str(counter_badge.get("kind", "")) != "counter" or not str(counter_badge.get("text", "")).contains("반"): failures.append("melee target should expose counter badge: %s" % str(counter_badge)) @@ -3629,21 +3629,21 @@ func _check_counter_attack_badge_variants(failures: Array[String]) -> void: scene.basic_attack_targeting = false infantry["pos"] = Vector2i(10, 8) - archer["pos"] = Vector2i(2, 3) + archer["pos"] = Vector2i(2, 6) archer["hp"] = 99 archer["max_hp"] = 99 - scene.hover_cell = Vector2i(2, 3) + scene.hover_cell = Vector2i(2, 6) var no_counter_badge := scene._target_preview_badge() if str(no_counter_badge.get("kind", "")) != "damage" or str(no_counter_badge.get("text", "")).contains("반"): failures.append("adjacent archer target should not expose counter badge: %s" % str(no_counter_badge)) archer["pos"] = Vector2i(10, 8) - infantry["pos"] = Vector2i(2, 3) + infantry["pos"] = Vector2i(2, 6) infantry["hp"] = 99 infantry["max_hp"] = 99 infantry["atk"] = 999 cao_cao["hp"] = 1 - scene.hover_cell = Vector2i(2, 3) + scene.hover_cell = Vector2i(2, 6) var danger_badge := scene._target_preview_badge() if str(danger_badge.get("kind", "")) != "danger" or not str(danger_badge.get("text", "")).contains("반격!"): failures.append("lethal counter should expose danger badge: %s" % str(danger_badge)) @@ -3656,7 +3656,7 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: failures.append("could not load battle scene state for presentation helpers") scene.free() return - if scene._terrain_key_at(Vector2i(3, 1)) != "F": + if scene._terrain_key_at(Vector2i(4, 1)) != "F": failures.append("terrain helper should read forest cells") if scene._terrain_key_at(Vector2i(6, 5)) != "T": failures.append("terrain helper should read village cells") @@ -3664,10 +3664,10 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: failures.append("terrain helper should return empty outside the board") if scene.state.get_terrain_name(Vector2i(6, 5)) != "마을" or scene.state.get_terrain_heal(Vector2i(6, 5)) != 6: failures.append("village terrain should expose Korean name and healing") - if scene.state.get_terrain_heal(Vector2i(12, 1)) != 8: + if scene.state.get_terrain_heal(Vector2i(20, 1)) != 8: failures.append("castle terrain should expose stronger healing") scene._create_hud() - scene.hover_cell = Vector2i(12, 1) + scene.hover_cell = Vector2i(20, 1) scene._update_cell_info() if scene.cell_info_label == null or not scene.cell_info_label.text.contains("목표: 성채 장악") or not scene.cell_info_label.text.contains("회복 +8"): failures.append("castle objective cell info should expose capture label and healing: %s" % scene.cell_info_label.text) @@ -3778,11 +3778,11 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: failures.append("could not load battle scene state for attack profiles") scene.free() return - _check_attack_profile(failures, scene, "cao_cao", Vector2i(1, 3), Vector2i(2, 3), "command_strike") - _check_attack_profile(failures, scene, "xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "cavalry_charge") - _check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(12, 1), Vector2i(11, 1), "heavy_strike") - _check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(9, 4), Vector2i(8, 4), "infantry_strike") - _check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(11, 2), Vector2i(11, 0), "arrow") + _check_attack_profile(failures, scene, "cao_cao", Vector2i(1, 6), Vector2i(2, 6), "command_strike") + _check_attack_profile(failures, scene, "xiahou_dun", Vector2i(1, 7), Vector2i(2, 7), "cavalry_charge") + _check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(20, 1), Vector2i(19, 1), "heavy_strike") + _check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(12, 5), Vector2i(11, 5), "infantry_strike") + _check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(17, 3), Vector2i(17, 1), "arrow") _check_synthetic_attack_profile(failures, scene, "elite_cavalry", "cavalry_charge") _check_synthetic_attack_profile(failures, scene, "guard_captain", "infantry_strike") _check_synthetic_attack_profile(failures, scene, "marksman", "arrow", 2) @@ -3793,13 +3793,13 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: _check_skill_action_profile(failures, scene, "skill_damage", "tactic_damage", "tactic_flare") _check_skill_action_profile(failures, scene, "skill_heal", "tactic_heal", "healing_sigil") _check_skill_action_profile(failures, scene, "skill_support", "tactic_support", "order_banner") - scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "attack") + scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 7), Vector2i(2, 7), "attack") _check_stored_attack_motion(failures, scene, "xiahou_dun", "cavalry_charge", "melee", "attack", "charge_dust") - scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(11, 2), Vector2i(11, 0), "attack") + scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(17, 3), Vector2i(17, 1), "attack") _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack", "piercing_trace") - scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(3, 3), "skill_damage") + scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 6), Vector2i(3, 6), "skill_damage") _check_stored_attack_motion(failures, scene, "cao_cao", "tactic_damage", "tactic", "skill_damage", "tactic_flare") - scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(1, 3), "skill_heal") + scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 6), Vector2i(1, 6), "skill_heal") _check_stored_attack_motion(failures, scene, "cao_cao", "tactic_heal", "tactic", "skill_heal", "healing_sigil") _check_attack_vfx_signature(failures, scene, "arrow", "piercing_trace") _check_attack_vfx_signature(failures, scene, "cavalry_charge", "charge_dust") @@ -3977,15 +3977,15 @@ func _check_attack_motion_signal(failures: Array[String]) -> void: return if not motions.has("cao_cao|attack|6,2|7,2"): failures.append("attack motion signal missing expected Cao Cao attack: %s" % str(motions)) - _check_skill_motion_signal(failures, "spark", "skill_damage", Vector2i(3, 3), func(skill_state) -> void: + _check_skill_motion_signal(failures, "spark", "skill_damage", Vector2i(3, 6), func(skill_state) -> void: var enemy: Dictionary = skill_state.get_unit("yellow_turban_1") - enemy["pos"] = Vector2i(3, 3) + enemy["pos"] = Vector2i(3, 6) ) - _check_skill_motion_signal(failures, "mend", "skill_heal", Vector2i(1, 3), func(skill_state) -> void: + _check_skill_motion_signal(failures, "mend", "skill_heal", Vector2i(1, 6), func(skill_state) -> void: var caster: Dictionary = skill_state.get_unit("cao_cao") caster["hp"] = max(1, int(caster.get("hp", 1)) - 10) ) - _check_skill_motion_signal(failures, "guard_order", "skill_support", Vector2i(1, 3), func(_skill_state) -> void: + _check_skill_motion_signal(failures, "guard_order", "skill_support", Vector2i(1, 6), func(_skill_state) -> void: pass ) @@ -4006,7 +4006,9 @@ func _check_skill_motion_signal(failures: Array[String], skill_id: String, expec if not state.try_cast_selected_skill(skill_id, target_cell): failures.append("%s should cast successfully for motion check" % skill_id) return - var expected := "cao_cao|%s|1,3|%d,%d" % [expected_kind, target_cell.x, target_cell.y] + var caster: Dictionary = state.get_unit("cao_cao") + var caster_pos: Vector2i = caster.get("pos", Vector2i(1, 6)) + var expected := "cao_cao|%s|%d,%d|%d,%d" % [expected_kind, caster_pos.x, caster_pos.y, target_cell.x, target_cell.y] if not motions.has(expected): failures.append("%s motion signal missing `%s`: %s" % [skill_id, expected, str(motions)])