From 8349d1044f9b4d642c1929b670b719a197cf5d14 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 09:14:41 +0900 Subject: [PATCH] Add Fu Pass entry chapter --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 175 ++++- .../battle/twenty-eighth-battle-map.svg | 121 ++++ src/game/data/battles.ts | 73 +- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 628 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 51 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 197 +++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1260 insertions(+), 34 deletions(-) create mode 100644 src/assets/images/battle/twenty-eighth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 64d9bbb..ec270db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -49,15 +49,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Twenty-fifth battle Wuling mountain road route, pushing ten recruited officers into a six-officer sortie choice across a 50x34 mountain battlefield and recruiting Gong Zhi after Jin Xuan withdraws - Twenty-sixth battle Changsha veteran audience route, turning the southern Jing Province finale into a 52x34 castle approach battle and recruiting Huang Zhong plus Wei Yan after Han Xuan withdraws - Twenty-seventh battle Yi Province relief-road route, using the thirteen-officer roster in sortie selection across a 54x36 mountain-gate battlefield and recruiting Pang Tong after Yang Huai withdraws +- Twenty-eighth battle Fu Pass entry route, putting Pang Tong into the sortie choice across a 56x38 fortified mountain pass and recruiting Fa Zheng after Gao Pei withdraws - 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-seventh battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province camp states, campaign timeline state, and camp save state +- Flow verification script from title through the twenty-eighth 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 camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue the Yi Province arc into Fu Pass and Luo Castle, with Pang Tong/Zhuge Liang planning pressure and harder officer selection +1. Continue the Yi Province arc into Luo Castle, with Pang Tong/Fa Zheng planning pressure and harder officer selection 2. Add more recruitable officers as the campaign moves deeper through Yi Province and toward 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 ce00dc6..559aee0 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 !== 27 || + progressTabState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postBowangProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postChangbanProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postWulingProgressState.campaignProgress?.totalKnown !== 28 || 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 !== 27 || + postChangshaProgressState.campaignProgress?.totalKnown !== 28 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' @@ -3166,15 +3166,162 @@ try { if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || - postYizhouProgressState.campaignProgress?.totalKnown !== 27 || + postYizhouProgressState.campaignProgress?.totalKnown !== 28 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || - postYizhouProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' ) { - throw new Error(`Expected post-Yi Province progress tab to complete the relief-road battle and pause on Fu Pass preparation: ${JSON.stringify(postYizhouProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Yi Province progress tab to complete the relief-road battle and open Fu Pass entry: ${JSON.stringify(postYizhouProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-yizhou-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const fuPassSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fuPassSortieState?.sortieVisible || + !fuPassSortieState.sortiePlan?.objectiveLine?.includes('부수관 진입전') || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited && unit.recommended) || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng') || + fuPassSortieState.sortieRoster?.length < 14 || + fuPassSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-seventh camp sortie prep to target Fu Pass with Pang Tong selectable and Fa Zheng absent before victory: ${JSON.stringify(fuPassSortieState)}`); + } + assertSortieTacticalRoster(fuPassSortieState, [ + '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' + ]); + + const fuPassPriorityUnits = ['pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun']; + for (const unitId of fuPassPriorityUnits) { + 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' && !fuPassPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const fuPassSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fuPassPriorityUnits.every((unitId) => + fuPassSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + fuPassSortieReadyState.sortiePlan?.selectedCount !== 6 || + fuPassSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Fu Pass sortie to deploy Pang Tong, Zhuge Liang, Wei Yan, Huang Zhong, and Zhao Yun while preserving six-officer pressure: ${JSON.stringify(fuPassSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-fupass-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-fupass-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentyEighthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-eighth-battle-fu-pass-entry'; + }); + if (enteredTwentyEighthBattle) { + 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-eighth-battle-fu-pass-entry' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-eighth-battle.png', fullPage: true }); + + const twentyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentyEighthEnemies = twentyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentyEighthAllies = twentyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentyEighthEnemyBehaviors = new Set(twentyEighthEnemies.map((unit) => unit.ai)); + if ( + twentyEighthBattleState.camera?.mapWidth !== 56 || + twentyEighthBattleState.camera?.mapHeight !== 38 || + twentyEighthBattleState.victoryConditionLabel !== '고패 퇴각' || + twentyEighthEnemies.length < 20 || + !twentyEighthEnemyBehaviors.has('aggressive') || + !twentyEighthEnemyBehaviors.has('guard') || + !twentyEighthEnemyBehaviors.has('hold') || + !twentyEighthEnemies.some((unit) => unit.id === 'fupass-leader-gao-pei') || + !twentyEighthAllies.some((unit) => unit.id === 'pang-tong') || + !twentyEighthAllies.some((unit) => unit.id === 'huang-zhong') || + !twentyEighthAllies.some((unit) => unit.id === 'wei-yan') || + twentyEighthAllies.some((unit) => unit.id === 'fa-zheng') + ) { + throw new Error(`Expected twenty-eighth battle to use Fu Pass map, Gao Pei objective, Pang Tong as selectable ally, mixed AI, and no Fa Zheng before victory: ${JSON.stringify(twentyEighthBattleState)}`); + } + + 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 twentyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentyEighthCampState?.campBattleId !== 'twenty-eighth-battle-fu-pass-entry' || + twentyEighthCampState.campTitle !== '부수관 진입 후 군영' || + twentyEighthCampState.availableDialogueIds?.length !== 3 || + !twentyEighthCampState.availableDialogueIds.every((id) => id.endsWith('fupass')) || + twentyEighthCampState.availableVisitIds?.length !== 2 || + !twentyEighthCampState.campaign?.roster?.some((unit) => unit.id === 'fa-zheng') || + !twentyEighthCampState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited) || + twentyEighthCampState.rosterCollection?.total < 15 + ) { + throw new Error(`Expected twenty-eighth camp to recruit Fa Zheng and expose Fu Pass dialogue/visit sets: ${JSON.stringify(twentyEighthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-fupass-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postFuPassProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postFuPassProgressState?.activeTab !== 'progress' || + postFuPassProgressState.campaignProgress?.completedKnown !== 28 || + postFuPassProgressState.campaignProgress?.totalKnown !== 28 || + postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || + postFuPassProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Fu Pass progress tab to complete the pass-entry battle and pause on Luo Castle preparation: ${JSON.stringify(postFuPassProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-fupass-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -3186,7 +3333,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-twenty-seventh-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, and Pang Tong joins, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twenty-eighth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, and Fa Zheng joins, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twenty-eighth-battle-map.svg b/src/assets/images/battle/twenty-eighth-battle-map.svg new file mode 100644 index 0000000..87ae462 --- /dev/null +++ b/src/assets/images/battle/twenty-eighth-battle-map.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 934d989..93ce3d0 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -31,6 +31,10 @@ import { nineteenthBattleMap, nineteenthBattleUnits, nineteenthBattleVictoryPages, + twentyEighthBattleBonds, + twentyEighthBattleMap, + twentyEighthBattleUnits, + twentyEighthBattleVictoryPages, tenthBattleBonds, tenthBattleMap, tenthBattleUnits, @@ -140,7 +144,8 @@ export type BattleScenarioId = | 'twenty-fourth-battle-guiyang-persuasion' | 'twenty-fifth-battle-wuling-mountain-road' | 'twenty-sixth-battle-changsha-veteran' - | 'twenty-seventh-battle-yizhou-relief-road'; + | 'twenty-seventh-battle-yizhou-relief-road' + | 'twenty-eighth-battle-fu-pass-entry'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1701,6 +1706,69 @@ export const twentySeventhBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentyEighthBattleScenario: BattleScenarioDefinition = { + id: 'twenty-eighth-battle-fu-pass-entry', + title: '부수관 진입전', + victoryConditionLabel: '고패 퇴각', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '익주 원군로를 연 유비군은 부수관의 첫 문 앞에 섰습니다. 고패는 좁은 협곡과 성루 궁병을 이용해 원군의 명분을 관문 앞에서 묶으려 합니다.', + '이번 전투부터 방통이 출전 후보에 들어옵니다. 와룡과 봉추를 함께 세우면 계책은 강해지지만, 전열과 보급 선택이 더 날카로워집니다.', + '유비와 제갈량을 지키며 고패를 물러나게 하면 익주 내부 인재 법정이 군영에 합류합니다.' + ], + map: twentyEighthBattleMap, + units: twentyEighthBattleUnits, + bonds: twentyEighthBattleBonds, + mapTextureKey: 'battle-map-twenty-eighth', + leaderUnitId: 'fupass-leader-gao-pei', + quickVictoryTurnLimit: 37, + baseVictoryGold: 3980, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '고패 퇴각', + rewardGold: 2520, + unitId: 'fupass-leader-gao-pei' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 740, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 1020, + unitId: 'zhuge-liang' + }, + { + id: 'fu-pass-village', + kind: 'secure-terrain', + label: '부수관 마을 안정', + rewardGold: 1220, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '37턴 이내 승리', + rewardGold: 960, + maxTurn: 37 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 13', '상처약 9', '탁주 6', '부수관 보급표 +1', '법정 합류'], + victoryPages: twentyEighthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1730,7 +1798,8 @@ export const battleScenarios: Record 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario, 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario, 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario, - 'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario + 'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario, + 'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index c90ec79..74f8ef7 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -17,6 +17,7 @@ import { seventeenthBattleScenario, tenthBattleScenario, thirteenthBattleScenario, + twentyEighthBattleScenario, twentyFirstBattleScenario, twentySecondBattleScenario, twentyThirdBattleScenario, @@ -63,6 +64,8 @@ import { tenthBattleVictoryPages, thirteenthBattleIntroPages, thirteenthBattleVictoryPages, + twentyEighthBattleIntroPages, + twentyEighthBattleVictoryPages, twentyFirstBattleIntroPages, twentyFirstBattleVictoryPages, twentySecondBattleIntroPages, @@ -375,13 +378,24 @@ const sortieFlows: Record = { }, [twentySeventhBattleScenario.id]: { afterBattleId: twentySeventhBattleScenario.id, - eyebrow: '다음 장 준비', - title: '부수관 진입 준비', + eyebrow: '다음 전장', + title: twentyEighthBattleScenario.title, description: - '익주 원군로가 열리고 방통이 합류했습니다. 이제 유비군은 촉의 깊은 산문과 부수관으로 들어가기 전, 와룡과 봉추의 계책을 함께 다듬어야 합니다.', - rewardHint: '다음 장: 부수관 진입 준비 중', - pages: twentySeventhBattleVictoryPages, - unavailableNotice: '부수관 이후 익주 심부 장은 다음 작업에서 이어집니다. 군영에서 방통과 기존 무장들의 공명도를 올리고 다음 대장정의 출전 후보를 정비하십시오.' + '익주 원군로가 열리고 방통이 합류했습니다. 이제 와룡과 봉추를 함께 편성할지 고민하며, 촉의 깊은 산문인 부수관으로 들어가야 합니다.', + rewardHint: `예상 보상: ${twentyEighthBattleScenario.title} 개방 / 법정 합류`, + nextBattleId: twentyEighthBattleScenario.id, + campaignStep: 'twenty-eighth-battle', + pages: [...twentySeventhBattleVictoryPages, ...twentyEighthBattleIntroPages] + }, + [twentyEighthBattleScenario.id]: { + afterBattleId: twentyEighthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '낙성 진입 준비', + description: + '부수관이 열리고 법정이 합류했습니다. 이제 유비군은 익주 안쪽의 마음과 길을 동시에 읽으며 낙성으로 이어지는 더 어려운 전장을 준비해야 합니다.', + rewardHint: '다음 장: 낙성 진입 준비 중', + pages: twentyEighthBattleVictoryPages, + unavailableNotice: '낙성 이후 익주 심부 장은 다음 작업에서 이어집니다. 군영에서 법정과 방통의 공명도를 올리고 다음 관문을 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 70b49b7..a49194b 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1766,6 +1766,68 @@ export const twentySeventhBattleVictoryPages: StoryPage[] = [ } ]; +export const twentyEighthBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-eighth-fu-pass-shadow', + bgm: 'story-dark', + chapter: '부수관의 그림자', + background: 'story-three-heroes', + speaker: '방통', + text: '익주 원군로는 열렸으나 부수관은 아직 닫혀 있습니다. 고패가 관문 병력을 모아 길을 끊으면, 우리 명분은 산문 앞에서 멈출 것입니다.' + }, + { + id: 'twenty-eighth-fa-zheng-message', + bgm: 'battle-prep', + chapter: '익주 내부의 손', + background: 'story-liu-bei', + speaker: '제갈량', + text: '익주 안쪽에서 법정이라는 인물이 은밀히 소식을 보냈습니다. 그는 유장의 답답한 정치를 걱정하며, 부수관을 넘을 길목을 알려 주겠다고 합니다.' + }, + { + id: 'twenty-eighth-roster-pressure', + bgm: 'battle-prep', + chapter: '와룡과 봉추의 첫 편성', + background: 'story-militia', + speaker: '마량', + text: '이번 전투부터 방통도 출전 후보가 됩니다. 책사 둘을 모두 데려가면 관문 계책은 강해지지만, 전열과 보급 중 하나가 얇아질 수 있습니다.' + }, + { + id: 'twenty-eighth-fu-pass-sortie', + bgm: 'battle-prep', + chapter: '부수관 진입전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '관문을 부수는 일보다 중요한 것은, 그 너머의 백성이 우리를 어떻게 기억하느냐요. 방통의 기책과 제갈량의 큰 판을 살려 부수관의 길을 열겠소.' + } +]; + +export const twentyEighthBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-eighth-victory-pass-open', + bgm: 'militia-theme', + chapter: '부수관이 열리다', + background: 'story-sortie', + text: '고패의 관문 병력이 물러나고 부수관의 첫 문이 열렸습니다. 유비군은 관문 창고를 봉인하고 마을의 곡식을 지켜, 익주 내부의 의심을 조금 누그러뜨렸습니다.' + }, + { + id: 'twenty-eighth-fa-zheng-joins', + bgm: 'battle-prep', + chapter: '법정의 합류', + background: 'story-liu-bei', + speaker: '법정', + text: '익주는 험한 땅이지만, 더 험한 것은 닫힌 마음입니다. 주공께서 그 마음을 함부로 베지 않으신다면, 이 법정이 안쪽 길을 밝히겠습니다.' + }, + { + id: 'twenty-eighth-next-luo-castle', + bgm: 'story-dark', + chapter: '낙성으로 이어지는 길', + background: 'story-three-heroes', + speaker: '방통', + text: '부수관은 문에 불과합니다. 낙성으로 이어지는 길에는 더 깊은 매복과 더 복잡한 마음이 기다립니다. 이제 법정의 내부 정보까지 함께 써야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2073,6 +2135,12 @@ export const twentySeventhBattleMap: BattleMap = { terrain: createTwentySeventhBattleTerrain() }; +export const twentyEighthBattleMap: BattleMap = { + width: 56, + height: 38, + terrain: createTwentyEighthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4170,6 +4238,30 @@ export const yizhouRecruitUnits: UnitData[] = [ } ]; +export const fuPassRecruitUnits: UnitData[] = [ + { + id: 'fa-zheng', + name: '법정', + faction: 'ally', + className: '익주 모사', + classKey: 'strategist', + level: 17, + exp: 0, + hp: 38, + maxHp: 38, + attack: 9, + move: 4, + stats: { might: 42, intelligence: 94, leadership: 84, agility: 70, luck: 80 }, + equipment: { + weapon: { itemId: 'white-feather-fan', level: 2, exp: 18 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 16 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 8, + y: 25 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -11105,6 +11197,458 @@ export const twentySeventhBattleUnits: UnitData[] = [ } ]; +const twentyEighthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 32 }, + 'guan-yu': { x: 6, y: 31 }, + 'zhang-fei': { x: 5, y: 34 }, + 'jian-yong': { x: 3, y: 35 }, + 'mi-zhu': { x: 4, y: 36 }, + 'sun-qian': { x: 7, y: 35 }, + 'zhao-yun': { x: 8, y: 32 }, + 'zhuge-liang': { x: 5, y: 32 }, + 'ma-liang': { x: 8, y: 36 }, + 'yi-ji': { x: 9, y: 34 }, + 'gong-zhi': { x: 10, y: 36 }, + 'huang-zhong': { x: 7, y: 33 }, + 'wei-yan': { x: 10, y: 31 }, + 'pang-tong': { x: 9, y: 33 } +}; + +export const twentyEighthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentyEighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'fupass-scout-a', + name: '부수관 척후', + faction: 'enemy', + className: '관문 척후', + classKey: 'bandit', + level: 44, + exp: 68, + hp: 112, + maxHp: 112, + attack: 43, + move: 4, + stats: { might: 134, intelligence: 96, leadership: 110, agility: 126, luck: 86 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 6 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 40 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 16, + y: 32 + }, + { + id: 'fupass-scout-b', + name: '부수관 척후', + faction: 'enemy', + className: '서문 척후', + classKey: 'bandit', + level: 44, + exp: 68, + hp: 112, + maxHp: 112, + attack: 43, + move: 4, + stats: { might: 134, intelligence: 96, leadership: 110, agility: 126, luck: 86 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 6 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 40 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 21, + y: 30 + }, + { + id: 'fupass-infantry-a', + name: '부수관 보병', + faction: 'enemy', + className: '산문 보병', + classKey: 'yellowTurban', + level: 45, + exp: 70, + hp: 154, + maxHp: 154, + attack: 48, + move: 3, + stats: { might: 150, intelligence: 96, leadership: 128, agility: 104, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 18 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 25, + y: 29 + }, + { + id: 'fupass-infantry-b', + name: '부수관 보병', + faction: 'enemy', + className: '협곡 보병', + classKey: 'yellowTurban', + level: 45, + exp: 70, + hp: 154, + maxHp: 154, + attack: 48, + move: 3, + stats: { might: 150, intelligence: 96, leadership: 128, agility: 104, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 18 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 30, + y: 26 + }, + { + id: 'fupass-infantry-c', + name: '부수관 보병', + faction: 'enemy', + className: '관문 보병', + classKey: 'yellowTurban', + level: 46, + exp: 72, + hp: 162, + maxHp: 162, + attack: 50, + move: 3, + stats: { might: 154, intelligence: 98, leadership: 132, agility: 106, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 26 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 38, + y: 23 + }, + { + id: 'fupass-infantry-d', + name: '부수관 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 46, + exp: 72, + hp: 162, + maxHp: 162, + attack: 50, + move: 3, + stats: { might: 154, intelligence: 98, leadership: 132, agility: 106, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 26 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 44, + y: 19 + }, + { + id: 'fupass-archer-a', + name: '부수관 궁병', + faction: 'enemy', + className: '산루 궁병', + classKey: 'archer', + level: 45, + exp: 70, + hp: 112, + maxHp: 112, + attack: 45, + move: 3, + stats: { might: 108, intelligence: 108, leadership: 114, agility: 122, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 14 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 30, + y: 22 + }, + { + id: 'fupass-archer-b', + name: '부수관 궁병', + faction: 'enemy', + className: '관문 궁병', + classKey: 'archer', + level: 46, + exp: 72, + hp: 116, + maxHp: 116, + attack: 46, + move: 3, + stats: { might: 110, intelligence: 110, leadership: 116, agility: 124, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 22 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 42, + y: 16 + }, + { + id: 'fupass-archer-c', + name: '부수관 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 46, + exp: 72, + hp: 116, + maxHp: 116, + attack: 46, + move: 3, + stats: { might: 110, intelligence: 110, leadership: 116, agility: 124, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 22 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 50, + y: 20 + }, + { + id: 'fupass-cavalry-a', + name: '부수관 기병', + faction: 'enemy', + className: '고개 기병', + classKey: 'cavalry', + level: 46, + exp: 72, + hp: 162, + maxHp: 162, + attack: 51, + move: 5, + stats: { might: 154, intelligence: 98, leadership: 130, agility: 126, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 28 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 16 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 23, + y: 34 + }, + { + id: 'fupass-cavalry-b', + name: '부수관 기병', + faction: 'enemy', + className: '관문 기병', + classKey: 'cavalry', + level: 46, + exp: 72, + hp: 162, + maxHp: 162, + attack: 51, + move: 5, + stats: { might: 154, intelligence: 98, leadership: 130, agility: 126, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 28 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 16 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 40, + y: 29 + }, + { + id: 'fupass-cavalry-c', + name: '부수관 기병', + faction: 'enemy', + className: '성문 기병', + classKey: 'cavalry', + level: 47, + exp: 74, + hp: 170, + maxHp: 170, + attack: 53, + move: 5, + stats: { might: 158, intelligence: 100, leadership: 134, agility: 128, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 36 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 24 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 49, + y: 27 + }, + { + id: 'fupass-guard-a', + name: '고패 친위대', + faction: 'enemy', + className: '관문 친위대', + classKey: 'yellowTurban', + level: 47, + exp: 74, + hp: 174, + maxHp: 174, + attack: 53, + move: 3, + stats: { might: 160, intelligence: 100, leadership: 138, agility: 108, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 40 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 47, + y: 17 + }, + { + id: 'fupass-guard-b', + name: '고패 친위대', + faction: 'enemy', + className: '성문 친위대', + classKey: 'yellowTurban', + level: 47, + exp: 74, + hp: 174, + maxHp: 174, + attack: 53, + move: 3, + stats: { might: 160, intelligence: 100, leadership: 138, agility: 108, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 40 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 51, + y: 19 + }, + { + id: 'fupass-strategist-a', + name: '부수관 책사', + faction: 'enemy', + className: '관문 책사', + classKey: 'archer', + level: 46, + exp: 72, + hp: 116, + maxHp: 116, + attack: 46, + move: 3, + stats: { might: 94, intelligence: 124, leadership: 116, agility: 100, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 24 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 12 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 43, + y: 14 + }, + { + id: 'fupass-strategist-b', + name: '부수관 책사', + faction: 'enemy', + className: '성문 책사', + classKey: 'archer', + level: 46, + exp: 72, + hp: 116, + maxHp: 116, + attack: 46, + move: 3, + stats: { might: 94, intelligence: 124, leadership: 116, agility: 100, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 24 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 12 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 51, + y: 23 + }, + { + id: 'fupass-ambusher-a', + name: '산길 복병', + faction: 'enemy', + className: '서쪽 복병', + classKey: 'bandit', + level: 45, + exp: 70, + hp: 120, + maxHp: 120, + attack: 45, + move: 4, + stats: { might: 140, intelligence: 98, leadership: 112, agility: 128, luck: 86 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 18 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 31, + y: 34 + }, + { + id: 'fupass-ambusher-b', + name: '산길 복병', + faction: 'enemy', + className: '북쪽 복병', + classKey: 'bandit', + level: 45, + exp: 70, + hp: 120, + maxHp: 120, + attack: 45, + move: 4, + stats: { might: 140, intelligence: 98, leadership: 112, agility: 128, luck: 86 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 18 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 35, + y: 8 + }, + { + id: 'fupass-ambusher-c', + name: '산길 복병', + faction: 'enemy', + className: '남쪽 복병', + classKey: 'bandit', + level: 45, + exp: 70, + hp: 120, + maxHp: 120, + attack: 45, + move: 4, + stats: { might: 140, intelligence: 98, leadership: 112, agility: 128, luck: 86 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 18 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 45, + y: 31 + }, + { + id: 'fupass-leader-gao-pei', + name: '고패', + faction: 'enemy', + className: '부수관 수장', + classKey: 'rebelLeader', + level: 48, + exp: 78, + hp: 222, + maxHp: 222, + attack: 55, + move: 4, + stats: { might: 162, intelligence: 116, leadership: 144, agility: 110, luck: 90 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 5, exp: 34 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 52, + y: 18 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -11375,6 +11919,33 @@ export const yizhouRecruitBonds: BattleBond[] = [ } ]; +export const fuPassRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__fa-zheng', + unitIds: ['liu-bei', 'fa-zheng'], + title: '익주 내부의 길', + level: 34, + exp: 0, + description: '유비의 명분과 법정의 내부 정보가 만나, 익주의 닫힌 마음과 관문을 함께 열어 갑니다.' + }, + { + id: 'pang-tong__fa-zheng', + unitIds: ['pang-tong', 'fa-zheng'], + title: '봉추와 내부책', + level: 32, + exp: 0, + description: '방통의 과감한 기책과 법정의 익주 사정이 맞물려, 적이 숨긴 길목을 먼저 흔듭니다.' + }, + { + id: 'zhuge-liang__fa-zheng', + unitIds: ['zhuge-liang', 'fa-zheng'], + title: '큰 판과 속내', + level: 30, + exp: 0, + description: '제갈량의 큰 전략과 법정의 내부 정세 판단이 더해져, 익주 공략의 무리한 피를 줄입니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -11407,6 +11978,7 @@ export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario); export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario); export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds, ...changshaRecruitBonds].map(cloneBattleBondForScenario); +export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBonds, ...yizhouRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -12428,6 +13000,62 @@ function createTwentySeventhBattleTerrain(): TerrainType[][] { ); } +function createTwentyEighthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 38 }, (_, y) => + Array.from({ length: 56 }, (_, x): TerrainType => { + if (x <= 4 && y >= 31) { + return 'camp'; + } + if ((x >= 46 && x <= 54 && y >= 13 && y <= 22) || (x >= 42 && x <= 48 && y >= 10 && y <= 15)) { + return 'fort'; + } + if ((x >= 17 && x <= 20 && y >= 28 && y <= 30) || (x >= 32 && x <= 34 && y >= 24 && y <= 26) || (x >= 39 && x <= 41 && y >= 18 && y <= 20)) { + return 'village'; + } + if ( + (y >= 33 && x >= 4 && x <= 15) || + (x >= 14 && x <= 25 && y >= 29 && y <= 33) || + (x >= 24 && x <= 34 && y >= 25 && y <= 29) || + (x >= 33 && x <= 42 && y >= 20 && y <= 25) || + (x >= 41 && x <= 50 && y >= 16 && y <= 20) || + (x >= 49 && x <= 53 && y >= 17 && y <= 19) + ) { + return 'road'; + } + if ((x === 12 || x === 13) && y >= 0 && y <= 27) { + return 'river'; + } + if ((x === 30 || x === 31) && y >= 2 && y <= 23) { + return 'river'; + } + if ((x === 38 || x === 39) && y >= 28 && y <= 37) { + return 'river'; + } + if ( + (x >= 4 && x <= 17 && y >= 8 && y <= 22) || + (x >= 18 && x <= 29 && y >= 11 && y <= 23) || + (x >= 31 && x <= 44 && y >= 28 && y <= 37) || + (x >= 43 && x <= 55 && y >= 24 && y <= 36) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 10 && y <= 12) || + (x >= 19 && x <= 29 && y <= 10) || + (x >= 31 && x <= 45 && y >= 3 && y <= 13) || + (x >= 48 && x <= 55 && y <= 12) || + (x >= 23 && x <= 33 && y >= 31 && y <= 37) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 30) || (x >= 54 && y >= 22) || (x >= 44 && x <= 46 && y <= 9)) { + 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 c05add4..d3f27dd 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -35,6 +35,7 @@ const unitTexture: Record = { 'huang-zhong': 'unit-rebel-archer', 'wei-yan': 'unit-guan-yu', 'pang-tong': 'unit-liu-bei', + 'fa-zheng': 'unit-liu-bei', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -336,7 +337,27 @@ const unitTexture: Record = { 'yizhou-strategist-b': 'unit-rebel-archer', 'yizhou-ambusher-a': 'unit-rebel', 'yizhou-ambusher-b': 'unit-rebel', - 'yizhou-leader-yang-huai': 'unit-rebel-leader' + 'yizhou-leader-yang-huai': 'unit-rebel-leader', + 'fupass-scout-a': 'unit-rebel', + 'fupass-scout-b': 'unit-rebel', + 'fupass-infantry-a': 'unit-rebel', + 'fupass-infantry-b': 'unit-rebel', + 'fupass-infantry-c': 'unit-rebel', + 'fupass-infantry-d': 'unit-rebel', + 'fupass-archer-a': 'unit-rebel-archer', + 'fupass-archer-b': 'unit-rebel-archer', + 'fupass-archer-c': 'unit-rebel-archer', + 'fupass-cavalry-a': 'unit-rebel-cavalry', + 'fupass-cavalry-b': 'unit-rebel-cavalry', + 'fupass-cavalry-c': 'unit-rebel-cavalry', + 'fupass-guard-a': 'unit-rebel', + 'fupass-guard-b': 'unit-rebel', + 'fupass-strategist-a': 'unit-rebel-archer', + 'fupass-strategist-b': 'unit-rebel-archer', + 'fupass-ambusher-a': 'unit-rebel', + 'fupass-ambusher-b': 'unit-rebel', + 'fupass-ambusher-c': 'unit-rebel', + 'fupass-leader-gao-pei': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -763,7 +784,8 @@ const unitStrategyIds: Record = { 'gong-zhi': ['encourage'], 'huang-zhong': ['encourage'], 'wei-yan': ['roar'], - 'pang-tong': ['aid', 'encourage', 'fireTactic'] + 'pang-tong': ['aid', 'encourage', 'fireTactic'], + 'fa-zheng': ['aid', 'encourage', 'fireTactic'] }; const initialItemStocks: Record> = { @@ -780,7 +802,8 @@ const initialItemStocks: Record> = { 'gong-zhi': { bean: 2 }, 'huang-zhong': { bean: 1, wine: 1 }, 'wei-yan': { bean: 2 }, - 'pang-tong': { bean: 1, salve: 1, wine: 1 } + 'pang-tong': { bean: 1, salve: 1, wine: 1 }, + 'fa-zheng': { bean: 1, salve: 1, wine: 1 } }; const attackRangeByClass: Partial> = { @@ -1134,7 +1157,27 @@ const enemyAiByUnitId: Record = { 'yizhou-strategist-b': 'hold', 'yizhou-ambusher-a': 'aggressive', 'yizhou-ambusher-b': 'aggressive', - 'yizhou-leader-yang-huai': 'guard' + 'yizhou-leader-yang-huai': 'guard', + 'fupass-scout-a': 'aggressive', + 'fupass-scout-b': 'aggressive', + 'fupass-infantry-a': 'guard', + 'fupass-infantry-b': 'guard', + 'fupass-infantry-c': 'guard', + 'fupass-infantry-d': 'guard', + 'fupass-archer-a': 'hold', + 'fupass-archer-b': 'hold', + 'fupass-archer-c': 'hold', + 'fupass-cavalry-a': 'aggressive', + 'fupass-cavalry-b': 'aggressive', + 'fupass-cavalry-c': 'aggressive', + 'fupass-guard-a': 'guard', + 'fupass-guard-b': 'guard', + 'fupass-strategist-a': 'hold', + 'fupass-strategist-b': 'hold', + 'fupass-ambusher-a': 'aggressive', + 'fupass-ambusher-b': 'aggressive', + 'fupass-ambusher-c': 'aggressive', + 'fupass-leader-gao-pei': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 8791aeb..295d9af 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -17,6 +17,7 @@ import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-batt 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 twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-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 twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg'; @@ -112,6 +113,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl); this.load.image('battle-map-twenty-sixth', twentySixthBattleMapUrl); this.load.image('battle-map-twenty-seventh', twentySeventhBattleMapUrl); + this.load.image('battle-map-twenty-eighth', twentyEighthBattleMapUrl); 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 007b0ea..581c50d 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -11,6 +11,8 @@ import { changshaRecruitUnits, firstBattleBonds, firstBattleUnits, + fuPassRecruitBonds, + fuPassRecruitUnits, guiyangRecruitBonds, guiyangRecruitUnits, jingzhouRecruitBonds, @@ -310,9 +312,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'twenty-fourth-battle-guiyang-persuasion', 'twenty-fifth-battle-wuling-mountain-road', 'twenty-sixth-battle-changsha-veteran', - 'twenty-seventh-battle-yizhou-relief-road' + 'twenty-seventh-battle-yizhou-relief-road', + 'twenty-eighth-battle-fu-pass-entry' ], - nextHints: ['형주 방위', '익주 진입', '방통 합류'] + nextHints: ['형주 방위', '익주 진입', '방통·법정 합류'] }, { id: 'shu-han', @@ -351,7 +354,8 @@ const campBattleIds = { twentyFourth: 'twenty-fourth-battle-guiyang-persuasion', twentyFifth: 'twenty-fifth-battle-wuling-mountain-road', twentySixth: 'twenty-sixth-battle-changsha-veteran', - twentySeventh: 'twenty-seventh-battle-yizhou-relief-road' + twentySeventh: 'twenty-seventh-battle-yizhou-relief-road', + twentyEighth: 'twenty-eighth-battle-fu-pass-entry' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -650,6 +654,18 @@ const sortieRulesByBattleId: Partial