diff --git a/docs/roadmap.md b/docs/roadmap.md index 842b98f..b4786db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -57,15 +57,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-third battle Chengdu surrender route, using Li Yan's first sortie choice across a 66x48 Chengdu outer-wall battlefield and recruiting Huang Quan after Yi Province yields - Thirty-fourth battle Jiameng Pass Ma Chao route, using Huang Quan's first sortie choice across a 68x50 Hanzhong gate battlefield and recruiting Ma Chao after his charge is redirected - Thirty-fifth battle Yangping scout route, using Ma Chao's first allied sortie across a 70x52 Hanzhong gate battlefield and recruiting Ma Dai after the pass is surveyed +- Thirty-sixth battle Dingjun Mountain vanguard route, using Huang Zhong and Fa Zheng's high-ground plan across a 72x54 Hanzhong battlefield and recruiting Wang Ping after the ridge is secured - Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter - Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer - Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers - Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses -- Flow verification script from title through the thirty-fifth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping camp states, campaign timeline state, and camp save state +- Flow verification script from title through the thirty-sixth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue from Yangping scout work into the Hanzhong main front, using Ma Chao and Ma Dai as the cavalry decision axis +1. Continue from Dingjun Mountain into the Hanzhong decisive battle, using Huang Zhong/Fa Zheng and Wang Ping's terrain knowledge as the tactical axis 2. Add the formal Shu-Han foundation setup once Hanzhong's military basis is strong enough 3. Expand sortie selection into richer pre-battle officer planning, including explicit reserve drills, 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 aa72576..f2dbb34 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 !== 35 || + progressTabState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postBowangProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postChangbanProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postWulingProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postChangshaProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postYizhouProgressState.campaignProgress?.totalKnown !== 36 || 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 !== 35 || + postFuPassProgressState.campaignProgress?.totalKnown !== 36 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3462,7 +3462,7 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 35 || + postLuoProgressState.campaignProgress?.totalKnown !== 36 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' @@ -3608,7 +3608,7 @@ try { if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || - postLuofengProgressState.campaignProgress?.totalKnown !== 35 || + postLuofengProgressState.campaignProgress?.totalKnown !== 36 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' @@ -3760,7 +3760,7 @@ try { if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || - postLuoMainProgressState.campaignProgress?.totalKnown !== 35 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 36 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' @@ -3913,7 +3913,7 @@ try { if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || - postMianzhuProgressState.campaignProgress?.totalKnown !== 35 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 36 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' @@ -4067,7 +4067,7 @@ try { if ( postChengduProgressState?.activeTab !== 'progress' || postChengduProgressState.campaignProgress?.completedKnown !== 33 || - postChengduProgressState.campaignProgress?.totalKnown !== 35 || + postChengduProgressState.campaignProgress?.totalKnown !== 36 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' @@ -4222,7 +4222,7 @@ try { if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || - postJiamengProgressState.campaignProgress?.totalKnown !== 35 || + postJiamengProgressState.campaignProgress?.totalKnown !== 36 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' @@ -4378,15 +4378,172 @@ try { if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || - postYangpingProgressState.campaignProgress?.totalKnown !== 35 || + postYangpingProgressState.campaignProgress?.totalKnown !== 36 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || - postYangpingProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' ) { - throw new Error(`Expected post-Yangping progress tab to complete Ma Dai chapter and pause on the next Hanzhong step: ${JSON.stringify(postYangpingProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Yangping progress tab to complete Ma Dai chapter and open Dingjun vanguard battle: ${JSON.stringify(postYangpingProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-yangping-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const dingjunSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !dingjunSortieState?.sortieVisible || + !dingjunSortieState.sortiePlan?.objectiveLine?.includes('정군산 전초전') || + !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || + !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || + !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) || + !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited && unit.recommended) || + !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping') || + dingjunSortieState.sortieRoster?.length < 21 || + dingjunSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected Yangping camp sortie prep to target Dingjun vanguard with Ma Dai selectable and Wang Ping absent before victory: ${JSON.stringify(dingjunSortieState)}`); + } + assertSortieTacticalRoster(dingjunSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang', + 'yi-ji', + 'gong-zhi', + 'huang-zhong', + 'wei-yan', + 'pang-tong', + 'fa-zheng', + 'wu-yi', + 'yan-yan', + 'li-yan', + 'huang-quan', + 'ma-chao', + 'ma-dai' + ]); + + const dingjunPriorityUnits = ['huang-zhong', 'fa-zheng', 'ma-chao', 'ma-dai', 'zhuge-liang']; + for (const unitId of dingjunPriorityUnits) { + 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' && !dingjunPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const dingjunSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !dingjunPriorityUnits.every((unitId) => + dingjunSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + dingjunSortieReadyState.sortiePlan?.selectedCount !== 6 || + dingjunSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Dingjun sortie to deploy Huang Zhong, Fa Zheng, Ma Chao, Ma Dai, and Zhuge Liang while preserving six-officer pressure: ${JSON.stringify(dingjunSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-dingjun-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-dingjun-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredThirtySixthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard'; + }); + if (enteredThirtySixthBattle) { + break; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(320); + } + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-thirty-sixth-battle.png', fullPage: true }); + + const thirtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const thirtySixthEnemies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const thirtySixthAllies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'ally'); + const thirtySixthEnemyBehaviors = new Set(thirtySixthEnemies.map((unit) => unit.ai)); + if ( + thirtySixthBattleState.camera?.mapWidth !== 72 || + thirtySixthBattleState.camera?.mapHeight !== 54 || + thirtySixthBattleState.victoryConditionLabel !== '하후연 선봉 격파' || + thirtySixthEnemies.length < 25 || + !thirtySixthEnemyBehaviors.has('aggressive') || + !thirtySixthEnemyBehaviors.has('guard') || + !thirtySixthEnemyBehaviors.has('hold') || + !thirtySixthEnemies.some((unit) => unit.id === 'dingjun-leader-xiahou-yuan') || + !thirtySixthEnemies.some((unit) => unit.id === 'dingjun-officer-wang-ping') || + !thirtySixthAllies.some((unit) => unit.id === 'huang-zhong') || + !thirtySixthAllies.some((unit) => unit.id === 'fa-zheng') || + !thirtySixthAllies.some((unit) => unit.id === 'ma-chao') || + !thirtySixthAllies.some((unit) => unit.id === 'ma-dai') || + !thirtySixthAllies.some((unit) => unit.id === 'zhuge-liang') || + thirtySixthAllies.some((unit) => unit.id === 'wang-ping') + ) { + throw new Error(`Expected thirty-sixth battle to use Dingjun map, Xiahou Yuan objective, Huang Zhong first pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySixthBattleState)}`); + } + + 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 thirtySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + thirtySixthCampState?.campBattleId !== 'thirty-sixth-battle-dingjun-vanguard' || + thirtySixthCampState.campTitle !== '정군산 전초전 후 군영' || + thirtySixthCampState.availableDialogueIds?.length !== 3 || + !thirtySixthCampState.availableDialogueIds.every((id) => id.endsWith('dingjun')) || + thirtySixthCampState.availableVisitIds?.length !== 2 || + !thirtySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') || + !thirtySixthCampState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited) || + thirtySixthCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected thirty-sixth camp to recruit Wang Ping and expose Dingjun dialogue/visit sets: ${JSON.stringify(thirtySixthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-dingjun-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postDingjunProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postDingjunProgressState?.activeTab !== 'progress' || + postDingjunProgressState.campaignProgress?.completedKnown !== 36 || + postDingjunProgressState.campaignProgress?.totalKnown !== 36 || + postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || + postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || + postDingjunProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Dingjun progress tab to complete Wang Ping chapter and pause on the next Hanzhong step: ${JSON.stringify(postDingjunProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-dingjun-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -4405,7 +4562,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirty-fifth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, and Ma Dai joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng Pass, and Yangping scout battles, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-thirty-sixth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, and Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng Pass, Yangping scout, and Dingjun vanguard battles, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/thirty-sixth-battle-map.svg b/src/assets/images/battle/thirty-sixth-battle-map.svg new file mode 100644 index 0000000..3526b47 --- /dev/null +++ b/src/assets/images/battle/thirty-sixth-battle-map.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battleItems.ts b/src/game/data/battleItems.ts index 2c62736..8e623ee 100644 --- a/src/game/data/battleItems.ts +++ b/src/game/data/battleItems.ts @@ -194,6 +194,15 @@ export const itemCatalog: Record = { strategyBonus: 2, effects: ['공명 연계 판정 보정'] }, + 'mountain-guide-scroll': { + id: 'mountain-guide-scroll', + name: '산악 지형도', + slot: 'accessory', + rank: 'treasure', + description: '한중 산길과 능선의 보급로를 적은 지형도.', + strategyBonus: 2, + effects: ['산지와 숲 지형 판단에 도움을 준다'] + }, 'bravery-token': { id: 'bravery-token', name: '호담패', diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 1131a19..cb879d3 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -63,6 +63,10 @@ import { thirtyThirdBattleMap, thirtyThirdBattleUnits, thirtyThirdBattleVictoryPages, + thirtySixthBattleBonds, + thirtySixthBattleMap, + thirtySixthBattleUnits, + thirtySixthBattleVictoryPages, thirteenthBattleBonds, thirteenthBattleMap, thirteenthBattleUnits, @@ -180,7 +184,8 @@ export type BattleScenarioId = | 'thirty-second-battle-mianzhu-gate' | 'thirty-third-battle-chengdu-surrender' | 'thirty-fourth-battle-jiameng-pass' - | 'thirty-fifth-battle-yangping-scout'; + | 'thirty-fifth-battle-yangping-scout' + | 'thirty-sixth-battle-dingjun-vanguard'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2245,6 +2250,69 @@ export const thirtyFifthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const thirtySixthBattleScenario: BattleScenarioDefinition = { + id: 'thirty-sixth-battle-dingjun-vanguard', + title: '정군산 전초전', + victoryConditionLabel: '하후연 선봉 격파', + defeatConditionLabel: '유비 또는 황충 퇴각', + openingObjectiveLines: [ + '정군산은 한중 전선의 고지입니다. 능선을 장악하면 한중 본전의 주도권을 유비군이 가져올 수 있습니다.', + '황충과 법정의 계책이 이번 전투의 중심입니다. 황충이 무너지면 정군산의 기세도 꺾입니다.', + '마초와 마대는 서쪽 능선을 흔들고, 왕평을 설득할 실마리를 찾아 한중 산길의 마음을 유비군 쪽으로 돌리십시오.' + ], + map: thirtySixthBattleMap, + units: thirtySixthBattleUnits, + bonds: thirtySixthBattleBonds, + mapTextureKey: 'battle-map-thirty-sixth', + leaderUnitId: 'dingjun-leader-xiahou-yuan', + quickVictoryTurnLimit: 54, + baseVictoryGold: 6240, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '하후연 선봉 격파', + rewardGold: 3920, + unitId: 'dingjun-leader-xiahou-yuan' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1000, + unitId: 'liu-bei' + }, + { + id: 'huang-zhong', + kind: 'keep-unit-alive', + label: '황충 생존', + rewardGold: 1640, + unitId: 'huang-zhong' + }, + { + id: 'dingjun-ridge-storehouse', + kind: 'secure-terrain', + label: '정군산 보급 창고 확보', + rewardGold: 1860, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '54턴 이내 승리', + rewardGold: 1440, + maxTurn: 54 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'huang-zhong' } + ], + itemRewards: ['콩 26', '상처약 20', '탁주 9', '산악 지형도 +1', '왕평 합류'], + victoryPages: thirtySixthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2282,7 +2350,8 @@ export const battleScenarios: Record 'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario, 'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario, 'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario, - 'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario + 'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario, + 'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 7f213f9..28896ec 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -22,6 +22,7 @@ import { thirtyFirstBattleScenario, thirtySecondBattleScenario, thirtyThirdBattleScenario, + thirtySixthBattleScenario, thirtiethBattleScenario, twentyEighthBattleScenario, twentyNinthBattleScenario, @@ -81,6 +82,8 @@ import { thirtySecondBattleVictoryPages, thirtyThirdBattleIntroPages, thirtyThirdBattleVictoryPages, + thirtySixthBattleIntroPages, + thirtySixthBattleVictoryPages, thirtiethBattleIntroPages, thirtiethBattleVictoryPages, twentyEighthBattleIntroPages, @@ -487,13 +490,24 @@ const sortieFlows: Record = { }, [thirtyFifthBattleScenario.id]: { afterBattleId: thirtyFifthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '한중 본전 준비', + eyebrow: '다음 전장', + title: thirtySixthBattleScenario.title, description: - '양평관 정찰로 한중의 길목과 장로군의 배치를 확인했습니다. 마대까지 합류했으니 다음은 한중을 둘러싼 본격적인 전선을 준비해야 합니다.', - rewardHint: '다음 장: 한중 본전 준비 중', - pages: thirtyFifthBattleVictoryPages, - unavailableNotice: '한중 본전과 촉한 건국 선포는 다음 작업에서 이어집니다. 마초와 마대의 기병 운용, 황권의 지형 판단, 제갈량의 전선 설계를 준비하십시오.' + '양평관 정찰로 한중의 길목과 장로군의 배치를 확인했습니다. 이제 정군산으로 올라 하후연의 선봉을 흔들고, 한중 산길을 아는 왕평을 유비군 쪽으로 끌어들일 차례입니다.', + rewardHint: `예상 보상: ${thirtySixthBattleScenario.title} 개방 / 왕평 합류`, + nextBattleId: thirtySixthBattleScenario.id, + campaignStep: 'thirty-sixth-battle', + pages: [...thirtyFifthBattleVictoryPages, ...thirtySixthBattleIntroPages] + }, + [thirtySixthBattleScenario.id]: { + afterBattleId: thirtySixthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '한중 결전 준비', + description: + '정군산 전초전으로 한중의 고지를 확보했고 왕평이 합류했습니다. 다음은 한중 결전과 촉한 건국 선포로 이어질 큰 흐름을 준비해야 합니다.', + rewardHint: '다음 장: 한중 결전 준비 중', + pages: thirtySixthBattleVictoryPages, + unavailableNotice: '한중 결전과 촉한 건국 선포는 다음 작업에서 이어집니다. 황충, 법정, 왕평의 산악 전선과 마초·마대의 기병 축을 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 081850d..3ed81aa 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2262,6 +2262,68 @@ export const thirtyFifthBattleVictoryPages: StoryPage[] = [ } ]; +export const thirtySixthBattleIntroPages: StoryPage[] = [ + { + id: 'thirty-sixth-dingjun-mountain', + bgm: 'story-dark', + chapter: '정군산으로', + background: 'story-three-heroes', + speaker: '제갈량', + text: '양평관의 길을 살핀 뒤 한중의 핵심은 정군산으로 모였습니다. 산을 잡으면 한중을 굽어보고, 산을 잃으면 북문 전체가 흔들립니다.' + }, + { + id: 'thirty-sixth-huang-zhong-plan', + bgm: 'battle-prep', + chapter: '노장의 결심', + background: 'story-liu-bei', + speaker: '황충', + text: '나이가 전열을 막지는 못합니다. 법정의 계책으로 능선을 잡아 주신다면, 이 늙은 활과 칼이 하후연의 기세를 꺾어 보이겠습니다.' + }, + { + id: 'thirty-sixth-western-cavalry', + bgm: 'battle-prep', + chapter: '서량 기병의 우회로', + background: 'story-militia', + speaker: '마초', + text: '정군산 서쪽 능선은 말이 달리기 어렵지만, 마대와 함께 길을 잡으면 적의 눈을 돌릴 수 있습니다. 황충 장군이 치는 순간을 만들겠습니다.' + }, + { + id: 'thirty-sixth-sortie', + bgm: 'battle-prep', + chapter: '정군산 전초전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '정군산을 얻으면 한중의 이름이 가까워질 것이오. 황충과 법정은 산을 보고, 마초와 마대는 길을 흔들며, 모두 유비군의 깃발을 산 위에 세우시오.' + } +]; + +export const thirtySixthBattleVictoryPages: StoryPage[] = [ + { + id: 'thirty-sixth-victory-ridge-held', + bgm: 'militia-theme', + chapter: '정군산 능선을 잡다', + background: 'story-sortie', + text: '정군산의 첫 능선이 유비군 손에 들어오자 한중 전선의 무게가 바뀌었습니다. 하후연의 선봉은 물러났고, 산길의 병사들은 새 주인을 가늠하기 시작했습니다.' + }, + { + id: 'thirty-sixth-wang-ping-joins', + bgm: 'battle-prep', + chapter: '왕평의 투항', + background: 'story-liu-bei', + speaker: '왕평', + text: '저는 한중의 산길과 병사들의 마음을 압니다. 조조군의 군율은 엄하나 이 땅을 품지는 못했습니다. 유비공께서 한중을 안정시킨다면 제 길도 그곳에 있겠습니다.' + }, + { + id: 'thirty-sixth-next-foundation', + bgm: 'story-dark', + chapter: '한중의 깃발', + background: 'story-three-heroes', + speaker: '제갈량', + text: '정군산의 승리와 왕평의 합류로 한중의 산길은 조금씩 유비군 편으로 기울었습니다. 이제 한중 본전과 촉한의 이름이 더 가까워집니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2617,6 +2679,12 @@ export const thirtyFifthBattleMap: BattleMap = { terrain: createThirtyFifthBattleTerrain() }; +export const thirtySixthBattleMap: BattleMap = { + width: 72, + height: 54, + terrain: createThirtySixthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4882,6 +4950,30 @@ export const hanzhongScoutRecruitUnits: UnitData[] = [ } ]; +export const hanzhongMainRecruitUnits: UnitData[] = [ + { + id: 'wang-ping', + name: '왕평', + faction: 'ally', + className: '한중 보병', + classKey: 'spearman', + level: 25, + exp: 0, + hp: 54, + maxHp: 54, + attack: 17, + move: 4, + stats: { might: 82, intelligence: 70, leadership: 78, agility: 72, luck: 66 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 22 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 18 }, + accessory: { itemId: 'mountain-guide-scroll', level: 2, exp: 10 } + }, + x: 15, + y: 36 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -13667,6 +13759,77 @@ export const thirtyFifthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('yangping-leader-zhang-wei', '장위', '양평관 선봉장', 'rebelLeader', 73, 382, 108, 66, 24) ]; +const thirtySixthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 48 }, + 'guan-yu': { x: 5, y: 46 }, + 'zhang-fei': { x: 4, y: 50 }, + 'jian-yong': { x: 3, y: 51 }, + 'mi-zhu': { x: 5, y: 52 }, + 'sun-qian': { x: 7, y: 51 }, + 'zhao-yun': { x: 8, y: 47 }, + 'zhuge-liang': { x: 6, y: 46 }, + 'ma-liang': { x: 8, y: 52 }, + 'yi-ji': { x: 9, y: 49 }, + 'gong-zhi': { x: 10, y: 52 }, + 'huang-zhong': { x: 7, y: 48 }, + 'wei-yan': { x: 10, y: 46 }, + 'pang-tong': { x: 9, y: 47 }, + 'fa-zheng': { x: 11, y: 49 }, + 'wu-yi': { x: 12, y: 47 }, + 'yan-yan': { x: 11, y: 45 }, + 'li-yan': { x: 13, y: 46 }, + 'huang-quan': { x: 14, y: 48 }, + 'ma-chao': { x: 13, y: 44 }, + 'ma-dai': { x: 15, y: 45 } +}; + +export const thirtySixthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits, + ...hanzhongScoutRecruitUnits + ].map((unit) => placeScenarioUnit(unit, thirtySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('dingjun-scout-a', '정군산 척후', '서령 척후', 'bandit', 72, 238, 82, 20, 47), + createThirtyFirstEnemyUnit('dingjun-scout-b', '정군산 척후', '산길 척후', 'bandit', 72, 238, 82, 29, 45), + createThirtyFirstEnemyUnit('dingjun-scout-c', '정군산 척후', '강변 척후', 'bandit', 73, 246, 84, 38, 43), + createThirtyFirstEnemyUnit('dingjun-infantry-a', '조조 보병', '서령 보병', 'yellowTurban', 73, 348, 100, 36, 40), + createThirtyFirstEnemyUnit('dingjun-infantry-b', '조조 보병', '중턱 보병', 'yellowTurban', 73, 348, 100, 42, 39), + createThirtyFirstEnemyUnit('dingjun-infantry-c', '하후연 보병', '능선 보병', 'yellowTurban', 74, 360, 104, 49, 35), + createThirtyFirstEnemyUnit('dingjun-infantry-d', '하후연 보병', '본진 보병', 'yellowTurban', 74, 360, 104, 53, 33), + createThirtyFirstEnemyUnit('dingjun-infantry-e', '정군산 보병', '창고 보병', 'yellowTurban', 75, 372, 106, 63, 30), + createThirtyFirstEnemyUnit('dingjun-archer-a', '조조 궁병', '서벽 궁병', 'archer', 73, 246, 84, 39, 37), + createThirtyFirstEnemyUnit('dingjun-archer-b', '조조 궁병', '능선 궁병', 'archer', 74, 254, 86, 51, 29), + createThirtyFirstEnemyUnit('dingjun-archer-c', '조조 궁병', '고지 궁병', 'archer', 74, 254, 86, 60, 24), + createThirtyFirstEnemyUnit('dingjun-archer-d', '조조 궁병', '창고 궁병', 'archer', 75, 262, 88, 66, 32), + createThirtyFirstEnemyUnit('dingjun-cavalry-a', '조조 기병', '좌익 기병', 'cavalry', 74, 364, 104, 32, 49), + createThirtyFirstEnemyUnit('dingjun-cavalry-b', '조조 기병', '우익 기병', 'cavalry', 74, 364, 104, 47, 44), + createThirtyFirstEnemyUnit('dingjun-cavalry-c', '하후연 기병', '돌격 기병', 'cavalry', 75, 376, 106, 59, 38), + createThirtyFirstEnemyUnit('dingjun-cavalry-d', '하후연 기병', '친위 기병', 'cavalry', 75, 376, 106, 67, 33), + createThirtyFirstEnemyUnit('dingjun-guard-a', '하후연 친위대', '능선 친위대', 'yellowTurban', 75, 388, 108, 56, 29), + createThirtyFirstEnemyUnit('dingjun-guard-b', '하후연 친위대', '본진 친위대', 'yellowTurban', 75, 388, 108, 64, 28), + createThirtyFirstEnemyUnit('dingjun-strategist-a', '조조 책사', '고지 책사', 'strategist', 74, 254, 88, 54, 25), + createThirtyFirstEnemyUnit('dingjun-strategist-b', '조조 책사', '본진 책사', 'strategist', 75, 262, 90, 66, 22), + createThirtyFirstEnemyUnit('dingjun-ambusher-a', '조조 복병', '남령 복병', 'bandit', 73, 248, 86, 43, 47), + createThirtyFirstEnemyUnit('dingjun-ambusher-b', '조조 복병', '동령 복병', 'bandit', 73, 248, 86, 56, 41), + createThirtyFirstEnemyUnit('dingjun-officer-zhang-he', '장합', '조조 선봉장', 'cavalry', 76, 392, 112, 61, 27), + createThirtyFirstEnemyUnit('dingjun-officer-wang-ping', '왕평', '한중 길잡이', 'spearman', 75, 330, 100, 58, 31), + createThirtyFirstEnemyUnit('dingjun-leader-xiahou-yuan', '하후연', '정군산 주장', 'rebelLeader', 77, 418, 116, 68, 25) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -14126,6 +14289,33 @@ export const hanzhongScoutRecruitBonds: BattleBond[] = [ } ]; +export const hanzhongMainRecruitBonds: BattleBond[] = [ + { + id: 'huang-zhong__wang-ping', + unitIds: ['huang-zhong', 'wang-ping'], + title: '노장과 산길', + level: 28, + exp: 0, + description: '황충의 과감한 고지 돌파와 왕평의 산길 판단이 맞물려 정군산 전투의 흐름을 안정시킵니다.' + }, + { + id: 'zhuge-liang__wang-ping', + unitIds: ['zhuge-liang', 'wang-ping'], + title: '한중 지형책', + level: 26, + exp: 0, + description: '제갈량의 큰 전선 설계와 왕평의 실제 산길 지식이 만나 한중 공략의 정확도를 높입니다.' + }, + { + id: 'ma-dai__wang-ping', + unitIds: ['ma-dai', 'wang-ping'], + title: '기병과 산길잡이', + level: 24, + exp: 0, + description: '마대의 안정된 기병 운용과 왕평의 지형 감각이 합쳐져 좁은 능선에서도 퇴로를 잃지 않습니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -14174,6 +14364,9 @@ export const thirtyFourthBattleBonds: BattleBond[] = [...thirtyThirdBattleBonds, export const thirtyFifthBattleBonds: BattleBond[] = [...thirtyFourthBattleBonds, ...hanzhongOpeningRecruitBonds].map( cloneBattleBondForScenario ); +export const thirtySixthBattleBonds: BattleBond[] = [...thirtyFifthBattleBonds, ...hanzhongScoutRecruitBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -15709,6 +15902,77 @@ function createThirtyFifthBattleTerrain(): TerrainType[][] { ); } +function createThirtySixthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 54 }, (_, y) => + Array.from({ length: 72 }, (_, x): TerrainType => { + if (x <= 4 && y >= 47) { + return 'camp'; + } + if ( + (x >= 54 && x <= 70 && y >= 12 && y <= 31) || + (x >= 50 && x <= 62 && y >= 8 && y <= 20) || + (x >= 60 && x <= 71 && y >= 23 && y <= 38) || + (x >= 57 && x <= 65 && y >= 34 && y <= 41) + ) { + return 'fort'; + } + if ( + (x >= 19 && x <= 23 && y >= 45 && y <= 47) || + (x >= 39 && x <= 42 && y >= 39 && y <= 41) || + (x >= 50 && x <= 52 && y >= 34 && y <= 36) || + (x >= 62 && x <= 65 && y >= 37 && y <= 40) + ) { + return 'village'; + } + if ( + (y >= 49 && x >= 4 && x <= 24) || + (x >= 23 && x <= 36 && y >= 45 && y <= 49) || + (x >= 35 && x <= 50 && y >= 40 && y <= 45) || + (x >= 49 && x <= 61 && y >= 34 && y <= 40) || + (x >= 60 && x <= 69 && y >= 28 && y <= 34) || + (x >= 64 && x <= 71 && y >= 24 && y <= 28) + ) { + return 'road'; + } + if ((x === 17 || x === 18) && y >= 0 && y <= 43) { + return 'river'; + } + if ((x === 40 || x === 41) && y >= 3 && y <= 38) { + return 'river'; + } + if ((x === 52 || x === 53) && y >= 39 && y <= 53) { + return 'river'; + } + if ( + (x >= 4 && x <= 25 && y >= 8 && y <= 38) || + (x >= 26 && x <= 39 && y >= 15 && y <= 39) || + (x >= 43 && x <= 57 && y >= 41 && y <= 53) || + (x >= 55 && x <= 71 && y >= 37 && y <= 52) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 16 && y <= 22) || + (x >= 26 && x <= 39 && y <= 14) || + (x >= 40 && x <= 58 && y >= 1 && y <= 17) || + (x >= 60 && x <= 71 && y <= 15) || + (x >= 30 && x <= 44 && y >= 46 && y <= 53) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 46) || + (x >= 69 && y >= 33) || + (x >= 54 && x <= 57 && y <= 8) || + (x >= 25 && x <= 29 && y >= 24 && y <= 36) + ) { + 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 99a170b..3e23f60 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -484,6 +484,7 @@ const unitTexture: Record = { 'chengdu-final-leader-huang-quan': 'unit-rebel-leader', 'ma-chao': 'unit-rebel-cavalry', 'ma-dai': 'unit-rebel-cavalry', + 'wang-ping': 'unit-rebel', 'jiameng-scout-a': 'unit-rebel', 'jiameng-scout-b': 'unit-rebel', 'jiameng-scout-c': 'unit-rebel', @@ -529,7 +530,32 @@ const unitTexture: Record = { 'yangping-ambusher-a': 'unit-rebel', 'yangping-ambusher-b': 'unit-rebel', 'yangping-officer-yan-pu': 'unit-rebel-archer', - 'yangping-leader-zhang-wei': 'unit-rebel-leader' + 'yangping-leader-zhang-wei': 'unit-rebel-leader', + 'dingjun-scout-a': 'unit-rebel', + 'dingjun-scout-b': 'unit-rebel', + 'dingjun-scout-c': 'unit-rebel', + 'dingjun-infantry-a': 'unit-rebel', + 'dingjun-infantry-b': 'unit-rebel', + 'dingjun-infantry-c': 'unit-rebel', + 'dingjun-infantry-d': 'unit-rebel', + 'dingjun-infantry-e': 'unit-rebel', + 'dingjun-archer-a': 'unit-rebel-archer', + 'dingjun-archer-b': 'unit-rebel-archer', + 'dingjun-archer-c': 'unit-rebel-archer', + 'dingjun-archer-d': 'unit-rebel-archer', + 'dingjun-cavalry-a': 'unit-rebel-cavalry', + 'dingjun-cavalry-b': 'unit-rebel-cavalry', + 'dingjun-cavalry-c': 'unit-rebel-cavalry', + 'dingjun-cavalry-d': 'unit-rebel-cavalry', + 'dingjun-guard-a': 'unit-rebel', + 'dingjun-guard-b': 'unit-rebel', + 'dingjun-strategist-a': 'unit-rebel-archer', + 'dingjun-strategist-b': 'unit-rebel-archer', + 'dingjun-ambusher-a': 'unit-rebel', + 'dingjun-ambusher-b': 'unit-rebel', + 'dingjun-officer-zhang-he': 'unit-rebel-cavalry', + 'dingjun-officer-wang-ping': 'unit-rebel', + 'dingjun-leader-xiahou-yuan': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1517,7 +1543,32 @@ const enemyAiByUnitId: Record = { 'yangping-ambusher-a': 'aggressive', 'yangping-ambusher-b': 'aggressive', 'yangping-officer-yan-pu': 'hold', - 'yangping-leader-zhang-wei': 'guard' + 'yangping-leader-zhang-wei': 'guard', + 'dingjun-scout-a': 'aggressive', + 'dingjun-scout-b': 'aggressive', + 'dingjun-scout-c': 'aggressive', + 'dingjun-infantry-a': 'guard', + 'dingjun-infantry-b': 'guard', + 'dingjun-infantry-c': 'guard', + 'dingjun-infantry-d': 'guard', + 'dingjun-infantry-e': 'guard', + 'dingjun-archer-a': 'hold', + 'dingjun-archer-b': 'hold', + 'dingjun-archer-c': 'hold', + 'dingjun-archer-d': 'hold', + 'dingjun-cavalry-a': 'aggressive', + 'dingjun-cavalry-b': 'aggressive', + 'dingjun-cavalry-c': 'aggressive', + 'dingjun-cavalry-d': 'aggressive', + 'dingjun-guard-a': 'guard', + 'dingjun-guard-b': 'guard', + 'dingjun-strategist-a': 'hold', + 'dingjun-strategist-b': 'hold', + 'dingjun-ambusher-a': 'aggressive', + 'dingjun-ambusher-b': 'aggressive', + 'dingjun-officer-zhang-he': 'aggressive', + 'dingjun-officer-wang-ping': 'guard', + 'dingjun-leader-xiahou-yuan': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 483e230..08ac2e4 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -21,6 +21,7 @@ import thirtyFifthBattleMapUrl from '../../assets/images/battle/thirty-fifth-bat import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg'; import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg'; import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg'; +import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-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'; @@ -128,6 +129,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirty-third', thirtyThirdBattleMapUrl); this.load.image('battle-map-thirty-fourth', thirtyFourthBattleMapUrl); this.load.image('battle-map-thirty-fifth', thirtyFifthBattleMapUrl); + this.load.image('battle-map-thirty-sixth', thirtySixthBattleMapUrl); 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 a3534c2..7a2fab8 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -21,6 +21,8 @@ import { guiyangRecruitUnits, hanzhongOpeningRecruitBonds, hanzhongOpeningRecruitUnits, + hanzhongMainRecruitBonds, + hanzhongMainRecruitUnits, hanzhongScoutRecruitBonds, hanzhongScoutRecruitUnits, jingzhouRecruitBonds, @@ -341,7 +343,11 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ title: '촉한 건국', period: '한실을 잇는 깃발', description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.', - battleIds: ['thirty-fourth-battle-jiameng-pass', 'thirty-fifth-battle-yangping-scout'], + battleIds: [ + 'thirty-fourth-battle-jiameng-pass', + 'thirty-fifth-battle-yangping-scout', + 'thirty-sixth-battle-dingjun-vanguard' + ], nextHints: ['왕업 선언', '오호대장군', '북벌 준비'] } ]; @@ -381,7 +387,8 @@ const campBattleIds = { thirtySecond: 'thirty-second-battle-mianzhu-gate', thirtyThird: 'thirty-third-battle-chengdu-surrender', thirtyFourth: 'thirty-fourth-battle-jiameng-pass', - thirtyFifth: 'thirty-fifth-battle-yangping-scout' + thirtyFifth: 'thirty-fifth-battle-yangping-scout', + thirtySixth: 'thirty-sixth-battle-dingjun-vanguard' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -776,6 +783,18 @@ const sortieRulesByBattleId: Partial