From cc6365d5ba98649652489a2e5b553de436f19a30 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 09:43:18 +0900 Subject: [PATCH] Add Luo Castle outer wall chapter --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 179 ++++- .../images/battle/twenty-ninth-battle-map.svg | 121 ++++ src/game/data/battles.ts | 73 +- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 634 ++++++++++++++++++ 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, 1270 insertions(+), 34 deletions(-) create mode 100644 src/assets/images/battle/twenty-ninth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index ec270db..9cdb544 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -50,15 +50,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 +- 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 - 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-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 +- 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 ## Next Steps -1. Continue the Yi Province arc into Luo Castle, with Pang Tong/Fa Zheng planning pressure and harder officer selection +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 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 559aee0..46e391c 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 !== 28 || + progressTabState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postBowangProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postChangbanProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postWulingProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postChangshaProgressState.campaignProgress?.totalKnown !== 29 || 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 !== 28 || + postYizhouProgressState.campaignProgress?.totalKnown !== 29 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' @@ -3313,15 +3313,164 @@ try { if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || - postFuPassProgressState.campaignProgress?.totalKnown !== 28 || + postFuPassProgressState.campaignProgress?.totalKnown !== 29 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || - postFuPassProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + 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)}`); + throw new Error(`Expected post-Fu Pass progress tab to complete the pass-entry battle and open Luo Castle outer wall: ${JSON.stringify(postFuPassProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fupass-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const luoSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !luoSortieState?.sortieVisible || + !luoSortieState.sortiePlan?.objectiveLine?.includes('낙성 외곽전') || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + luoSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi') || + luoSortieState.sortieRoster?.length < 15 || + luoSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-eighth camp sortie prep to target Luo Castle with Fa Zheng selectable and Wu Yi absent before victory: ${JSON.stringify(luoSortieState)}`); + } + assertSortieTacticalRoster(luoSortieState, [ + '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' + ]); + + const luoPriorityUnits = ['fa-zheng', 'pang-tong', 'zhuge-liang', 'huang-zhong', 'zhao-yun']; + for (const unitId of luoPriorityUnits) { + 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' && !luoPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const luoSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !luoPriorityUnits.every((unitId) => + luoSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + luoSortieReadyState.sortiePlan?.selectedCount !== 6 || + luoSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Luo Castle sortie to deploy Fa Zheng, Pang Tong, Zhuge Liang, Huang Zhong, and Zhao Yun while preserving six-officer pressure: ${JSON.stringify(luoSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-luo-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-luo-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentyNinthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-ninth-battle-luo-outer-wall'; + }); + if (enteredTwentyNinthBattle) { + 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-ninth-battle-luo-outer-wall' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-ninth-battle.png', fullPage: true }); + + const twentyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentyNinthEnemies = twentyNinthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentyNinthAllies = twentyNinthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentyNinthEnemyBehaviors = new Set(twentyNinthEnemies.map((unit) => unit.ai)); + if ( + twentyNinthBattleState.camera?.mapWidth !== 58 || + twentyNinthBattleState.camera?.mapHeight !== 40 || + twentyNinthBattleState.victoryConditionLabel !== '장임 퇴각' || + twentyNinthEnemies.length < 20 || + !twentyNinthEnemyBehaviors.has('aggressive') || + !twentyNinthEnemyBehaviors.has('guard') || + !twentyNinthEnemyBehaviors.has('hold') || + !twentyNinthEnemies.some((unit) => unit.id === 'luo-leader-zhang-ren') || + !twentyNinthEnemies.some((unit) => unit.id === 'luo-officer-wu-yi') || + !twentyNinthAllies.some((unit) => unit.id === 'fa-zheng') || + !twentyNinthAllies.some((unit) => unit.id === 'pang-tong') || + !twentyNinthAllies.some((unit) => unit.id === 'zhuge-liang') || + twentyNinthAllies.some((unit) => unit.id === 'wu-yi') + ) { + throw new Error(`Expected twenty-ninth battle to use Luo Castle map, Zhang Ren objective, Wu Yi as enemy officer, selected allied officers, mixed AI, and no Wu Yi before victory: ${JSON.stringify(twentyNinthBattleState)}`); + } + + 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 twentyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentyNinthCampState?.campBattleId !== 'twenty-ninth-battle-luo-outer-wall' || + twentyNinthCampState.campTitle !== '낙성 외곽전 후 군영' || + twentyNinthCampState.availableDialogueIds?.length !== 3 || + !twentyNinthCampState.availableDialogueIds.every((id) => id.endsWith('luo')) || + twentyNinthCampState.availableVisitIds?.length !== 2 || + !twentyNinthCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') || + !twentyNinthCampState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited) || + twentyNinthCampState.rosterCollection?.total < 16 + ) { + throw new Error(`Expected twenty-ninth camp to recruit Wu Yi and expose Luo dialogue/visit sets: ${JSON.stringify(twentyNinthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-luo-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postLuoProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postLuoProgressState?.activeTab !== 'progress' || + postLuoProgressState.campaignProgress?.completedKnown !== 29 || + postLuoProgressState.campaignProgress?.totalKnown !== 29 || + postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -3333,7 +3482,7 @@ try { return activeScenes.includes('CampScene'); }); - 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}`); + 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}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twenty-ninth-battle-map.svg b/src/assets/images/battle/twenty-ninth-battle-map.svg new file mode 100644 index 0000000..9104673 --- /dev/null +++ b/src/assets/images/battle/twenty-ninth-battle-map.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 93ce3d0..b9baa0e 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -35,6 +35,10 @@ import { twentyEighthBattleMap, twentyEighthBattleUnits, twentyEighthBattleVictoryPages, + twentyNinthBattleBonds, + twentyNinthBattleMap, + twentyNinthBattleUnits, + twentyNinthBattleVictoryPages, tenthBattleBonds, tenthBattleMap, tenthBattleUnits, @@ -145,7 +149,8 @@ export type BattleScenarioId = | 'twenty-fifth-battle-wuling-mountain-road' | 'twenty-sixth-battle-changsha-veteran' | 'twenty-seventh-battle-yizhou-relief-road' - | 'twenty-eighth-battle-fu-pass-entry'; + | 'twenty-eighth-battle-fu-pass-entry' + | 'twenty-ninth-battle-luo-outer-wall'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1769,6 +1774,69 @@ export const twentyEighthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentyNinthBattleScenario: BattleScenarioDefinition = { + id: 'twenty-ninth-battle-luo-outer-wall', + title: '낙성 외곽전', + victoryConditionLabel: '장임 퇴각', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '부수관을 넘어선 유비군은 낙성 외곽 방어선에 도착했습니다. 장임은 성 밖 마을과 고개를 이용해 시간을 벌고 있습니다.', + '이번 전투부터 법정도 출전 후보입니다. 익주 내부 사정을 아는 법정을 데려가면 오의와 성문 장수들을 설득할 실마리가 커집니다.', + '유비와 제갈량을 지키며 장임을 물러나게 하십시오. 마을을 안정시키면 오의가 낙성으로 향하는 길을 열 수 있습니다.' + ], + map: twentyNinthBattleMap, + units: twentyNinthBattleUnits, + bonds: twentyNinthBattleBonds, + mapTextureKey: 'battle-map-twenty-ninth', + leaderUnitId: 'luo-leader-zhang-ren', + quickVictoryTurnLimit: 38, + baseVictoryGold: 4200, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '장임 퇴각', + rewardGold: 2640, + unitId: 'luo-leader-zhang-ren' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 780, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 1080, + unitId: 'zhuge-liang' + }, + { + id: 'luo-village', + kind: 'secure-terrain', + label: '낙성 외곽 마을 안정', + rewardGold: 1280, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '38턴 이내 승리', + rewardGold: 1000, + maxTurn: 38 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 14', '상처약 10', '탁주 6', '낙성 외곽 지도 +1', '오의 합류'], + victoryPages: twentyNinthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1799,7 +1867,8 @@ export const battleScenarios: Record 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario, 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario, 'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario, - 'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario + 'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario, + 'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 74f8ef7..fa10402 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -18,6 +18,7 @@ import { tenthBattleScenario, thirteenthBattleScenario, twentyEighthBattleScenario, + twentyNinthBattleScenario, twentyFirstBattleScenario, twentySecondBattleScenario, twentyThirdBattleScenario, @@ -66,6 +67,8 @@ import { thirteenthBattleVictoryPages, twentyEighthBattleIntroPages, twentyEighthBattleVictoryPages, + twentyNinthBattleIntroPages, + twentyNinthBattleVictoryPages, twentyFirstBattleIntroPages, twentyFirstBattleVictoryPages, twentySecondBattleIntroPages, @@ -389,13 +392,24 @@ const sortieFlows: Record = { }, [twentyEighthBattleScenario.id]: { afterBattleId: twentyEighthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '낙성 진입 준비', + eyebrow: '다음 전장', + title: twentyNinthBattleScenario.title, description: - '부수관이 열리고 법정이 합류했습니다. 이제 유비군은 익주 안쪽의 마음과 길을 동시에 읽으며 낙성으로 이어지는 더 어려운 전장을 준비해야 합니다.', - rewardHint: '다음 장: 낙성 진입 준비 중', - pages: twentyEighthBattleVictoryPages, - unavailableNotice: '낙성 이후 익주 심부 장은 다음 작업에서 이어집니다. 군영에서 법정과 방통의 공명도를 올리고 다음 관문을 준비하십시오.' + '부수관이 열리고 법정이 합류했습니다. 이제 낙성 외곽으로 들어가 장임의 방어선과 오의의 흔들리는 마음을 동시에 상대해야 합니다.', + rewardHint: `예상 보상: ${twentyNinthBattleScenario.title} 개방 / 오의 합류`, + nextBattleId: twentyNinthBattleScenario.id, + campaignStep: 'twenty-ninth-battle', + pages: [...twentyEighthBattleVictoryPages, ...twentyNinthBattleIntroPages] + }, + [twentyNinthBattleScenario.id]: { + afterBattleId: twentyNinthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '낙봉파 진입 준비', + description: + '낙성 외곽을 넘고 오의가 합류했습니다. 장임은 더 안쪽으로 물러났고, 방통은 낙봉파라는 좁은 길의 불길한 그림자를 살피고 있습니다.', + rewardHint: '다음 장: 낙봉파 진입 준비 중', + pages: twentyNinthBattleVictoryPages, + unavailableNotice: '낙봉파와 낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 새로 합류한 오의를 포함해 출전 조합과 공명 이벤트를 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index a49194b..55e5597 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1828,6 +1828,68 @@ export const twentyEighthBattleVictoryPages: StoryPage[] = [ } ]; +export const twentyNinthBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-ninth-luo-outer-wall', + bgm: 'story-dark', + chapter: '낙성의 바깥 성벽', + background: 'story-three-heroes', + speaker: '법정', + text: '부수관을 넘으면 낙성의 바깥 성벽이 기다립니다. 장임은 성 밖의 고개와 마을을 겹쳐 방어선을 세웠고, 성 안의 장수들은 아직 우리 뜻을 믿지 않습니다.' + }, + { + id: 'twenty-ninth-wu-yi-rumor', + bgm: 'battle-prep', + chapter: '흔들리는 오의', + background: 'story-liu-bei', + speaker: '방통', + text: '오의는 성문을 지키지만 백성을 함부로 몰아붙이는 명령을 달가워하지 않는다고 합니다. 전장을 거칠게 밀지 않고 길을 열면, 그의 마음도 움직일 수 있습니다.' + }, + { + id: 'twenty-ninth-roster-pressure', + bgm: 'battle-prep', + chapter: '법정의 첫 출전 선택', + background: 'story-militia', + speaker: '제갈량', + text: '이번 전투부터 법정도 출전 후보입니다. 익주 내부 사정을 아는 법정을 데려가면 설득은 쉬워지지만, 무장을 줄이는 부담은 더 커집니다.' + }, + { + id: 'twenty-ninth-luo-sortie', + bgm: 'battle-prep', + chapter: '낙성 외곽전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '낙성의 문을 여는 일은 칼보다 마음이 먼저요. 마을을 지키고 장임의 전열을 물리쳐, 성 안 사람들에게 우리가 어떤 군인지 보이겠소.' + } +]; + +export const twentyNinthBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-ninth-victory-outer-wall', + bgm: 'militia-theme', + chapter: '외곽 방어선 붕괴', + background: 'story-sortie', + text: '장임은 낙성 안쪽으로 물러났고, 외곽 마을은 큰 피해 없이 유비군의 보호 아래 들어왔습니다. 성 안의 장수들은 유비군을 단순한 침입군으로만 볼 수 없게 되었습니다.' + }, + { + id: 'twenty-ninth-wu-yi-joins', + bgm: 'battle-prep', + chapter: '오의의 합류', + background: 'story-liu-bei', + speaker: '오의', + text: '성문을 지킨다는 명분으로 백성을 몰아넣는 일은 장수의 길이 아니었습니다. 주공께서 마을을 먼저 살피셨으니, 이 오의도 낙성의 다음 길을 열겠습니다.' + }, + { + id: 'twenty-ninth-next-luofeng', + bgm: 'story-dark', + chapter: '낙봉파의 그늘', + background: 'story-three-heroes', + speaker: '방통', + text: '낙성 바깥은 열렸지만 길은 더 좁아집니다. 봉황이 떨어진다는 이름의 고개가 있다 하니, 다음 진군은 가볍게 볼 수 없습니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2141,6 +2203,12 @@ export const twentyEighthBattleMap: BattleMap = { terrain: createTwentyEighthBattleTerrain() }; +export const twentyNinthBattleMap: BattleMap = { + width: 58, + height: 40, + terrain: createTwentyNinthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4262,6 +4330,30 @@ export const fuPassRecruitUnits: UnitData[] = [ } ]; +export const luoCastleRecruitUnits: UnitData[] = [ + { + id: 'wu-yi', + name: '오의', + faction: 'ally', + className: '익주 장군', + classKey: 'infantry', + level: 18, + exp: 0, + hp: 46, + maxHp: 46, + attack: 15, + move: 4, + stats: { might: 82, intelligence: 72, leadership: 86, agility: 74, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 16 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 18 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 9, + y: 26 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -11649,6 +11741,460 @@ export const twentyEighthBattleUnits: UnitData[] = [ } ]; +const twentyNinthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 34 }, + 'guan-yu': { x: 6, y: 33 }, + 'zhang-fei': { x: 5, y: 36 }, + 'jian-yong': { x: 3, y: 37 }, + 'mi-zhu': { x: 4, y: 38 }, + 'sun-qian': { x: 7, y: 37 }, + 'zhao-yun': { x: 8, y: 34 }, + 'zhuge-liang': { x: 5, y: 34 }, + 'ma-liang': { x: 8, y: 38 }, + 'yi-ji': { x: 9, y: 36 }, + 'gong-zhi': { x: 10, y: 38 }, + 'huang-zhong': { x: 7, y: 35 }, + 'wei-yan': { x: 10, y: 33 }, + 'pang-tong': { x: 9, y: 35 }, + 'fa-zheng': { x: 11, y: 36 } +}; + +export const twentyNinthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentyNinthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'luo-scout-a', + name: '낙성 척후', + faction: 'enemy', + className: '외곽 척후', + classKey: 'bandit', + level: 47, + exp: 74, + hp: 122, + maxHp: 122, + attack: 46, + move: 4, + stats: { might: 142, intelligence: 100, leadership: 116, agility: 130, luck: 88 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 24 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 16, + y: 34 + }, + { + id: 'luo-scout-b', + name: '낙성 척후', + faction: 'enemy', + className: '동문 척후', + classKey: 'bandit', + level: 47, + exp: 74, + hp: 122, + maxHp: 122, + attack: 46, + move: 4, + stats: { might: 142, intelligence: 100, leadership: 116, agility: 130, luck: 88 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 24 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 22, + y: 32 + }, + { + id: 'luo-infantry-a', + name: '낙성 보병', + faction: 'enemy', + className: '외곽 보병', + classKey: 'yellowTurban', + level: 48, + exp: 76, + hp: 168, + maxHp: 168, + attack: 52, + move: 3, + stats: { might: 158, intelligence: 100, leadership: 136, agility: 108, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 36 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 27, + y: 31 + }, + { + id: 'luo-infantry-b', + name: '낙성 보병', + faction: 'enemy', + className: '성벽 보병', + classKey: 'yellowTurban', + level: 48, + exp: 76, + hp: 168, + maxHp: 168, + attack: 52, + move: 3, + stats: { might: 158, intelligence: 100, leadership: 136, agility: 108, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 36 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 34, + y: 27 + }, + { + id: 'luo-infantry-c', + name: '낙성 보병', + faction: 'enemy', + className: '서문 보병', + classKey: 'yellowTurban', + level: 49, + exp: 78, + hp: 176, + maxHp: 176, + attack: 54, + move: 3, + stats: { might: 162, intelligence: 102, leadership: 140, agility: 110, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 44 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 42, + y: 22 + }, + { + id: 'luo-infantry-d', + name: '낙성 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 49, + exp: 78, + hp: 176, + maxHp: 176, + attack: 54, + move: 3, + stats: { might: 162, intelligence: 102, leadership: 140, agility: 110, luck: 88 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 44 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 50, + y: 18 + }, + { + id: 'luo-archer-a', + name: '낙성 궁병', + faction: 'enemy', + className: '산루 궁병', + classKey: 'archer', + level: 48, + exp: 76, + hp: 120, + maxHp: 120, + attack: 48, + move: 3, + stats: { might: 112, intelligence: 112, leadership: 118, agility: 126, luck: 90 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 30 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 18 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 31, + y: 24 + }, + { + id: 'luo-archer-b', + name: '낙성 궁병', + faction: 'enemy', + className: '성벽 궁병', + classKey: 'archer', + level: 49, + exp: 78, + hp: 124, + maxHp: 124, + attack: 49, + move: 3, + stats: { might: 114, intelligence: 114, leadership: 120, agility: 128, luck: 90 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 38 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 45, + y: 16 + }, + { + id: 'luo-archer-c', + name: '낙성 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 49, + exp: 78, + hp: 124, + maxHp: 124, + attack: 49, + move: 3, + stats: { might: 114, intelligence: 114, leadership: 120, agility: 128, luck: 90 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 38 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 53, + y: 21 + }, + { + id: 'luo-cavalry-a', + name: '낙성 기병', + faction: 'enemy', + className: '외곽 기병', + classKey: 'cavalry', + level: 49, + exp: 78, + hp: 178, + maxHp: 178, + attack: 55, + move: 5, + stats: { might: 162, intelligence: 102, leadership: 138, agility: 130, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 46 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 32 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 36 + }, + { + id: 'luo-cavalry-b', + name: '낙성 기병', + faction: 'enemy', + className: '성문 기병', + classKey: 'cavalry', + level: 49, + exp: 78, + hp: 178, + maxHp: 178, + attack: 55, + move: 5, + stats: { might: 162, intelligence: 102, leadership: 138, agility: 130, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 46 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 32 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 44, + y: 31 + }, + { + id: 'luo-cavalry-c', + name: '낙성 기병', + faction: 'enemy', + className: '동문 기병', + classKey: 'cavalry', + level: 50, + exp: 80, + hp: 186, + maxHp: 186, + attack: 57, + move: 5, + stats: { might: 166, intelligence: 104, leadership: 142, agility: 132, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 54 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 40 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 52, + y: 29 + }, + { + id: 'luo-guard-a', + name: '장임 친위대', + faction: 'enemy', + className: '낙성 친위대', + classKey: 'yellowTurban', + level: 50, + exp: 80, + hp: 188, + maxHp: 188, + attack: 57, + move: 3, + stats: { might: 168, intelligence: 104, leadership: 146, agility: 112, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 58 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 50, + y: 16 + }, + { + id: 'luo-guard-b', + name: '장임 친위대', + faction: 'enemy', + className: '성문 친위대', + classKey: 'yellowTurban', + level: 50, + exp: 80, + hp: 188, + maxHp: 188, + attack: 57, + move: 3, + stats: { might: 168, intelligence: 104, leadership: 146, agility: 112, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 58 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 54, + y: 19 + }, + { + id: 'luo-strategist-a', + name: '낙성 책사', + faction: 'enemy', + className: '성벽 책사', + classKey: 'archer', + level: 49, + exp: 78, + hp: 124, + maxHp: 124, + attack: 49, + move: 3, + stats: { might: 96, intelligence: 128, leadership: 120, agility: 102, luck: 90 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 40 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 46, + y: 14 + }, + { + id: 'luo-strategist-b', + name: '낙성 책사', + faction: 'enemy', + className: '외곽 책사', + classKey: 'archer', + level: 49, + exp: 78, + hp: 124, + maxHp: 124, + attack: 49, + move: 3, + stats: { might: 96, intelligence: 128, leadership: 120, agility: 102, luck: 90 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 40 }, + armor: { itemId: 'cloth-armor', level: 5, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 53, + y: 24 + }, + { + id: 'luo-ambusher-a', + name: '낙성 복병', + faction: 'enemy', + className: '서쪽 복병', + classKey: 'bandit', + level: 48, + exp: 76, + hp: 128, + maxHp: 128, + attack: 48, + move: 4, + stats: { might: 146, intelligence: 102, leadership: 118, agility: 132, luck: 88 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 34 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 20 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 34, + y: 36 + }, + { + id: 'luo-ambusher-b', + name: '낙성 복병', + faction: 'enemy', + className: '북쪽 복병', + classKey: 'bandit', + level: 48, + exp: 76, + hp: 128, + maxHp: 128, + attack: 48, + move: 4, + stats: { might: 146, intelligence: 102, leadership: 118, agility: 132, luck: 88 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 34 }, + armor: { itemId: 'rebel-vest', level: 5, exp: 20 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 37, + y: 8 + }, + { + id: 'luo-officer-wu-yi', + name: '오의', + faction: 'enemy', + className: '익주 장군', + classKey: 'yellowTurban', + level: 50, + exp: 82, + hp: 190, + maxHp: 190, + attack: 56, + move: 4, + stats: { might: 158, intelligence: 108, leadership: 148, agility: 114, luck: 90 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 60 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 46 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 47, + y: 23 + }, + { + id: 'luo-leader-zhang-ren', + name: '장임', + faction: 'enemy', + className: '낙성 수장', + classKey: 'rebelLeader', + level: 51, + exp: 84, + hp: 238, + maxHp: 238, + attack: 59, + move: 4, + stats: { might: 170, intelligence: 118, leadership: 152, agility: 116, luck: 92 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 5, exp: 48 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 54, + y: 18 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -11946,6 +12492,33 @@ export const fuPassRecruitBonds: BattleBond[] = [ } ]; +export const luoCastleRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__wu-yi', + unitIds: ['liu-bei', 'wu-yi'], + title: '낙성의 새 길', + level: 34, + exp: 0, + description: '유비의 절제된 진군과 오의의 성문 지식이 만나 낙성으로 들어가는 길을 열어 갑니다.' + }, + { + id: 'fa-zheng__wu-yi', + unitIds: ['fa-zheng', 'wu-yi'], + title: '익주 내부 설득', + level: 32, + exp: 0, + description: '법정의 내부 정세 판단과 오의의 현장 인맥이 맞물려 익주 장수들의 마음을 흔듭니다.' + }, + { + id: 'pang-tong__wu-yi', + unitIds: ['pang-tong', 'wu-yi'], + 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); @@ -11979,6 +12552,7 @@ export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario); export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds, ...changshaRecruitBonds].map(cloneBattleBondForScenario); export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBonds, ...yizhouRecruitBonds].map(cloneBattleBondForScenario); +export const twentyNinthBattleBonds: BattleBond[] = [...twentyEighthBattleBonds, ...fuPassRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -13056,6 +13630,66 @@ function createTwentyEighthBattleTerrain(): TerrainType[][] { ); } +function createTwentyNinthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 40 }, (_, y) => + Array.from({ length: 58 }, (_, x): TerrainType => { + if (x <= 4 && y >= 33) { + return 'camp'; + } + if ((x >= 48 && x <= 56 && y >= 13 && y <= 23) || (x >= 44 && x <= 50 && y >= 10 && y <= 15)) { + return 'fort'; + } + if ( + (x >= 18 && x <= 21 && y >= 30 && y <= 32) || + (x >= 34 && x <= 36 && y >= 25 && y <= 27) || + (x >= 41 && x <= 43 && y >= 19 && y <= 21) + ) { + return 'village'; + } + if ( + (y >= 35 && x >= 4 && x <= 16) || + (x >= 15 && x <= 27 && y >= 31 && y <= 35) || + (x >= 26 && x <= 36 && y >= 27 && y <= 31) || + (x >= 35 && x <= 45 && y >= 22 && y <= 27) || + (x >= 44 && x <= 53 && y >= 17 && y <= 22) || + (x >= 52 && x <= 56 && y >= 18 && y <= 20) + ) { + return 'road'; + } + if ((x === 12 || x === 13) && y >= 0 && y <= 29) { + return 'river'; + } + if ((x === 31 || x === 32) && y >= 2 && y <= 25) { + return 'river'; + } + if ((x === 40 || x === 41) && y >= 30 && y <= 39) { + return 'river'; + } + if ( + (x >= 4 && x <= 18 && y >= 8 && y <= 24) || + (x >= 19 && x <= 30 && y >= 12 && y <= 25) || + (x >= 33 && x <= 46 && y >= 30 && y <= 39) || + (x >= 45 && x <= 57 && y >= 25 && y <= 38) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 10 && y <= 13) || + (x >= 20 && x <= 30 && y <= 11) || + (x >= 32 && x <= 47 && y >= 3 && y <= 14) || + (x >= 50 && x <= 57 && y <= 12) || + (x >= 24 && x <= 34 && y >= 33 && y <= 39) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 32) || (x >= 56 && y >= 23) || (x >= 45 && x <= 47 && 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 d3f27dd..5c8a24e 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -36,6 +36,7 @@ const unitTexture: Record = { 'wei-yan': 'unit-guan-yu', 'pang-tong': 'unit-liu-bei', 'fa-zheng': 'unit-liu-bei', + 'wu-yi': 'unit-guan-yu', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -357,7 +358,27 @@ const unitTexture: Record = { 'fupass-ambusher-a': 'unit-rebel', 'fupass-ambusher-b': 'unit-rebel', 'fupass-ambusher-c': 'unit-rebel', - 'fupass-leader-gao-pei': 'unit-rebel-leader' + 'fupass-leader-gao-pei': 'unit-rebel-leader', + 'luo-scout-a': 'unit-rebel', + 'luo-scout-b': 'unit-rebel', + 'luo-infantry-a': 'unit-rebel', + 'luo-infantry-b': 'unit-rebel', + 'luo-infantry-c': 'unit-rebel', + 'luo-infantry-d': 'unit-rebel', + 'luo-archer-a': 'unit-rebel-archer', + 'luo-archer-b': 'unit-rebel-archer', + 'luo-archer-c': 'unit-rebel-archer', + 'luo-cavalry-a': 'unit-rebel-cavalry', + 'luo-cavalry-b': 'unit-rebel-cavalry', + 'luo-cavalry-c': 'unit-rebel-cavalry', + 'luo-guard-a': 'unit-rebel', + 'luo-guard-b': 'unit-rebel', + 'luo-strategist-a': 'unit-rebel-archer', + 'luo-strategist-b': 'unit-rebel-archer', + 'luo-ambusher-a': 'unit-rebel', + 'luo-ambusher-b': 'unit-rebel', + 'luo-officer-wu-yi': 'unit-guan-yu', + 'luo-leader-zhang-ren': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -785,7 +806,8 @@ const unitStrategyIds: Record = { 'huang-zhong': ['encourage'], 'wei-yan': ['roar'], 'pang-tong': ['aid', 'encourage', 'fireTactic'], - 'fa-zheng': ['aid', 'encourage', 'fireTactic'] + 'fa-zheng': ['aid', 'encourage', 'fireTactic'], + 'wu-yi': ['encourage'] }; const initialItemStocks: Record> = { @@ -803,7 +825,8 @@ const initialItemStocks: Record> = { 'huang-zhong': { bean: 1, wine: 1 }, 'wei-yan': { bean: 2 }, 'pang-tong': { bean: 1, salve: 1, wine: 1 }, - 'fa-zheng': { bean: 1, salve: 1, wine: 1 } + 'fa-zheng': { bean: 1, salve: 1, wine: 1 }, + 'wu-yi': { bean: 2, wine: 1 } }; const attackRangeByClass: Partial> = { @@ -1177,7 +1200,27 @@ const enemyAiByUnitId: Record = { 'fupass-ambusher-a': 'aggressive', 'fupass-ambusher-b': 'aggressive', 'fupass-ambusher-c': 'aggressive', - 'fupass-leader-gao-pei': 'guard' + 'fupass-leader-gao-pei': 'guard', + 'luo-scout-a': 'aggressive', + 'luo-scout-b': 'aggressive', + 'luo-infantry-a': 'guard', + 'luo-infantry-b': 'guard', + 'luo-infantry-c': 'guard', + 'luo-infantry-d': 'guard', + 'luo-archer-a': 'hold', + 'luo-archer-b': 'hold', + 'luo-archer-c': 'hold', + 'luo-cavalry-a': 'aggressive', + 'luo-cavalry-b': 'aggressive', + 'luo-cavalry-c': 'aggressive', + 'luo-guard-a': 'guard', + 'luo-guard-b': 'guard', + 'luo-strategist-a': 'hold', + 'luo-strategist-b': 'hold', + 'luo-ambusher-a': 'aggressive', + 'luo-ambusher-b': 'aggressive', + 'luo-officer-wu-yi': 'guard', + 'luo-leader-zhang-ren': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 295d9af..f8bcfd3 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -23,6 +23,7 @@ import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-b import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg'; import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg'; import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg'; +import twentyNinthBattleMapUrl from '../../assets/images/battle/twenty-ninth-battle-map.svg'; import twentySixthBattleMapUrl from '../../assets/images/battle/twenty-sixth-battle-map.svg'; import twentySeventhBattleMapUrl from '../../assets/images/battle/twenty-seventh-battle-map.svg'; import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg'; @@ -114,6 +115,7 @@ export class BootScene extends Phaser.Scene { 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('battle-map-twenty-ninth', twentyNinthBattleMapUrl); 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 581c50d..67376ab 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -19,6 +19,8 @@ import { jingzhouRecruitUnits, liuBiaoRecruitBonds, liuBiaoRecruitUnits, + luoCastleRecruitBonds, + luoCastleRecruitUnits, wulingRecruitBonds, wulingRecruitUnits, xuzhouRecruitBonds, @@ -313,7 +315,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'twenty-fifth-battle-wuling-mountain-road', 'twenty-sixth-battle-changsha-veteran', 'twenty-seventh-battle-yizhou-relief-road', - 'twenty-eighth-battle-fu-pass-entry' + 'twenty-eighth-battle-fu-pass-entry', + 'twenty-ninth-battle-luo-outer-wall' ], nextHints: ['형주 방위', '익주 진입', '방통·법정 합류'] }, @@ -355,7 +358,8 @@ const campBattleIds = { twentyFifth: 'twenty-fifth-battle-wuling-mountain-road', twentySixth: 'twenty-sixth-battle-changsha-veteran', twentySeventh: 'twenty-seventh-battle-yizhou-relief-road', - twentyEighth: 'twenty-eighth-battle-fu-pass-entry' + twentyEighth: 'twenty-eighth-battle-fu-pass-entry', + twentyNinth: 'twenty-ninth-battle-luo-outer-wall' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -666,6 +670,18 @@ const sortieRulesByBattleId: Partial