diff --git a/docs/roadmap.md b/docs/roadmap.md index d9ea71d..b5000f0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -61,16 +61,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-seventh battle Hanzhong decisive route, using Wang Ping's terrain knowledge and a seven-officer sortie choice across a 74x56 Hanzhong battlefield to push Cao Cao out and prepare Liu Bei's King of Hanzhong step - Post-Hanzhong King of Hanzhong council flow, where the sortie button can advance a story-only political camp milestone and persist a dedicated campaign step before Shu-Han foundation - Shu-Han foundation story milestone, where the King of Hanzhong council advances into Liu Bei's formal state-building scene and points the next arc toward Guan Yu's Jing Province defense +- Thirty-eighth battle Jing Province defense vanguard route, using a twenty-two-officer roster but limiting sortie to seven officers so Guan Yu's front, Ma Liang's local judgment, Mi Zhu's supply role, and Wu/Wei pressure become a deliberate pre-battle choice - 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-seventh battle victory, King of Hanzhong council milestone, and Shu-Han foundation milestone, 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/Hanzhong camp states, campaign timeline state, and camp save state +- Flow verification script from title through the thirty-eighth battle victory, King of Hanzhong council milestone, Shu-Han foundation milestone, 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/Hanzhong/Jing defense camp states, campaign timeline state, and camp save state ## Next Steps -1. Add the next tension around Guan Yu's Jing Province defense so the Shu-Han foundation has an immediate strategic cost -2. Build the first post-foundation Jing Province defense battle, using Guan Yu's isolated front and Wu/Wei pressure as the new tactical axis +1. Build the next Fan Castle front setup, using Guan Yu's northward pressure and Wu/Wei diplomacy as the new campaign tension +2. Expand Jing Province defense events between battles, including patrol assignments, bond choices, and supply decisions that affect sortie recommendations 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 a4d6c67..c114c0f 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 !== 37 || + progressTabState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postBowangProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postChangbanProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postWulingProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postChangshaProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postYizhouProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postFuPassProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postLuoProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postLuofengProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postChengduProgressState.campaignProgress?.totalKnown !== 38 || 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 !== 37 || + postJiamengProgressState.campaignProgress?.totalKnown !== 38 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' @@ -4378,7 +4378,7 @@ try { if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || - postYangpingProgressState.campaignProgress?.totalKnown !== 37 || + postYangpingProgressState.campaignProgress?.totalKnown !== 38 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' @@ -4535,7 +4535,7 @@ try { if ( postDingjunProgressState?.activeTab !== 'progress' || postDingjunProgressState.campaignProgress?.completedKnown !== 36 || - postDingjunProgressState.campaignProgress?.totalKnown !== 37 || + postDingjunProgressState.campaignProgress?.totalKnown !== 38 || postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전' @@ -4688,7 +4688,7 @@ try { if ( postHanzhongProgressState?.activeTab !== 'progress' || postHanzhongProgressState.campaignProgress?.completedKnown !== 37 || - postHanzhongProgressState.campaignProgress?.totalKnown !== 37 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 38 || postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비' @@ -4749,7 +4749,7 @@ try { if ( postKingCouncilProgressState?.activeTab !== 'progress' || postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || - postKingCouncilProgressState.campaignProgress?.totalKnown !== 37 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 38 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4809,14 +4809,165 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 37 || + postFoundationProgressState.campaignProgress?.totalKnown !== 38 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || - postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 긴장' + postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { throw new Error(`Expected Shu-Han foundation camp to point the progress tab toward Jing Province defense: ${JSON.stringify(postFoundationProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-foundation-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const jingDefenseSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jingDefenseSortieState?.sortieVisible || + jingDefenseSortieState.campaign?.step !== 'shu-han-foundation-camp' || + jingDefenseSortieState.sortiePlan?.objectiveLine !== '형주 방위 전초전 · 여몽 정찰대 격파' || + jingDefenseSortieState.sortiePlan?.maxCount !== 7 || + jingDefenseSortieState.sortiePlan?.recommendedTotal !== 7 || + !jingDefenseSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) + ) { + throw new Error(`Expected Jing defense sortie prep to use a seven-officer roster and recommend Guan Yu's front: ${JSON.stringify(jingDefenseSortieState)}`); + } + assertSortieTacticalRoster(jingDefenseSortieState, [ + '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', + 'wang-ping' + ]); + + const jingDefensePriorityUnits = ['guan-yu', 'zhang-fei', 'ma-liang', 'mi-zhu', 'zhuge-liang', 'zhao-yun']; + for (const unitId of jingDefensePriorityUnits) { + 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' && !jingDefensePriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const jingDefenseSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jingDefensePriorityUnits.every((unitId) => + jingDefenseSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + jingDefenseSortieReadyState.sortiePlan?.selectedCount !== 7 || + jingDefenseSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Jing defense sortie to deploy Guan Yu, Zhang Fei, Ma Liang, Mi Zhu, Zhuge Liang, and Zhao Yun with seven-officer pressure: ${JSON.stringify(jingDefenseSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-jing-defense-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-jing-defense-story.png', fullPage: true }); + + for (let i = 0; i < 45; i += 1) { + const enteredThirtyEighthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense'; + }); + if (enteredThirtyEighthBattle) { + 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-eighth-battle-jing-defense' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-thirty-eighth-battle.png', fullPage: true }); + + const thirtyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const thirtyEighthEnemies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const thirtyEighthAllies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); + const thirtyEighthEnemyBehaviors = new Set(thirtyEighthEnemies.map((unit) => unit.ai)); + if ( + thirtyEighthBattleState.camera?.mapWidth !== 76 || + thirtyEighthBattleState.camera?.mapHeight !== 58 || + thirtyEighthBattleState.victoryConditionLabel !== '여몽 정찰대 격파' || + thirtyEighthEnemies.length < 30 || + !thirtyEighthEnemyBehaviors.has('aggressive') || + !thirtyEighthEnemyBehaviors.has('guard') || + !thirtyEighthEnemyBehaviors.has('hold') || + !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-leader-lu-meng') || + !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-cao-ren') || + !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-lu-xun') || + !thirtyEighthAllies.some((unit) => unit.id === 'guan-yu') || + !thirtyEighthAllies.some((unit) => unit.id === 'ma-liang') || + !thirtyEighthAllies.some((unit) => unit.id === 'mi-zhu') || + !thirtyEighthAllies.some((unit) => unit.id === 'zhuge-liang') || + !thirtyEighthAllies.some((unit) => unit.id === 'zhao-yun') + ) { + throw new Error(`Expected thirty-eighth battle to use Jing defense map, Lu Meng objective, selected allied officers, and mixed Wu/Wei AI: ${JSON.stringify(thirtyEighthBattleState)}`); + } + + 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 thirtyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + thirtyEighthCampState?.campBattleId !== 'thirty-eighth-battle-jing-defense' || + thirtyEighthCampState.campTitle !== '형주 방위 전초전 후 군영' || + thirtyEighthCampState.availableDialogueIds?.length !== 3 || + !thirtyEighthCampState.availableDialogueIds.every((id) => id.endsWith('jing-defense')) || + thirtyEighthCampState.availableVisitIds?.length !== 2 || + thirtyEighthCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected thirty-eighth camp to expose Jing defense dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(thirtyEighthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-jing-defense-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postJingDefenseProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postJingDefenseProgressState?.activeTab !== 'progress' || + postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 38 || + postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || + postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Jing defense progress tab to complete the thirty-eighth battle and leave Fan Castle as the next prepared slice: ${JSON.stringify(postJingDefenseProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-jing-defense-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -4835,7 +4986,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirty-seventh-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, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, and Shu-Han foundation state at ${targetUrl}`); + console.log(`Verified title-to-thirty-eighth-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, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, and Jing Province defense vanguard state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/thirty-eighth-battle-map.svg b/src/assets/images/battle/thirty-eighth-battle-map.svg new file mode 100644 index 0000000..6a7386a --- /dev/null +++ b/src/assets/images/battle/thirty-eighth-battle-map.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index b24c5d8..0666a0a 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -47,6 +47,10 @@ import { thirtyFifthBattleMap, thirtyFifthBattleUnits, thirtyFifthBattleVictoryPages, + thirtyEighthBattleBonds, + thirtyEighthBattleMap, + thirtyEighthBattleUnits, + thirtyEighthBattleVictoryPages, thirtyFourthBattleBonds, thirtyFourthBattleMap, thirtyFourthBattleUnits, @@ -190,7 +194,8 @@ export type BattleScenarioId = | 'thirty-fourth-battle-jiameng-pass' | 'thirty-fifth-battle-yangping-scout' | 'thirty-sixth-battle-dingjun-vanguard' - | 'thirty-seventh-battle-hanzhong-decisive'; + | 'thirty-seventh-battle-hanzhong-decisive' + | 'thirty-eighth-battle-jing-defense'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2381,6 +2386,69 @@ export const thirtySeventhBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const thirtyEighthBattleScenario: BattleScenarioDefinition = { + id: 'thirty-eighth-battle-jing-defense', + title: '형주 방위 전초전', + victoryConditionLabel: '여몽 정찰대 격파', + defeatConditionLabel: '유비 또는 관우 퇴각', + openingObjectiveLines: [ + '촉한 건국 직후 형주는 위와 오의 시선이 동시에 모이는 전선이 되었습니다. 봉화대와 나루를 지켜 형주의 불안을 먼저 누르십시오.', + '관우는 이번 전투의 핵심입니다. 관우가 무너지면 형주의 방위 명분도 함께 흔들립니다.', + '강변 마을을 확보하고 오군의 정찰선을 몰아내면, 번성으로 이어질 다음 전선의 숨통을 열 수 있습니다.' + ], + map: thirtyEighthBattleMap, + units: thirtyEighthBattleUnits, + bonds: thirtyEighthBattleBonds, + mapTextureKey: 'battle-map-thirty-eighth', + leaderUnitId: 'jing-defense-leader-lu-meng', + quickVictoryTurnLimit: 60, + baseVictoryGold: 7280, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '여몽 정찰대 격파', + rewardGold: 4580, + unitId: 'jing-defense-leader-lu-meng' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1160, + unitId: 'liu-bei' + }, + { + id: 'guan-yu', + kind: 'keep-unit-alive', + label: '관우 생존', + rewardGold: 1820, + unitId: 'guan-yu' + }, + { + id: 'jing-beacon', + kind: 'secure-terrain', + label: '형주 봉화대 확보', + rewardGold: 2180, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '60턴 이내 승리', + rewardGold: 1640, + maxTurn: 60 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'guan-yu' } + ], + itemRewards: ['콩 32', '상처약 24', '탁주 10', '형주 봉화도 1', '번성 전선 준비'], + victoryPages: thirtyEighthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2420,7 +2488,8 @@ export const battleScenarios: Record 'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario, 'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario, 'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario, - 'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario + 'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario, + 'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 9e32bba..4c62e88 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -19,6 +19,7 @@ import { thirteenthBattleScenario, thirtyFifthBattleScenario, thirtyFourthBattleScenario, + thirtyEighthBattleScenario, thirtyFirstBattleScenario, thirtySecondBattleScenario, thirtyThirdBattleScenario, @@ -79,6 +80,8 @@ import { thirtyFifthBattleVictoryPages, thirtyFourthBattleIntroPages, thirtyFourthBattleVictoryPages, + thirtyEighthBattleIntroPages, + thirtyEighthBattleVictoryPages, thirtyFirstBattleIntroPages, thirtyFirstBattleVictoryPages, thirtySecondBattleIntroPages, @@ -538,13 +541,24 @@ const sortieFlows: Record = { }, 'shu-han-foundation-camp': { afterBattleId: thirtySeventhBattleScenario.id, - eyebrow: '다음 장 준비', - title: '형주 방위 긴장', + eyebrow: '다음 전장', + title: thirtyEighthBattleScenario.title, description: - '촉한의 이름이 세워졌습니다. 다음은 관우가 지키는 형주에서 손권과 조조의 시선이 겹치는 전선을 준비해야 합니다.', - rewardHint: '다음 장: 형주 방위 준비 중', - pages: [], - unavailableNotice: '형주 방위와 관우 전선은 다음 작업에서 이어집니다. 촉한 건국 이후의 첫 전략적 대가를 준비하십시오.' + '촉한의 이름이 세워졌습니다. 이제 관우가 지키는 형주에서 위와 오의 시선이 겹치며, 강변 봉화대와 나루를 먼저 안정시켜야 합니다.', + rewardHint: `예상 보상: ${thirtyEighthBattleScenario.title} 개방 / 형주 봉화도`, + nextBattleId: thirtyEighthBattleScenario.id, + campaignStep: 'thirty-eighth-battle', + pages: thirtyEighthBattleIntroPages + }, + [thirtyEighthBattleScenario.id]: { + afterBattleId: thirtyEighthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '번성 전선 준비', + description: + '형주 봉화대와 나루를 안정시켰지만, 북쪽 번성의 문과 오군의 시선은 더 큰 압박으로 남았습니다. 다음은 관우의 형주 전선이 본격적으로 시험받는 흐름입니다.', + rewardHint: '다음 장: 번성 전선 준비 중', + pages: thirtyEighthBattleVictoryPages, + unavailableNotice: '번성 전선과 관우의 북상은 다음 작업에서 이어집니다. 형주 방위, 강변 민심, 오·위 양면 압박을 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 4c82c3d..66d3d49 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2461,6 +2461,70 @@ export const shuHanFoundationPages: StoryPage[] = [ } ]; +export const thirtyEighthBattleIntroPages: StoryPage[] = [ + { + id: 'thirty-eighth-jing-warning', + bgm: 'story-dark', + chapter: '형주의 먹구름', + background: 'story-three-heroes', + speaker: '제갈량', + text: '촉한의 이름이 세워지자 형주의 무게가 더 커졌습니다. 관우가 지키는 강릉과 남군은 조조와 손권이 동시에 바라보는 칼끝이 되었습니다.' + }, + { + id: 'thirty-eighth-guan-yu-front', + bgm: 'battle-prep', + chapter: '관우의 전선', + background: 'story-liu-bei', + speaker: '관우', + portrait: 'guanYu', + text: '형주는 형님의 등을 지키는 문입니다. 위의 정찰대가 북쪽에서 움직이고, 오의 배가 강가를 맴돈다면 먼저 봉화대와 나루를 묶어야 합니다.' + }, + { + id: 'thirty-eighth-two-fronts', + bgm: 'battle-prep', + chapter: '강과 성 사이', + background: 'story-militia', + speaker: '마량', + text: '강변 마을은 오의 말에 흔들리고, 북쪽 성채는 위의 군세를 두려워합니다. 백성이 흔들리면 형주는 성벽보다 먼저 무너집니다.' + }, + { + id: 'thirty-eighth-sortie', + bgm: 'battle-prep', + chapter: '형주 방위 전초전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '촉한의 이름을 세운 지금, 형주를 잃는다면 그 이름도 흔들릴 것이오. 관우는 봉화대를 지키고, 모두 강과 성 사이의 불안을 잠재우시오.' + } +]; + +export const thirtyEighthBattleVictoryPages: StoryPage[] = [ + { + id: 'thirty-eighth-victory-beacon-secured', + bgm: 'militia-theme', + chapter: '봉화가 다시 오르다', + background: 'story-sortie', + text: '형주의 봉화대가 다시 불을 밝혔습니다. 위의 정찰대는 물러났고 오의 배는 강 건너로 물러났지만, 두 시선이 동시에 형주를 향하고 있다는 사실은 분명해졌습니다.' + }, + { + id: 'thirty-eighth-guan-yu-resolve', + bgm: 'battle-prep', + chapter: '관우의 결의', + background: 'story-liu-bei', + speaker: '관우', + portrait: 'guanYu', + text: '적이 둘이라 하여 뜻도 둘이 될 수는 없습니다. 형주를 지키는 칼은 더욱 곧아야 합니다. 다만 강가의 마음까지 지켜야 오래 버틸 수 있겠습니다.' + }, + { + id: 'thirty-eighth-next-fan-castle', + bgm: 'story-dark', + chapter: '번성의 그림자', + background: 'story-three-heroes', + speaker: '제갈량', + text: '형주의 불안은 잠시 가라앉았지만 북쪽 번성의 문은 여전히 굳게 닫혀 있습니다. 촉한의 첫 큰 시험은 관우의 전선에서 시작될 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2828,6 +2892,12 @@ export const thirtySeventhBattleMap: BattleMap = { terrain: createThirtySeventhBattleTerrain() }; +export const thirtyEighthBattleMap: BattleMap = { + width: 76, + height: 58, + terrain: createThirtyEighthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -14051,6 +14121,84 @@ export const thirtySeventhBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('hanzhong-leader-cao-cao', '조조', '위왕', 'rebelLeader', 84, 520, 138, 70, 18) ]; +const thirtyEighthBattleAllyPositions: Record = { + 'liu-bei': { x: 5, y: 51 }, + 'guan-yu': { x: 9, y: 46 }, + 'zhang-fei': { x: 6, y: 53 }, + 'jian-yong': { x: 4, y: 54 }, + 'mi-zhu': { x: 8, y: 52 }, + 'sun-qian': { x: 7, y: 55 }, + 'zhao-yun': { x: 11, y: 49 }, + 'zhuge-liang': { x: 8, y: 48 }, + 'ma-liang': { x: 10, y: 51 }, + 'yi-ji': { x: 12, y: 54 }, + 'gong-zhi': { x: 13, y: 52 }, + 'huang-zhong': { x: 14, y: 48 }, + 'wei-yan': { x: 12, y: 46 }, + 'pang-tong': { x: 9, y: 49 }, + 'fa-zheng': { x: 15, y: 51 }, + 'wu-yi': { x: 16, y: 48 }, + 'yan-yan': { x: 17, y: 50 }, + 'li-yan': { x: 15, y: 46 }, + 'huang-quan': { x: 18, y: 52 }, + 'ma-chao': { x: 19, y: 47 }, + 'ma-dai': { x: 20, y: 49 }, + 'wang-ping': { x: 18, y: 45 } +}; + +export const thirtyEighthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits, + ...hanzhongScoutRecruitUnits, + ...hanzhongMainRecruitUnits + ].map((unit) => placeScenarioUnit(unit, thirtyEighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('jing-defense-wu-scout-a', '오군 척후', '강변 척후', 'bandit', 82, 286, 96, 24, 47), + createThirtyFirstEnemyUnit('jing-defense-wu-scout-b', '오군 척후', '나루 척후', 'bandit', 82, 286, 96, 32, 45), + createThirtyFirstEnemyUnit('jing-defense-wei-scout-a', '위군 척후', '북로 척후', 'bandit', 82, 286, 96, 42, 43), + createThirtyFirstEnemyUnit('jing-defense-wei-scout-b', '위군 척후', '성채 척후', 'bandit', 83, 294, 98, 51, 40), + createThirtyFirstEnemyUnit('jing-defense-wu-infantry-a', '오군 보병', '나루 보병', 'yellowTurban', 83, 432, 122, 37, 39), + createThirtyFirstEnemyUnit('jing-defense-wu-infantry-b', '오군 보병', '강변 보병', 'yellowTurban', 83, 432, 122, 44, 38), + createThirtyFirstEnemyUnit('jing-defense-wei-infantry-a', '위군 보병', '북문 보병', 'yellowTurban', 84, 444, 124, 53, 35), + createThirtyFirstEnemyUnit('jing-defense-wei-infantry-b', '위군 보병', '성채 보병', 'yellowTurban', 84, 444, 124, 61, 31), + createThirtyFirstEnemyUnit('jing-defense-wei-infantry-c', '위군 보병', '봉화 보병', 'yellowTurban', 85, 456, 126, 68, 26), + createThirtyFirstEnemyUnit('jing-defense-wu-archer-a', '오군 궁병', '강안 궁병', 'archer', 83, 298, 100, 39, 41), + createThirtyFirstEnemyUnit('jing-defense-wu-archer-b', '오군 궁병', '나루 궁병', 'archer', 84, 306, 102, 48, 36), + createThirtyFirstEnemyUnit('jing-defense-wei-archer-a', '위군 궁병', '성벽 궁병', 'archer', 84, 306, 102, 58, 28), + createThirtyFirstEnemyUnit('jing-defense-wei-archer-b', '위군 궁병', '봉화 궁병', 'archer', 85, 314, 104, 70, 23), + createThirtyFirstEnemyUnit('jing-defense-wu-cavalry-a', '오군 기병', '강변 기병', 'cavalry', 84, 446, 126, 31, 49), + createThirtyFirstEnemyUnit('jing-defense-wu-cavalry-b', '오군 기병', '나루 기병', 'cavalry', 84, 446, 126, 46, 44), + createThirtyFirstEnemyUnit('jing-defense-wei-cavalry-a', '위군 기병', '북로 기병', 'cavalry', 85, 458, 128, 59, 38), + createThirtyFirstEnemyUnit('jing-defense-wei-cavalry-b', '위군 기병', '성채 기병', 'cavalry', 85, 458, 128, 68, 33), + createThirtyFirstEnemyUnit('jing-defense-wu-guard-a', '오군 수비대', '나루 수비대', 'yellowTurban', 85, 468, 130, 55, 34), + createThirtyFirstEnemyUnit('jing-defense-wu-guard-b', '오군 수비대', '강안 수비대', 'yellowTurban', 85, 468, 130, 63, 36), + createThirtyFirstEnemyUnit('jing-defense-wei-guard-a', '위군 친위대', '성채 친위대', 'yellowTurban', 86, 480, 132, 66, 25), + createThirtyFirstEnemyUnit('jing-defense-wei-guard-b', '위군 친위대', '북문 친위대', 'yellowTurban', 86, 480, 132, 71, 20), + createThirtyFirstEnemyUnit('jing-defense-wu-strategist-a', '오군 책사', '강동 설객', 'strategist', 84, 306, 104, 52, 37), + createThirtyFirstEnemyUnit('jing-defense-wu-strategist-b', '오군 책사', '나루 책사', 'strategist', 85, 314, 106, 66, 35), + createThirtyFirstEnemyUnit('jing-defense-wei-strategist-a', '위군 책사', '북로 책사', 'strategist', 85, 314, 106, 63, 24), + createThirtyFirstEnemyUnit('jing-defense-ambusher-a', '오군 복병', '남안 복병', 'bandit', 83, 300, 102, 36, 51), + createThirtyFirstEnemyUnit('jing-defense-ambusher-b', '위군 복병', '서림 복병', 'bandit', 83, 300, 102, 49, 46), + createThirtyFirstEnemyUnit('jing-defense-ambusher-c', '오군 복병', '동림 복병', 'bandit', 84, 308, 104, 60, 42), + createThirtyFirstEnemyUnit('jing-defense-officer-lu-xun', '육손', '강동 참모', 'strategist', 86, 330, 112, 64, 37), + createThirtyFirstEnemyUnit('jing-defense-officer-cao-ren', '조인', '위군 수비장', 'rebelLeader', 87, 500, 136, 70, 23), + createThirtyFirstEnemyUnit('jing-defense-leader-lu-meng', '여몽', '오군 도독', 'rebelLeader', 88, 520, 140, 72, 35) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -14564,6 +14712,33 @@ export const hanzhongDecisiveBonds: BattleBond[] = [ } ]; +export const jingDefenseBonds: BattleBond[] = [ + { + id: 'guan-yu__ma-liang', + unitIds: ['guan-yu', 'ma-liang'], + title: '형주 문무', + level: 30, + exp: 0, + description: '관우의 강한 방위와 마량의 민심 판단이 맞물려 형주의 강변 마을을 오래 붙잡습니다.' + }, + { + id: 'guan-yu__mi-zhu', + unitIds: ['guan-yu', 'mi-zhu'], + title: '강릉 보급', + level: 26, + exp: 0, + description: '관우의 전선 유지와 미축의 보급 감각이 이어져 강릉과 나루 사이의 군량 흐름을 안정시킵니다.' + }, + { + id: 'guan-yu__wang-ping', + unitIds: ['guan-yu', '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); @@ -14620,6 +14795,9 @@ export const thirtySeventhBattleBonds: BattleBond[] = [ ...hanzhongMainRecruitBonds, ...hanzhongDecisiveBonds ].map(cloneBattleBondForScenario); +export const thirtyEighthBattleBonds: BattleBond[] = [...thirtySeventhBattleBonds, ...jingDefenseBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -16298,6 +16476,77 @@ function createThirtySeventhBattleTerrain(): TerrainType[][] { ); } +function createThirtyEighthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 58 }, (_, y) => + Array.from({ length: 76 }, (_, x): TerrainType => { + if (x <= 5 && y >= 51) { + return 'camp'; + } + if ( + (x >= 58 && x <= 74 && y >= 8 && y <= 27) || + (x >= 51 && x <= 63 && y >= 9 && y <= 22) || + (x >= 64 && x <= 75 && y >= 21 && y <= 38) || + (x >= 66 && x <= 72 && y >= 34 && y <= 42) + ) { + return 'fort'; + } + if ( + (x >= 20 && x <= 24 && y >= 48 && y <= 52) || + (x >= 41 && x <= 45 && y >= 41 && y <= 44) || + (x >= 55 && x <= 59 && y >= 35 && y <= 38) || + (x >= 69 && x <= 72 && y >= 39 && y <= 42) + ) { + return 'village'; + } + if ( + (y >= 53 && x >= 5 && x <= 25) || + (x >= 24 && x <= 40 && y >= 48 && y <= 53) || + (x >= 39 && x <= 54 && y >= 42 && y <= 48) || + (x >= 53 && x <= 66 && y >= 36 && y <= 42) || + (x >= 65 && x <= 74 && y >= 28 && y <= 36) || + (x >= 68 && x <= 75 && y >= 19 && y <= 28) + ) { + return 'road'; + } + if ((x === 18 || x === 19 || x === 20) && y >= 0 && y <= 49) { + return 'river'; + } + if ((x === 46 || x === 47) && y >= 5 && y <= 42) { + return 'river'; + } + if ((x === 57 || x === 58) && y >= 39 && y <= 57) { + return 'river'; + } + if ( + (x >= 4 && x <= 27 && y >= 9 && y <= 41) || + (x >= 28 && x <= 42 && y >= 17 && y <= 44) || + (x >= 47 && x <= 62 && y >= 43 && y <= 57) || + (x >= 59 && x <= 75 && y >= 40 && y <= 56) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 17 && y <= 25) || + (x >= 27 && x <= 44 && y <= 16) || + (x >= 45 && x <= 63 && y >= 1 && y <= 18) || + (x >= 64 && x <= 75 && y <= 14) || + (x >= 31 && x <= 48 && y >= 50 && y <= 57) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 50) || + (x >= 73 && y >= 35) || + (x >= 56 && x <= 60 && y <= 8) || + (x >= 26 && x <= 31 && y >= 26 && y <= 39) + ) { + 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 22a0029..896b1ef 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -585,7 +585,37 @@ const unitTexture: Record = { 'hanzhong-ambusher-b': 'unit-rebel', 'hanzhong-officer-zhang-he': 'unit-rebel-cavalry', 'hanzhong-officer-xu-huang': 'unit-rebel-leader', - 'hanzhong-leader-cao-cao': 'unit-rebel-leader' + 'hanzhong-leader-cao-cao': 'unit-rebel-leader', + 'jing-defense-wu-scout-a': 'unit-rebel', + 'jing-defense-wu-scout-b': 'unit-rebel', + 'jing-defense-wei-scout-a': 'unit-rebel', + 'jing-defense-wei-scout-b': 'unit-rebel', + 'jing-defense-wu-infantry-a': 'unit-rebel', + 'jing-defense-wu-infantry-b': 'unit-rebel', + 'jing-defense-wei-infantry-a': 'unit-rebel', + 'jing-defense-wei-infantry-b': 'unit-rebel', + 'jing-defense-wei-infantry-c': 'unit-rebel', + 'jing-defense-wu-archer-a': 'unit-rebel-archer', + 'jing-defense-wu-archer-b': 'unit-rebel-archer', + 'jing-defense-wei-archer-a': 'unit-rebel-archer', + 'jing-defense-wei-archer-b': 'unit-rebel-archer', + 'jing-defense-wu-cavalry-a': 'unit-rebel-cavalry', + 'jing-defense-wu-cavalry-b': 'unit-rebel-cavalry', + 'jing-defense-wei-cavalry-a': 'unit-rebel-cavalry', + 'jing-defense-wei-cavalry-b': 'unit-rebel-cavalry', + 'jing-defense-wu-guard-a': 'unit-rebel', + 'jing-defense-wu-guard-b': 'unit-rebel', + 'jing-defense-wei-guard-a': 'unit-rebel', + 'jing-defense-wei-guard-b': 'unit-rebel', + 'jing-defense-wu-strategist-a': 'unit-rebel-archer', + 'jing-defense-wu-strategist-b': 'unit-rebel-archer', + 'jing-defense-wei-strategist-a': 'unit-rebel-archer', + 'jing-defense-ambusher-a': 'unit-rebel', + 'jing-defense-ambusher-b': 'unit-rebel', + 'jing-defense-ambusher-c': 'unit-rebel', + 'jing-defense-officer-lu-xun': 'unit-rebel-archer', + 'jing-defense-officer-cao-ren': 'unit-rebel-leader', + 'jing-defense-leader-lu-meng': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1628,7 +1658,37 @@ const enemyAiByUnitId: Record = { 'hanzhong-ambusher-b': 'aggressive', 'hanzhong-officer-zhang-he': 'aggressive', 'hanzhong-officer-xu-huang': 'guard', - 'hanzhong-leader-cao-cao': 'guard' + 'hanzhong-leader-cao-cao': 'guard', + 'jing-defense-wu-scout-a': 'aggressive', + 'jing-defense-wu-scout-b': 'aggressive', + 'jing-defense-wei-scout-a': 'aggressive', + 'jing-defense-wei-scout-b': 'aggressive', + 'jing-defense-wu-infantry-a': 'guard', + 'jing-defense-wu-infantry-b': 'guard', + 'jing-defense-wei-infantry-a': 'guard', + 'jing-defense-wei-infantry-b': 'guard', + 'jing-defense-wei-infantry-c': 'guard', + 'jing-defense-wu-archer-a': 'hold', + 'jing-defense-wu-archer-b': 'hold', + 'jing-defense-wei-archer-a': 'hold', + 'jing-defense-wei-archer-b': 'hold', + 'jing-defense-wu-cavalry-a': 'aggressive', + 'jing-defense-wu-cavalry-b': 'aggressive', + 'jing-defense-wei-cavalry-a': 'aggressive', + 'jing-defense-wei-cavalry-b': 'aggressive', + 'jing-defense-wu-guard-a': 'guard', + 'jing-defense-wu-guard-b': 'guard', + 'jing-defense-wei-guard-a': 'guard', + 'jing-defense-wei-guard-b': 'guard', + 'jing-defense-wu-strategist-a': 'hold', + 'jing-defense-wu-strategist-b': 'hold', + 'jing-defense-wei-strategist-a': 'hold', + 'jing-defense-ambusher-a': 'aggressive', + 'jing-defense-ambusher-b': 'aggressive', + 'jing-defense-ambusher-c': 'aggressive', + 'jing-defense-officer-lu-xun': 'hold', + 'jing-defense-officer-cao-ren': 'guard', + 'jing-defense-leader-lu-meng': 'aggressive' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index a772031..dd2a1b4 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 thirtyEighthBattleMapUrl from '../../assets/images/battle/thirty-eighth-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'; @@ -132,6 +133,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirty-fifth', thirtyFifthBattleMapUrl); this.load.image('battle-map-thirty-sixth', thirtySixthBattleMapUrl); this.load.image('battle-map-thirty-seventh', thirtySeventhBattleMapUrl); + this.load.image('battle-map-thirty-eighth', thirtyEighthBattleMapUrl); 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 8c34030..773dbf1 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -26,6 +26,7 @@ import { hanzhongMainRecruitUnits, hanzhongScoutRecruitBonds, hanzhongScoutRecruitUnits, + jingDefenseBonds, jingzhouRecruitBonds, jingzhouRecruitUnits, liuBiaoRecruitBonds, @@ -348,7 +349,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'thirty-fourth-battle-jiameng-pass', 'thirty-fifth-battle-yangping-scout', 'thirty-sixth-battle-dingjun-vanguard', - 'thirty-seventh-battle-hanzhong-decisive' + 'thirty-seventh-battle-hanzhong-decisive', + 'thirty-eighth-battle-jing-defense' ], nextHints: ['왕업 선언', '오호대장군', '북벌 준비'] } @@ -391,7 +393,8 @@ const campBattleIds = { thirtyFourth: 'thirty-fourth-battle-jiameng-pass', thirtyFifth: 'thirty-fifth-battle-yangping-scout', thirtySixth: 'thirty-sixth-battle-dingjun-vanguard', - thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive' + thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive', + thirtyEighth: 'thirty-eighth-battle-jing-defense' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -811,6 +814,19 @@ const sortieRulesByBattleId: Partial