diff --git a/docs/roadmap.md b/docs/roadmap.md index 2f8d361..842b98f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -56,16 +56,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-second battle Mianzhu Gate pressure route, using Yan Yan's first sortie choice across a 64x46 Chengdu approach battlefield and recruiting Li Yan after the defense line yields - 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 - 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-fourth 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 camp states, campaign timeline state, and camp save state +- 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 ## Next Steps -1. Continue the Hanzhong front after Jiameng Pass, turning Ma Chao's cavalry and Huang Quan's counsel into the first clear Shu-Han northern campaign choices -2. Add more recruitable officers as the campaign moves from Hanzhong preparation toward the formal Shu-Han foundation +1. Continue from Yangping scout work into the Hanzhong main front, using Ma Chao and Ma Dai as the cavalry decision 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 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 9f1d801..aa72576 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 !== 34 || + progressTabState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postBowangProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postChangbanProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postWulingProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postChangshaProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postYizhouProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postFuPassProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postLuoProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postLuofengProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 35 || 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 !== 34 || + postChengduProgressState.campaignProgress?.totalKnown !== 35 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' @@ -4222,15 +4222,171 @@ try { if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || - postJiamengProgressState.campaignProgress?.totalKnown !== 34 || + postJiamengProgressState.campaignProgress?.totalKnown !== 35 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || - postJiamengProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' ) { - throw new Error(`Expected post-Jiameng progress tab to complete Ma Chao chapter and pause on the next Hanzhong step: ${JSON.stringify(postJiamengProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Jiameng progress tab to complete Ma Chao chapter and open Yangping scout battle: ${JSON.stringify(postJiamengProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jiameng-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const yangpingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yangpingSortieState?.sortieVisible || + !yangpingSortieState.sortiePlan?.objectiveLine?.includes('양평관 정찰전') || + !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) || + !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) || + !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || + yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai') || + yangpingSortieState.sortieRoster?.length < 20 || + yangpingSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected Jiameng camp sortie prep to target Yangping scout battle with Ma Chao selectable and Ma Dai absent before victory: ${JSON.stringify(yangpingSortieState)}`); + } + assertSortieTacticalRoster(yangpingSortieState, [ + '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' + ]); + + const yangpingPriorityUnits = ['ma-chao', 'huang-quan', 'zhuge-liang', 'zhao-yun', 'fa-zheng']; + for (const unitId of yangpingPriorityUnits) { + 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' && !yangpingPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const yangpingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yangpingPriorityUnits.every((unitId) => + yangpingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + yangpingSortieReadyState.sortiePlan?.selectedCount !== 6 || + yangpingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Yangping sortie to deploy Ma Chao, Huang Quan, Zhuge Liang, Zhao Yun, and Fa Zheng while preserving six-officer pressure: ${JSON.stringify(yangpingSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-yangping-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-yangping-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredThirtyFifthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout'; + }); + if (enteredThirtyFifthBattle) { + 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-fifth-battle-yangping-scout' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-thirty-fifth-battle.png', fullPage: true }); + + const thirtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const thirtyFifthEnemies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const thirtyFifthAllies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'ally'); + const thirtyFifthEnemyBehaviors = new Set(thirtyFifthEnemies.map((unit) => unit.ai)); + if ( + thirtyFifthBattleState.camera?.mapWidth !== 70 || + thirtyFifthBattleState.camera?.mapHeight !== 52 || + thirtyFifthBattleState.victoryConditionLabel !== '양평관 선봉 격파' || + thirtyFifthEnemies.length < 24 || + !thirtyFifthEnemyBehaviors.has('aggressive') || + !thirtyFifthEnemyBehaviors.has('guard') || + !thirtyFifthEnemyBehaviors.has('hold') || + !thirtyFifthEnemies.some((unit) => unit.id === 'yangping-leader-zhang-wei') || + !thirtyFifthEnemies.some((unit) => unit.id === 'yangping-officer-yan-pu') || + !thirtyFifthAllies.some((unit) => unit.id === 'ma-chao') || + !thirtyFifthAllies.some((unit) => unit.id === 'huang-quan') || + !thirtyFifthAllies.some((unit) => unit.id === 'zhuge-liang') || + !thirtyFifthAllies.some((unit) => unit.id === 'zhao-yun') || + !thirtyFifthAllies.some((unit) => unit.id === 'fa-zheng') || + thirtyFifthAllies.some((unit) => unit.id === 'ma-dai') + ) { + throw new Error(`Expected thirty-fifth battle to use Yangping map, Zhang Wei objective, Ma Chao first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFifthBattleState)}`); + } + + 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 thirtyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + thirtyFifthCampState?.campBattleId !== 'thirty-fifth-battle-yangping-scout' || + thirtyFifthCampState.campTitle !== '양평관 정찰 후 군영' || + thirtyFifthCampState.availableDialogueIds?.length !== 3 || + !thirtyFifthCampState.availableDialogueIds.every((id) => id.endsWith('yangping')) || + thirtyFifthCampState.availableVisitIds?.length !== 2 || + !thirtyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-dai') || + !thirtyFifthCampState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited) || + thirtyFifthCampState.rosterCollection?.total < 21 + ) { + throw new Error(`Expected thirty-fifth camp to recruit Ma Dai and expose Yangping dialogue/visit sets: ${JSON.stringify(thirtyFifthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-yangping-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postYangpingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postYangpingProgressState?.activeTab !== 'progress' || + postYangpingProgressState.campaignProgress?.completedKnown !== 35 || + postYangpingProgressState.campaignProgress?.totalKnown !== 35 || + postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || + postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-yangping-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -4249,7 +4405,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirty-fourth-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, and Ma Chao joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, and Jiameng Pass battles, result states, and debug API at ${targetUrl}`); + 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}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/thirty-fifth-battle-map.svg b/src/assets/images/battle/thirty-fifth-battle-map.svg new file mode 100644 index 0000000..cf56f79 --- /dev/null +++ b/src/assets/images/battle/thirty-fifth-battle-map.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battleItems.ts b/src/game/data/battleItems.ts index c8e5c1e..2c62736 100644 --- a/src/game/data/battleItems.ts +++ b/src/game/data/battleItems.ts @@ -67,6 +67,15 @@ export const itemCatalog: Record = { attackBonus: 5, effects: ['연속 공격 판정 보정'] }, + 'iron-spear': { + id: 'iron-spear', + name: '철창', + slot: 'weapon', + rank: 'common', + description: '숙련된 보병과 기병이 두루 쓰는 견고한 창.', + attackBonus: 3, + effects: ['전열 유지와 반격에 무난하다'] + }, 'sky-piercer-halberd': { id: 'sky-piercer-halberd', name: '방천화극', @@ -76,6 +85,15 @@ export const itemCatalog: Record = { attackBonus: 6, effects: ['단독으로 돌파할 때 피해 보너스'] }, + 'western-cavalry-spear': { + id: 'western-cavalry-spear', + name: '서량장창', + slot: 'weapon', + rank: 'common', + description: '서량 기병이 말 위에서 다루기 좋게 균형을 맞춘 장창.', + attackBonus: 4, + effects: ['기병 돌격과 추격전에 잘 맞는다'] + }, 'white-feather-fan': { id: 'white-feather-fan', name: '백우선', diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 6d79412..1131a19 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -43,6 +43,10 @@ import { tenthBattleMap, tenthBattleUnits, tenthBattleVictoryPages, + thirtyFifthBattleBonds, + thirtyFifthBattleMap, + thirtyFifthBattleUnits, + thirtyFifthBattleVictoryPages, thirtyFourthBattleBonds, thirtyFourthBattleMap, thirtyFourthBattleUnits, @@ -175,7 +179,8 @@ export type BattleScenarioId = | 'thirty-first-battle-luo-main-gate' | 'thirty-second-battle-mianzhu-gate' | 'thirty-third-battle-chengdu-surrender' - | 'thirty-fourth-battle-jiameng-pass'; + | 'thirty-fourth-battle-jiameng-pass' + | 'thirty-fifth-battle-yangping-scout'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2177,6 +2182,69 @@ export const thirtyFourthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const thirtyFifthBattleScenario: BattleScenarioDefinition = { + id: 'thirty-fifth-battle-yangping-scout', + title: '양평관 정찰전', + victoryConditionLabel: '양평관 선봉 격파', + defeatConditionLabel: '유비 또는 마초 퇴각', + openingObjectiveLines: [ + '양평관은 한중 전선으로 들어가는 첫 관문입니다. 관문을 빼앗기보다 지형과 보급로를 확인하는 것이 목표입니다.', + '마초는 이번 전장이 합류 후 첫 출전입니다. 서량 기병의 속도를 활용하되 유비군의 전열과 너무 멀어지지 않게 해야 합니다.', + '장위의 선봉과 염포의 책략대를 물리치고 관문 창고를 확보하면, 흩어진 서량 기병과 마대가 합류할 길이 열립니다.' + ], + map: thirtyFifthBattleMap, + units: thirtyFifthBattleUnits, + bonds: thirtyFifthBattleBonds, + mapTextureKey: 'battle-map-thirty-fifth', + leaderUnitId: 'yangping-leader-zhang-wei', + quickVictoryTurnLimit: 52, + baseVictoryGold: 5900, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '양평관 선봉 격파', + rewardGold: 3720, + unitId: 'yangping-leader-zhang-wei' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1000, + unitId: 'liu-bei' + }, + { + id: 'ma-chao', + kind: 'keep-unit-alive', + label: '마초 생존', + rewardGold: 1500, + unitId: 'ma-chao' + }, + { + id: 'yangping-storehouse', + kind: 'secure-terrain', + label: '양평관 창고 확보', + rewardGold: 1760, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '52턴 이내 승리', + rewardGold: 1360, + maxTurn: 52 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'ma-chao' } + ], + itemRewards: ['콩 24', '상처약 18', '탁주 8', '서량장창 +1', '마대 합류'], + victoryPages: thirtyFifthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2213,7 +2281,8 @@ export const battleScenarios: Record 'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario, 'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario, 'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario, - 'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario + 'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario, + 'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 7b7e076..7f213f9 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -17,6 +17,7 @@ import { seventeenthBattleScenario, tenthBattleScenario, thirteenthBattleScenario, + thirtyFifthBattleScenario, thirtyFourthBattleScenario, thirtyFirstBattleScenario, thirtySecondBattleScenario, @@ -70,6 +71,8 @@ import { tenthBattleVictoryPages, thirteenthBattleIntroPages, thirteenthBattleVictoryPages, + thirtyFifthBattleIntroPages, + thirtyFifthBattleVictoryPages, thirtyFourthBattleIntroPages, thirtyFourthBattleVictoryPages, thirtyFirstBattleIntroPages, @@ -473,13 +476,24 @@ const sortieFlows: Record = { }, [thirtyFourthBattleScenario.id]: { afterBattleId: thirtyFourthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '한중 전선 준비', + eyebrow: '다음 전장', + title: thirtyFifthBattleScenario.title, description: - '가맹관에서 마초가 합류했습니다. 촉한 건국의 군사 기반을 다지려면 이제 한중 전선을 본격적으로 준비해야 합니다.', - rewardHint: '다음 장: 한중 전선 준비 중', - pages: thirtyFourthBattleVictoryPages, - unavailableNotice: '한중 전선과 촉한 건국 선포는 다음 작업에서 이어집니다. 새로 합류한 마초를 포함해 기병 돌파와 내정 안정 조합을 준비하십시오.' + '가맹관에서 마초가 합류했습니다. 이제 양평관으로 정찰대를 보내 한중 전선의 첫 지형과 보급 창고를 확인하고, 흩어진 서량 기병을 규합할 차례입니다.', + rewardHint: `예상 보상: ${thirtyFifthBattleScenario.title} 개방 / 마대 합류`, + nextBattleId: thirtyFifthBattleScenario.id, + campaignStep: 'thirty-fifth-battle', + pages: [...thirtyFourthBattleVictoryPages, ...thirtyFifthBattleIntroPages] + }, + [thirtyFifthBattleScenario.id]: { + afterBattleId: thirtyFifthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '한중 본전 준비', + description: + '양평관 정찰로 한중의 길목과 장로군의 배치를 확인했습니다. 마대까지 합류했으니 다음은 한중을 둘러싼 본격적인 전선을 준비해야 합니다.', + rewardHint: '다음 장: 한중 본전 준비 중', + pages: thirtyFifthBattleVictoryPages, + unavailableNotice: '한중 본전과 촉한 건국 선포는 다음 작업에서 이어집니다. 마초와 마대의 기병 운용, 황권의 지형 판단, 제갈량의 전선 설계를 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 1822917..081850d 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2200,6 +2200,68 @@ export const thirtyFourthBattleVictoryPages: StoryPage[] = [ } ]; +export const thirtyFifthBattleIntroPages: StoryPage[] = [ + { + id: 'thirty-fifth-hanzhong-front', + bgm: 'story-dark', + chapter: '한중 전선의 첫 정찰', + background: 'story-three-heroes', + speaker: '제갈량', + text: '마초가 합류하자 가맹관 너머의 길이 열렸습니다. 그러나 양평관은 장로의 군세가 한중으로 드나드는 목으로, 먼저 길과 병력을 살펴야 합니다.' + }, + { + id: 'thirty-fifth-ma-chao-first-sortie', + bgm: 'battle-prep', + chapter: '마초의 첫 출전', + background: 'story-liu-bei', + speaker: '마초', + text: '양평관의 말길은 거칠지만 서량 기병이 숨을 맞추면 뚫을 수 있습니다. 이번에는 제가 앞에서 달리며 유비군의 깃발을 보이겠습니다.' + }, + { + id: 'thirty-fifth-huang-quan-map', + bgm: 'battle-prep', + chapter: '황권의 길잡이', + background: 'story-militia', + speaker: '황권', + text: '적을 모두 몰아내는 것보다 관문 창고와 강변 길을 잡는 것이 중요합니다. 한중을 향한 첫 정찰은 병력보다 지형을 얻는 싸움입니다.' + }, + { + id: 'thirty-fifth-sortie', + bgm: 'battle-prep', + chapter: '양평관 정찰전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '마초는 선봉을 맡고, 황권과 법정은 길을 보시오. 이 싸움은 한중을 빼앗는 첫걸음이 아니라, 한중을 얻을 자격을 살피는 첫걸음이오.' + } +]; + +export const thirtyFifthBattleVictoryPages: StoryPage[] = [ + { + id: 'thirty-fifth-victory-pass-opened', + bgm: 'militia-theme', + chapter: '양평관 길을 열다', + background: 'story-sortie', + text: '양평관의 선봉대가 물러나자 한중으로 향하는 길목과 보급 창고가 유비군의 눈앞에 드러났습니다. 마초의 첫 돌격은 이제 유비군의 돌격이 되었습니다.' + }, + { + id: 'thirty-fifth-ma-dai-joins', + bgm: 'battle-prep', + chapter: '마대의 합류', + background: 'story-liu-bei', + speaker: '마대', + text: '서량의 흩어진 기병을 거두어 마초 형님의 깃발 아래로 데려왔습니다. 유비공께서 허락하신다면 저도 한중 길을 여는 창이 되겠습니다.' + }, + { + id: 'thirty-fifth-next-hanzhong', + bgm: 'story-dark', + chapter: '북문을 향하여', + background: 'story-three-heroes', + speaker: '제갈량', + text: '마초와 마대의 기병이 갖춰지자 북쪽 전선은 더 빠르게 움직일 수 있게 되었습니다. 이제 한중을 둘러싼 큰 싸움이 가까워집니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2549,6 +2611,12 @@ export const thirtyFourthBattleMap: BattleMap = { terrain: createThirtyFourthBattleTerrain() }; +export const thirtyFifthBattleMap: BattleMap = { + width: 70, + height: 52, + terrain: createThirtyFifthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4790,6 +4858,30 @@ export const hanzhongOpeningRecruitUnits: UnitData[] = [ } ]; +export const hanzhongScoutRecruitUnits: UnitData[] = [ + { + id: 'ma-dai', + name: '마대', + faction: 'ally', + className: '서량 기병', + classKey: 'cavalry', + level: 24, + exp: 0, + hp: 50, + maxHp: 50, + attack: 17, + move: 6, + stats: { might: 86, intelligence: 58, leadership: 80, agility: 82, luck: 68 }, + equipment: { + weapon: { itemId: 'western-cavalry-spear', level: 3, exp: 18 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 16 }, + accessory: { itemId: 'bravery-token', level: 2, exp: 10 } + }, + x: 14, + y: 34 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -13507,6 +13599,74 @@ export const thirtyFourthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('jiameng-leader-ma-chao', '마초', '서량 기병장', 'rebelLeader', 69, 364, 102, 63, 20) ]; +const thirtyFifthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 46 }, + 'guan-yu': { x: 5, y: 44 }, + 'zhang-fei': { x: 4, y: 48 }, + 'jian-yong': { x: 3, y: 49 }, + 'mi-zhu': { x: 5, y: 50 }, + 'sun-qian': { x: 7, y: 49 }, + 'zhao-yun': { x: 8, y: 45 }, + 'zhuge-liang': { x: 6, y: 44 }, + 'ma-liang': { x: 8, y: 50 }, + 'yi-ji': { x: 9, y: 47 }, + 'gong-zhi': { x: 10, y: 50 }, + 'huang-zhong': { x: 7, y: 46 }, + 'wei-yan': { x: 10, y: 44 }, + 'pang-tong': { x: 9, y: 45 }, + 'fa-zheng': { x: 11, y: 47 }, + 'wu-yi': { x: 12, y: 45 }, + 'yan-yan': { x: 11, y: 43 }, + 'li-yan': { x: 13, y: 44 }, + 'huang-quan': { x: 14, y: 46 }, + 'ma-chao': { x: 13, y: 42 } +}; + +export const thirtyFifthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits + ].map((unit) => placeScenarioUnit(unit, thirtyFifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('yangping-scout-a', '양평관 척후', '서쪽 척후', 'bandit', 68, 214, 76, 19, 45), + createThirtyFirstEnemyUnit('yangping-scout-b', '양평관 척후', '강변 척후', 'bandit', 68, 214, 76, 27, 43), + createThirtyFirstEnemyUnit('yangping-scout-c', '양평관 척후', '산허리 척후', 'bandit', 69, 222, 78, 36, 41), + createThirtyFirstEnemyUnit('yangping-infantry-a', '장로 보병', '서문 보병', 'yellowTurban', 69, 316, 94, 35, 38), + createThirtyFirstEnemyUnit('yangping-infantry-b', '장로 보병', '강변 보병', 'yellowTurban', 69, 316, 94, 40, 37), + createThirtyFirstEnemyUnit('yangping-infantry-c', '양평관 보병', '관문 보병', 'yellowTurban', 70, 328, 96, 47, 33), + createThirtyFirstEnemyUnit('yangping-infantry-d', '양평관 보병', '관문 보병', 'yellowTurban', 70, 328, 96, 51, 31), + createThirtyFirstEnemyUnit('yangping-infantry-e', '한중 보병', '창고 보병', 'yellowTurban', 71, 340, 98, 61, 28), + createThirtyFirstEnemyUnit('yangping-archer-a', '한중 궁병', '서벽 궁병', 'archer', 69, 222, 78, 38, 35), + createThirtyFirstEnemyUnit('yangping-archer-b', '한중 궁병', '관문 궁병', 'archer', 70, 230, 80, 50, 27), + createThirtyFirstEnemyUnit('yangping-archer-c', '한중 궁병', '고지 궁병', 'archer', 70, 230, 80, 58, 22), + createThirtyFirstEnemyUnit('yangping-archer-d', '한중 궁병', '창고 궁병', 'archer', 71, 238, 82, 64, 30), + createThirtyFirstEnemyUnit('yangping-cavalry-a', '한중 기병', '좌익 기병', 'cavalry', 70, 332, 98, 31, 47), + createThirtyFirstEnemyUnit('yangping-cavalry-b', '한중 기병', '우익 기병', 'cavalry', 70, 332, 98, 45, 42), + createThirtyFirstEnemyUnit('yangping-cavalry-c', '한중 기병', '돌격 기병', 'cavalry', 71, 344, 100, 57, 36), + createThirtyFirstEnemyUnit('yangping-cavalry-d', '한중 기병', '친위 기병', 'cavalry', 71, 344, 100, 65, 31), + createThirtyFirstEnemyUnit('yangping-guard-a', '장로 친위대', '서문 친위대', 'yellowTurban', 71, 356, 102, 55, 27), + createThirtyFirstEnemyUnit('yangping-guard-b', '장로 친위대', '창고 친위대', 'yellowTurban', 71, 356, 102, 62, 26), + createThirtyFirstEnemyUnit('yangping-strategist-a', '한중 책사', '도술 책사', 'strategist', 70, 230, 80, 53, 23), + createThirtyFirstEnemyUnit('yangping-strategist-b', '한중 책사', '장로 사자', 'strategist', 71, 238, 82, 64, 20), + createThirtyFirstEnemyUnit('yangping-ambusher-a', '한중 복병', '남령 복병', 'bandit', 69, 224, 80, 41, 45), + createThirtyFirstEnemyUnit('yangping-ambusher-b', '한중 복병', '동령 복병', 'bandit', 69, 224, 80, 54, 39), + createThirtyFirstEnemyUnit('yangping-officer-yan-pu', '염포', '한중 참모', 'strategist', 72, 252, 86, 60, 24), + createThirtyFirstEnemyUnit('yangping-leader-zhang-wei', '장위', '양평관 선봉장', 'rebelLeader', 73, 382, 108, 66, 24) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -13939,6 +14099,33 @@ export const hanzhongOpeningRecruitBonds: BattleBond[] = [ } ]; +export const hanzhongScoutRecruitBonds: BattleBond[] = [ + { + id: 'ma-chao__ma-dai', + unitIds: ['ma-chao', 'ma-dai'], + title: '서량의 두 창', + level: 30, + exp: 0, + description: '마초의 기세와 마대의 침착함이 함께 움직이며 서량 기병의 돌파와 회수를 안정시킵니다.' + }, + { + id: 'liu-bei__ma-dai', + unitIds: ['liu-bei', 'ma-dai'], + title: '새 깃발의 기병', + level: 24, + exp: 0, + description: '유비가 흩어진 서량 기병을 받아들이며 마대가 새 깃발 아래서 싸울 명분을 얻습니다.' + }, + { + id: 'zhao-yun__ma-dai', + unitIds: ['zhao-yun', 'ma-dai'], + title: '침착한 말발굽', + level: 22, + exp: 0, + description: '조운의 절제와 마대의 안정된 기동이 맞물려 기병대의 후속 움직임을 다듬습니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -13984,6 +14171,9 @@ export const thirtyThirdBattleBonds: BattleBond[] = [...thirtySecondBattleBonds, export const thirtyFourthBattleBonds: BattleBond[] = [...thirtyThirdBattleBonds, ...chengduSurrenderRecruitBonds].map( cloneBattleBondForScenario ); +export const thirtyFifthBattleBonds: BattleBond[] = [...thirtyFourthBattleBonds, ...hanzhongOpeningRecruitBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -15448,6 +15638,77 @@ function createThirtyFourthBattleTerrain(): TerrainType[][] { ); } +function createThirtyFifthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 52 }, (_, y) => + Array.from({ length: 70 }, (_, x): TerrainType => { + if (x <= 4 && y >= 45) { + return 'camp'; + } + if ( + (x >= 52 && x <= 68 && y >= 12 && y <= 30) || + (x >= 48 && x <= 60 && y >= 8 && y <= 19) || + (x >= 58 && x <= 69 && y >= 22 && y <= 36) || + (x >= 55 && x <= 63 && y >= 32 && y <= 39) + ) { + return 'fort'; + } + if ( + (x >= 18 && x <= 22 && y >= 43 && y <= 45) || + (x >= 38 && x <= 41 && y >= 37 && y <= 39) || + (x >= 48 && x <= 50 && y >= 32 && y <= 34) || + (x >= 60 && x <= 63 && y >= 35 && y <= 38) + ) { + return 'village'; + } + if ( + (y >= 47 && x >= 4 && x <= 23) || + (x >= 22 && x <= 35 && y >= 43 && y <= 47) || + (x >= 34 && x <= 48 && y >= 38 && y <= 43) || + (x >= 47 && x <= 59 && y >= 32 && y <= 38) || + (x >= 58 && x <= 67 && y >= 26 && y <= 32) || + (x >= 62 && x <= 69 && y >= 22 && y <= 26) + ) { + return 'road'; + } + if ((x === 16 || x === 17) && y >= 0 && y <= 41) { + return 'river'; + } + if ((x === 39 || x === 40) && y >= 3 && y <= 36) { + return 'river'; + } + if ((x === 50 || x === 51) && y >= 37 && y <= 51) { + return 'river'; + } + if ( + (x >= 4 && x <= 24 && y >= 8 && y <= 36) || + (x >= 25 && x <= 38 && y >= 15 && y <= 37) || + (x >= 41 && x <= 55 && y >= 39 && y <= 51) || + (x >= 53 && x <= 69 && y >= 35 && y <= 50) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 15 && y <= 20) || + (x >= 25 && x <= 38 && y <= 14) || + (x >= 39 && x <= 56 && y >= 1 && y <= 16) || + (x >= 58 && x <= 69 && y <= 14) || + (x >= 29 && x <= 43 && y >= 44 && y <= 51) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 44) || + (x >= 67 && y >= 31) || + (x >= 52 && x <= 55 && y <= 8) || + (x >= 24 && x <= 28 && y >= 23 && y <= 34) + ) { + 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 3700afe..99a170b 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -483,6 +483,7 @@ const unitTexture: Record = { 'chengdu-final-officer-a': 'unit-rebel-leader', 'chengdu-final-leader-huang-quan': 'unit-rebel-leader', 'ma-chao': 'unit-rebel-cavalry', + 'ma-dai': 'unit-rebel-cavalry', 'jiameng-scout-a': 'unit-rebel', 'jiameng-scout-b': 'unit-rebel', 'jiameng-scout-c': 'unit-rebel', @@ -504,7 +505,31 @@ const unitTexture: Record = { 'jiameng-ambusher-a': 'unit-rebel', 'jiameng-ambusher-b': 'unit-rebel', 'jiameng-officer-pang-de': 'unit-rebel-leader', - 'jiameng-leader-ma-chao': 'unit-rebel-cavalry' + 'jiameng-leader-ma-chao': 'unit-rebel-cavalry', + 'yangping-scout-a': 'unit-rebel', + 'yangping-scout-b': 'unit-rebel', + 'yangping-scout-c': 'unit-rebel', + 'yangping-infantry-a': 'unit-rebel', + 'yangping-infantry-b': 'unit-rebel', + 'yangping-infantry-c': 'unit-rebel', + 'yangping-infantry-d': 'unit-rebel', + 'yangping-infantry-e': 'unit-rebel', + 'yangping-archer-a': 'unit-rebel-archer', + 'yangping-archer-b': 'unit-rebel-archer', + 'yangping-archer-c': 'unit-rebel-archer', + 'yangping-archer-d': 'unit-rebel-archer', + 'yangping-cavalry-a': 'unit-rebel-cavalry', + 'yangping-cavalry-b': 'unit-rebel-cavalry', + 'yangping-cavalry-c': 'unit-rebel-cavalry', + 'yangping-cavalry-d': 'unit-rebel-cavalry', + 'yangping-guard-a': 'unit-rebel', + 'yangping-guard-b': 'unit-rebel', + 'yangping-strategist-a': 'unit-rebel-archer', + 'yangping-strategist-b': 'unit-rebel-archer', + 'yangping-ambusher-a': 'unit-rebel', + 'yangping-ambusher-b': 'unit-rebel', + 'yangping-officer-yan-pu': 'unit-rebel-archer', + 'yangping-leader-zhang-wei': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1468,7 +1493,31 @@ const enemyAiByUnitId: Record = { 'jiameng-ambusher-a': 'aggressive', 'jiameng-ambusher-b': 'aggressive', 'jiameng-officer-pang-de': 'guard', - 'jiameng-leader-ma-chao': 'aggressive' + 'jiameng-leader-ma-chao': 'aggressive', + 'yangping-scout-a': 'aggressive', + 'yangping-scout-b': 'aggressive', + 'yangping-scout-c': 'aggressive', + 'yangping-infantry-a': 'guard', + 'yangping-infantry-b': 'guard', + 'yangping-infantry-c': 'guard', + 'yangping-infantry-d': 'guard', + 'yangping-infantry-e': 'guard', + 'yangping-archer-a': 'hold', + 'yangping-archer-b': 'hold', + 'yangping-archer-c': 'hold', + 'yangping-archer-d': 'hold', + 'yangping-cavalry-a': 'aggressive', + 'yangping-cavalry-b': 'aggressive', + 'yangping-cavalry-c': 'aggressive', + 'yangping-cavalry-d': 'aggressive', + 'yangping-guard-a': 'guard', + 'yangping-guard-b': 'guard', + 'yangping-strategist-a': 'hold', + 'yangping-strategist-b': 'hold', + 'yangping-ambusher-a': 'aggressive', + 'yangping-ambusher-b': 'aggressive', + 'yangping-officer-yan-pu': 'hold', + 'yangping-leader-zhang-wei': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 413d753..483e230 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -17,6 +17,7 @@ import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-batt import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg'; import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg'; import thirtyFourthBattleMapUrl from '../../assets/images/battle/thirty-fourth-battle-map.svg'; +import thirtyFifthBattleMapUrl from '../../assets/images/battle/thirty-fifth-battle-map.svg'; 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'; @@ -126,6 +127,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl); 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('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 44af3a3..a3534c2 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -21,6 +21,8 @@ import { guiyangRecruitUnits, hanzhongOpeningRecruitBonds, hanzhongOpeningRecruitUnits, + hanzhongScoutRecruitBonds, + hanzhongScoutRecruitUnits, jingzhouRecruitBonds, jingzhouRecruitUnits, liuBiaoRecruitBonds, @@ -339,7 +341,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ title: '촉한 건국', period: '한실을 잇는 깃발', description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.', - battleIds: ['thirty-fourth-battle-jiameng-pass'], + battleIds: ['thirty-fourth-battle-jiameng-pass', 'thirty-fifth-battle-yangping-scout'], nextHints: ['왕업 선언', '오호대장군', '북벌 준비'] } ]; @@ -378,7 +380,8 @@ const campBattleIds = { thirtyFirst: 'thirty-first-battle-luo-main-gate', thirtySecond: 'thirty-second-battle-mianzhu-gate', thirtyThird: 'thirty-third-battle-chengdu-surrender', - thirtyFourth: 'thirty-fourth-battle-jiameng-pass' + thirtyFourth: 'thirty-fourth-battle-jiameng-pass', + thirtyFifth: 'thirty-fifth-battle-yangping-scout' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -761,6 +764,18 @@ const sortieRulesByBattleId: Partial