diff --git a/docs/roadmap.md b/docs/roadmap.md
index 890ec87..d904c61 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -32,12 +32,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Tenth battle Xuzhou breakout, covering Zhang Fei's mistake, the loss of Xu Province to Lu Bu, and the retreat toward Cao Cao's shadow
- Eleventh battle Xudu refuge road, opening Liu Bei's uneasy service under Cao Cao through a Yuan Shu remnant test battle
- Twelfth battle Xiapi outer siege, putting Liu Bei under Cao Cao's command against Lu Bu while preserving Liu Bei's independent motive and camp tension
-- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, and readiness advice for each deployable officer
-- Flow verification script from title through the twelfth battle victory, recruit sortie selection, and camp save state
+- Thirteenth battle Xiapi final clash, ending Lu Bu's arc and opening the tension that will lead toward Liu Bei breaking from Cao Cao
+- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer
+- Flow verification script from title through the thirteenth battle victory, recruit sortie selection, and camp save state
## Next Steps
-1. Continue the Xiapi siege toward Lu Bu's fall, then build toward Liu Bei's break from Cao Cao
+1. Build the Cao Cao break chapter, then move into Yuan Shao and Liu Biao refuge arcs
2. Add a chapter/progress view so long campaign arcs are easier to read between battles
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
4. Add more recruitable officers as the campaign moves toward Cao Cao, Yuan Shao, Liu Biao, and Zhuge Liang
diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs
index fda780c..b3c5d42 100644
--- a/scripts/verify-flow.mjs
+++ b/scripts/verify-flow.mjs
@@ -1257,6 +1257,93 @@ try {
throw new Error(`Expected twelfth camp to expose Xiapi siege dialogue set and preserve full roster: ${JSON.stringify(twelfthCampState)}`);
}
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(160);
+ const twelfthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !twelfthCampSortieState?.sortieVisible ||
+ !twelfthCampSortieState.sortiePlan?.objectiveLine?.includes('하비 결전') ||
+ twelfthCampSortieState.sortiePlan?.selectedRecruitedCount < 1 ||
+ !twelfthCampSortieState.sortiePlan?.recruitedLine?.includes('합류 무장')
+ ) {
+ throw new Error(`Expected twelfth camp sortie prep to target Xiapi final battle with recruit selection summary: ${JSON.stringify(twelfthCampSortieState)}`);
+ }
+ assertSortieTacticalRoster(twelfthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']);
+ if (!twelfthCampSortieState.sortieRoster.some((unit) => unit.id === 'jian-yong' && unit.recruited)) {
+ throw new Error(`Expected recruit officers to be marked in sortie roster: ${JSON.stringify(twelfthCampSortieState.sortieRoster)}`);
+ }
+ await page.mouse.click(1068, 646);
+ await page.waitForFunction(() => {
+ const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
+ return activeScenes.includes('StoryScene');
+ });
+ await page.screenshot({ path: 'dist/verification-xiapi-final-story.png', fullPage: true });
+
+ for (let i = 0; i < 18; i += 1) {
+ const enteredThirteenthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final';
+ });
+ if (enteredThirteenthBattle) {
+ break;
+ }
+ await page.keyboard.press('Space');
+ await page.waitForTimeout(320);
+ }
+ await page.waitForFunction(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-thirteenth-battle.png', fullPage: true });
+
+ const thirteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const thirteenthEnemies = thirteenthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const thirteenthAllies = thirteenthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const thirteenthEnemyBehaviors = new Set(thirteenthEnemies.map((unit) => unit.ai));
+ if (
+ thirteenthBattleState.camera?.mapWidth !== 32 ||
+ thirteenthBattleState.camera?.mapHeight !== 24 ||
+ thirteenthBattleState.victoryConditionLabel !== '여포 격파' ||
+ thirteenthEnemies.length < 15 ||
+ !thirteenthEnemyBehaviors.has('aggressive') ||
+ !thirteenthEnemyBehaviors.has('guard') ||
+ !thirteenthEnemyBehaviors.has('hold') ||
+ !thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-leader-lu-bu') ||
+ !thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-strategist-chen-gong') ||
+ !thirteenthAllies.some((unit) => unit.id === 'liu-bei') ||
+ !thirteenthAllies.some((unit) => unit.id === 'guan-yu') ||
+ !thirteenthAllies.some((unit) => unit.id === 'zhang-fei') ||
+ !thirteenthAllies.some((unit) => unit.id === 'jian-yong') ||
+ !thirteenthAllies.some((unit) => unit.id === 'mi-zhu')
+ ) {
+ throw new Error(`Expected thirteenth battle to use Xiapi final map, Lu Bu objective, mixed AI, and selected recruit sortie: ${JSON.stringify(thirteenthBattleState)}`);
+ }
+
+ await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
+ await page.waitForFunction(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
+ });
+
+ await page.mouse.click(738, 642);
+ await page.waitForFunction(() => {
+ const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
+ return activeScenes.includes('CampScene');
+ });
+
+ const thirteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ thirteenthCampState?.campBattleId !== 'thirteenth-battle-xiapi-final' ||
+ thirteenthCampState.campTitle !== '하비 결전 후 군영' ||
+ thirteenthCampState.availableDialogueIds?.length !== 3 ||
+ !thirteenthCampState.availableDialogueIds.every((id) => id.endsWith('lu-bu-fall')) ||
+ !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') ||
+ !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') ||
+ !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu')
+ ) {
+ throw new Error(`Expected thirteenth camp to expose Lu Bu fall dialogue set and preserve full roster: ${JSON.stringify(thirteenthCampState)}`);
+ }
+
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -1268,7 +1355,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-twelfth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-thirteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/thirteenth-battle-map.svg b/src/assets/images/battle/thirteenth-battle-map.svg
new file mode 100644
index 0000000..a2dcc0c
--- /dev/null
+++ b/src/assets/images/battle/thirteenth-battle-map.svg
@@ -0,0 +1,95 @@
+
diff --git a/src/game/data/battleItems.ts b/src/game/data/battleItems.ts
index d046a78..fede1fc 100644
--- a/src/game/data/battleItems.ts
+++ b/src/game/data/battleItems.ts
@@ -67,6 +67,15 @@ export const itemCatalog: Record = {
attackBonus: 5,
effects: ['연속 공격 판정 보정']
},
+ 'sky-piercer-halberd': {
+ id: 'sky-piercer-halberd',
+ name: '방천화극',
+ slot: 'weapon',
+ rank: 'treasure',
+ description: '여포의 상징으로 두려움을 산 긴 화극. 한 번 휘두르면 전열이 크게 흔들린다.',
+ attackBonus: 6,
+ effects: ['단독으로 돌파할 때 피해 보너스']
+ },
'yellow-turban-saber': {
id: 'yellow-turban-saber',
name: '황건도',
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 3f3cd8d..78cb35c 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -19,6 +19,10 @@ import {
tenthBattleMap,
tenthBattleUnits,
tenthBattleVictoryPages,
+ thirteenthBattleBonds,
+ thirteenthBattleMap,
+ thirteenthBattleUnits,
+ thirteenthBattleVictoryPages,
twelfthBattleBonds,
twelfthBattleMap,
twelfthBattleUnits,
@@ -65,7 +69,8 @@ export type BattleScenarioId =
| 'ninth-battle-xuzhou-gate-night-raid'
| 'tenth-battle-xuzhou-breakout'
| 'eleventh-battle-xudu-refuge-road'
- | 'twelfth-battle-xiapi-outer-siege';
+ | 'twelfth-battle-xiapi-outer-siege'
+ | 'thirteenth-battle-xiapi-final';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -740,6 +745,59 @@ export const twelfthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const thirteenthBattleScenario: BattleScenarioDefinition = {
+ id: 'thirteenth-battle-xiapi-final',
+ title: '하비 결전',
+ victoryConditionLabel: '여포 격파',
+ defeatConditionLabel: '유비 퇴각',
+ openingObjectiveLines: [
+ '여포가 하비 성 안에서 마지막 돌파를 준비합니다. 여포를 격파하면 서주를 뒤흔든 난전도 끝을 맞습니다.',
+ '진궁은 성루 궁병과 수문 수비를 이용해 접근로를 끊으려 합니다. 기병 돌파대와 성문 수비대를 나누어 상대하십시오.',
+ '성문 요새를 확보하고 22턴 안에 승리하면 조조 휘하에서도 유비군의 전공과 독자 보급 명분을 모두 남길 수 있습니다.'
+ ],
+ map: thirteenthBattleMap,
+ units: thirteenthBattleUnits,
+ bonds: thirteenthBattleBonds,
+ mapTextureKey: 'battle-map-thirteenth',
+ leaderUnitId: 'xiapi-final-leader-lu-bu',
+ quickVictoryTurnLimit: 22,
+ baseVictoryGold: 1560,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '여포 격파',
+ rewardGold: 980,
+ unitId: 'xiapi-final-leader-lu-bu'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 360,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'gate',
+ kind: 'secure-terrain',
+ label: '하비 성문 확보',
+ rewardGold: 560,
+ terrain: 'fort'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '22턴 이내 승리',
+ rewardGold: 420,
+ maxTurn: 22
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
+ itemRewards: ['콩 5', '상처약 3', '탁주 2', '하비 결전 공적 +1'],
+ victoryPages: thirteenthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -754,7 +812,8 @@ export const battleScenarios: Record
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario,
'tenth-battle-xuzhou-breakout': tenthBattleScenario,
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
- 'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario
+ 'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario,
+ 'thirteenth-battle-xiapi-final': thirteenthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 79443b9..8782ddc 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -10,6 +10,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
tenthBattleScenario,
+ thirteenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
type BattleScenarioId
@@ -34,6 +35,8 @@ import {
sixthBattleVictoryPages,
tenthBattleIntroPages,
tenthBattleVictoryPages,
+ thirteenthBattleIntroPages,
+ thirteenthBattleVictoryPages,
twelfthBattleIntroPages,
twelfthBattleVictoryPages,
thirdBattleIntroPages,
@@ -166,12 +169,22 @@ const sortieFlows: Record = {
},
[twelfthBattleScenario.id]: {
afterBattleId: twelfthBattleScenario.id,
+ eyebrow: '다음 전장',
+ title: thirteenthBattleScenario.title,
+ description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 성문과 수문을 뚫고 여포를 격파하면 조조 휘하에서 깊어지는 긴장이 다음 흐름으로 이어집니다.',
+ rewardHint: `예상 보상: ${thirteenthBattleScenario.title} 개방`,
+ nextBattleId: thirteenthBattleScenario.id,
+ campaignStep: 'thirteenth-battle',
+ pages: [...twelfthBattleVictoryPages, ...thirteenthBattleIntroPages]
+ },
+ [thirteenthBattleScenario.id]: {
+ afterBattleId: thirteenthBattleScenario.id,
eyebrow: '다음 장 준비',
- title: '하비 포위의 끝',
- description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지는 긴장으로 이어집니다.',
- rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중',
- pages: twelfthBattleVictoryPages,
- unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
+ title: '조조 이탈의 불씨',
+ description: '여포가 무너지고 하비 결전은 끝났지만, 조조의 칭찬과 경계는 동시에 깊어졌습니다. 다음 흐름은 조조 휘하를 벗어날 명분과 때를 준비하는 장으로 이어집니다.',
+ rewardHint: '다음 장: 조조 이탈과 원소 의탁 준비 중',
+ pages: thirteenthBattleVictoryPages,
+ unavailableNotice: '조조 이탈과 원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 37b2925..5bb2ab5 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -813,6 +813,70 @@ export const twelfthBattleVictoryPages: StoryPage[] = [
}
];
+export const thirteenthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'thirteenth-cao-cao-final-order',
+ bgm: 'story-dark',
+ chapter: '하비 결전의 명',
+ background: 'story-liu-bei',
+ text: '하비 외곽이 무너지자 조조는 마지막 총공세를 명했다. 성 안의 여포는 아직 강했고, 진궁은 남은 성루와 수문을 엮어 마지막 반격을 준비하고 있었다.'
+ },
+ {
+ id: 'thirteenth-brothers-before-final',
+ bgm: 'militia-theme',
+ chapter: '여포를 앞두고',
+ background: 'story-three-heroes',
+ speaker: '관우',
+ portrait: 'guanYu',
+ text: '형님, 여포는 용맹하나 믿음이 얕습니다. 다만 그 칼끝이 어지러운 세상의 그림자라면, 오늘 싸움 뒤에 우리 길도 더 뚜렷해질 것입니다.'
+ },
+ {
+ id: 'thirteenth-liu-bei-resolve',
+ bgm: 'story-dark',
+ chapter: '조조의 그늘',
+ background: 'story-liu-bei',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '조조의 명으로 싸우되, 백성의 원망을 덜고 난세의 사슬을 끊기 위해 싸운다. 여포의 끝을 보더라도 우리의 뜻까지 맡기지는 말자.'
+ },
+ {
+ id: 'thirteenth-xiapi-final-sortie',
+ bgm: 'battle-prep',
+ chapter: '하비 결전',
+ background: 'story-sortie',
+ speaker: '장비',
+ portrait: 'zhangFei',
+ text: '좋습니다, 형님. 이번엔 성문을 열고 여포의 깃발까지 밀고 들어가겠습니다. 누가 함께 나설지 정해 주십시오. 대기한 자도 다음 싸움을 위해 힘을 아껴야 합니다.'
+ }
+];
+
+export const thirteenthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'thirteenth-victory-lubu-falls',
+ bgm: 'militia-theme',
+ chapter: '무너진 여포',
+ background: 'story-militia',
+ text: '하비 성내의 마지막 저항이 꺾이고 여포의 깃발이 내려갔다. 뛰어난 무용도 믿음을 잃으면 성 하나를 지키지 못한다는 소문이 병사들 사이에 퍼졌다.'
+ },
+ {
+ id: 'thirteenth-cao-cao-weighs-liu-bei',
+ bgm: 'story-dark',
+ chapter: '칭찬과 경계',
+ background: 'story-liu-bei',
+ speaker: '간옹',
+ text: '조조는 형님의 말을 들은 뒤 오래 웃지 않았네. 여포의 운명은 끝났지만, 이제부터는 조조가 우리를 어디까지 품고 어디서부터 경계할지 살펴야 하네.'
+ },
+ {
+ id: 'thirteenth-road-to-cao-break',
+ bgm: 'story-dark',
+ chapter: '떠날 때를 헤아리다',
+ background: 'story-three-heroes',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '오늘 싸움으로 빚도 공도 쌓였다. 그러나 의로 시작한 길을 남의 장부 속 숫자로 끝낼 수는 없다. 떠날 때가 오면 망설이지 않도록 마음을 단단히 하자.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1030,6 +1094,12 @@ export const twelfthBattleMap: BattleMap = {
terrain: createTwelfthBattleTerrain()
};
+export const thirteenthBattleMap: BattleMap = {
+ width: 32,
+ height: 24,
+ terrain: createThirteenthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4181,6 +4251,335 @@ export const twelfthBattleUnits: UnitData[] = [
}
];
+const thirteenthBattleAllyPositions: Record = {
+ 'liu-bei': { x: 3, y: 19 },
+ 'guan-yu': { x: 4, y: 18 },
+ 'zhang-fei': { x: 4, y: 20 },
+ 'jian-yong': { x: 5, y: 19 },
+ 'mi-zhu': { x: 3, y: 21 }
+};
+
+export const thirteenthBattleUnits: UnitData[] = [
+ ...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
+ placeScenarioUnit(unit, thirteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
+ ),
+ {
+ id: 'xiapi-final-cavalry-a',
+ name: '여포군 돌격기병',
+ faction: 'enemy',
+ className: '여포군 기병',
+ classKey: 'cavalry',
+ level: 14,
+ exp: 84,
+ hp: 62,
+ maxHp: 62,
+ attack: 21,
+ move: 5,
+ stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 15,
+ y: 15
+ },
+ {
+ id: 'xiapi-final-cavalry-b',
+ name: '여포군 돌격기병',
+ faction: 'enemy',
+ className: '여포군 기병',
+ classKey: 'cavalry',
+ level: 14,
+ exp: 84,
+ hp: 62,
+ maxHp: 62,
+ attack: 21,
+ move: 5,
+ stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 20,
+ y: 7
+ },
+ {
+ id: 'xiapi-final-cavalry-c',
+ name: '여포군 돌격기병',
+ faction: 'enemy',
+ className: '여포군 기병',
+ classKey: 'cavalry',
+ level: 14,
+ exp: 86,
+ hp: 63,
+ maxHp: 63,
+ attack: 21,
+ move: 5,
+ stats: { might: 99, intelligence: 56, leadership: 77, agility: 100, luck: 63 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 25,
+ y: 17
+ },
+ {
+ id: 'xiapi-final-cavalry-d',
+ name: '여포군 친위기병',
+ faction: 'enemy',
+ className: '여포군 기병',
+ classKey: 'cavalry',
+ level: 15,
+ exp: 88,
+ hp: 66,
+ maxHp: 66,
+ attack: 22,
+ move: 5,
+ stats: { might: 101, intelligence: 58, leadership: 80, agility: 101, luck: 64 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 28 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 27,
+ y: 12
+ },
+ {
+ id: 'xiapi-final-gate-guard-a',
+ name: '하비 성문 수비병',
+ faction: 'enemy',
+ className: '여포군 보병',
+ classKey: 'yellowTurban',
+ level: 13,
+ exp: 82,
+ hp: 56,
+ maxHp: 56,
+ attack: 18,
+ move: 3,
+ stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
+ accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
+ },
+ x: 18,
+ y: 13
+ },
+ {
+ id: 'xiapi-final-gate-guard-b',
+ name: '하비 성문 수비병',
+ faction: 'enemy',
+ className: '여포군 보병',
+ classKey: 'yellowTurban',
+ level: 13,
+ exp: 82,
+ hp: 56,
+ maxHp: 56,
+ attack: 18,
+ move: 3,
+ stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
+ accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
+ },
+ x: 19,
+ y: 14
+ },
+ {
+ id: 'xiapi-final-gate-guard-c',
+ name: '하비 성문 수비병',
+ faction: 'enemy',
+ className: '여포군 보병',
+ classKey: 'yellowTurban',
+ level: 13,
+ exp: 84,
+ hp: 57,
+ maxHp: 57,
+ attack: 18,
+ move: 3,
+ stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
+ accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
+ },
+ x: 23,
+ y: 10
+ },
+ {
+ id: 'xiapi-final-gate-guard-d',
+ name: '하비 성문 수비병',
+ faction: 'enemy',
+ className: '여포군 보병',
+ classKey: 'yellowTurban',
+ level: 13,
+ exp: 84,
+ hp: 57,
+ maxHp: 57,
+ attack: 18,
+ move: 3,
+ stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
+ accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
+ },
+ x: 24,
+ y: 11
+ },
+ {
+ id: 'xiapi-final-archer-a',
+ name: '하비 성루 궁병',
+ faction: 'enemy',
+ className: '여포군 궁병',
+ classKey: 'archer',
+ level: 13,
+ exp: 82,
+ hp: 46,
+ maxHp: 46,
+ attack: 19,
+ move: 3,
+ stats: { might: 80, intelligence: 70, leadership: 68, agility: 82, luck: 60 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 2, exp: 56 },
+ armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 22,
+ y: 7
+ },
+ {
+ id: 'xiapi-final-archer-b',
+ name: '하비 성루 궁병',
+ faction: 'enemy',
+ className: '여포군 궁병',
+ classKey: 'archer',
+ level: 13,
+ exp: 84,
+ hp: 47,
+ maxHp: 47,
+ attack: 19,
+ move: 3,
+ stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 2, exp: 58 },
+ armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 25,
+ y: 8
+ },
+ {
+ id: 'xiapi-final-archer-c',
+ name: '하비 성루 궁병',
+ faction: 'enemy',
+ className: '여포군 궁병',
+ classKey: 'archer',
+ level: 13,
+ exp: 84,
+ hp: 47,
+ maxHp: 47,
+ attack: 19,
+ move: 3,
+ stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 2, exp: 58 },
+ armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 27,
+ y: 15
+ },
+ {
+ id: 'xiapi-final-raider-a',
+ name: '여포군 성내 기습병',
+ faction: 'enemy',
+ className: '여포군 기습병',
+ classKey: 'bandit',
+ level: 13,
+ exp: 82,
+ hp: 52,
+ maxHp: 52,
+ attack: 19,
+ move: 4,
+ stats: { might: 91, intelligence: 57, leadership: 67, agility: 85, luck: 61 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 11,
+ y: 10
+ },
+ {
+ id: 'xiapi-final-raider-b',
+ name: '여포군 성내 기습병',
+ faction: 'enemy',
+ className: '여포군 기습병',
+ classKey: 'bandit',
+ level: 13,
+ exp: 84,
+ hp: 53,
+ maxHp: 53,
+ attack: 19,
+ move: 4,
+ stats: { might: 92, intelligence: 57, leadership: 67, agility: 86, luck: 61 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
+ armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 12,
+ y: 21
+ },
+ {
+ id: 'xiapi-final-strategist-chen-gong',
+ name: '진궁',
+ faction: 'enemy',
+ className: '여포군 책사',
+ classKey: 'strategist',
+ level: 14,
+ exp: 90,
+ hp: 56,
+ maxHp: 56,
+ attack: 19,
+ move: 3,
+ stats: { might: 64, intelligence: 98, leadership: 88, agility: 72, luck: 66 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 2, exp: 60 },
+ armor: { itemId: 'cloth-armor', level: 2, exp: 24 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 25,
+ y: 11
+ },
+ {
+ id: 'xiapi-final-leader-lu-bu',
+ name: '여포',
+ faction: 'enemy',
+ className: '비장군',
+ classKey: 'rebelLeader',
+ level: 16,
+ exp: 96,
+ hp: 94,
+ maxHp: 94,
+ attack: 25,
+ move: 5,
+ stats: { might: 108, intelligence: 46, leadership: 88, agility: 102, luck: 65 },
+ equipment: {
+ weapon: { itemId: 'sky-piercer-halberd', level: 3, exp: 20 },
+ armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 28,
+ y: 11
+ }
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -4238,6 +4637,7 @@ export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleB
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
+export const thirteenthBattleBonds: BattleBond[] = twelfthBattleBonds.map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -4432,6 +4832,62 @@ function createTwelfthBattleTerrain(): TerrainType[][] {
);
}
+function createThirteenthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 24 }, (_, y) =>
+ Array.from({ length: 32 }, (_, x): TerrainType => {
+ if (x <= 2 && y >= 18 && y <= 22) {
+ return 'camp';
+ }
+ if (
+ (x >= 20 && x <= 29 && y >= 8 && y <= 15) ||
+ (x >= 23 && x <= 28 && y >= 5 && y <= 7) ||
+ (x >= 25 && x <= 30 && y >= 16 && y <= 18)
+ ) {
+ return 'fort';
+ }
+ if ((x >= 10 && x <= 12 && y >= 17 && y <= 19) || (x >= 17 && x <= 19 && y >= 13 && y <= 15)) {
+ return 'village';
+ }
+ if ((x === 14 || x === 15) && y >= 0 && y <= 22) {
+ return 'river';
+ }
+ if ((x === 18 || x === 19) && y >= 2 && y <= 20) {
+ return 'river';
+ }
+ if (
+ (y === 19 && x >= 2 && x <= 19) ||
+ (y === 14 && x >= 15 && x <= 24) ||
+ (y === 11 && x >= 19 && x <= 29) ||
+ (x === 24 && y >= 7 && y <= 18)
+ ) {
+ return 'road';
+ }
+ if ((x >= 4 && x <= 16 && y === 23 - x) || (x >= 10 && x <= 22 && y === x - 3)) {
+ return 'road';
+ }
+ if (
+ (x <= 8 && y <= 8) ||
+ (x >= 4 && x <= 12 && y >= 9 && y <= 13) ||
+ (x >= 2 && x <= 9 && y >= 14 && y <= 17) ||
+ (x >= 28 && y >= 19 && y <= 22)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 6 && x <= 10 && y >= 2 && y <= 5) ||
+ (x >= 27 && x <= 31 && y >= 1 && y <= 5) ||
+ (x >= 9 && x <= 13 && y >= 20)
+ ) {
+ return 'hill';
+ }
+ if ((x >= 30 && y <= 8) || (x >= 30 && y >= 14) || (x <= 1 && y <= 4)) {
+ return 'cliff';
+ }
+ return 'plain';
+ })
+ );
+}
+
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),
diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts
index a45c2cf..31b0224 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -73,7 +73,22 @@ const unitTexture: Record = {
'xiapi-camp-raider-a': 'unit-rebel',
'xiapi-camp-raider-b': 'unit-rebel',
'xiapi-strategist-chen-gong': 'unit-rebel-archer',
- 'xiapi-leader-zhang-liao': 'unit-rebel-leader'
+ 'xiapi-leader-zhang-liao': 'unit-rebel-leader',
+ 'xiapi-final-cavalry-a': 'unit-rebel-cavalry',
+ 'xiapi-final-cavalry-b': 'unit-rebel-cavalry',
+ 'xiapi-final-cavalry-c': 'unit-rebel-cavalry',
+ 'xiapi-final-cavalry-d': 'unit-rebel-cavalry',
+ 'xiapi-final-gate-guard-a': 'unit-rebel',
+ 'xiapi-final-gate-guard-b': 'unit-rebel',
+ 'xiapi-final-gate-guard-c': 'unit-rebel',
+ 'xiapi-final-gate-guard-d': 'unit-rebel',
+ 'xiapi-final-archer-a': 'unit-rebel-archer',
+ 'xiapi-final-archer-b': 'unit-rebel-archer',
+ 'xiapi-final-archer-c': 'unit-rebel-archer',
+ 'xiapi-final-raider-a': 'unit-rebel',
+ 'xiapi-final-raider-b': 'unit-rebel',
+ 'xiapi-final-strategist-chen-gong': 'unit-rebel-archer',
+ 'xiapi-final-leader-lu-bu': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -594,7 +609,22 @@ const enemyAiByUnitId: Record = {
'xiapi-camp-raider-a': 'aggressive',
'xiapi-camp-raider-b': 'aggressive',
'xiapi-strategist-chen-gong': 'hold',
- 'xiapi-leader-zhang-liao': 'guard'
+ 'xiapi-leader-zhang-liao': 'guard',
+ 'xiapi-final-cavalry-a': 'aggressive',
+ 'xiapi-final-cavalry-b': 'aggressive',
+ 'xiapi-final-cavalry-c': 'aggressive',
+ 'xiapi-final-cavalry-d': 'aggressive',
+ 'xiapi-final-gate-guard-a': 'guard',
+ 'xiapi-final-gate-guard-b': 'guard',
+ 'xiapi-final-gate-guard-c': 'guard',
+ 'xiapi-final-gate-guard-d': 'guard',
+ 'xiapi-final-archer-a': 'hold',
+ 'xiapi-final-archer-b': 'hold',
+ 'xiapi-final-archer-c': 'hold',
+ 'xiapi-final-raider-a': 'aggressive',
+ 'xiapi-final-raider-b': 'aggressive',
+ 'xiapi-final-strategist-chen-gong': 'hold',
+ 'xiapi-final-leader-lu-bu': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 0ed1848..16ae100 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -9,6 +9,7 @@ import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
+import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
@@ -82,6 +83,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-tenth', tenthBattleMapUrl);
this.load.image('battle-map-eleventh', eleventhBattleMapUrl);
this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
+ this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
@@ -123,6 +125,7 @@ export class BootScene extends Phaser.Scene {
});
this.createItemIcon('item-green-dragon-glaive', (graphics) => this.drawPolearmIcon(graphics, 0x5fbf8f, 0xd8b15f));
this.createItemIcon('item-serpent-spear', (graphics) => this.drawSpearIcon(graphics, 0xe0e8ef, 0xb86b55));
+ this.createItemIcon('item-sky-piercer-halberd', (graphics) => this.drawPolearmIcon(graphics, 0xd8dfe6, 0xd95f4f));
this.createItemIcon('item-yellow-turban-saber', (graphics) => this.drawSaberIcon(graphics, 0xd8b15f, 0x9c6b2f));
this.createItemIcon('item-short-bow', (graphics) => this.drawBowIcon(graphics, 0xc58a4c, 0xe8dfca));
this.createItemIcon('item-leader-axe', (graphics) => this.drawAxeIcon(graphics, 0xd8dfe6, 0x8d5a3a));
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index f97a7c3..4babd3f 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -97,10 +97,15 @@ type SortiePlanSummary = {
terrainScore: number;
terrainGrade: string;
activeBondCount: number;
+ recruitedCount: number;
+ selectedRecruitedCount: number;
+ reserveCount: number;
recommendedSelectedCount: number;
recommendedTotal: number;
deploymentLine: string;
recommendationLine: string;
+ recruitedLine: string;
+ reserveLine: string;
objectiveLine: string;
formationSlots: SortieFormationSlot[];
warnings: string[];
@@ -177,10 +182,12 @@ const campBattleIds = {
ninth: 'ninth-battle-xuzhou-gate-night-raid',
tenth: 'tenth-battle-xuzhou-breakout',
eleventh: 'eleventh-battle-xudu-refuge-road',
- twelfth: 'twelfth-battle-xiapi-outer-siege'
+ twelfth: 'twelfth-battle-xiapi-outer-siege',
+ thirteenth: 'thirteenth-battle-xiapi-final'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
+const foundingSortieUnitIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']);
const maxSortieUnits = 6;
const sortieFormationSlotDefinitions: Omit[] = [
{ role: 'front', label: '전열', description: '길목을 막고 반격을 받아냄' },
@@ -299,6 +306,16 @@ const sortieRulesByBattleId: Partial this.toggleSortieUnit(unit.id));
const marker = selected ? '출전' : '대기';
- const markerLabel = required ? '필수' : recommendation ? '추천' : marker;
+ const markerLabel = required ? '필수' : recommendation ? '추천' : recruited ? '합류' : marker;
this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
this.trackSortie(this.add.text(x + 214, rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, 19), this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
@@ -1724,17 +1827,18 @@ export class CampScene extends Phaser.Scene {
const summaryLines = [
plan.deploymentLine,
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개`,
+ plan.recruitedLine,
plan.objectiveLine
];
summaryLines.forEach((line, index) => {
- this.trackSortie(this.add.text(x + 18, y + 150 + index * 18, this.compactText(line, 31), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
+ this.trackSortie(this.add.text(x + 18, y + 146 + index * 16, this.compactText(line, 32), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
});
plan.formationSlots.forEach((slot, index) => {
const column = index % 2;
const row = Math.floor(index / 2);
const rowX = x + 18 + column * 176;
- const rowY = y + 208 + row * 28;
+ const rowY = y + 218 + row * 28;
const names = slot.unitNames.length > 0 ? slot.unitNames.join(', ') : '미배치';
const color = slot.unitNames.length > 0 ? '#a8ffd0' : '#9fb0bf';
this.trackSortie(this.add.text(rowX, rowY, slot.label, this.textStyle(12, color, true))).setDepth(depth + 1);
@@ -1742,7 +1846,7 @@ export class CampScene extends Phaser.Scene {
this.trackSortie(this.add.text(rowX + 42, rowY + 13, this.compactText(slot.description, 14), this.textStyle(10, '#87919c'))).setDepth(depth + 1);
});
- const warning = plan.warnings[0] ?? '전열, 돌파, 지원의 균형이 좋습니다.';
+ const warning = plan.warnings[0] ?? plan.reserveLine;
this.trackSortie(this.add.text(x + 18, y + height - 28, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
}
@@ -1914,13 +2018,18 @@ export class CampScene extends Phaser.Scene {
}
private sortiePlanSummary(): SortiePlanSummary {
+ const allAllies = this.sortieAllies();
const selectedUnits = this.selectedSortieUnits();
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
const scenario = this.nextSortieScenario();
const rule = this.nextSortieRule(scenario);
- const availableIds = new Set(this.sortieAllies().map((unit) => unit.id));
+ const availableIds = new Set(allAllies.map((unit) => unit.id));
const recommended = rule.recommended.filter((entry) => availableIds.has(entry.unitId));
const missingRecommended = recommended.filter((entry) => !selectedIds.has(entry.unitId));
+ const recruitedUnits = allAllies.filter((unit) => !foundingSortieUnitIds.has(unit.id));
+ const selectedRecruitedCount = recruitedUnits.filter((unit) => selectedIds.has(unit.id)).length;
+ const reserveUnits = allAllies.filter((unit) => !selectedIds.has(unit.id));
+ const reserveNames = reserveUnits.map((unit) => unit.name).join(', ');
const terrainScores = selectedUnits.map((unit) => this.sortieTerrainScore(unit, scenario));
const terrainScore = terrainScores.length
? Math.round(terrainScores.reduce((sum, score) => sum + score, 0) / terrainScores.length)
@@ -1970,10 +2079,18 @@ export class CampScene extends Phaser.Scene {
terrainScore,
terrainGrade,
activeBondCount,
+ recruitedCount: recruitedUnits.length,
+ selectedRecruitedCount,
+ reserveCount: reserveUnits.length,
recommendedSelectedCount,
recommendedTotal: recommended.length,
deploymentLine: `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}`,
recommendationLine: recommended.length > 0 ? `추천 ${recommendedSelectedCount}/${recommended.length}` : '추천 없음',
+ recruitedLine:
+ recruitedUnits.length > 0
+ ? `합류 무장 ${selectedRecruitedCount}/${recruitedUnits.length} · 대기 ${reserveUnits.length}`
+ : '합류 무장 없음',
+ reserveLine: reserveUnits.length > 0 ? `대기 무장: ${reserveNames}` : '전원 출전 중입니다.',
objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : '다음 전투 정보 없음',
formationSlots,
warnings
@@ -2903,6 +3020,8 @@ export class CampScene extends Phaser.Scene {
id: unit.id,
name: unit.name,
selected: this.isSortieSelected(unit.id),
+ recruited: !foundingSortieUnitIds.has(unit.id),
+ required: requiredSortieUnitIds.has(unit.id),
className: unit.className,
classRole: getUnitClass(unit.classKey).role,
formationRole: this.sortieFormationRole(unit),
diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts
index 345d56b..9e6c3f1 100644
--- a/src/game/scenes/TitleScene.ts
+++ b/src/game/scenes/TitleScene.ts
@@ -11,6 +11,7 @@ import {
seventhBattleScenario,
sixthBattleScenario,
tenthBattleScenario,
+ thirteenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario
} from '../data/battles';
@@ -323,7 +324,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'ninth-camp' ||
campaign.step === 'tenth-camp' ||
campaign.step === 'eleventh-camp' ||
- campaign.step === 'twelfth-camp'
+ campaign.step === 'twelfth-camp' ||
+ campaign.step === 'thirteenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -389,6 +391,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
+ if (campaign.step === 'thirteenth-battle') {
+ this.scene.start('BattleScene', { battleId: thirteenthBattleScenario.id });
+ return;
+ }
+
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;
diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts
index 8bbc2e3..aa3e51c 100644
--- a/src/game/state/campaignState.ts
+++ b/src/game/state/campaignState.ts
@@ -63,7 +63,9 @@ export type CampaignStep =
| 'eleventh-battle'
| 'eleventh-camp'
| 'twelfth-battle'
- | 'twelfth-camp';
+ | 'twelfth-camp'
+ | 'thirteenth-battle'
+ | 'thirteenth-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -126,7 +128,8 @@ const campaignBattleSteps: Record