From aca7d6568aa13607cc36dce87a7ec1decfd0c173 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 12:38:53 +0900 Subject: [PATCH] Add Chengdu surrender chapter --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 192 +++++++++++-- .../images/battle/thirty-third-battle-map.svg | 85 ++++++ src/game/data/battles.ts | 73 ++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 254 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 57 +++- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 205 +++++++++++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 879 insertions(+), 38 deletions(-) create mode 100644 src/assets/images/battle/thirty-third-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index e74a75e..0029d11 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -54,16 +54,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirtieth battle Luofeng Slope ambush route, using Wu Yi's first sortie choice and Pang Tong survival pressure across a 60x42 ambush battlefield - Thirty-first battle Luo Castle main-gate route, turning the siege into a 62x44 castle assault with Zhang Ren's final defense and Yan Yan recruitment after the gate opens - Thirty-second battle Mianzhu Gate pressure route, using Yan Yan's first sortie choice across a 64x46 Chengdu approach battlefield and recruiting Li Yan after the defense line yields +- Thirty-third battle Chengdu surrender route, using Li Yan's first sortie choice across a 66x48 Chengdu outer-wall battlefield and recruiting Huang Quan after Yi Province yields - 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 thirty-second battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu camp states, campaign timeline state, and camp save state +- Flow verification script from title through the thirty-third battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue the Yi Province arc into the final Chengdu surrender and Shu-Han foundation setup, using Yan Yan and Li Yan's local legitimacy plus Pang Tong/Fa Zheng/Wu Yi planning pressure -2. Add more recruitable officers as the campaign moves deeper through Yi Province and toward Shu Han +1. Open the Shu-Han foundation setup after Yi Province, turning the new Chengdu camp into policy, title, and northward Hanzhong preparation choices +2. Add more recruitable officers as the campaign moves from Yi Province stabilization toward Hanzhong 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 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index b2b0263..cf0ae47 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 !== 32 || + progressTabState.campaignProgress?.totalKnown !== 33 || 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 !== 32 || + postBowangProgressState.campaignProgress?.totalKnown !== 33 || 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 !== 32 || + postChangbanProgressState.campaignProgress?.totalKnown !== 33 || 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 !== 32 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 33 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' @@ -2304,7 +2304,7 @@ try { if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || - postRedCliffsProgressState.campaignProgress?.totalKnown !== 32 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 33 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' @@ -2444,7 +2444,7 @@ try { if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || - postFireAttackProgressState.campaignProgress?.totalKnown !== 32 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 33 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2587,7 +2587,7 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 32 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 33 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2729,7 +2729,7 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 32 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 33 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2872,7 +2872,7 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 32 || + postWulingProgressState.campaignProgress?.totalKnown !== 33 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' @@ -3021,7 +3021,7 @@ try { if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || - postChangshaProgressState.campaignProgress?.totalKnown !== 32 || + postChangshaProgressState.campaignProgress?.totalKnown !== 33 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' @@ -3166,7 +3166,7 @@ try { if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || - postYizhouProgressState.campaignProgress?.totalKnown !== 32 || + postYizhouProgressState.campaignProgress?.totalKnown !== 33 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' @@ -3313,7 +3313,7 @@ try { if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || - postFuPassProgressState.campaignProgress?.totalKnown !== 32 || + postFuPassProgressState.campaignProgress?.totalKnown !== 33 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3462,7 +3462,7 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 32 || + postLuoProgressState.campaignProgress?.totalKnown !== 33 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' @@ -3608,7 +3608,7 @@ try { if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || - postLuofengProgressState.campaignProgress?.totalKnown !== 32 || + postLuofengProgressState.campaignProgress?.totalKnown !== 33 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' @@ -3760,7 +3760,7 @@ try { if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || - postLuoMainProgressState.campaignProgress?.totalKnown !== 32 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 33 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' @@ -3913,15 +3913,169 @@ try { if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || - postMianzhuProgressState.campaignProgress?.totalKnown !== 32 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 33 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || - postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' ) { - throw new Error(`Expected post-Mianzhu progress tab to complete Mianzhu pressure and pause before Chengdu surrender: ${JSON.stringify(postMianzhuProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Mianzhu progress tab to complete Mianzhu pressure and open Chengdu surrender: ${JSON.stringify(postMianzhuProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-mianzhu-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const chengduSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !chengduSortieState?.sortieVisible || + !chengduSortieState.sortiePlan?.objectiveLine?.includes('성도 항복 권고전') || + !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited && unit.recommended) || + !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || + !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) || + !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || + chengduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan') || + chengduSortieState.sortieRoster?.length < 18 || + chengduSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected Mianzhu camp sortie prep to target Chengdu surrender with Li Yan selectable and Huang Quan absent before victory: ${JSON.stringify(chengduSortieState)}`); + } + assertSortieTacticalRoster(chengduSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang', + 'yi-ji', + 'gong-zhi', + 'huang-zhong', + 'wei-yan', + 'pang-tong', + 'fa-zheng', + 'wu-yi', + 'yan-yan', + 'li-yan' + ]); + + const chengduPriorityUnits = ['li-yan', 'zhuge-liang', 'fa-zheng', 'yan-yan', 'wu-yi']; + for (const unitId of chengduPriorityUnits) { + 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' && !chengduPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const chengduSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !chengduPriorityUnits.every((unitId) => + chengduSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + chengduSortieReadyState.sortiePlan?.selectedCount !== 6 || + chengduSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Chengdu surrender sortie to deploy Li Yan, Zhuge Liang, Fa Zheng, Yan Yan, and Wu Yi while preserving six-officer pressure: ${JSON.stringify(chengduSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-chengdu-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-chengdu-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredThirtyThirdBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender'; + }); + if (enteredThirtyThirdBattle) { + 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 === 'thirty-third-battle-chengdu-surrender' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-thirty-third-battle.png', fullPage: true }); + + const thirtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const thirtyThirdEnemies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'enemy'); + const thirtyThirdAllies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'ally'); + const thirtyThirdEnemyBehaviors = new Set(thirtyThirdEnemies.map((unit) => unit.ai)); + if ( + thirtyThirdBattleState.camera?.mapWidth !== 66 || + thirtyThirdBattleState.camera?.mapHeight !== 48 || + thirtyThirdBattleState.victoryConditionLabel !== '황권 방어선 격파' || + thirtyThirdEnemies.length < 26 || + !thirtyThirdEnemyBehaviors.has('aggressive') || + !thirtyThirdEnemyBehaviors.has('guard') || + !thirtyThirdEnemyBehaviors.has('hold') || + !thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-leader-huang-quan') || + !thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-officer-a') || + !thirtyThirdAllies.some((unit) => unit.id === 'li-yan') || + !thirtyThirdAllies.some((unit) => unit.id === 'zhuge-liang') || + !thirtyThirdAllies.some((unit) => unit.id === 'fa-zheng') || + !thirtyThirdAllies.some((unit) => unit.id === 'yan-yan') || + !thirtyThirdAllies.some((unit) => unit.id === 'wu-yi') || + thirtyThirdAllies.some((unit) => unit.id === 'huang-quan') + ) { + throw new Error(`Expected thirty-third battle to use Chengdu surrender map, Huang Quan objective, Li Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyThirdBattleState)}`); + } + + 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 thirtyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + thirtyThirdCampState?.campBattleId !== 'thirty-third-battle-chengdu-surrender' || + thirtyThirdCampState.campTitle !== '성도 항복 후 군영' || + thirtyThirdCampState.availableDialogueIds?.length !== 3 || + !thirtyThirdCampState.availableDialogueIds.every((id) => id.endsWith('chengdu')) || + thirtyThirdCampState.availableVisitIds?.length !== 2 || + !thirtyThirdCampState.campaign?.roster?.some((unit) => unit.id === 'huang-quan') || + !thirtyThirdCampState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited) || + thirtyThirdCampState.rosterCollection?.total < 19 + ) { + throw new Error(`Expected thirty-third camp to recruit Huang Quan and expose Chengdu dialogue/visit sets: ${JSON.stringify(thirtyThirdCampState)}`); + } + await page.screenshot({ path: 'dist/verification-chengdu-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postChengduProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postChengduProgressState?.activeTab !== 'progress' || + postChengduProgressState.campaignProgress?.completedKnown !== 33 || + postChengduProgressState.campaignProgress?.totalKnown !== 33 || + postChengduProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || + postChengduProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Chengdu progress tab to complete Yi Province capture and pause before Shu-Han foundation: ${JSON.stringify(postChengduProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-chengdu-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -3940,7 +4094,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirty-second-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan joins, Luofeng, Luo main gate, and Mianzhu battles, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-thirty-third-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan joins, Luofeng, Luo main gate, Mianzhu, and Chengdu surrender battles, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/thirty-third-battle-map.svg b/src/assets/images/battle/thirty-third-battle-map.svg new file mode 100644 index 0000000..526284e --- /dev/null +++ b/src/assets/images/battle/thirty-third-battle-map.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 13812a2..c79d8dd 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -51,6 +51,10 @@ import { thirtySecondBattleMap, thirtySecondBattleUnits, thirtySecondBattleVictoryPages, + thirtyThirdBattleBonds, + thirtyThirdBattleMap, + thirtyThirdBattleUnits, + thirtyThirdBattleVictoryPages, thirteenthBattleBonds, thirteenthBattleMap, thirteenthBattleUnits, @@ -165,7 +169,8 @@ export type BattleScenarioId = | 'twenty-ninth-battle-luo-outer-wall' | 'thirtieth-battle-luofeng-ambush' | 'thirty-first-battle-luo-main-gate' - | 'thirty-second-battle-mianzhu-gate'; + | 'thirty-second-battle-mianzhu-gate' + | 'thirty-third-battle-chengdu-surrender'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2041,6 +2046,69 @@ export const thirtySecondBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const thirtyThirdBattleScenario: BattleScenarioDefinition = { + id: 'thirty-third-battle-chengdu-surrender', + title: '성도 항복 권고전', + victoryConditionLabel: '황권 방어선 격파', + defeatConditionLabel: '유비 또는 이엄 퇴각', + openingObjectiveLines: [ + '성도 외곽의 마지막 방어선입니다. 황권을 물리치면 유장의 항복 권고가 실제 선택지로 바뀝니다.', + '이엄은 방금 합류한 익주 장수로, 이번 전장에서 유비군의 군율을 증언하는 핵심 인물입니다. 이엄을 반드시 지키십시오.', + '성도 창고와 마을을 안정시키면 항복 조건에 대한 신뢰가 커지고, 익주 확보 이후의 수습이 쉬워집니다.' + ], + map: thirtyThirdBattleMap, + units: thirtyThirdBattleUnits, + bonds: thirtyThirdBattleBonds, + mapTextureKey: 'battle-map-thirty-third', + leaderUnitId: 'chengdu-final-leader-huang-quan', + quickVictoryTurnLimit: 48, + baseVictoryGold: 5340, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '황권 방어선 격파', + rewardGold: 3360, + unitId: 'chengdu-final-leader-huang-quan' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 980, + unitId: 'liu-bei' + }, + { + id: 'li-yan', + kind: 'keep-unit-alive', + label: '이엄 생존', + rewardGold: 1320, + unitId: 'li-yan' + }, + { + id: 'chengdu-storehouse', + kind: 'secure-terrain', + label: '성도 창고 확보', + rewardGold: 1620, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '48턴 이내 승리', + rewardGold: 1240, + maxTurn: 48 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'li-yan' } + ], + itemRewards: ['콩 20', '상처약 15', '탁주 8', '익주 안정문 +1', '황권 합류'], + victoryPages: thirtyThirdBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2075,7 +2143,8 @@ export const battleScenarios: Record 'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario, 'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario, 'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario, - 'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario + 'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario, + 'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 158a2a8..4b20565 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -19,6 +19,7 @@ import { thirteenthBattleScenario, thirtyFirstBattleScenario, thirtySecondBattleScenario, + thirtyThirdBattleScenario, thirtiethBattleScenario, twentyEighthBattleScenario, twentyNinthBattleScenario, @@ -72,6 +73,8 @@ import { thirtyFirstBattleVictoryPages, thirtySecondBattleIntroPages, thirtySecondBattleVictoryPages, + thirtyThirdBattleIntroPages, + thirtyThirdBattleVictoryPages, thirtiethBattleIntroPages, thirtiethBattleVictoryPages, twentyEighthBattleIntroPages, @@ -445,13 +448,24 @@ const sortieFlows: Record = { }, [thirtySecondBattleScenario.id]: { afterBattleId: thirtySecondBattleScenario.id, - eyebrow: '다음 장 준비', - title: '성도 항복 권고 준비', + eyebrow: '다음 전장', + title: thirtyThirdBattleScenario.title, description: - '면죽관이 열리고 이엄이 합류했습니다. 이제 성도 포위와 유장의 항복 권고, 그리고 익주 통치의 첫 선택을 준비해야 합니다.', - rewardHint: '다음 장: 성도 항복 권고 준비 중', - pages: thirtySecondBattleVictoryPages, - unavailableNotice: '성도 항복 권고와 익주 최종 수습은 다음 작업에서 이어집니다. 새로 합류한 이엄을 포함해 보급과 내정형 무장 조합을 준비하십시오.' + '면죽관이 열리고 이엄이 합류했습니다. 이제 이엄의 증언과 법정·제갈량의 권고문을 앞세워 성도 외곽의 마지막 방어선을 눌러야 합니다.', + rewardHint: `예상 보상: ${thirtyThirdBattleScenario.title} 개방 / 황권 합류`, + nextBattleId: thirtyThirdBattleScenario.id, + campaignStep: 'thirty-third-battle', + pages: [...thirtySecondBattleVictoryPages, ...thirtyThirdBattleIntroPages] + }, + [thirtyThirdBattleScenario.id]: { + afterBattleId: thirtyThirdBattleScenario.id, + eyebrow: '다음 장 준비', + title: '촉한 건국 준비', + description: + '성도가 항복하고 황권이 합류했습니다. 형주와 익주의 기반이 마련되었으니, 다음 장은 나라의 틀을 세우고 한중과 촉한 건국으로 향하는 준비입니다.', + rewardHint: '다음 장: 촉한 건국 준비 중', + pages: thirtyThirdBattleVictoryPages, + unavailableNotice: '촉한 건국과 한중 방면 전개는 다음 작업에서 이어집니다. 새로 합류한 황권을 포함해 내정과 방어형 무장 조합을 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 5a8760f..3638a12 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2076,6 +2076,68 @@ export const thirtySecondBattleVictoryPages: StoryPage[] = [ } ]; +export const thirtyThirdBattleIntroPages: StoryPage[] = [ + { + id: 'thirty-third-chengdu-encircled', + bgm: 'story-dark', + chapter: '성도 앞의 마지막 권고', + background: 'story-three-heroes', + speaker: '제갈량', + text: '면죽관이 열렸고 이엄까지 합류했습니다. 성도는 아직 문을 닫았으나, 이제 유장은 싸움과 항복 사이에서 더 이상 오래 머물 수 없습니다.' + }, + { + id: 'thirty-third-li-yan-counsel', + bgm: 'battle-prep', + chapter: '이엄의 증언', + background: 'story-liu-bei', + speaker: '이엄', + text: '황권은 성도의 체면과 백성의 안위를 함께 보는 장수입니다. 그가 물러서면 유장공도 백성을 살리는 길을 택할 수 있습니다.' + }, + { + id: 'thirty-third-final-plan', + bgm: 'battle-prep', + chapter: '성도 항복 권고전', + background: 'story-militia', + speaker: '법정', + text: '성도 성문을 부수는 것이 능사가 아닙니다. 황권의 방어선을 눌러 항복 조건을 받아들이게 하고, 마을과 창고를 지켜 유비군의 약속을 증명해야 합니다.' + }, + { + id: 'thirty-third-sortie', + bgm: 'battle-prep', + chapter: '익주를 얻는 길', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '오늘의 승리는 성도의 문을 여는 것보다, 문 안의 백성이 두려움을 내려놓게 하는 데 있소. 칼을 들되 마음을 먼저 잃지 마시오.' + } +]; + +export const thirtyThirdBattleVictoryPages: StoryPage[] = [ + { + id: 'thirty-third-victory-chengdu-yields', + bgm: 'militia-theme', + chapter: '성도 항복', + background: 'story-sortie', + text: '성도 외곽의 방어선은 무너졌지만 성문 안쪽은 불타지 않았습니다. 유장에게 보내진 마지막 권고문은 백성을 보전하고 장수의 체면을 남기는 조건을 담았습니다.' + }, + { + id: 'thirty-third-huang-quan-joins', + bgm: 'battle-prep', + chapter: '황권의 귀순', + background: 'story-liu-bei', + speaker: '황권', + text: '익주를 지키려던 마음은 변하지 않았습니다. 다만 이제는 백성을 살리는 길이 유비공의 군율 아래 있다는 것을 보았습니다. 황권이 그 길에 서겠습니다.' + }, + { + id: 'thirty-third-yizhou-secured', + bgm: 'story-dark', + chapter: '익주 확보', + background: 'story-three-heroes', + speaker: '제갈량', + text: '형주와 익주의 기반이 마련되었습니다. 유비군은 이제 유랑군이 아니라 나라의 뿌리를 갖춘 세력으로 바뀌며, 촉한 건국을 향한 다음 장을 준비합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2413,6 +2475,12 @@ export const thirtySecondBattleMap: BattleMap = { terrain: createThirtySecondBattleTerrain() }; +export const thirtyThirdBattleMap: BattleMap = { + width: 66, + height: 48, + terrain: createThirtyThirdBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4606,6 +4674,30 @@ export const chengduPressureRecruitUnits: UnitData[] = [ } ]; +export const chengduSurrenderRecruitUnits: UnitData[] = [ + { + id: 'huang-quan', + name: '황권', + faction: 'ally', + className: '익주 참모', + classKey: 'strategist', + level: 21, + exp: 0, + hp: 38, + maxHp: 38, + attack: 13, + move: 4, + stats: { might: 67, intelligence: 86, leadership: 82, agility: 64, luck: 74 }, + equipment: { + weapon: { itemId: 'white-feather-fan', level: 3, exp: 18 }, + armor: { itemId: 'oath-robe', level: 3, exp: 8 }, + accessory: { itemId: 'war-manual', level: 2, exp: 12 } + }, + x: 12, + y: 30 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -13193,6 +13285,72 @@ export const thirtySecondBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('chengdu-leader-li-yan', '이엄', '면죽관 수비장', 'rebelLeader', 61, 306, 86, 60, 18) ]; +const thirtyThirdBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 42 }, + 'guan-yu': { x: 5, y: 40 }, + 'zhang-fei': { x: 4, y: 44 }, + 'jian-yong': { x: 3, y: 45 }, + 'mi-zhu': { x: 5, y: 46 }, + 'sun-qian': { x: 7, y: 45 }, + 'zhao-yun': { x: 8, y: 41 }, + 'zhuge-liang': { x: 6, y: 40 }, + 'ma-liang': { x: 8, y: 46 }, + 'yi-ji': { x: 9, y: 43 }, + 'gong-zhi': { x: 10, y: 46 }, + 'huang-zhong': { x: 7, y: 42 }, + 'wei-yan': { x: 10, y: 40 }, + 'pang-tong': { x: 9, y: 41 }, + 'fa-zheng': { x: 11, y: 43 }, + 'wu-yi': { x: 12, y: 41 }, + 'yan-yan': { x: 11, y: 39 }, + 'li-yan': { x: 13, y: 40 } +}; + +export const thirtyThirdBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits + ].map((unit) => placeScenarioUnit(unit, thirtyThirdBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('chengdu-final-scout-a', '성도 척후', '남문 척후', 'bandit', 60, 182, 66, 19, 41), + createThirtyFirstEnemyUnit('chengdu-final-scout-b', '성도 척후', '서문 척후', 'bandit', 60, 182, 66, 25, 39), + createThirtyFirstEnemyUnit('chengdu-final-scout-c', '성도 척후', '수로 척후', 'bandit', 61, 190, 68, 32, 37), + createThirtyFirstEnemyUnit('chengdu-final-infantry-a', '성도 보병', '외곽 보병', 'yellowTurban', 61, 266, 82, 31, 34), + createThirtyFirstEnemyUnit('chengdu-final-infantry-b', '성도 보병', '외곽 보병', 'yellowTurban', 61, 266, 82, 36, 33), + createThirtyFirstEnemyUnit('chengdu-final-infantry-c', '성도 보병', '남문 보병', 'yellowTurban', 62, 276, 84, 43, 28), + createThirtyFirstEnemyUnit('chengdu-final-infantry-d', '성도 보병', '남문 보병', 'yellowTurban', 62, 276, 84, 47, 27), + createThirtyFirstEnemyUnit('chengdu-final-infantry-e', '유장 친위대', '성문 친위대', 'yellowTurban', 63, 288, 86, 55, 23), + createThirtyFirstEnemyUnit('chengdu-final-infantry-f', '유장 친위대', '창고 친위대', 'yellowTurban', 63, 288, 86, 60, 25), + createThirtyFirstEnemyUnit('chengdu-final-archer-a', '성도 궁병', '성벽 궁병', 'archer', 61, 182, 66, 34, 31), + createThirtyFirstEnemyUnit('chengdu-final-archer-b', '성도 궁병', '남문 궁병', 'archer', 62, 190, 68, 46, 23), + createThirtyFirstEnemyUnit('chengdu-final-archer-c', '성도 궁병', '고지 궁병', 'archer', 62, 190, 68, 52, 18), + createThirtyFirstEnemyUnit('chengdu-final-archer-d', '성도 궁병', '창고 궁병', 'archer', 63, 198, 70, 61, 20), + createThirtyFirstEnemyUnit('chengdu-final-cavalry-a', '성도 기병', '우익 기병', 'cavalry', 62, 276, 86, 28, 43), + createThirtyFirstEnemyUnit('chengdu-final-cavalry-b', '성도 기병', '좌익 기병', 'cavalry', 62, 276, 86, 43, 38), + createThirtyFirstEnemyUnit('chengdu-final-cavalry-c', '성도 기병', '내성 기병', 'cavalry', 63, 288, 88, 58, 31), + createThirtyFirstEnemyUnit('chengdu-final-guard-a', '황권 친위대', '권고 반대파', 'yellowTurban', 63, 300, 88, 50, 22), + createThirtyFirstEnemyUnit('chengdu-final-guard-b', '황권 친위대', '권고 반대파', 'yellowTurban', 63, 300, 88, 54, 21), + createThirtyFirstEnemyUnit('chengdu-final-guard-c', '황권 친위대', '성도 친위대', 'yellowTurban', 64, 312, 90, 60, 18), + createThirtyFirstEnemyUnit('chengdu-final-strategist-a', '성도 책사', '항복 반대파', 'strategist', 62, 190, 68, 48, 19), + createThirtyFirstEnemyUnit('chengdu-final-strategist-b', '성도 책사', '유장 사자', 'strategist', 63, 198, 70, 57, 16), + createThirtyFirstEnemyUnit('chengdu-final-ambusher-a', '성도 복병', '남벽 복병', 'bandit', 61, 190, 70, 37, 41), + createThirtyFirstEnemyUnit('chengdu-final-ambusher-b', '성도 복병', '동문 복병', 'bandit', 61, 190, 70, 51, 35), + createThirtyFirstEnemyUnit('chengdu-final-ambusher-c', '성도 복병', '북벽 복병', 'bandit', 62, 198, 72, 62, 12), + createThirtyFirstEnemyUnit('chengdu-final-officer-a', '성도 부장', '남문 부장', 'rebelLeader', 64, 318, 92, 58, 22), + createThirtyFirstEnemyUnit('chengdu-final-leader-huang-quan', '황권', '성도 수비 참모', 'rebelLeader', 65, 336, 94, 62, 18) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -13571,6 +13729,33 @@ export const chengduPressureRecruitBonds: BattleBond[] = [ } ]; +export const chengduSurrenderRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__huang-quan', + unitIds: ['liu-bei', 'huang-quan'], + title: '항복과 보전', + level: 31, + exp: 0, + description: '유비의 보전 약속과 황권의 현실적인 판단이 맞닿아, 성도 항복 뒤 백성과 장수의 불안을 낮춥니다.' + }, + { + id: 'zhuge-liang__huang-quan', + unitIds: ['zhuge-liang', 'huang-quan'], + title: '성도 수습책', + level: 29, + exp: 0, + description: '제갈량의 큰 내정 구상과 황권의 현지 행정 감각이 만나, 익주 통치의 첫 질서를 세웁니다.' + }, + { + id: 'fa-zheng__huang-quan', + unitIds: ['fa-zheng', 'huang-quan'], + title: '익주 책사들의 균형', + level: 28, + exp: 0, + description: '법정의 결단력과 황권의 신중함이 서로를 보완하며, 익주 장수들을 다루는 말길을 넓힙니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -13610,6 +13795,9 @@ export const thirtyFirstBattleBonds: BattleBond[] = [...thirtiethBattleBonds].ma export const thirtySecondBattleBonds: BattleBond[] = [...thirtyFirstBattleBonds, ...luoCastleProperRecruitBonds].map( cloneBattleBondForScenario ); +export const thirtyThirdBattleBonds: BattleBond[] = [...thirtySecondBattleBonds, ...chengduPressureRecruitBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -14937,6 +15125,72 @@ function createThirtySecondBattleTerrain(): TerrainType[][] { ); } +function createThirtyThirdBattleTerrain(): TerrainType[][] { + return Array.from({ length: 48 }, (_, y) => + Array.from({ length: 66 }, (_, x): TerrainType => { + if (x <= 4 && y >= 41) { + return 'camp'; + } + if ( + (x >= 50 && x <= 64 && y >= 10 && y <= 27) || + (x >= 46 && x <= 56 && y >= 7 && y <= 18) || + (x >= 56 && x <= 65 && y >= 18 && y <= 32) || + (x >= 53 && x <= 61 && y >= 29 && y <= 35) + ) { + return 'fort'; + } + if ( + (x >= 18 && x <= 22 && y >= 39 && y <= 41) || + (x >= 36 && x <= 39 && y >= 33 && y <= 35) || + (x >= 46 && x <= 48 && y >= 28 && y <= 30) || + (x >= 57 && x <= 60 && y >= 31 && y <= 34) + ) { + return 'village'; + } + if ( + (y >= 43 && x >= 4 && x <= 20) || + (x >= 19 && x <= 32 && y >= 39 && y <= 43) || + (x >= 31 && x <= 44 && y >= 34 && y <= 39) || + (x >= 43 && x <= 55 && y >= 28 && y <= 34) || + (x >= 54 && x <= 63 && y >= 22 && y <= 28) || + (x >= 59 && x <= 65 && y >= 19 && y <= 23) + ) { + return 'road'; + } + if ((x === 15 || x === 16) && y >= 0 && y <= 37) { + return 'river'; + } + if ((x === 36 || x === 37) && y >= 4 && y <= 32) { + return 'river'; + } + if ((x === 48 || x === 49) && y >= 34 && y <= 47) { + return 'river'; + } + if ( + (x >= 4 && x <= 22 && y >= 8 && y <= 32) || + (x >= 23 && x <= 35 && y >= 14 && y <= 33) || + (x >= 38 && x <= 52 && y >= 35 && y <= 47) || + (x >= 51 && x <= 65 && y >= 31 && y <= 46) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 13 && y <= 17) || + (x >= 23 && x <= 35 && y <= 13) || + (x >= 37 && x <= 54 && y >= 2 && y <= 14) || + (x >= 57 && x <= 65 && y <= 12) || + (x >= 27 && x <= 40 && y >= 40 && y <= 47) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 40) || (x >= 64 && y >= 28) || (x >= 50 && x <= 53 && y <= 8)) { + 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 58480dc..920ee72 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -454,7 +454,34 @@ const unitTexture: Record = { 'chengdu-ambusher-b': 'unit-rebel', 'chengdu-ambusher-c': 'unit-rebel', 'chengdu-officer-fei-guan': 'unit-rebel-leader', - 'chengdu-leader-li-yan': 'unit-rebel-leader' + 'chengdu-leader-li-yan': 'unit-rebel-leader', + 'huang-quan': 'unit-liu-bei', + 'chengdu-final-scout-a': 'unit-rebel', + 'chengdu-final-scout-b': 'unit-rebel', + 'chengdu-final-scout-c': 'unit-rebel', + 'chengdu-final-infantry-a': 'unit-rebel', + 'chengdu-final-infantry-b': 'unit-rebel', + 'chengdu-final-infantry-c': 'unit-rebel', + 'chengdu-final-infantry-d': 'unit-rebel', + 'chengdu-final-infantry-e': 'unit-rebel', + 'chengdu-final-infantry-f': 'unit-rebel', + 'chengdu-final-archer-a': 'unit-rebel-archer', + 'chengdu-final-archer-b': 'unit-rebel-archer', + 'chengdu-final-archer-c': 'unit-rebel-archer', + 'chengdu-final-archer-d': 'unit-rebel-archer', + 'chengdu-final-cavalry-a': 'unit-rebel-cavalry', + 'chengdu-final-cavalry-b': 'unit-rebel-cavalry', + 'chengdu-final-cavalry-c': 'unit-rebel-cavalry', + 'chengdu-final-guard-a': 'unit-rebel', + 'chengdu-final-guard-b': 'unit-rebel', + 'chengdu-final-guard-c': 'unit-rebel', + 'chengdu-final-strategist-a': 'unit-rebel-archer', + 'chengdu-final-strategist-b': 'unit-rebel-archer', + 'chengdu-final-ambusher-a': 'unit-rebel', + 'chengdu-final-ambusher-b': 'unit-rebel', + 'chengdu-final-ambusher-c': 'unit-rebel', + 'chengdu-final-officer-a': 'unit-rebel-leader', + 'chengdu-final-leader-huang-quan': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1370,7 +1397,33 @@ const enemyAiByUnitId: Record = { 'chengdu-ambusher-b': 'aggressive', 'chengdu-ambusher-c': 'aggressive', 'chengdu-officer-fei-guan': 'guard', - 'chengdu-leader-li-yan': 'guard' + 'chengdu-leader-li-yan': 'guard', + 'chengdu-final-scout-a': 'aggressive', + 'chengdu-final-scout-b': 'aggressive', + 'chengdu-final-scout-c': 'aggressive', + 'chengdu-final-infantry-a': 'guard', + 'chengdu-final-infantry-b': 'guard', + 'chengdu-final-infantry-c': 'guard', + 'chengdu-final-infantry-d': 'guard', + 'chengdu-final-infantry-e': 'guard', + 'chengdu-final-infantry-f': 'guard', + 'chengdu-final-archer-a': 'hold', + 'chengdu-final-archer-b': 'hold', + 'chengdu-final-archer-c': 'hold', + 'chengdu-final-archer-d': 'hold', + 'chengdu-final-cavalry-a': 'aggressive', + 'chengdu-final-cavalry-b': 'aggressive', + 'chengdu-final-cavalry-c': 'aggressive', + 'chengdu-final-guard-a': 'guard', + 'chengdu-final-guard-b': 'guard', + 'chengdu-final-guard-c': 'guard', + 'chengdu-final-strategist-a': 'hold', + 'chengdu-final-strategist-b': 'hold', + 'chengdu-final-ambusher-a': 'aggressive', + 'chengdu-final-ambusher-b': 'aggressive', + 'chengdu-final-ambusher-c': 'aggressive', + 'chengdu-final-officer-a': 'guard', + 'chengdu-final-leader-huang-quan': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index d601797..7090ba7 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 thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg'; import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg'; +import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg'; import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg'; import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg'; @@ -122,6 +123,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl); this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl); this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl); + this.load.image('battle-map-thirty-third', thirtyThirdBattleMapUrl); 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 f158450..a2e1a64 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -9,6 +9,8 @@ import { caoBreakRecruitUnits, chengduPressureRecruitBonds, chengduPressureRecruitUnits, + chengduSurrenderRecruitBonds, + chengduSurrenderRecruitUnits, changshaRecruitBonds, changshaRecruitUnits, firstBattleBonds, @@ -325,9 +327,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'twenty-ninth-battle-luo-outer-wall', 'thirtieth-battle-luofeng-ambush', 'thirty-first-battle-luo-main-gate', - 'thirty-second-battle-mianzhu-gate' + 'thirty-second-battle-mianzhu-gate', + 'thirty-third-battle-chengdu-surrender' ], - nextHints: ['형주 방위', '익주 진입', '방통·법정 합류'] + nextHints: ['형주 방위', '익주 진입', '성도 항복'] }, { id: 'shu-han', @@ -371,7 +374,8 @@ const campBattleIds = { twentyNinth: 'twenty-ninth-battle-luo-outer-wall', thirtieth: 'thirtieth-battle-luofeng-ambush', thirtyFirst: 'thirty-first-battle-luo-main-gate', - thirtySecond: 'thirty-second-battle-mianzhu-gate' + thirtySecond: 'thirty-second-battle-mianzhu-gate', + thirtyThird: 'thirty-third-battle-chengdu-surrender' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -730,6 +734,18 @@ const sortieRulesByBattleId: Partial