diff --git a/docs/roadmap.md b/docs/roadmap.md
index 9a74374..51532d8 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -43,15 +43,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Nineteenth battle Changban refuge route, extending Red Cliffs preparation with a larger refugee-road battlefield, Cao Chun's cavalry pursuit, Zhao Yun survival pressure, and a sortie choice that highlights growing roster tradeoffs
- Twentieth battle Jiangdong envoy route, shifting sortie choice toward protecting Zhuge Liang and Sun Qian while opening the diplomatic path to Sun Quan and Zhou Yu
- Twenty-first battle Red Cliffs vanguard route, opening the first allied river-line clash with Cai Mao, six-officer sortie pressure, and post-battle fire-attack preparation events
+- Twenty-second battle Red Cliffs fire attack, turning Huang Gai's bitter-flesh plan and Zhuge Liang's wind timing into the decisive fire battle against Cao Cao's chained fleet
- Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter
- 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
- Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers
- Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses
-- Flow verification script from title through the twenty-first battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the twenty-second battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs fire camp states, campaign timeline state, and camp save state
## Next Steps
-1. Continue the Red Cliffs preparation chapter into Huang Gai's bitter-flesh plan and the main fire-attack confrontation
+1. Continue from Red Cliffs victory into Jing Province southern commandery acquisition and the first post-Red-Cliffs land campaign
2. Add more recruitable officers as the campaign moves toward Red Cliffs, Jing Province, Yi Province, and Shu Han
3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments
4. Add a dedicated treasure/equipment management view so players can compare special effects and growth
diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs
index e3fa600..16a589f 100644
--- a/scripts/verify-flow.mjs
+++ b/scripts/verify-flow.mjs
@@ -1736,7 +1736,7 @@ try {
if (
progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 ||
- progressTabState.campaignProgress?.totalKnown !== 21 ||
+ progressTabState.campaignProgress?.totalKnown !== 22 ||
progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' ||
progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전'
@@ -1888,7 +1888,7 @@ try {
if (
postBowangProgressState?.activeTab !== 'progress' ||
postBowangProgressState.campaignProgress?.completedKnown !== 18 ||
- postBowangProgressState.campaignProgress?.totalKnown !== 21 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 22 ||
postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' ||
postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로'
@@ -2026,7 +2026,7 @@ try {
if (
postChangbanProgressState?.activeTab !== 'progress' ||
postChangbanProgressState.campaignProgress?.completedKnown !== 19 ||
- postChangbanProgressState.campaignProgress?.totalKnown !== 21 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 22 ||
postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' ||
postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로'
@@ -2164,7 +2164,7 @@ try {
if (
postJiangdongProgressState?.activeTab !== 'progress' ||
postJiangdongProgressState.campaignProgress?.completedKnown !== 20 ||
- postJiangdongProgressState.campaignProgress?.totalKnown !== 21 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 22 ||
postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' ||
postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전'
@@ -2304,15 +2304,155 @@ try {
if (
postRedCliffsProgressState?.activeTab !== 'progress' ||
postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 ||
- postRedCliffsProgressState.campaignProgress?.totalKnown !== 21 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 22 ||
postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' ||
- postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전'
) {
throw new Error(`Expected post-Red Cliffs vanguard progress tab to complete known battles and keep fire attack preparation pending: ${JSON.stringify(postRedCliffsProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-redcliffs-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const fireAttackSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !fireAttackSortieState?.sortieVisible ||
+ !fireAttackSortieState.sortiePlan?.objectiveLine?.includes('적벽 화공전') ||
+ !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
+ !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) ||
+ !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
+ !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) ||
+ fireAttackSortieState.sortieRoster?.length < 8 ||
+ fireAttackSortieState.sortiePlan?.maxCount !== 6
+ ) {
+ throw new Error(`Expected twenty-first camp sortie prep to target Red Cliffs fire attack with Zhuge Liang, Guan Yu, Zhao Yun, and Mi Zhu recommendations: ${JSON.stringify(fireAttackSortieState)}`);
+ }
+ assertSortieTacticalRoster(fireAttackSortieState, [
+ 'liu-bei',
+ 'guan-yu',
+ 'zhang-fei',
+ 'jian-yong',
+ 'mi-zhu',
+ 'sun-qian',
+ 'zhao-yun',
+ 'zhuge-liang'
+ ]);
+
+ const fireAttackPriorityUnits = ['zhuge-liang', 'guan-yu', 'zhao-yun', 'mi-zhu'];
+ for (const unitId of fireAttackPriorityUnits) {
+ const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
+ const removable = currentSortieState.sortieRoster?.find(
+ (unit) => unit.selected && unit.id !== 'liu-bei' && !fireAttackPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const fireAttackSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !fireAttackPriorityUnits.every((unitId) =>
+ fireAttackSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ fireAttackSortieReadyState.sortiePlan?.selectedCount !== 6 ||
+ fireAttackSortieReadyState.sortiePlan?.recommendedSelectedCount < 5
+ ) {
+ throw new Error(`Expected Red Cliffs fire attack sortie to deploy priority officers while preserving six-officer choice pressure: ${JSON.stringify(fireAttackSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-redcliffs-fire-sortie.png', fullPage: true });
+
+ 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-redcliffs-fire-story.png', fullPage: true });
+
+ for (let i = 0; i < 36; i += 1) {
+ const enteredTwentySecondBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'twenty-second-battle-red-cliffs-fire';
+ });
+ if (enteredTwentySecondBattle) {
+ 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 === 'twenty-second-battle-red-cliffs-fire' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-twenty-second-battle.png', fullPage: true });
+
+ const twentySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const twentySecondEnemies = twentySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const twentySecondAllies = twentySecondBattleState.units.filter((unit) => unit.faction === 'ally');
+ const twentySecondEnemyBehaviors = new Set(twentySecondEnemies.map((unit) => unit.ai));
+ if (
+ twentySecondBattleState.camera?.mapWidth !== 44 ||
+ twentySecondBattleState.camera?.mapHeight !== 30 ||
+ twentySecondBattleState.victoryConditionLabel !== '조조 본선 퇴각' ||
+ twentySecondEnemies.length < 18 ||
+ !twentySecondEnemyBehaviors.has('aggressive') ||
+ !twentySecondEnemyBehaviors.has('guard') ||
+ !twentySecondEnemyBehaviors.has('hold') ||
+ !twentySecondEnemies.some((unit) => unit.id === 'redcliff-fire-leader-cao-cao') ||
+ !twentySecondAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !twentySecondAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !twentySecondAllies.some((unit) => unit.id === 'mi-zhu')
+ ) {
+ throw new Error(`Expected twenty-second battle to use Red Cliffs fire map, Cao Cao objective, mixed AI, and selected allied officers: ${JSON.stringify(twentySecondBattleState)}`);
+ }
+
+ 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 twentySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ twentySecondCampState?.campBattleId !== 'twenty-second-battle-red-cliffs-fire' ||
+ twentySecondCampState.campTitle !== '적벽 화공전 후 군영' ||
+ twentySecondCampState.availableDialogueIds?.length !== 3 ||
+ !twentySecondCampState.availableDialogueIds.every((id) => id.endsWith('red-cliffs-fire')) ||
+ twentySecondCampState.availableVisitIds?.length !== 2 ||
+ !twentySecondCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhuge-liang') ||
+ !twentySecondCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__mi-zhu') ||
+ twentySecondCampState.rosterCollection?.total < 8 ||
+ twentySecondCampState.rosterCollection?.reserveTrainingCount < 2 ||
+ twentySecondCampState.rosterCollection?.reserveTrainingExp < 12
+ ) {
+ throw new Error(`Expected twenty-second camp to expose Red Cliffs fire dialogue/visit sets, aftermath bonds, and reserve growth: ${JSON.stringify(twentySecondCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-redcliffs-fire-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postFireAttackProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postFireAttackProgressState?.activeTab !== 'progress' ||
+ postFireAttackProgressState.campaignProgress?.completedKnown !== 22 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 22 ||
+ postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
+ postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' ||
+ postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Red Cliffs fire progress tab to complete known battles and point toward Jing Province preparation: ${JSON.stringify(postFireAttackProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-redcliffs-fire-progress.png', fullPage: true });
+
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -2324,7 +2464,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-twenty-first-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-twenty-second-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/twenty-second-battle-map.svg b/src/assets/images/battle/twenty-second-battle-map.svg
new file mode 100644
index 0000000..277800b
--- /dev/null
+++ b/src/assets/images/battle/twenty-second-battle-map.svg
@@ -0,0 +1,80 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 018d38a..86624f3 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -43,6 +43,10 @@ import {
twentyFirstBattleMap,
twentyFirstBattleUnits,
twentyFirstBattleVictoryPages,
+ twentySecondBattleBonds,
+ twentySecondBattleMap,
+ twentySecondBattleUnits,
+ twentySecondBattleVictoryPages,
twentiethBattleBonds,
twentiethBattleMap,
twentiethBattleUnits,
@@ -110,7 +114,8 @@ export type BattleScenarioId =
| 'eighteenth-battle-bowang-ambush'
| 'nineteenth-battle-changban-refuge'
| 'twentieth-battle-jiangdong-envoy'
- | 'twenty-first-battle-red-cliffs-vanguard';
+ | 'twenty-first-battle-red-cliffs-vanguard'
+ | 'twenty-second-battle-red-cliffs-fire';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1293,6 +1298,69 @@ export const twentyFirstBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const twentySecondBattleScenario: BattleScenarioDefinition = {
+ id: 'twenty-second-battle-red-cliffs-fire',
+ title: '적벽 화공전',
+ victoryConditionLabel: '조조 본선 퇴각',
+ defeatConditionLabel: '유비 또는 제갈량 퇴각',
+ openingObjectiveLines: [
+ '황개의 거짓 항복선이 조조 함대로 향합니다. 조조군 감시선이 화선을 알아차리면 적벽의 불길은 시작되기도 전에 꺼집니다.',
+ '강 위에는 연환선과 배다리가 얽혀 있습니다. 전열 장수로 갑판 보병을 묶고, 기동 장수로 강안 기병을 끊어 화선 접안로를 열어야 합니다.',
+ '제갈량을 지키고 조조 본선을 퇴각시키십시오. 화선 준비 지점을 확보하면 적벽대전의 결정적 승리를 얻을 수 있습니다.'
+ ],
+ map: twentySecondBattleMap,
+ units: twentySecondBattleUnits,
+ bonds: twentySecondBattleBonds,
+ mapTextureKey: 'battle-map-twenty-second',
+ leaderUnitId: 'redcliff-fire-leader-cao-cao',
+ quickVictoryTurnLimit: 31,
+ baseVictoryGold: 2840,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '조조 본선 퇴각',
+ rewardGold: 1900,
+ unitId: 'redcliff-fire-leader-cao-cao'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 560,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 860,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'fire-ship-landing',
+ kind: 'secure-terrain',
+ label: '화선 접안로 확보',
+ rewardGold: 980,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '31턴 이내 승리',
+ rewardGold: 720,
+ maxTurn: 31
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'zhuge-liang' }
+ ],
+ itemRewards: ['콩 9', '상처약 6', '탁주 4', '화공 전과 +1', '동남풍 기록 +1'],
+ victoryPages: twentySecondBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -1316,7 +1384,8 @@ export const battleScenarios: Record
'eighteenth-battle-bowang-ambush': eighteenthBattleScenario,
'nineteenth-battle-changban-refuge': nineteenthBattleScenario,
'twentieth-battle-jiangdong-envoy': twentiethBattleScenario,
- 'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario
+ 'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario,
+ 'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 0c8b129..4ca39b3 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -18,6 +18,7 @@ import {
tenthBattleScenario,
thirteenthBattleScenario,
twentyFirstBattleScenario,
+ twentySecondBattleScenario,
twentiethBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
@@ -59,6 +60,8 @@ import {
thirteenthBattleVictoryPages,
twentyFirstBattleIntroPages,
twentyFirstBattleVictoryPages,
+ twentySecondBattleIntroPages,
+ twentySecondBattleVictoryPages,
twentiethBattleIntroPages,
twentiethBattleVictoryPages,
twelfthBattleIntroPages,
@@ -291,13 +294,24 @@ const sortieFlows: Record = {
},
[twentyFirstBattleScenario.id]: {
afterBattleId: twentyFirstBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '적벽 화공 준비',
+ eyebrow: '다음 전장',
+ title: twentySecondBattleScenario.title,
description:
'조조군 수군 선봉을 밀어내며 손유 동맹의 첫 전선이 세워졌습니다. 이제 주유와 황개, 제갈량의 계책이 맞물려 본격적인 화공을 준비해야 합니다.',
- rewardHint: '다음 장: 황개 고육계와 적벽 화공 준비 중',
- pages: twentyFirstBattleVictoryPages,
- unavailableNotice: '황개 고육계와 적벽 화공 장은 다음 작업에서 이어집니다. 군영에서 화공 논의, 공명, 보급을 정비하십시오.'
+ rewardHint: `예상 보상: ${twentySecondBattleScenario.title} 개방 / 적벽대전 결전`,
+ nextBattleId: twentySecondBattleScenario.id,
+ campaignStep: 'twenty-second-battle',
+ pages: [...twentyFirstBattleVictoryPages, ...twentySecondBattleIntroPages]
+ },
+ [twentySecondBattleScenario.id]: {
+ afterBattleId: twentySecondBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '형주 확보 준비',
+ description:
+ '적벽의 불길로 조조의 남하가 꺾였습니다. 이제 유비군은 머물 땅을 마련하기 위해 형주 남부와 익주로 이어질 근거지를 준비해야 합니다.',
+ rewardHint: '다음 장: 형주 남부 확보 준비 중',
+ pages: twentySecondBattleVictoryPages,
+ unavailableNotice: '형주 남부 확보 장은 다음 작업에서 이어집니다. 군영에서 전후 수습, 공명, 보급을 정비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index c5d8836..3c7be50 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -1385,6 +1385,69 @@ export const twentyFirstBattleVictoryPages: StoryPage[] = [
}
];
+export const twentySecondBattleIntroPages: StoryPage[] = [
+ {
+ id: 'twenty-second-huang-gai-bitter-plan',
+ bgm: 'story-dark',
+ chapter: '황개의 결단',
+ background: 'story-liu-bei',
+ speaker: '주유',
+ text: '황개 장군은 거짓 항복의 배를 준비했습니다. 조조가 그 배를 의심하지 않게 하려면, 유비군은 강안의 감시선을 흔들어 화선이 다가갈 길을 열어야 합니다.'
+ },
+ {
+ id: 'twenty-second-zhuge-wind-watch',
+ bgm: 'battle-prep',
+ chapter: '동남풍을 기다리다',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '바람은 아직 완전히 돌지 않았습니다. 그러나 때가 오면 불길은 강 위에서 산처럼 번질 것입니다. 그 전까지 조조의 척후와 갑판 궁병을 묶어 두어야 합니다.'
+ },
+ {
+ id: 'twenty-second-liu-bei-fire-line',
+ bgm: 'militia-theme',
+ chapter: '불길 앞의 뜻',
+ background: 'story-militia',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '오늘의 불은 백성을 태우기 위한 불이 아니라 난세의 물결을 멈추기 위한 불이오. 화선이 닿을 때까지 강안 전선을 흔들림 없이 지킵시다.'
+ },
+ {
+ id: 'twenty-second-fire-attack-sortie',
+ bgm: 'battle-prep',
+ chapter: '적벽 화공전',
+ background: 'story-sortie',
+ speaker: '제갈량',
+ text: '조조의 함대가 사슬로 묶인 채 강북에 모였습니다. 화선의 길을 막는 초병을 베고, 조조 본선이 불길에 휩싸일 때까지 전선을 지키십시오.'
+ }
+];
+
+export const twentySecondBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'twenty-second-victory-fire-rises',
+ bgm: 'battle-prep',
+ chapter: '불길이 일다',
+ background: 'story-sortie',
+ text: '화선이 조조군 함대에 닿자 강 위가 붉게 일렁였습니다. 사슬로 묶인 배들은 서로를 구하지 못했고, 장강의 바람은 불길을 북쪽으로 몰아붙였습니다.'
+ },
+ {
+ id: 'twenty-second-cao-cao-retreat',
+ bgm: 'story-dark',
+ chapter: '조조의 퇴각',
+ background: 'story-three-heroes',
+ speaker: '조조',
+ text: '오늘은 하늘과 강이 나를 막는구나. 그러나 난세는 아직 끝나지 않았다. 살아 돌아가 다시 군을 모으면, 이 치욕도 갚을 날이 있을 것이다.'
+ },
+ {
+ id: 'twenty-second-liu-bei-next-land',
+ bgm: 'militia-theme',
+ chapter: '형주를 바라보다',
+ background: 'story-militia',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '조조의 대군은 물러났지만 우리의 근본은 아직 없습니다. 이제 백성이 머물 땅을 얻고, 형주와 익주로 향할 길을 차근히 세워야 합니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1656,6 +1719,12 @@ export const twentyFirstBattleMap: BattleMap = {
terrain: createTwentyFirstBattleTerrain()
};
+export const twentySecondBattleMap: BattleMap = {
+ width: 44,
+ height: 30,
+ terrain: createTwentySecondBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -8132,6 +8201,405 @@ export const twentyFirstBattleUnits: UnitData[] = [
}
];
+const twentySecondBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 23 },
+ 'guan-yu': { x: 6, y: 21 },
+ 'zhang-fei': { x: 6, y: 25 },
+ 'jian-yong': { x: 3, y: 26 },
+ 'mi-zhu': { x: 5, y: 27 },
+ 'sun-qian': { x: 8, y: 23 },
+ 'zhao-yun': { x: 8, y: 20 },
+ 'zhuge-liang': { x: 6, y: 22 }
+};
+
+export const twentySecondBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, twentySecondBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ {
+ id: 'redcliff-fire-scout-a',
+ name: '조조 화선 감시병',
+ faction: 'enemy',
+ className: '강안 척후',
+ classKey: 'bandit',
+ level: 28,
+ exp: 42,
+ hp: 78,
+ maxHp: 78,
+ attack: 31,
+ move: 4,
+ stats: { might: 110, intelligence: 82, leadership: 88, agility: 108, luck: 74 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 4 },
+ armor: { itemId: 'rebel-vest', level: 3, exp: 42 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 14,
+ y: 21
+ },
+ {
+ id: 'redcliff-fire-scout-b',
+ name: '조조 화선 감시병',
+ faction: 'enemy',
+ className: '강변 척후',
+ classKey: 'bandit',
+ level: 28,
+ exp: 42,
+ hp: 78,
+ maxHp: 78,
+ attack: 31,
+ move: 4,
+ stats: { might: 110, intelligence: 82, leadership: 88, agility: 108, luck: 74 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 4 },
+ armor: { itemId: 'rebel-vest', level: 3, exp: 42 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 16,
+ y: 24
+ },
+ {
+ id: 'redcliff-fire-marine-a',
+ name: '조조 갑판 보병',
+ faction: 'enemy',
+ className: '갑판 보병',
+ classKey: 'yellowTurban',
+ level: 28,
+ exp: 44,
+ hp: 102,
+ maxHp: 102,
+ attack: 33,
+ move: 3,
+ stats: { might: 120, intelligence: 82, leadership: 102, agility: 90, luck: 74 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 12 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 2 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 21,
+ y: 19
+ },
+ {
+ id: 'redcliff-fire-marine-b',
+ name: '조조 갑판 보병',
+ faction: 'enemy',
+ className: '갑판 보병',
+ classKey: 'yellowTurban',
+ level: 28,
+ exp: 44,
+ hp: 102,
+ maxHp: 102,
+ attack: 33,
+ move: 3,
+ stats: { might: 120, intelligence: 82, leadership: 102, agility: 90, luck: 74 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 12 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 2 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 23,
+ y: 21
+ },
+ {
+ id: 'redcliff-fire-marine-c',
+ name: '조조 갑판 보병',
+ faction: 'enemy',
+ className: '연환선 보병',
+ classKey: 'yellowTurban',
+ level: 29,
+ exp: 46,
+ hp: 106,
+ maxHp: 106,
+ attack: 34,
+ move: 3,
+ stats: { might: 122, intelligence: 84, leadership: 104, agility: 92, luck: 75 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 18 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 8 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 30,
+ y: 14
+ },
+ {
+ id: 'redcliff-fire-marine-d',
+ name: '조조 갑판 보병',
+ faction: 'enemy',
+ className: '연환선 보병',
+ classKey: 'yellowTurban',
+ level: 29,
+ exp: 46,
+ hp: 106,
+ maxHp: 106,
+ attack: 34,
+ move: 3,
+ stats: { might: 122, intelligence: 84, leadership: 104, agility: 92, luck: 75 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 18 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 8 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 32,
+ y: 17
+ },
+ {
+ id: 'redcliff-fire-archer-a',
+ name: '조조 선상 궁병',
+ faction: 'enemy',
+ className: '선상 궁병',
+ classKey: 'archer',
+ level: 28,
+ exp: 42,
+ hp: 76,
+ maxHp: 76,
+ attack: 33,
+ move: 3,
+ stats: { might: 106, intelligence: 96, leadership: 92, agility: 104, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 12 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 4 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 22,
+ y: 15
+ },
+ {
+ id: 'redcliff-fire-archer-b',
+ name: '조조 선상 궁병',
+ faction: 'enemy',
+ className: '선상 궁병',
+ classKey: 'archer',
+ level: 28,
+ exp: 42,
+ hp: 76,
+ maxHp: 76,
+ attack: 33,
+ move: 3,
+ stats: { might: 106, intelligence: 96, leadership: 92, agility: 104, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 12 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 4 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 26,
+ y: 18
+ },
+ {
+ id: 'redcliff-fire-archer-c',
+ name: '조조 연환 궁병',
+ faction: 'enemy',
+ className: '연환선 궁병',
+ classKey: 'archer',
+ level: 29,
+ exp: 46,
+ hp: 78,
+ maxHp: 78,
+ attack: 34,
+ move: 3,
+ stats: { might: 108, intelligence: 98, leadership: 94, agility: 106, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 20 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 8 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 35,
+ y: 12
+ },
+ {
+ id: 'redcliff-fire-archer-d',
+ name: '조조 연환 궁병',
+ faction: 'enemy',
+ className: '연환선 궁병',
+ classKey: 'archer',
+ level: 29,
+ exp: 46,
+ hp: 78,
+ maxHp: 78,
+ attack: 34,
+ move: 3,
+ stats: { might: 108, intelligence: 98, leadership: 94, agility: 106, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 20 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 8 },
+ accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
+ },
+ x: 37,
+ y: 17
+ },
+ {
+ id: 'redcliff-fire-cavalry-a',
+ name: '조조 강안 기병',
+ faction: 'enemy',
+ className: '강안 기병',
+ classKey: 'cavalry',
+ level: 29,
+ exp: 46,
+ hp: 110,
+ maxHp: 110,
+ attack: 36,
+ move: 5,
+ stats: { might: 126, intelligence: 82, leadership: 100, agility: 118, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 22 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 12 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 18,
+ y: 25
+ },
+ {
+ id: 'redcliff-fire-cavalry-b',
+ name: '조조 강안 기병',
+ faction: 'enemy',
+ className: '강안 기병',
+ classKey: 'cavalry',
+ level: 29,
+ exp: 46,
+ hp: 110,
+ maxHp: 110,
+ attack: 36,
+ move: 5,
+ stats: { might: 126, intelligence: 82, leadership: 100, agility: 118, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 22 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 12 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 27,
+ y: 24
+ },
+ {
+ id: 'redcliff-fire-cavalry-c',
+ name: '조조 강안 기병',
+ faction: 'enemy',
+ className: '강북 기병',
+ classKey: 'cavalry',
+ level: 30,
+ exp: 48,
+ hp: 114,
+ maxHp: 114,
+ attack: 37,
+ move: 5,
+ stats: { might: 128, intelligence: 84, leadership: 102, agility: 120, luck: 77 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 28 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 16 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 34,
+ y: 22
+ },
+ {
+ id: 'redcliff-fire-guard-a',
+ name: '조조 본선 친위',
+ faction: 'enemy',
+ className: '본선 친위',
+ classKey: 'yellowTurban',
+ level: 30,
+ exp: 50,
+ hp: 116,
+ maxHp: 116,
+ attack: 36,
+ move: 3,
+ stats: { might: 128, intelligence: 86, leadership: 110, agility: 94, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 34 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 22 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 38,
+ y: 13
+ },
+ {
+ id: 'redcliff-fire-guard-b',
+ name: '조조 본선 친위',
+ faction: 'enemy',
+ className: '본선 친위',
+ classKey: 'yellowTurban',
+ level: 30,
+ exp: 50,
+ hp: 116,
+ maxHp: 116,
+ attack: 36,
+ move: 3,
+ stats: { might: 128, intelligence: 86, leadership: 110, agility: 94, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 34 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 22 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 39,
+ y: 16
+ },
+ {
+ id: 'redcliff-fire-strategist-a',
+ name: '조조 수군 군리',
+ faction: 'enemy',
+ className: '연환선 군리',
+ classKey: 'archer',
+ level: 29,
+ exp: 48,
+ hp: 82,
+ maxHp: 82,
+ attack: 33,
+ move: 3,
+ stats: { might: 84, intelligence: 104, leadership: 98, agility: 90, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 28 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 14 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 36,
+ y: 11
+ },
+ {
+ id: 'redcliff-fire-strategist-b',
+ name: '조조 수군 군리',
+ faction: 'enemy',
+ className: '연환선 군리',
+ classKey: 'archer',
+ level: 29,
+ exp: 48,
+ hp: 82,
+ maxHp: 82,
+ attack: 33,
+ move: 3,
+ stats: { might: 84, intelligence: 104, leadership: 98, agility: 90, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 28 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 14 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 40,
+ y: 12
+ },
+ {
+ id: 'redcliff-fire-leader-cao-cao',
+ name: '조조',
+ faction: 'enemy',
+ className: '위왕군 총대장',
+ classKey: 'rebelLeader',
+ level: 32,
+ exp: 54,
+ hp: 156,
+ maxHp: 156,
+ attack: 39,
+ move: 4,
+ stats: { might: 130, intelligence: 118, leadership: 132, agility: 96, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'leader-axe', level: 4, exp: 42 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 41,
+ y: 14
+ }
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -8285,6 +8753,7 @@ export const eighteenthBattleBonds: BattleBond[] = [...sixteenthBattleBonds, ...
export const nineteenthBattleBonds: BattleBond[] = [...eighteenthBattleBonds].map(cloneBattleBondForScenario);
export const twentiethBattleBonds: BattleBond[] = [...nineteenthBattleBonds].map(cloneBattleBondForScenario);
export const twentyFirstBattleBonds: BattleBond[] = [...twentiethBattleBonds].map(cloneBattleBondForScenario);
+export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -8964,6 +9433,61 @@ function createTwentyFirstBattleTerrain(): TerrainType[][] {
);
}
+function createTwentySecondBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 30 }, (_, y) =>
+ Array.from({ length: 44 }, (_, x): TerrainType => {
+ if ((x <= 4 && y >= 21 && y <= 28) || (x >= 5 && x <= 9 && y >= 24 && y <= 29)) {
+ return 'camp';
+ }
+ if ((x >= 36 && x <= 43 && y >= 10 && y <= 18) || (x >= 33 && x <= 37 && y >= 12 && y <= 16)) {
+ return 'fort';
+ }
+ if ((x >= 12 && x <= 15 && y >= 20 && y <= 22) || (x >= 27 && x <= 30 && y >= 11 && y <= 13)) {
+ return 'village';
+ }
+ if (
+ (y === 23 && x >= 3 && x <= 17) ||
+ (y === 20 && x >= 15 && x <= 28) ||
+ (y === 15 && x >= 28 && x <= 40) ||
+ (x === 36 && y >= 10 && y <= 22)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 7 && x <= 18 && y === 26 - Math.floor(x / 2)) ||
+ (x >= 19 && x <= 35 && y === 25 - Math.floor(x / 3)) ||
+ (x >= 30 && x <= 39 && y === 14)
+ ) {
+ return 'road';
+ }
+ if ((x >= 18 && x <= 25 && y >= 0 && y <= 29) || (x >= 29 && x <= 31 && y >= 0 && y <= 22)) {
+ return 'river';
+ }
+ if ((x >= 14 && x <= 16 && y >= 24 && y <= 29) || (x >= 26 && x <= 28 && y >= 20 && y <= 29)) {
+ return 'river';
+ }
+ if (
+ (x >= 4 && x <= 13 && y >= 8 && y <= 17) ||
+ (x <= 8 && y <= 7) ||
+ (x >= 31 && x <= 42 && y <= 8)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 9 && x <= 18 && y >= 2 && y <= 6) ||
+ (x >= 32 && x <= 43 && y >= 21 && y <= 29) ||
+ (x >= 38 && x <= 43 && y >= 3 && y <= 7)
+ ) {
+ return 'hill';
+ }
+ if ((x <= 1 && y <= 13) || (x >= 42 && y <= 9) || (x >= 42 && y >= 20)) {
+ 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 9f03c43..8396173 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -222,7 +222,25 @@ const unitTexture: Record = {
'redcliff-vanguard-guard-b': 'unit-rebel',
'redcliff-vanguard-strategist-a': 'unit-rebel-archer',
'redcliff-vanguard-strategist-b': 'unit-rebel-archer',
- 'redcliff-vanguard-leader-cai-mao': 'unit-rebel-leader'
+ 'redcliff-vanguard-leader-cai-mao': 'unit-rebel-leader',
+ 'redcliff-fire-scout-a': 'unit-rebel',
+ 'redcliff-fire-scout-b': 'unit-rebel',
+ 'redcliff-fire-marine-a': 'unit-rebel',
+ 'redcliff-fire-marine-b': 'unit-rebel',
+ 'redcliff-fire-marine-c': 'unit-rebel',
+ 'redcliff-fire-marine-d': 'unit-rebel',
+ 'redcliff-fire-archer-a': 'unit-rebel-archer',
+ 'redcliff-fire-archer-b': 'unit-rebel-archer',
+ 'redcliff-fire-archer-c': 'unit-rebel-archer',
+ 'redcliff-fire-archer-d': 'unit-rebel-archer',
+ 'redcliff-fire-cavalry-a': 'unit-rebel-cavalry',
+ 'redcliff-fire-cavalry-b': 'unit-rebel-cavalry',
+ 'redcliff-fire-cavalry-c': 'unit-rebel-cavalry',
+ 'redcliff-fire-guard-a': 'unit-rebel',
+ 'redcliff-fire-guard-b': 'unit-rebel',
+ 'redcliff-fire-strategist-a': 'unit-rebel-archer',
+ 'redcliff-fire-strategist-b': 'unit-rebel-archer',
+ 'redcliff-fire-leader-cao-cao': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -900,7 +918,25 @@ const enemyAiByUnitId: Record = {
'redcliff-vanguard-guard-b': 'guard',
'redcliff-vanguard-strategist-a': 'hold',
'redcliff-vanguard-strategist-b': 'hold',
- 'redcliff-vanguard-leader-cai-mao': 'guard'
+ 'redcliff-vanguard-leader-cai-mao': 'guard',
+ 'redcliff-fire-scout-a': 'aggressive',
+ 'redcliff-fire-scout-b': 'aggressive',
+ 'redcliff-fire-marine-a': 'guard',
+ 'redcliff-fire-marine-b': 'guard',
+ 'redcliff-fire-marine-c': 'guard',
+ 'redcliff-fire-marine-d': 'guard',
+ 'redcliff-fire-archer-a': 'hold',
+ 'redcliff-fire-archer-b': 'hold',
+ 'redcliff-fire-archer-c': 'hold',
+ 'redcliff-fire-archer-d': 'hold',
+ 'redcliff-fire-cavalry-a': 'aggressive',
+ 'redcliff-fire-cavalry-b': 'aggressive',
+ 'redcliff-fire-cavalry-c': 'aggressive',
+ 'redcliff-fire-guard-a': 'guard',
+ 'redcliff-fire-guard-b': 'guard',
+ 'redcliff-fire-strategist-a': 'hold',
+ 'redcliff-fire-strategist-b': 'hold',
+ 'redcliff-fire-leader-cao-cao': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 2eeb834..17d7ac2 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -18,6 +18,7 @@ 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 twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
+import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg';
import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
@@ -100,6 +101,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-nineteenth', nineteenthBattleMapUrl);
this.load.image('battle-map-twentieth', twentiethBattleMapUrl);
this.load.image('battle-map-twenty-first', twentyFirstBattleMapUrl);
+ this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index c8ebd17..89bebd2 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -285,7 +285,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'eighteenth-battle-bowang-ambush',
'nineteenth-battle-changban-refuge',
'twentieth-battle-jiangdong-envoy',
- 'twenty-first-battle-red-cliffs-vanguard'
+ 'twenty-first-battle-red-cliffs-vanguard',
+ 'twenty-second-battle-red-cliffs-fire'
],
nextHints: ['강동 사절', '화공 준비', '조조 남하']
},
@@ -328,7 +329,8 @@ const campBattleIds = {
eighteenth: 'eighteenth-battle-bowang-ambush',
nineteenth: 'nineteenth-battle-changban-refuge',
twentieth: 'twentieth-battle-jiangdong-envoy',
- twentyFirst: 'twenty-first-battle-red-cliffs-vanguard'
+ twentyFirst: 'twenty-first-battle-red-cliffs-vanguard',
+ twentySecond: 'twenty-second-battle-red-cliffs-fire'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -555,6 +557,18 @@ const sortieRulesByBattleId: Partial