diff --git a/docs/roadmap.md b/docs/roadmap.md index c0aac6a..9370d8d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -46,16 +46,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Twenty-second battle Red Cliffs fire attack, turning Huang Gai's bitter-flesh plan and Zhuge Liang's wind timing into the decisive fire battle against Cao Cao's chained fleet - Twenty-third battle Jing Province south entry route, opening the post-Red-Cliffs land campaign with Xing Daorong, village stabilization, a larger 46x32 battlefield, and Ma Liang recruitment after victory - Twenty-fourth battle Guiyang persuasion route, using Ma Liang's local knowledge in sortie selection, expanding the southern-commandery map to 48x32, and recruiting Yi Ji after Zhao Fan yields +- Twenty-fifth battle Wuling mountain road route, pushing ten recruited officers into a six-officer sortie choice across a 50x34 mountain battlefield and recruiting Gong Zhi after Jin Xuan withdraws - 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-fourth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang camp states, campaign timeline state, and camp save state +- Flow verification script from title through the twenty-fifth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue Jing Province south from Guiyang into Wuling and Changsha acquisition -2. Add more recruitable officers as the campaign moves through Jing Province toward Huang Zhong, Wei Yan, Yi Province, and Shu Han +1. Continue Jing Province south from Wuling into Changsha acquisition, including Huang Zhong and Wei Yan recruitment pressure +2. Add more recruitable officers as the campaign moves through Changsha, Yi Province, and Shu Han 3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 549e10c..f4a7c8c 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 !== 24 || + progressTabState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postBowangProgressState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postChangbanProgressState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 25 || 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 !== 24 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 25 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2729,15 +2729,158 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 24 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 25 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || - postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' ) { throw new Error(`Expected post-Guiyang progress tab to complete the new commandery battle and pause on Wuling preparation: ${JSON.stringify(postGuiyangProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-guiyang-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const wulingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !wulingSortieState?.sortieVisible || + !wulingSortieState.sortiePlan?.objectiveLine?.includes('무릉 산길 확보전') || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || + wulingSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi') || + wulingSortieState.sortieRoster?.length < 10 || + wulingSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-fourth camp sortie prep to target Wuling with Yi Ji recommended and Gong Zhi absent before victory: ${JSON.stringify(wulingSortieState)}`); + } + assertSortieTacticalRoster(wulingSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang', + 'yi-ji' + ]); + + const wulingPriorityUnits = ['ma-liang', 'yi-ji', 'zhuge-liang', 'zhao-yun', 'zhang-fei']; + for (const unitId of wulingPriorityUnits) { + 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' && !wulingPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const wulingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !wulingPriorityUnits.every((unitId) => + wulingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + wulingSortieReadyState.sortiePlan?.selectedCount !== 6 || + wulingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Wuling sortie to deploy Ma Liang, Yi Ji, and the mountain-road priority officers while preserving six-officer pressure: ${JSON.stringify(wulingSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-wuling-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-wuling-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentyFifthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fifth-battle-wuling-mountain-road'; + }); + if (enteredTwentyFifthBattle) { + 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-fifth-battle-wuling-mountain-road' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-fifth-battle.png', fullPage: true }); + + const twentyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentyFifthEnemies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentyFifthAllies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentyFifthEnemyBehaviors = new Set(twentyFifthEnemies.map((unit) => unit.ai)); + if ( + twentyFifthBattleState.camera?.mapWidth !== 50 || + twentyFifthBattleState.camera?.mapHeight !== 34 || + twentyFifthBattleState.victoryConditionLabel !== '김선 퇴각' || + twentyFifthEnemies.length < 19 || + !twentyFifthEnemyBehaviors.has('aggressive') || + !twentyFifthEnemyBehaviors.has('guard') || + !twentyFifthEnemyBehaviors.has('hold') || + !twentyFifthEnemies.some((unit) => unit.id === 'wuling-leader-jin-xuan') || + !twentyFifthAllies.some((unit) => unit.id === 'ma-liang') || + !twentyFifthAllies.some((unit) => unit.id === 'yi-ji') || + !twentyFifthAllies.some((unit) => unit.id === 'zhao-yun') || + twentyFifthAllies.some((unit) => unit.id === 'gong-zhi') + ) { + throw new Error(`Expected twenty-fifth battle to use Wuling map, Jin Xuan objective, mixed AI, selected allied officers, and no Gong Zhi before victory: ${JSON.stringify(twentyFifthBattleState)}`); + } + + 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 twentyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentyFifthCampState?.campBattleId !== 'twenty-fifth-battle-wuling-mountain-road' || + twentyFifthCampState.campTitle !== '무릉 산길 확보 후 군영' || + twentyFifthCampState.availableDialogueIds?.length !== 3 || + !twentyFifthCampState.availableDialogueIds.every((id) => id.endsWith('wuling')) || + twentyFifthCampState.availableVisitIds?.length !== 2 || + !twentyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'gong-zhi') || + !twentyFifthCampState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited) || + twentyFifthCampState.rosterCollection?.total < 11 + ) { + throw new Error(`Expected twenty-fifth camp to recruit Gong Zhi and expose Wuling dialogue/visit sets: ${JSON.stringify(twentyFifthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-wuling-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postWulingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postWulingProgressState?.activeTab !== 'progress' || + postWulingProgressState.campaignProgress?.completedKnown !== 25 || + postWulingProgressState.campaignProgress?.totalKnown !== 25 || + postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || + postWulingProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Wuling progress tab to complete the mountain-road battle and pause on Changsha preparation: ${JSON.stringify(postWulingProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-wuling-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -2749,7 +2892,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-twenty-fourth-battle flow, recruited officer sortie selection, Ma Liang and Yi Ji joins, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twenty-fifth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, and Gong Zhi joins, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twenty-fifth-battle-map.svg b/src/assets/images/battle/twenty-fifth-battle-map.svg new file mode 100644 index 0000000..d96bcbd --- /dev/null +++ b/src/assets/images/battle/twenty-fifth-battle-map.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 0467051..7711908 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -55,6 +55,10 @@ import { twentyFourthBattleMap, twentyFourthBattleUnits, twentyFourthBattleVictoryPages, + twentyFifthBattleBonds, + twentyFifthBattleMap, + twentyFifthBattleUnits, + twentyFifthBattleVictoryPages, twentiethBattleBonds, twentiethBattleMap, twentiethBattleUnits, @@ -125,7 +129,8 @@ export type BattleScenarioId = | 'twenty-first-battle-red-cliffs-vanguard' | 'twenty-second-battle-red-cliffs-fire' | 'twenty-third-battle-jingzhou-south-entry' - | 'twenty-fourth-battle-guiyang-persuasion'; + | 'twenty-fourth-battle-guiyang-persuasion' + | 'twenty-fifth-battle-wuling-mountain-road'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1497,6 +1502,69 @@ export const twentyFourthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentyFifthBattleScenario: BattleScenarioDefinition = { + id: 'twenty-fifth-battle-wuling-mountain-road', + title: '무릉 산길 확보전', + victoryConditionLabel: '김선 퇴각', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '계양이 열린 뒤 유비군은 무릉의 산길로 들어섭니다. 좁은 길과 흩어진 마을 때문에 단순 돌파만으로는 근거지를 얻을 수 없습니다.', + '마량과 이적은 무릉의 내부 동요를 읽고, 김선 아래 장수 공지가 백성 피해를 염려한다고 보고합니다.', + '유비와 제갈량을 지키며 김선을 물러나게 하고 산길 마을을 안정시키면, 공지가 군영에 합류합니다.' + ], + map: twentyFifthBattleMap, + units: twentyFifthBattleUnits, + bonds: twentyFifthBattleBonds, + mapTextureKey: 'battle-map-twenty-fifth', + leaderUnitId: 'wuling-leader-jin-xuan', + quickVictoryTurnLimit: 34, + baseVictoryGold: 3360, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '김선 퇴각', + rewardGold: 2200, + unitId: 'wuling-leader-jin-xuan' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 640, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 900, + unitId: 'zhuge-liang' + }, + { + id: 'wuling-village', + kind: 'secure-terrain', + label: '무릉 마을 안정', + rewardGold: 1080, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '34턴 이내 승리', + rewardGold: 840, + maxTurn: 34 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 11', '상처약 8', '탁주 4', '무릉 산길 지도 +1', '공지 합류'], + victoryPages: twentyFifthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1523,7 +1591,8 @@ export const battleScenarios: Record 'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario, 'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario, 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario, - 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario + 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario, + 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 475d3e4..61ccfef 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -20,6 +20,7 @@ import { twentyFirstBattleScenario, twentySecondBattleScenario, twentyThirdBattleScenario, + twentyFifthBattleScenario, twentyFourthBattleScenario, twentiethBattleScenario, twelfthBattleScenario, @@ -66,6 +67,8 @@ import { twentySecondBattleVictoryPages, twentyThirdBattleIntroPages, twentyThirdBattleVictoryPages, + twentyFifthBattleIntroPages, + twentyFifthBattleVictoryPages, twentyFourthBattleIntroPages, twentyFourthBattleVictoryPages, twentiethBattleIntroPages, @@ -333,13 +336,24 @@ const sortieFlows: Record = { }, [twentyFourthBattleScenario.id]: { afterBattleId: twentyFourthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '무릉 산길 준비', + eyebrow: '다음 전장', + title: twentyFifthBattleScenario.title, description: - '계양이 열리고 이적이 합류했습니다. 다음은 험한 산길과 흩어진 마을이 기다리는 무릉으로 향해야 합니다.', - rewardHint: '다음 장: 무릉 산길 확보 준비 중', - pages: twentyFourthBattleVictoryPages, - unavailableNotice: '무릉 산길 확보 장은 다음 작업에서 이어집니다. 군영에서 이적과 대화하고 산길 전투를 준비하십시오.' + '계양이 열리고 이적이 합류했습니다. 이제 마량과 이적의 형주 지식을 앞세워 험한 무릉 산길과 흩어진 마을을 이어야 합니다.', + rewardHint: `예상 보상: ${twentyFifthBattleScenario.title} 개방 / 공지 합류`, + nextBattleId: twentyFifthBattleScenario.id, + campaignStep: 'twenty-fifth-battle', + pages: [...twentyFourthBattleVictoryPages, ...twentyFifthBattleIntroPages] + }, + [twentyFifthBattleScenario.id]: { + afterBattleId: twentyFifthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '장사 노장 대면 준비', + description: + '무릉 산길이 열리고 공지가 합류했습니다. 다음은 장사의 노장 황충과 태수 한현을 마주하며 형주 남부 확보의 큰 고비를 넘겨야 합니다.', + rewardHint: '다음 장: 장사 노장 대면 준비 중', + pages: twentyFifthBattleVictoryPages, + unavailableNotice: '장사 노장 대면 장은 다음 작업에서 이어집니다. 군영에서 공지와 대화하고 황충을 맞을 준비를 하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 293f5f3..6dbfbb8 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1572,6 +1572,68 @@ export const twentyFourthBattleVictoryPages: StoryPage[] = [ } ]; +export const twentyFifthBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-fifth-wuling-road', + bgm: 'story-dark', + chapter: '무릉의 닫힌 산길', + background: 'story-three-heroes', + speaker: '마량', + text: '무릉은 계양처럼 성문 하나만 바라보는 땅이 아닙니다. 산길과 작은 마을이 흩어져 있어, 길을 잃으면 군량도 마음도 끊어집니다.' + }, + { + id: 'twenty-fifth-yi-ji-letter', + bgm: 'battle-prep', + chapter: '먼저 보내는 예', + background: 'story-militia', + speaker: '이적', + text: '김선은 강하게 버티려 하지만, 아래 장수 공지는 백성의 피해를 걱정한다고 들었습니다. 산길 마을을 지키면 그에게 말이 닿을 것입니다.' + }, + { + id: 'twenty-fifth-zhuge-formation', + bgm: 'battle-prep', + chapter: '좁은 길의 진형', + background: 'story-liu-bei', + speaker: '제갈량', + text: '좁은 산길에서는 무장이 많다고 곧 힘이 되는 것이 아닙니다. 앞길을 여는 자, 마을을 달래는 자, 보급을 잇는 자를 골라야 합니다.' + }, + { + id: 'twenty-fifth-wuling-sortie', + bgm: 'battle-prep', + chapter: '무릉 산길 확보전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '산길을 불태우지 말고 이어야 하오. 무릉의 길과 마을을 지켜 내면, 닫힌 마음도 함께 열릴 것이오.' + } +]; + +export const twentyFifthBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-fifth-victory-wuling-open', + bgm: 'militia-theme', + chapter: '무릉의 길이 이어지다', + background: 'story-sortie', + text: '김선의 산길 방어선이 무너지자 무릉의 마을들은 먼저 불안한 문을 열었습니다. 유비군은 산길 창고를 봉하고 백성의 길을 지켰습니다.' + }, + { + id: 'twenty-fifth-gong-zhi-joins', + bgm: 'battle-prep', + chapter: '공지의 결심', + background: 'story-militia', + speaker: '공지', + text: '무릉을 힘으로만 누르지 않으셨군요. 백성을 앞세우는 군이라면, 저도 산길과 고을의 형세를 바쳐 따르겠습니다.' + }, + { + id: 'twenty-fifth-changsha-shadow', + bgm: 'story-dark', + chapter: '장사의 노장', + background: 'story-three-heroes', + speaker: '제갈량', + text: '무릉까지 길이 이어졌습니다. 남은 장사에는 황충이라는 노장이 있습니다. 다음 싸움은 힘보다 예우와 인내를 시험할 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1861,6 +1923,12 @@ export const twentyFourthBattleMap: BattleMap = { terrain: createTwentyFourthBattleTerrain() }; +export const twentyFifthBattleMap: BattleMap = { + width: 50, + height: 34, + terrain: createTwentyFifthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -3865,6 +3933,30 @@ export const guiyangRecruitUnits: UnitData[] = [ } ]; +export const wulingRecruitUnits: UnitData[] = [ + { + id: 'gong-zhi', + name: '공지', + faction: 'ally', + className: '무릉 의병장', + classKey: 'infantry', + level: 13, + exp: 0, + hp: 40, + maxHp: 40, + attack: 12, + move: 4, + stats: { might: 76, intelligence: 72, leadership: 82, agility: 74, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 6 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 8 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 8, + y: 23 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -9542,6 +9634,430 @@ export const twentyFourthBattleUnits: UnitData[] = [ } ]; +const twentyFifthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 28 }, + 'guan-yu': { x: 6, y: 26 }, + 'zhang-fei': { x: 6, y: 30 }, + 'jian-yong': { x: 3, y: 31 }, + 'mi-zhu': { x: 4, y: 32 }, + 'sun-qian': { x: 8, y: 29 }, + 'zhao-yun': { x: 8, y: 26 }, + 'zhuge-liang': { x: 5, y: 28 }, + 'ma-liang': { x: 7, y: 31 }, + 'yi-ji': { x: 9, y: 28 } +}; + +export const twentyFifthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentyFifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'wuling-scout-a', + name: '무릉 산길 척후', + faction: 'enemy', + className: '산길 척후', + classKey: 'bandit', + level: 34, + exp: 52, + hp: 92, + maxHp: 92, + attack: 36, + move: 4, + stats: { might: 120, intelligence: 88, leadership: 96, agility: 116, luck: 80 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 28 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 16 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 13, + y: 27 + }, + { + id: 'wuling-scout-b', + name: '무릉 산길 척후', + faction: 'enemy', + className: '북릉 척후', + classKey: 'bandit', + level: 34, + exp: 52, + hp: 92, + maxHp: 92, + attack: 36, + move: 4, + stats: { might: 120, intelligence: 88, leadership: 96, agility: 116, luck: 80 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 28 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 16 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 16, + y: 23 + }, + { + id: 'wuling-scout-c', + name: '무릉 산길 척후', + faction: 'enemy', + className: '서곡 척후', + classKey: 'bandit', + level: 35, + exp: 54, + hp: 96, + maxHp: 96, + attack: 37, + move: 4, + stats: { might: 122, intelligence: 88, leadership: 98, agility: 118, luck: 80 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 18 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 20, + y: 29 + }, + { + id: 'wuling-bandit-a', + name: '무릉 산적', + faction: 'enemy', + className: '산적', + classKey: 'bandit', + level: 34, + exp: 52, + hp: 102, + maxHp: 102, + attack: 37, + move: 4, + stats: { might: 126, intelligence: 80, leadership: 92, agility: 112, luck: 78 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 18 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 18, + y: 17 + }, + { + id: 'wuling-bandit-b', + name: '무릉 산적', + faction: 'enemy', + className: '숲길 산적', + classKey: 'bandit', + level: 34, + exp: 52, + hp: 102, + maxHp: 102, + attack: 37, + move: 4, + stats: { might: 126, intelligence: 80, leadership: 92, agility: 112, luck: 78 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 18 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 23, + y: 20 + }, + { + id: 'wuling-bandit-c', + name: '무릉 산적', + faction: 'enemy', + className: '협곡 산적', + classKey: 'bandit', + level: 35, + exp: 54, + hp: 106, + maxHp: 106, + attack: 38, + move: 4, + stats: { might: 128, intelligence: 82, leadership: 94, agility: 114, luck: 78 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 32 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 20 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 13 + }, + { + id: 'wuling-infantry-a', + name: '무릉 보병', + faction: 'enemy', + className: '무릉 보병', + classKey: 'yellowTurban', + level: 35, + exp: 54, + hp: 120, + maxHp: 120, + attack: 39, + move: 3, + stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 42 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 27, + y: 23 + }, + { + id: 'wuling-infantry-b', + name: '무릉 보병', + faction: 'enemy', + className: '마을 보병', + classKey: 'yellowTurban', + level: 35, + exp: 54, + hp: 120, + maxHp: 120, + attack: 39, + move: 3, + stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 42 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 31, + y: 26 + }, + { + id: 'wuling-infantry-c', + name: '무릉 보병', + faction: 'enemy', + className: '산문 보병', + classKey: 'yellowTurban', + level: 36, + exp: 56, + hp: 126, + maxHp: 126, + attack: 40, + move: 3, + stats: { might: 134, intelligence: 88, leadership: 114, agility: 96, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 46 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 37, + y: 18 + }, + { + id: 'wuling-infantry-d', + name: '무릉 보병', + faction: 'enemy', + className: '성채 보병', + classKey: 'yellowTurban', + level: 36, + exp: 56, + hp: 126, + maxHp: 126, + attack: 40, + move: 3, + stats: { might: 134, intelligence: 88, leadership: 114, agility: 96, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 46 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 41, + y: 14 + }, + { + id: 'wuling-archer-a', + name: '무릉 궁병', + faction: 'enemy', + className: '능선 궁병', + classKey: 'archer', + level: 35, + exp: 54, + hp: 90, + maxHp: 90, + attack: 36, + move: 3, + stats: { might: 92, intelligence: 96, leadership: 100, agility: 108, luck: 82 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 38 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 24, + y: 16 + }, + { + id: 'wuling-archer-b', + name: '무릉 궁병', + faction: 'enemy', + className: '산채 궁병', + classKey: 'archer', + level: 35, + exp: 54, + hp: 90, + maxHp: 90, + attack: 36, + move: 3, + stats: { might: 92, intelligence: 96, leadership: 100, agility: 108, luck: 82 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 38 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 34, + y: 21 + }, + { + id: 'wuling-archer-c', + name: '무릉 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 36, + exp: 56, + hp: 94, + maxHp: 94, + attack: 37, + move: 3, + stats: { might: 94, intelligence: 98, leadership: 102, agility: 110, luck: 82 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 42 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 43, + y: 16 + }, + { + id: 'wuling-cavalry-a', + name: '무릉 기병', + faction: 'enemy', + className: '산길 기병', + classKey: 'cavalry', + level: 36, + exp: 56, + hp: 130, + maxHp: 130, + attack: 41, + move: 5, + stats: { might: 136, intelligence: 88, leadership: 112, agility: 116, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 48 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 30 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 30, + y: 29 + }, + { + id: 'wuling-cavalry-b', + name: '무릉 기병', + faction: 'enemy', + className: '동릉 기병', + classKey: 'cavalry', + level: 36, + exp: 56, + hp: 130, + maxHp: 130, + attack: 41, + move: 5, + stats: { might: 136, intelligence: 88, leadership: 112, agility: 116, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 48 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 30 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 36, + y: 27 + }, + { + id: 'wuling-guard-a', + name: '김선 친위대', + faction: 'enemy', + className: '무릉 친위대', + classKey: 'yellowTurban', + level: 37, + exp: 58, + hp: 132, + maxHp: 132, + attack: 42, + move: 3, + stats: { might: 138, intelligence: 90, leadership: 118, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 52 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 44, + y: 13 + }, + { + id: 'wuling-guard-b', + name: '김선 친위대', + faction: 'enemy', + className: '무릉 친위대', + classKey: 'yellowTurban', + level: 37, + exp: 58, + hp: 132, + maxHp: 132, + attack: 42, + move: 3, + stats: { might: 138, intelligence: 90, leadership: 118, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 52 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 45, + y: 16 + }, + { + id: 'wuling-strategist-a', + name: '무릉 책사', + faction: 'enemy', + className: '무릉 책사', + classKey: 'archer', + level: 36, + exp: 56, + hp: 94, + maxHp: 94, + attack: 37, + move: 3, + stats: { might: 88, intelligence: 112, leadership: 104, agility: 94, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 42 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 24 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 42, + y: 12 + }, + { + id: 'wuling-leader-jin-xuan', + name: '김선', + faction: 'enemy', + className: '무릉 태수', + classKey: 'rebelLeader', + level: 38, + exp: 62, + hp: 184, + maxHp: 184, + attack: 44, + move: 4, + stats: { might: 140, intelligence: 98, leadership: 126, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 4, exp: 68 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 46, + y: 14 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -9723,6 +10239,33 @@ export const guiyangRecruitBonds: BattleBond[] = [ } ]; +export const wulingRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__gong-zhi', + unitIds: ['liu-bei', 'gong-zhi'], + title: '산길의 귀순', + level: 32, + exp: 0, + description: '유비의 군율과 공지의 무릉 지리가 맞물려 산길 마을을 해치지 않는 진군로를 엽니다.' + }, + { + id: 'ma-liang__gong-zhi', + unitIds: ['ma-liang', 'gong-zhi'], + title: '무릉 기록', + level: 30, + exp: 0, + description: '마량의 형주 기록과 공지의 현지 경험이 더해져 험한 길의 위험을 미리 읽어냅니다.' + }, + { + id: 'yi-ji__gong-zhi', + unitIds: ['yi-ji', 'gong-zhi'], + 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); @@ -9752,6 +10295,7 @@ export const twentyFirstBattleBonds: BattleBond[] = [...twentiethBattleBonds].ma export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds].map(cloneBattleBondForScenario); export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario); export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario); +export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -10612,6 +11156,62 @@ function createTwentyFourthBattleTerrain(): TerrainType[][] { ); } +function createTwentyFifthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 34 }, (_, y) => + Array.from({ length: 50 }, (_, x): TerrainType => { + if (x <= 3 && y >= 27) { + return 'camp'; + } + if ((x >= 43 && x <= 48 && y >= 11 && y <= 17) || (x >= 40 && x <= 45 && y >= 8 && y <= 12)) { + return 'fort'; + } + if ((x >= 28 && x <= 30 && y >= 25 && y <= 27) || (x >= 17 && x <= 19 && y >= 18 && y <= 20) || (x >= 37 && x <= 39 && y >= 23 && y <= 25)) { + return 'village'; + } + if ( + (y >= 28 && x >= 3 && x <= 13) || + (x >= 12 && x <= 16 && y >= 22 && y <= 28) || + (x >= 16 && x <= 23 && y >= 20 && y <= 22) || + (x >= 23 && x <= 31 && y >= 23 && y <= 26) || + (x >= 31 && x <= 39 && y >= 20 && y <= 23) || + (x >= 38 && x <= 44 && y >= 16 && y <= 20) || + (x >= 42 && x <= 47 && y >= 12 && y <= 16) + ) { + return 'road'; + } + if ((x === 9 || x === 10) && y >= 0 && y <= 22) { + return 'river'; + } + if ((x === 33 || x === 34) && y >= 1 && y <= 19) { + return 'river'; + } + if ((x === 27 || x === 28) && y >= 27 && y <= 33) { + return 'river'; + } + if ( + (x <= 7 && y <= 14) || + (x >= 13 && x <= 23 && y >= 4 && y <= 13) || + (x >= 18 && x <= 31 && y >= 14 && y <= 19) || + (x >= 35 && x <= 48 && y >= 21 && y <= 32) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 12 && y >= 15 && y <= 25) || + (x >= 21 && x <= 33 && y >= 6 && y <= 15) || + (x >= 36 && x <= 49 && y <= 10) || + (x >= 41 && x <= 49 && y >= 18 && y <= 24) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 25) || (x >= 48 && y <= 20) || (x >= 30 && x <= 32 && y >= 0 && 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 701be35..9ab4985 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -31,6 +31,7 @@ const unitTexture: Record = { 'zhuge-liang': 'unit-liu-bei', 'ma-liang': 'unit-liu-bei', 'yi-ji': 'unit-liu-bei', + 'gong-zhi': 'unit-guan-yu', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -276,7 +277,26 @@ const unitTexture: Record = { 'guiyang-guard-b': 'unit-rebel', 'guiyang-strategist-a': 'unit-rebel-archer', 'guiyang-strategist-b': 'unit-rebel-archer', - 'guiyang-leader-zhao-fan': 'unit-rebel-leader' + 'guiyang-leader-zhao-fan': 'unit-rebel-leader', + 'wuling-scout-a': 'unit-rebel', + 'wuling-scout-b': 'unit-rebel', + 'wuling-scout-c': 'unit-rebel', + 'wuling-bandit-a': 'unit-rebel', + 'wuling-bandit-b': 'unit-rebel', + 'wuling-bandit-c': 'unit-rebel', + 'wuling-infantry-a': 'unit-rebel', + 'wuling-infantry-b': 'unit-rebel', + 'wuling-infantry-c': 'unit-rebel', + 'wuling-infantry-d': 'unit-rebel', + 'wuling-archer-a': 'unit-rebel-archer', + 'wuling-archer-b': 'unit-rebel-archer', + 'wuling-archer-c': 'unit-rebel-archer', + 'wuling-cavalry-a': 'unit-rebel-cavalry', + 'wuling-cavalry-b': 'unit-rebel-cavalry', + 'wuling-guard-a': 'unit-rebel', + 'wuling-guard-b': 'unit-rebel', + 'wuling-strategist-a': 'unit-rebel-archer', + 'wuling-leader-jin-xuan': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -699,7 +719,8 @@ const unitStrategyIds: Record = { 'sun-qian': ['aid', 'encourage', 'fireTactic'], 'zhuge-liang': ['aid', 'encourage', 'fireTactic'], 'ma-liang': ['aid', 'encourage', 'fireTactic'], - 'yi-ji': ['aid', 'encourage'] + 'yi-ji': ['aid', 'encourage'], + 'gong-zhi': ['encourage'] }; const initialItemStocks: Record> = { @@ -712,7 +733,8 @@ const initialItemStocks: Record> = { 'zhao-yun': { bean: 1, wine: 1 }, 'zhuge-liang': { bean: 1, wine: 1 }, 'ma-liang': { bean: 1, salve: 1 }, - 'yi-ji': { bean: 2, salve: 1 } + 'yi-ji': { bean: 2, salve: 1 }, + 'gong-zhi': { bean: 2 } }; const attackRangeByClass: Partial> = { @@ -1010,7 +1032,26 @@ const enemyAiByUnitId: Record = { 'guiyang-guard-b': 'guard', 'guiyang-strategist-a': 'hold', 'guiyang-strategist-b': 'hold', - 'guiyang-leader-zhao-fan': 'guard' + 'guiyang-leader-zhao-fan': 'guard', + 'wuling-scout-a': 'aggressive', + 'wuling-scout-b': 'aggressive', + 'wuling-scout-c': 'aggressive', + 'wuling-bandit-a': 'aggressive', + 'wuling-bandit-b': 'aggressive', + 'wuling-bandit-c': 'aggressive', + 'wuling-infantry-a': 'guard', + 'wuling-infantry-b': 'guard', + 'wuling-infantry-c': 'guard', + 'wuling-infantry-d': 'guard', + 'wuling-archer-a': 'hold', + 'wuling-archer-b': 'hold', + 'wuling-archer-c': 'hold', + 'wuling-cavalry-a': 'aggressive', + 'wuling-cavalry-b': 'aggressive', + 'wuling-guard-a': 'guard', + 'wuling-guard-b': 'guard', + 'wuling-strategist-a': 'hold', + 'wuling-leader-jin-xuan': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 6f36016..76d3e42 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -20,6 +20,7 @@ import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg'; import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg'; import 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 twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg'; import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg'; @@ -106,6 +107,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl); this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl); this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl); + this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl); 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 ecfddca..8ef74eb 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -15,6 +15,8 @@ import { jingzhouRecruitUnits, liuBiaoRecruitBonds, liuBiaoRecruitUnits, + wulingRecruitBonds, + wulingRecruitUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, zhugeRecruitBonds, @@ -299,7 +301,11 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ title: '형주·익주 확보', period: '근본을 세우다', description: '형주와 익주를 기반으로 삼아 유비군이 유랑군에서 국가의 뿌리로 바뀌는 장입니다.', - battleIds: ['twenty-third-battle-jingzhou-south-entry', 'twenty-fourth-battle-guiyang-persuasion'], + battleIds: [ + 'twenty-third-battle-jingzhou-south-entry', + 'twenty-fourth-battle-guiyang-persuasion', + 'twenty-fifth-battle-wuling-mountain-road' + ], nextHints: ['형주 방위', '익주 진입', '장수 대거 합류'] }, { @@ -336,7 +342,8 @@ const campBattleIds = { twentyFirst: 'twenty-first-battle-red-cliffs-vanguard', twentySecond: 'twenty-second-battle-red-cliffs-fire', twentyThird: 'twenty-third-battle-jingzhou-south-entry', - twentyFourth: 'twenty-fourth-battle-guiyang-persuasion' + twentyFourth: 'twenty-fourth-battle-guiyang-persuasion', + twentyFifth: 'twenty-fifth-battle-wuling-mountain-road' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -599,6 +606,18 @@ const sortieRulesByBattleId: Partial