From 0872c584b46e3e40aef3e1036d7f538f2665e5b0 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 10:09:41 +0900 Subject: [PATCH] Add Luofeng Slope ambush chapter --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 178 ++++- .../images/battle/thirtieth-battle-map.svg | 122 ++++ src/game/data/battles.ts | 73 +- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 648 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 50 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 164 ++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1250 insertions(+), 34 deletions(-) create mode 100644 src/assets/images/battle/thirtieth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 9cdb544..62bf2e5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -51,15 +51,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 - Twenty-ninth battle Luo Castle outer wall route, putting Fa Zheng into the sortie choice across a 58x40 outer-wall battlefield and recruiting Wu Yi after Zhang Ren withdraws +- Thirtieth battle Luofeng Slope ambush route, using Wu Yi's first sortie choice and Pang Tong survival pressure across a 60x42 ambush battlefield - 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-ninth 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 camp states, campaign timeline state, and camp save state +- Flow verification script from title through the thirtieth 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 camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue the Yi Province arc into Luofeng Slope and Luo Castle proper, with Pang Tong/Fa Zheng/Wu Yi planning pressure and harder officer selection +1. Continue the Yi Province arc into Luo Castle proper, with Pang Tong/Fa Zheng/Wu Yi planning pressure and a stronger mix of siege, persuasion, and officer-selection tradeoffs 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 46e391c..0271ca4 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 !== 29 || + progressTabState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postBowangProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postChangbanProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postWulingProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postChangshaProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postYizhouProgressState.campaignProgress?.totalKnown !== 30 || 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 !== 29 || + postFuPassProgressState.campaignProgress?.totalKnown !== 30 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3462,15 +3462,161 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 29 || + postLuoProgressState.campaignProgress?.totalKnown !== 30 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || - postLuoProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' ) { - throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and pause on Luofeng preparation: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and open Luofeng Slope ambush: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const luofengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !luofengSortieState?.sortieVisible || + !luofengSortieState.sortiePlan?.objectiveLine?.includes('낙봉파 매복전') || + !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || + !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || + !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || + !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || + luofengSortieState.sortieRoster?.length < 16 || + luofengSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected Luo camp sortie prep to target Luofeng with Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong recommended: ${JSON.stringify(luofengSortieState)}`); + } + assertSortieTacticalRoster(luofengSortieState, [ + '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' + ]); + + const luofengPriorityUnits = ['pang-tong', 'wu-yi', 'fa-zheng', 'zhao-yun', 'huang-zhong']; + for (const unitId of luofengPriorityUnits) { + 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' && !luofengPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const luofengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !luofengPriorityUnits.every((unitId) => + luofengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + luofengSortieReadyState.sortiePlan?.selectedCount !== 6 || + luofengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Luofeng sortie to deploy Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(luofengSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-luofeng-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-luofeng-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredThirtiethBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush'; + }); + if (enteredThirtiethBattle) { + 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 === 'thirtieth-battle-luofeng-ambush' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-thirtieth-battle.png', fullPage: true }); + + const thirtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const thirtiethEnemies = thirtiethBattleState.units.filter((unit) => unit.faction === 'enemy'); + const thirtiethAllies = thirtiethBattleState.units.filter((unit) => unit.faction === 'ally'); + const thirtiethEnemyBehaviors = new Set(thirtiethEnemies.map((unit) => unit.ai)); + if ( + thirtiethBattleState.camera?.mapWidth !== 60 || + thirtiethBattleState.camera?.mapHeight !== 42 || + thirtiethBattleState.victoryConditionLabel !== '장임 매복 격파' || + thirtiethEnemies.length < 23 || + !thirtiethEnemyBehaviors.has('aggressive') || + !thirtiethEnemyBehaviors.has('guard') || + !thirtiethEnemyBehaviors.has('hold') || + !thirtiethEnemies.some((unit) => unit.id === 'luofeng-leader-zhang-ren') || + !thirtiethAllies.some((unit) => unit.id === 'pang-tong') || + !thirtiethAllies.some((unit) => unit.id === 'wu-yi') || + !thirtiethAllies.some((unit) => unit.id === 'fa-zheng') + ) { + throw new Error(`Expected thirtieth battle to use Luofeng map, Zhang Ren ambush objective, selected allied officers, and mixed AI: ${JSON.stringify(thirtiethBattleState)}`); + } + + 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 thirtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + thirtiethCampState?.campBattleId !== 'thirtieth-battle-luofeng-ambush' || + thirtiethCampState.campTitle !== '낙봉파 매복전 후 군영' || + thirtiethCampState.availableDialogueIds?.length !== 3 || + !thirtiethCampState.availableDialogueIds.every((id) => id.endsWith('luofeng')) || + thirtiethCampState.availableVisitIds?.length !== 2 || + !thirtiethCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') || + thirtiethCampState.rosterCollection?.total < 16 + ) { + throw new Error(`Expected thirtieth camp to preserve Wu Yi and expose Luofeng dialogue/visit sets: ${JSON.stringify(thirtiethCampState)}`); + } + await page.screenshot({ path: 'dist/verification-luofeng-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postLuofengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postLuofengProgressState?.activeTab !== 'progress' || + postLuofengProgressState.campaignProgress?.completedKnown !== 30 || + postLuofengProgressState.campaignProgress?.totalKnown !== 30 || + postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || + postLuofengProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Luofeng progress tab to complete Luofeng and pause before Luo Castle proper: ${JSON.stringify(postLuofengProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-luofeng-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -3482,7 +3628,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-twenty-ninth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, and Wu Yi joins, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-thirtieth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi joins, Luofeng ambush, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/thirtieth-battle-map.svg b/src/assets/images/battle/thirtieth-battle-map.svg new file mode 100644 index 0000000..fb7a9b8 --- /dev/null +++ b/src/assets/images/battle/thirtieth-battle-map.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index b9baa0e..4fbac12 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -95,6 +95,10 @@ import { seventhBattleMap, seventhBattleUnits, seventhBattleVictoryPages, + thirtiethBattleBonds, + thirtiethBattleMap, + thirtiethBattleUnits, + thirtiethBattleVictoryPages, sixthBattleBonds, sixthBattleMap, sixthBattleUnits, @@ -150,7 +154,8 @@ export type BattleScenarioId = | 'twenty-sixth-battle-changsha-veteran' | 'twenty-seventh-battle-yizhou-relief-road' | 'twenty-eighth-battle-fu-pass-entry' - | 'twenty-ninth-battle-luo-outer-wall'; + | 'twenty-ninth-battle-luo-outer-wall' + | 'thirtieth-battle-luofeng-ambush'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1837,6 +1842,69 @@ export const twentyNinthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const thirtiethBattleScenario: BattleScenarioDefinition = { + id: 'thirtieth-battle-luofeng-ambush', + title: '낙봉파 매복전', + victoryConditionLabel: '장임 매복 격파', + defeatConditionLabel: '유비 또는 방통 퇴각', + openingObjectiveLines: [ + '낙성 본성으로 가는 빠른 길인 낙봉파에 장임의 매복이 숨어 있습니다. 좁은 길과 고지 궁병을 동시에 살피며 진군해야 합니다.', + '오의가 새로 출전 후보에 들어왔습니다. 낙성의 길을 아는 오의를 데려가면 매복 위치를 읽기 쉽지만, 여섯 명 제한은 더 빡빡해집니다.', + '유비와 방통을 지키며 장임의 매복을 격파하십시오. 낙봉파를 넘으면 낙성 본성 공략이 열립니다.' + ], + map: thirtiethBattleMap, + units: thirtiethBattleUnits, + bonds: thirtiethBattleBonds, + mapTextureKey: 'battle-map-thirtieth', + leaderUnitId: 'luofeng-leader-zhang-ren', + quickVictoryTurnLimit: 39, + baseVictoryGold: 4440, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '장임 매복 격파', + rewardGold: 2760, + unitId: 'luofeng-leader-zhang-ren' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 820, + unitId: 'liu-bei' + }, + { + id: 'pang-tong', + kind: 'keep-unit-alive', + label: '방통 생존', + rewardGold: 1180, + unitId: 'pang-tong' + }, + { + id: 'luofeng-village', + kind: 'secure-terrain', + label: '낙봉파 길목 확보', + rewardGold: 1320, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '39턴 이내 승리', + rewardGold: 1040, + maxTurn: 39 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'pang-tong' } + ], + itemRewards: ['콩 15', '상처약 11', '탁주 6', '낙봉파 정찰표 +1', '봉추 생환 +1'], + victoryPages: thirtiethBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1868,7 +1936,8 @@ export const battleScenarios: Record 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario, 'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario, 'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario, - 'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario + 'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario, + 'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index fa10402..8ab371a 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -17,6 +17,7 @@ import { seventeenthBattleScenario, tenthBattleScenario, thirteenthBattleScenario, + thirtiethBattleScenario, twentyEighthBattleScenario, twentyNinthBattleScenario, twentyFirstBattleScenario, @@ -65,6 +66,8 @@ import { tenthBattleVictoryPages, thirteenthBattleIntroPages, thirteenthBattleVictoryPages, + thirtiethBattleIntroPages, + thirtiethBattleVictoryPages, twentyEighthBattleIntroPages, twentyEighthBattleVictoryPages, twentyNinthBattleIntroPages, @@ -403,13 +406,24 @@ const sortieFlows: Record = { }, [twentyNinthBattleScenario.id]: { afterBattleId: twentyNinthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '낙봉파 진입 준비', + eyebrow: '다음 전장', + title: thirtiethBattleScenario.title, description: - '낙성 외곽을 넘고 오의가 합류했습니다. 장임은 더 안쪽으로 물러났고, 방통은 낙봉파라는 좁은 길의 불길한 그림자를 살피고 있습니다.', - rewardHint: '다음 장: 낙봉파 진입 준비 중', - pages: twentyNinthBattleVictoryPages, - unavailableNotice: '낙봉파와 낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 새로 합류한 오의를 포함해 출전 조합과 공명 이벤트를 준비하십시오.' + '낙성 외곽을 넘고 오의가 합류했습니다. 이제 낙봉파의 좁은 길을 통과해야 하며, 방통의 불길한 직감과 오의의 길 안내를 함께 살펴야 합니다.', + rewardHint: `예상 보상: ${thirtiethBattleScenario.title} 개방 / 봉추 생환`, + nextBattleId: thirtiethBattleScenario.id, + campaignStep: 'thirtieth-battle', + pages: [...twentyNinthBattleVictoryPages, ...thirtiethBattleIntroPages] + }, + [thirtiethBattleScenario.id]: { + afterBattleId: thirtiethBattleScenario.id, + eyebrow: '다음 장 준비', + title: '낙성 본성 공략 준비', + description: + '낙봉파의 매복을 무너뜨리고 방통을 살려 냈습니다. 장임은 낙성 본성으로 물러났고, 익주의 성문과 장수들의 마음을 함께 열어야 할 차례입니다.', + rewardHint: '다음 장: 낙성 본성 공략 준비 중', + pages: thirtiethBattleVictoryPages, + unavailableNotice: '낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 방통, 법정, 오의의 정보와 출전 조합을 더 다듬어 두십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 55e5597..049be30 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1890,6 +1890,68 @@ export const twentyNinthBattleVictoryPages: StoryPage[] = [ } ]; +export const thirtiethBattleIntroPages: StoryPage[] = [ + { + id: 'thirtieth-luofeng-shadow', + bgm: 'story-dark', + chapter: '낙봉파의 좁은 길', + background: 'story-three-heroes', + speaker: '오의', + text: '낙성 안쪽으로 들어가는 빠른 길은 낙봉파입니다. 길은 짧지만 양쪽 산세가 높고, 장임이 기다리기에는 지나치게 좋은 곳입니다.' + }, + { + id: 'thirtieth-pang-tong-warning', + bgm: 'battle-prep', + chapter: '봉추의 불길한 직감', + background: 'story-liu-bei', + speaker: '방통', + text: '좋은 길이 언제나 좋은 길은 아닙니다. 적이 우리를 빠르게 오게 만들었다면, 빠르게 물러날 길도 함께 보아 두어야 합니다.' + }, + { + id: 'thirtieth-roster-pressure', + bgm: 'battle-prep', + chapter: '오의의 첫 출전 선택', + background: 'story-militia', + speaker: '법정', + text: '이번 전투부터 오의도 출전 후보입니다. 낙성의 길을 아는 오의를 데려가면 매복을 읽기 쉬우나, 전열과 보급 후보는 더 좁아집니다.' + }, + { + id: 'thirtieth-luofeng-sortie', + bgm: 'battle-prep', + chapter: '낙봉파 매복전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '빠른 길을 택하되 방심하지 않겠소. 방통의 눈, 법정의 정보, 오의의 길 안내를 함께 엮어 장임의 매복을 거꾸로 무너뜨리겠소.' + } +]; + +export const thirtiethBattleVictoryPages: StoryPage[] = [ + { + id: 'thirtieth-victory-ambush-broken', + bgm: 'militia-theme', + chapter: '매복을 꿰뚫다', + background: 'story-sortie', + text: '장임의 매복은 거칠게 덮쳐 왔으나, 유비군은 길목을 버티며 역으로 활로를 열었습니다. 낙봉파의 좁은 비탈에는 무너진 방책과 흩어진 깃발만 남았습니다.' + }, + { + id: 'thirtieth-pang-tong-survives', + bgm: 'battle-prep', + chapter: '봉추의 생환', + background: 'story-liu-bei', + speaker: '방통', + text: '하마터면 길의 이름값을 할 뻔했습니다. 그러나 이번에는 우리 쪽이 한 수 먼저 보았습니다. 이제 낙성 본성은 더 이상 멀지 않습니다.' + }, + { + id: 'thirtieth-next-luo-castle', + bgm: 'story-dark', + chapter: '낙성 본성 앞', + background: 'story-three-heroes', + speaker: '제갈량', + text: '낙봉파를 넘었으니 장임의 방어선은 본성에 몰렸습니다. 다음 전장은 낙성의 문과 익주 장수들의 마음을 함께 여는 싸움이 될 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2209,6 +2271,12 @@ export const twentyNinthBattleMap: BattleMap = { terrain: createTwentyNinthBattleTerrain() }; +export const thirtiethBattleMap: BattleMap = { + width: 60, + height: 42, + terrain: createThirtiethBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -12195,6 +12263,525 @@ export const twentyNinthBattleUnits: UnitData[] = [ } ]; +const thirtiethBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 36 }, + 'guan-yu': { x: 5, y: 34 }, + 'zhang-fei': { x: 4, y: 38 }, + 'jian-yong': { x: 3, y: 39 }, + 'mi-zhu': { x: 5, y: 40 }, + 'sun-qian': { x: 7, y: 39 }, + 'zhao-yun': { x: 8, y: 36 }, + 'zhuge-liang': { x: 6, y: 35 }, + 'ma-liang': { x: 8, y: 40 }, + 'yi-ji': { x: 9, y: 38 }, + 'gong-zhi': { x: 10, y: 40 }, + 'huang-zhong': { x: 7, y: 37 }, + 'wei-yan': { x: 10, y: 35 }, + 'pang-tong': { x: 9, y: 36 }, + 'fa-zheng': { x: 11, y: 38 }, + 'wu-yi': { x: 12, y: 36 } +}; + +export const thirtiethBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits + ].map((unit) => placeScenarioUnit(unit, thirtiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'luofeng-scout-a', + name: '낙봉 척후', + faction: 'enemy', + className: '비탈 척후', + classKey: 'bandit', + level: 50, + exp: 82, + hp: 132, + maxHp: 132, + attack: 50, + move: 4, + stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 18, + y: 35 + }, + { + id: 'luofeng-scout-b', + name: '낙봉 척후', + faction: 'enemy', + className: '동쪽 척후', + classKey: 'bandit', + level: 50, + exp: 82, + hp: 132, + maxHp: 132, + attack: 50, + move: 4, + stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 22, + y: 33 + }, + { + id: 'luofeng-scout-c', + name: '낙봉 척후', + faction: 'enemy', + className: '북쪽 척후', + classKey: 'bandit', + level: 50, + exp: 82, + hp: 132, + maxHp: 132, + attack: 50, + move: 4, + stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 19, + y: 28 + }, + { + id: 'luofeng-infantry-a', + name: '낙봉 보병', + faction: 'enemy', + className: '비탈 보병', + classKey: 'yellowTurban', + level: 51, + exp: 84, + hp: 184, + maxHp: 184, + attack: 58, + move: 3, + stats: { might: 170, intelligence: 108, leadership: 148, agility: 114, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 62 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 28, + y: 31 + }, + { + id: 'luofeng-infantry-b', + name: '낙봉 보병', + faction: 'enemy', + className: '협로 보병', + classKey: 'yellowTurban', + level: 51, + exp: 84, + hp: 184, + maxHp: 184, + attack: 58, + move: 3, + stats: { might: 170, intelligence: 108, leadership: 148, agility: 114, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 62 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 34, + y: 28 + }, + { + id: 'luofeng-infantry-c', + name: '낙봉 보병', + faction: 'enemy', + className: '산문 보병', + classKey: 'yellowTurban', + level: 52, + exp: 86, + hp: 192, + maxHp: 192, + attack: 60, + move: 3, + stats: { might: 174, intelligence: 110, leadership: 152, agility: 116, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 70 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 56 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 40, + y: 24 + }, + { + id: 'luofeng-infantry-d', + name: '낙봉 보병', + faction: 'enemy', + className: '후미 보병', + classKey: 'yellowTurban', + level: 52, + exp: 86, + hp: 192, + maxHp: 192, + attack: 60, + move: 3, + stats: { might: 174, intelligence: 110, leadership: 152, agility: 116, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 70 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 56 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 46, + y: 20 + }, + { + id: 'luofeng-archer-a', + name: '낙봉 궁병', + faction: 'enemy', + className: '매복 궁병', + classKey: 'archer', + level: 51, + exp: 84, + hp: 132, + maxHp: 132, + attack: 51, + move: 3, + stats: { might: 118, intelligence: 120, leadership: 126, agility: 132, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 48 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 32 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 31, + y: 27 + }, + { + id: 'luofeng-archer-b', + name: '낙봉 궁병', + faction: 'enemy', + className: '고지 궁병', + classKey: 'archer', + level: 51, + exp: 84, + hp: 132, + maxHp: 132, + attack: 51, + move: 3, + stats: { might: 118, intelligence: 120, leadership: 126, agility: 132, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 48 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 32 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 39, + y: 22 + }, + { + id: 'luofeng-archer-c', + name: '낙봉 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 52, + exp: 86, + hp: 136, + maxHp: 136, + attack: 52, + move: 3, + stats: { might: 120, intelligence: 122, leadership: 128, agility: 134, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 56 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 40 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 47, + y: 17 + }, + { + id: 'luofeng-archer-d', + name: '낙봉 궁병', + faction: 'enemy', + className: '후방 궁병', + classKey: 'archer', + level: 52, + exp: 86, + hp: 136, + maxHp: 136, + attack: 52, + move: 3, + stats: { might: 120, intelligence: 122, leadership: 128, agility: 134, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 56 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 40 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 52, + y: 24 + }, + { + id: 'luofeng-cavalry-a', + name: '낙봉 기병', + faction: 'enemy', + className: '협곡 기병', + classKey: 'cavalry', + level: 52, + exp: 86, + hp: 194, + maxHp: 194, + attack: 61, + move: 5, + stats: { might: 176, intelligence: 110, leadership: 148, agility: 138, luck: 92 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 66 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 52 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 38 + }, + { + id: 'luofeng-cavalry-b', + name: '낙봉 기병', + faction: 'enemy', + className: '동쪽 기병', + classKey: 'cavalry', + level: 52, + exp: 86, + hp: 194, + maxHp: 194, + attack: 61, + move: 5, + stats: { might: 176, intelligence: 110, leadership: 148, agility: 138, luck: 92 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 66 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 52 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 42, + y: 31 + }, + { + id: 'luofeng-cavalry-c', + name: '낙봉 기병', + faction: 'enemy', + className: '후방 기병', + classKey: 'cavalry', + level: 53, + exp: 88, + hp: 202, + maxHp: 202, + attack: 63, + move: 5, + stats: { might: 180, intelligence: 112, leadership: 152, agility: 140, luck: 92 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 74 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 60 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 51, + y: 29 + }, + { + id: 'luofeng-guard-a', + name: '장임 친위대', + faction: 'enemy', + className: '낙봉 친위대', + classKey: 'yellowTurban', + level: 53, + exp: 88, + hp: 204, + maxHp: 204, + attack: 63, + move: 3, + stats: { might: 182, intelligence: 112, leadership: 158, agility: 118, luck: 92 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 78 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 64 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 53, + y: 18 + }, + { + id: 'luofeng-guard-b', + name: '장임 친위대', + faction: 'enemy', + className: '성문 친위대', + classKey: 'yellowTurban', + level: 53, + exp: 88, + hp: 204, + maxHp: 204, + attack: 63, + move: 3, + stats: { might: 182, intelligence: 112, leadership: 158, agility: 118, luck: 92 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 78 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 64 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 55, + y: 22 + }, + { + id: 'luofeng-strategist-a', + name: '낙봉 책사', + faction: 'enemy', + className: '매복 책사', + classKey: 'archer', + level: 52, + exp: 86, + hp: 136, + maxHp: 136, + attack: 52, + move: 3, + stats: { might: 98, intelligence: 136, leadership: 126, agility: 106, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 58 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 48, + y: 15 + }, + { + id: 'luofeng-strategist-b', + name: '낙봉 책사', + faction: 'enemy', + className: '후방 책사', + classKey: 'archer', + level: 52, + exp: 86, + hp: 136, + maxHp: 136, + attack: 52, + move: 3, + stats: { might: 98, intelligence: 136, leadership: 126, agility: 106, luck: 92 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 58 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 55, + y: 26 + }, + { + id: 'luofeng-ambusher-a', + name: '낙봉 복병', + faction: 'enemy', + className: '남쪽 복병', + classKey: 'bandit', + level: 51, + exp: 84, + hp: 140, + maxHp: 140, + attack: 52, + move: 4, + stats: { might: 154, intelligence: 108, leadership: 124, agility: 140, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 50 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 36 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 30, + y: 36 + }, + { + id: 'luofeng-ambusher-b', + name: '낙봉 복병', + faction: 'enemy', + className: '북쪽 복병', + classKey: 'bandit', + level: 51, + exp: 84, + hp: 140, + maxHp: 140, + attack: 52, + move: 4, + stats: { might: 154, intelligence: 108, leadership: 124, agility: 140, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 50 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 36 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 35, + y: 15 + }, + { + id: 'luofeng-ambusher-c', + name: '낙봉 복병', + faction: 'enemy', + className: '동쪽 복병', + classKey: 'bandit', + level: 52, + exp: 86, + hp: 146, + maxHp: 146, + attack: 54, + move: 4, + stats: { might: 158, intelligence: 110, leadership: 126, agility: 142, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 58 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 44 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 44, + y: 34 + }, + { + id: 'luofeng-ambusher-d', + name: '낙봉 복병', + faction: 'enemy', + className: '절벽 복병', + classKey: 'bandit', + level: 52, + exp: 86, + hp: 146, + maxHp: 146, + attack: 54, + move: 4, + stats: { might: 158, intelligence: 110, leadership: 126, agility: 142, luck: 90 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 58 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 44 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 50, + y: 10 + }, + { + id: 'luofeng-leader-zhang-ren', + name: '장임', + faction: 'enemy', + className: '낙봉파 주장', + classKey: 'rebelLeader', + level: 54, + exp: 90, + hp: 256, + maxHp: 256, + attack: 66, + move: 4, + stats: { might: 184, intelligence: 124, leadership: 162, agility: 120, luck: 94 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 5, exp: 66 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 66 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 56, + y: 20 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -12553,6 +13140,7 @@ export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds, ...changshaRecruitBonds].map(cloneBattleBondForScenario); export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBonds, ...yizhouRecruitBonds].map(cloneBattleBondForScenario); export const twentyNinthBattleBonds: BattleBond[] = [...twentyEighthBattleBonds, ...fuPassRecruitBonds].map(cloneBattleBondForScenario); +export const thirtiethBattleBonds: BattleBond[] = [...twentyNinthBattleBonds, ...luoCastleRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -13690,6 +14278,66 @@ function createTwentyNinthBattleTerrain(): TerrainType[][] { ); } +function createThirtiethBattleTerrain(): TerrainType[][] { + return Array.from({ length: 42 }, (_, y) => + Array.from({ length: 60 }, (_, x): TerrainType => { + if (x <= 4 && y >= 35) { + return 'camp'; + } + if ((x >= 50 && x <= 58 && y >= 14 && y <= 24) || (x >= 46 && x <= 52 && y >= 10 && y <= 16)) { + return 'fort'; + } + if ( + (x >= 17 && x <= 20 && y >= 33 && y <= 35) || + (x >= 36 && x <= 38 && y >= 26 && y <= 28) || + (x >= 43 && x <= 45 && y >= 20 && y <= 22) + ) { + return 'village'; + } + if ( + (y >= 37 && x >= 4 && x <= 17) || + (x >= 16 && x <= 29 && y >= 33 && y <= 37) || + (x >= 28 && x <= 39 && y >= 28 && y <= 33) || + (x >= 38 && x <= 48 && y >= 23 && y <= 28) || + (x >= 47 && x <= 56 && y >= 18 && y <= 23) || + (x >= 54 && x <= 58 && y >= 19 && y <= 21) + ) { + return 'road'; + } + if ((x === 12 || x === 13) && y >= 0 && y <= 31) { + return 'river'; + } + if ((x === 32 || x === 33) && y >= 2 && y <= 26) { + return 'river'; + } + if ((x === 42 || x === 43) && y >= 31 && y <= 41) { + return 'river'; + } + if ( + (x >= 4 && x <= 19 && y >= 8 && y <= 26) || + (x >= 20 && x <= 31 && y >= 13 && y <= 27) || + (x >= 34 && x <= 48 && y >= 31 && y <= 41) || + (x >= 46 && x <= 59 && y >= 26 && y <= 40) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 10 && y <= 14) || + (x >= 20 && x <= 31 && y <= 12) || + (x >= 33 && x <= 49 && y >= 3 && y <= 15) || + (x >= 51 && x <= 59 && y <= 13) || + (x >= 24 && x <= 35 && y >= 34 && y <= 41) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 34) || (x >= 58 && y >= 24) || (x >= 46 && x <= 49 && y <= 10)) { + 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 5c8a24e..656ad8e 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -378,7 +378,30 @@ const unitTexture: Record = { 'luo-ambusher-a': 'unit-rebel', 'luo-ambusher-b': 'unit-rebel', 'luo-officer-wu-yi': 'unit-guan-yu', - 'luo-leader-zhang-ren': 'unit-rebel-leader' + 'luo-leader-zhang-ren': 'unit-rebel-leader', + 'luofeng-scout-a': 'unit-rebel', + 'luofeng-scout-b': 'unit-rebel', + 'luofeng-scout-c': 'unit-rebel', + 'luofeng-infantry-a': 'unit-rebel', + 'luofeng-infantry-b': 'unit-rebel', + 'luofeng-infantry-c': 'unit-rebel', + 'luofeng-infantry-d': 'unit-rebel', + 'luofeng-archer-a': 'unit-rebel-archer', + 'luofeng-archer-b': 'unit-rebel-archer', + 'luofeng-archer-c': 'unit-rebel-archer', + 'luofeng-archer-d': 'unit-rebel-archer', + 'luofeng-cavalry-a': 'unit-rebel-cavalry', + 'luofeng-cavalry-b': 'unit-rebel-cavalry', + 'luofeng-cavalry-c': 'unit-rebel-cavalry', + 'luofeng-guard-a': 'unit-rebel', + 'luofeng-guard-b': 'unit-rebel', + 'luofeng-strategist-a': 'unit-rebel-archer', + 'luofeng-strategist-b': 'unit-rebel-archer', + 'luofeng-ambusher-a': 'unit-rebel', + 'luofeng-ambusher-b': 'unit-rebel', + 'luofeng-ambusher-c': 'unit-rebel', + 'luofeng-ambusher-d': 'unit-rebel', + 'luofeng-leader-zhang-ren': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1220,7 +1243,30 @@ const enemyAiByUnitId: Record = { 'luo-ambusher-a': 'aggressive', 'luo-ambusher-b': 'aggressive', 'luo-officer-wu-yi': 'guard', - 'luo-leader-zhang-ren': 'guard' + 'luo-leader-zhang-ren': 'guard', + 'luofeng-scout-a': 'aggressive', + 'luofeng-scout-b': 'aggressive', + 'luofeng-scout-c': 'aggressive', + 'luofeng-infantry-a': 'guard', + 'luofeng-infantry-b': 'guard', + 'luofeng-infantry-c': 'guard', + 'luofeng-infantry-d': 'guard', + 'luofeng-archer-a': 'hold', + 'luofeng-archer-b': 'hold', + 'luofeng-archer-c': 'hold', + 'luofeng-archer-d': 'hold', + 'luofeng-cavalry-a': 'aggressive', + 'luofeng-cavalry-b': 'aggressive', + 'luofeng-cavalry-c': 'aggressive', + 'luofeng-guard-a': 'guard', + 'luofeng-guard-b': 'guard', + 'luofeng-strategist-a': 'hold', + 'luofeng-strategist-b': 'hold', + 'luofeng-ambusher-a': 'aggressive', + 'luofeng-ambusher-b': 'aggressive', + 'luofeng-ambusher-c': 'aggressive', + 'luofeng-ambusher-d': 'aggressive', + 'luofeng-leader-zhang-ren': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index f8bcfd3..8b09c3e 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -16,6 +16,7 @@ import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-m import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg'; import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg'; import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-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'; import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg'; @@ -116,6 +117,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-twenty-seventh', twentySeventhBattleMapUrl); this.load.image('battle-map-twenty-eighth', twentyEighthBattleMapUrl); this.load.image('battle-map-twenty-ninth', twentyNinthBattleMapUrl); + this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl); 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 67376ab..ff44eae 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -316,7 +316,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'twenty-sixth-battle-changsha-veteran', 'twenty-seventh-battle-yizhou-relief-road', 'twenty-eighth-battle-fu-pass-entry', - 'twenty-ninth-battle-luo-outer-wall' + 'twenty-ninth-battle-luo-outer-wall', + 'thirtieth-battle-luofeng-ambush' ], nextHints: ['형주 방위', '익주 진입', '방통·법정 합류'] }, @@ -359,7 +360,8 @@ const campBattleIds = { twentySixth: 'twenty-sixth-battle-changsha-veteran', twentySeventh: 'twenty-seventh-battle-yizhou-relief-road', twentyEighth: 'twenty-eighth-battle-fu-pass-entry', - twentyNinth: 'twenty-ninth-battle-luo-outer-wall' + twentyNinth: 'twenty-ninth-battle-luo-outer-wall', + thirtieth: 'thirtieth-battle-luofeng-ambush' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -682,6 +684,18 @@ const sortieRulesByBattleId: Partial