diff --git a/docs/roadmap.md b/docs/roadmap.md index 59bc1d5..b2f5823 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -65,16 +65,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-ninth battle Fan Castle outer-front route, locking Liu Bei and Guan Yu as required sortie officers while the player chooses five support officers from the twenty-two-officer roster to pressure Pang De, Cao Ren, and Yu Jin - Fortieth battle Han River flood route, using Guan Yu's water-control plan across an 80x62 flooded battlefield to break Yu Jin's seven armies and open the Fan Castle siege setup - Forty-first battle Fan Castle siege route, using a twenty-two-officer roster across an 82x64 castle battlefield to pressure Cao Ren's gate line, Pang De's desperate cavalry, and Wu's rear-shadow scouts +- Forty-second battle Jing rear-crisis route, using Ma Liang and Mi Zhu's rear-guard roles across an 84x66 Jiangling ferry battlefield to repel Lu Meng's disguised scouts and expose Mi Fang's wavering rear line - 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 forty-first 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/Fan Castle/Han River/Fan siege camp states, campaign timeline state, and camp save state +- Flow verification script from title through the forty-second 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/Fan Castle/Han River/Fan siege/Jing rear camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the Jing Province rear-crisis follow-up, using Fan Castle's pressure, Wu's river scouts, and the first signs of rear-line collapse as the next campaign tension -2. Expand Fan Castle aftermath events, including rear guard assignments, river signal choices, siege fatigue, and supply decisions that affect sortie recommendations +1. Build the Jing Province rear-collapse follow-up, using Lu Meng's infiltration, Mi Fang/Fu Shiren's wavering, and Guan Yu's overextended Fan Castle front as the next campaign tension +2. Expand Jing rear-crisis aftermath events, including warehouse trust, river rumor choices, officer loyalty checks, 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 7ebbb7d..8c27ab7 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 !== 41 || + progressTabState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postBowangProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postChangbanProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postWulingProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postChangshaProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postYizhouProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postFuPassProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postLuoProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postLuofengProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postChengduProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postJiamengProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postYangpingProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postDingjunProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 42 || 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 !== 41 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 42 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4809,7 +4809,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 41 || + postFoundationProgressState.campaignProgress?.totalKnown !== 42 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4960,7 +4960,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 41 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 42 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5113,7 +5113,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 41 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 42 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5267,7 +5267,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 41 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 42 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5421,14 +5421,168 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 41 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 42 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || - postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { - throw new Error(`Expected post-Fan Castle siege progress tab to complete the forty-first battle and leave Jing Province crisis as the next prepared slice: ${JSON.stringify(postFanSiegeProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Fan Castle siege progress tab to complete the forty-first battle and point to Jing rear crisis: ${JSON.stringify(postFanSiegeProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fan-siege-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const jingRearSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jingRearSortieState?.sortieVisible || + jingRearSortieState.campaign?.step !== 'forty-first-camp' || + jingRearSortieState.sortiePlan?.objectiveLine !== '강릉 나루 경계전 · 여몽 정찰대 격퇴' || + jingRearSortieState.sortiePlan?.maxCount !== 7 || + jingRearSortieState.sortiePlan?.recommendedTotal !== 7 || + !jingRearSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) + ) { + throw new Error(`Expected Jing rear sortie prep to lock Guan Yu and expose a seven-officer rear-guard choice: ${JSON.stringify(jingRearSortieState)}`); + } + assertSortieTacticalRoster(jingRearSortieState, [ + '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 jingRearPriorityUnits = ['ma-liang', 'mi-zhu', 'zhao-yun', 'huang-quan', 'wang-ping']; + for (const unitId of jingRearPriorityUnits) { + 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.required && !jingRearPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const jingRearSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jingRearPriorityUnits.every((unitId) => + jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + !jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) || + jingRearSortieReadyState.sortiePlan?.selectedCount !== 7 || + jingRearSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Jing rear sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Zhao Yun, Huang Quan, and Wang Ping: ${JSON.stringify(jingRearSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-jing-rear-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-rear-story.png', fullPage: true }); + + for (let i = 0; i < 45; i += 1) { + const enteredFortySecondBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-second-battle-jing-rear-crisis'; + }); + if (enteredFortySecondBattle) { + 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 === 'forty-second-battle-jing-rear-crisis' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-second-battle.png', fullPage: true }); + + const fortySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortySecondEnemies = fortySecondBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortySecondAllies = fortySecondBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortySecondEnemyBehaviors = new Set(fortySecondEnemies.map((unit) => unit.ai)); + if ( + fortySecondBattleState.camera?.mapWidth !== 84 || + fortySecondBattleState.camera?.mapHeight !== 66 || + fortySecondBattleState.victoryConditionLabel !== '여몽 정찰대 격퇴' || + fortySecondEnemies.length < 35 || + !fortySecondEnemyBehaviors.has('aggressive') || + !fortySecondEnemyBehaviors.has('guard') || + !fortySecondEnemyBehaviors.has('hold') || + !fortySecondEnemies.some((unit) => unit.id === 'jing-rear-leader-lu-meng') || + !fortySecondEnemies.some((unit) => unit.id === 'jing-rear-officer-lu-xun') || + !fortySecondEnemies.some((unit) => unit.id === 'jing-rear-officer-mi-fang') || + !fortySecondAllies.some((unit) => unit.id === 'guan-yu') || + !fortySecondAllies.some((unit) => unit.id === 'ma-liang') || + !fortySecondAllies.some((unit) => unit.id === 'mi-zhu') || + !fortySecondAllies.some((unit) => unit.id === 'huang-quan') || + !fortySecondAllies.some((unit) => unit.id === 'wang-ping') + ) { + throw new Error(`Expected forty-second battle to use Jing rear map, Lu Meng objective, selected rear-guard officers, and mixed Wu AI: ${JSON.stringify(fortySecondBattleState)}`); + } + + 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 fortySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortySecondCampState?.campBattleId !== 'forty-second-battle-jing-rear-crisis' || + fortySecondCampState.campTitle !== '강릉 나루 경계전 후 군영' || + fortySecondCampState.availableDialogueIds?.length !== 3 || + !fortySecondCampState.availableDialogueIds.every((id) => id.endsWith('jing-rear')) || + fortySecondCampState.availableVisitIds?.length !== 2 || + !fortySecondCampState.availableVisitIds.every((id) => id.includes('jing-rear')) || + fortySecondCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected forty-second camp to expose Jing rear dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortySecondCampState)}`); + } + await page.screenshot({ path: 'dist/verification-jing-rear-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postJingRearProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postJingRearProgressState?.activeTab !== 'progress' || + postJingRearProgressState.campaignProgress?.completedKnown !== 42 || + postJingRearProgressState.campaignProgress?.totalKnown !== 42 || + postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || + postJingRearProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Jing rear progress tab to complete the forty-second battle and leave rear collapse as the next prepared slice: ${JSON.stringify(postJingRearProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-jing-rear-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -5461,10 +5615,10 @@ try { camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); if (!titleContinueState.activeScenes.includes('CampScene')) { - throw new Error(`Expected title continue to reopen the latest camp after Fan Castle siege: ${JSON.stringify(titleContinueState)}`); + throw new Error(`Expected title continue to reopen the latest camp after Jing rear crisis: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-forty-first-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, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, and Fan Castle siege state at ${targetUrl}`); + console.log(`Verified title-to-forty-second-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, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan Castle siege, and Jing rear crisis state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-second-battle-map.svg b/src/assets/images/battle/forty-second-battle-map.svg new file mode 100644 index 0000000..456c16f --- /dev/null +++ b/src/assets/images/battle/forty-second-battle-map.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 3d0a9ca..aac341c 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -28,6 +28,10 @@ import { fortyFirstBattleMap, fortyFirstBattleUnits, fortyFirstBattleVictoryPages, + fortySecondBattleBonds, + fortySecondBattleMap, + fortySecondBattleUnits, + fortySecondBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -210,7 +214,8 @@ export type BattleScenarioId = | 'thirty-eighth-battle-jing-defense' | 'thirty-ninth-battle-fan-castle-vanguard' | 'fortieth-battle-han-river-flood' - | 'forty-first-battle-fan-castle-siege'; + | 'forty-first-battle-fan-castle-siege' + | 'forty-second-battle-jing-rear-crisis'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2653,6 +2658,69 @@ export const fortyFirstBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortySecondBattleScenario: BattleScenarioDefinition = { + id: 'forty-second-battle-jing-rear-crisis', + title: '강릉 나루 경계전', + victoryConditionLabel: '여몽 정찰대 격퇴', + defeatConditionLabel: '유비 또는 관우 퇴각', + openingObjectiveLines: [ + '번성의 성문은 흔들렸지만 강릉 나루에는 강동의 작은 배가 늘었습니다. 상선으로 위장한 정찰대를 몰아내 후방의 창고를 지키십시오.', + '관우가 번성 전선과 형주 후방을 함께 붙잡아야 합니다. 관우가 쓰러지면 전선의 승세와 후방의 민심이 동시에 흔들립니다.', + '여몽은 정찰대를 깊숙이 보내고, 육손은 부드러운 말로 빈틈을 살핍니다. 마량과 미축 같은 후방 담당을 데려가면 균열을 읽고 창고를 지키기 쉽습니다.' + ], + map: fortySecondBattleMap, + units: fortySecondBattleUnits, + bonds: fortySecondBattleBonds, + mapTextureKey: 'battle-map-forty-second', + leaderUnitId: 'jing-rear-leader-lu-meng', + quickVictoryTurnLimit: 68, + baseVictoryGold: 8520, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '여몽 정찰대 격퇴', + rewardGold: 5560, + unitId: 'jing-rear-leader-lu-meng' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1320, + unitId: 'liu-bei' + }, + { + id: 'guan-yu', + kind: 'keep-unit-alive', + label: '관우 생존', + rewardGold: 2080, + unitId: 'guan-yu' + }, + { + id: 'warehouse', + kind: 'secure-terrain', + label: '강릉 창고 확보', + rewardGold: 2680, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '68턴 이내 승리', + rewardGold: 1960, + maxTurn: 68 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'guan-yu' } + ], + itemRewards: ['콩 40', '상처약 30', '탁주 12', '강동 정찰 기록 1', '강릉 창고 장부 1'], + victoryPages: fortySecondBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2696,7 +2764,8 @@ export const battleScenarios: Record 'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario, 'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario, 'fortieth-battle-han-river-flood': fortiethBattleScenario, - 'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario + 'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario, + 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index c59d536..4a41961 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -6,6 +6,7 @@ import { eleventhBattleScenario, fortiethBattleScenario, fortyFirstBattleScenario, + fortySecondBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -54,6 +55,8 @@ import { fortiethBattleVictoryPages, fortyFirstBattleIntroPages, fortyFirstBattleVictoryPages, + fortySecondBattleIntroPages, + fortySecondBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -594,13 +597,24 @@ const sortieFlows: Record = { }, [fortyFirstBattleScenario.id]: { afterBattleId: fortyFirstBattleScenario.id, - eyebrow: '다음 장 준비', - title: '형주 후방 경계', + eyebrow: '다음 전장', + title: fortySecondBattleScenario.title, description: - '번성의 외성 방어선은 흔들렸지만 강동의 그림자가 더 가까워졌습니다. 다음 장에서는 번성 승세와 형주 후방의 균열을 함께 다루며 관우 전선의 위기를 키웁니다.', - rewardHint: '다음 장: 형주 후방 위기와 오군 침투 준비 중', - pages: fortyFirstBattleVictoryPages, - unavailableNotice: '형주 후방 위기와 오군 침투는 다음 작업에서 이어집니다. 지금은 군영에서 공성전 성장, 공명도, 보급을 정비할 수 있습니다.' + '번성의 외성 방어선은 흔들렸지만 강동의 그림자가 더 가까워졌습니다. 다음 출진에서는 강릉 동쪽 나루와 창고를 노리는 정찰대를 몰아내며 형주 후방의 첫 균열을 확인합니다.', + rewardHint: `예상 보상: ${fortySecondBattleScenario.title} 개방 / 강릉 창고 장부`, + nextBattleId: fortySecondBattleScenario.id, + campaignStep: 'forty-second-battle', + pages: [...fortyFirstBattleVictoryPages, ...fortySecondBattleIntroPages] + }, + [fortySecondBattleScenario.id]: { + afterBattleId: fortySecondBattleScenario.id, + eyebrow: '다음 장 준비', + title: '형주 균열 심화', + description: + '강릉 나루는 지켰지만 강동의 침투는 이미 더 넓은 후방을 살피고 있습니다. 다음 장에서는 강릉과 공안의 흔들림, 장수들의 불신, 오군의 본격적인 움직임을 다룹니다.', + rewardHint: '다음 장: 형주 후방 붕괴와 오군 침투 준비 중', + pages: fortySecondBattleVictoryPages, + unavailableNotice: '형주 후방 붕괴와 오군 침투는 다음 작업에서 이어집니다. 지금은 군영에서 나루 경계의 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index dd8b856..6897dc7 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2717,6 +2717,69 @@ export const fortyFirstBattleVictoryPages: StoryPage[] = [ } ]; +export const fortySecondBattleIntroPages: StoryPage[] = [ + { + id: 'forty-second-river-rumors', + bgm: 'story-dark', + chapter: '강릉의 불안', + background: 'story-militia', + speaker: '마량', + text: '번성 성문은 흔들리고 있지만, 강릉 동쪽 나루에는 낯선 상선이 늘었습니다. 장사꾼의 말투 속에 군령이 섞여 있고, 밤 신호는 평소보다 늦게 올라옵니다.' + }, + { + id: 'forty-second-lu-meng-shadow', + bgm: 'battle-prep', + chapter: '흰 옷의 그림자', + background: 'story-liu-bei', + speaker: '제갈량', + text: '강동의 장수들은 칼을 들고 오지 않을 수도 있습니다. 흰 옷의 상인, 약재 배, 빈 곡식 자루가 모두 길을 재는 눈이 될 수 있습니다.' + }, + { + id: 'forty-second-guan-yu-rear-order', + bgm: 'battle-prep', + chapter: '후방 경계령', + background: 'story-three-heroes', + speaker: '관우', + portrait: 'guanYu', + text: '번성을 치는 손을 늦출 수는 없으나, 형주의 뒤를 비울 수도 없소. 나루와 창고를 지키며 강동 정찰대를 몰아내시오.' + }, + { + id: 'forty-second-sortie', + bgm: 'battle-prep', + chapter: '강릉 나루 경계전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '전선의 승리와 후방의 평온은 따로 있지 않다. 관우의 깃발이 번성 앞에 선 동안, 강릉 나루의 작은 흔들림까지 놓치지 말라.' + } +]; + +export const fortySecondBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-second-victory-river-watch', + bgm: 'militia-theme', + chapter: '나루의 밤이 밝아지다', + background: 'story-sortie', + text: '강릉 동쪽 나루의 정찰선은 물러났고, 강변 창고의 불안은 잠시 가라앉았습니다. 그러나 붙잡힌 척후의 말은 오히려 더 큰 위기를 가리켰습니다.' + }, + { + id: 'forty-second-lu-xun-letter', + bgm: 'story-dark', + chapter: '부드러운 글과 날카로운 뜻', + background: 'story-three-heroes', + speaker: '마량', + text: '강동에서 온 문서는 예를 갖추고 있었지만, 글 사이에는 형주의 빈틈을 재는 뜻이 숨어 있었습니다. 승리보다 후방의 믿음이 더 급해졌습니다.' + }, + { + id: 'forty-second-next-crack', + bgm: 'story-dark', + chapter: '갈라지는 형주', + background: 'story-militia', + speaker: '제갈량', + text: '나루 하나는 지켰으나, 강릉과 공안의 장수들이 흔들리기 시작했습니다. 다음 장에서는 형주 후방의 균열이 더 노골적인 배신과 침투로 이어질 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3108,6 +3171,12 @@ export const fortyFirstBattleMap: BattleMap = { terrain: createFortyFirstBattleTerrain() }; +export const fortySecondBattleMap: BattleMap = { + width: 84, + height: 66, + terrain: createFortySecondBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -14648,6 +14717,89 @@ export const fortyFirstBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('fan-siege-leader-cao-ren', '조인', '번성 수비장', 'rebelLeader', 99, 676, 172, 78, 18) ]; +const fortySecondBattleAllyPositions: Record = { + 'liu-bei': { x: 6, y: 59 }, + 'guan-yu': { x: 13, y: 54 }, + 'zhang-fei': { x: 7, y: 61 }, + 'jian-yong': { x: 5, y: 62 }, + 'mi-zhu': { x: 9, y: 60 }, + 'sun-qian': { x: 10, y: 63 }, + 'zhao-yun': { x: 16, y: 57 }, + 'zhuge-liang': { x: 10, y: 56 }, + 'ma-liang': { x: 15, y: 59 }, + 'yi-ji': { x: 17, y: 62 }, + 'gong-zhi': { x: 18, y: 60 }, + 'huang-zhong': { x: 19, y: 55 }, + 'wei-yan': { x: 17, y: 53 }, + 'pang-tong': { x: 14, y: 57 }, + 'fa-zheng': { x: 20, y: 59 }, + 'wu-yi': { x: 21, y: 56 }, + 'yan-yan': { x: 22, y: 58 }, + 'li-yan': { x: 20, y: 53 }, + 'huang-quan': { x: 23, y: 60 }, + 'ma-chao': { x: 24, y: 54 }, + 'ma-dai': { x: 25, y: 56 }, + 'wang-ping': { x: 23, y: 52 } +}; + +export const fortySecondBattleUnits: 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, fortySecondBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('jing-rear-scout-a', '강동 척후', '흰 옷 척후', 'bandit', 94, 382, 126, 30, 57), + createThirtyFirstEnemyUnit('jing-rear-scout-b', '강동 척후', '상선 척후', 'bandit', 94, 382, 126, 39, 54), + createThirtyFirstEnemyUnit('jing-rear-scout-c', '강동 척후', '약재선 척후', 'bandit', 95, 390, 128, 51, 49), + createThirtyFirstEnemyUnit('jing-rear-scout-d', '강동 척후', '북나루 척후', 'bandit', 95, 390, 128, 64, 43), + createThirtyFirstEnemyUnit('jing-rear-scout-e', '강동 척후', '동림 척후', 'bandit', 96, 398, 130, 75, 36), + createThirtyFirstEnemyUnit('jing-rear-infantry-a', '강동 보병', '나루 보병', 'yellowTurban', 95, 558, 152, 43, 50), + createThirtyFirstEnemyUnit('jing-rear-infantry-b', '강동 보병', '창고 보병', 'yellowTurban', 95, 558, 152, 54, 45), + createThirtyFirstEnemyUnit('jing-rear-infantry-c', '강동 보병', '강릉 보병', 'yellowTurban', 96, 570, 154, 63, 38), + createThirtyFirstEnemyUnit('jing-rear-infantry-d', '강동 보병', '동문 보병', 'yellowTurban', 96, 570, 154, 72, 31), + createThirtyFirstEnemyUnit('jing-rear-infantry-e', '강동 보병', '후방 보병', 'yellowTurban', 97, 582, 156, 78, 24), + createThirtyFirstEnemyUnit('jing-rear-infantry-f', '강동 보병', '공안 보병', 'yellowTurban', 97, 582, 156, 80, 15), + createThirtyFirstEnemyUnit('jing-rear-archer-a', '강동 궁병', '강안 궁병', 'archer', 95, 406, 132, 35, 58), + createThirtyFirstEnemyUnit('jing-rear-archer-b', '강동 궁병', '상선 궁병', 'archer', 96, 414, 134, 49, 51), + createThirtyFirstEnemyUnit('jing-rear-archer-c', '강동 궁병', '창고 궁병', 'archer', 96, 414, 134, 59, 44), + createThirtyFirstEnemyUnit('jing-rear-archer-d', '강동 궁병', '동림 궁병', 'archer', 97, 422, 136, 70, 36), + createThirtyFirstEnemyUnit('jing-rear-archer-e', '강동 궁병', '공안 궁병', 'archer', 97, 422, 136, 79, 20), + createThirtyFirstEnemyUnit('jing-rear-cavalry-a', '강동 기병', '나루 기병', 'cavalry', 96, 576, 156, 37, 60), + createThirtyFirstEnemyUnit('jing-rear-cavalry-b', '강동 기병', '창고 기병', 'cavalry', 96, 576, 156, 52, 53), + createThirtyFirstEnemyUnit('jing-rear-cavalry-c', '강동 기병', '동림 기병', 'cavalry', 97, 588, 158, 67, 42), + createThirtyFirstEnemyUnit('jing-rear-cavalry-d', '강동 기병', '공안 기병', 'cavalry', 97, 588, 158, 77, 32), + createThirtyFirstEnemyUnit('jing-rear-guard-a', '강동 수비대', '상선 수비대', 'yellowTurban', 98, 626, 164, 58, 42), + createThirtyFirstEnemyUnit('jing-rear-guard-b', '강동 수비대', '창고 수비대', 'yellowTurban', 98, 626, 164, 68, 34), + createThirtyFirstEnemyUnit('jing-rear-guard-c', '강동 수비대', '여몽 수비대', 'yellowTurban', 99, 638, 166, 75, 27), + createThirtyFirstEnemyUnit('jing-rear-guard-d', '강동 수비대', '동문 수비대', 'yellowTurban', 99, 638, 166, 81, 18), + createThirtyFirstEnemyUnit('jing-rear-strategist-a', '강동 책사', '상선 책사', 'strategist', 96, 422, 136, 45, 55), + createThirtyFirstEnemyUnit('jing-rear-strategist-b', '강동 책사', '강릉 책사', 'strategist', 97, 430, 138, 62, 39), + createThirtyFirstEnemyUnit('jing-rear-strategist-c', '강동 책사', '공안 책사', 'strategist', 98, 438, 140, 77, 22), + createThirtyFirstEnemyUnit('jing-rear-ambusher-a', '강동 복병', '흰 옷 복병', 'bandit', 95, 392, 130, 32, 62), + createThirtyFirstEnemyUnit('jing-rear-ambusher-b', '강동 복병', '창고 복병', 'bandit', 96, 400, 132, 55, 57), + createThirtyFirstEnemyUnit('jing-rear-ambusher-c', '강동 복병', '동림 복병', 'bandit', 96, 400, 132, 72, 45), + createThirtyFirstEnemyUnit('jing-rear-marine-a', '강동 수군', '나루 수군', 'yellowTurban', 96, 570, 154, 41, 61), + createThirtyFirstEnemyUnit('jing-rear-marine-b', '강동 수군', '상선 수군', 'yellowTurban', 97, 582, 156, 48, 58), + createThirtyFirstEnemyUnit('jing-rear-officer-lu-xun', '육손', '강동 설객', 'strategist', 99, 462, 146, 71, 29), + createThirtyFirstEnemyUnit('jing-rear-officer-mi-fang', '미방', '흔들리는 후방장', 'rebelLeader', 98, 620, 162, 66, 48), + createThirtyFirstEnemyUnit('jing-rear-leader-lu-meng', '여몽', '강동 도독', 'rebelLeader', 100, 690, 176, 80, 26) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -15253,6 +15405,33 @@ export const fanCastleSiegeBonds: BattleBond[] = [ } ]; +export const jingRearCrisisBonds: BattleBond[] = [ + { + id: 'guan-yu__mi-zhu_rear', + unitIds: ['guan-yu', 'mi-zhu'], + title: '후방 창고의 신뢰', + level: 27, + exp: 0, + description: '관우의 엄정한 군령과 미축의 보급 감각이 이어지면, 강릉 창고와 나루를 노리는 침투에 더 빠르게 대응합니다.' + }, + { + id: 'guan-yu__ma-liang_rear', + unitIds: ['guan-yu', 'ma-liang'], + title: '형주 후방 경계', + level: 28, + exp: 0, + description: '관우의 전선 압박과 마량의 형주 민심 판단이 맞물리면, 강동의 부드러운 말과 숨은 정찰을 더 빨리 읽어 냅니다.' + }, + { + id: 'liu-bei__mi-zhu_rear', + unitIds: ['liu-bei', 'mi-zhu'], + title: '백성과 창고', + level: 25, + exp: 0, + description: '유비의 민심과 미축의 보급선이 이어지면, 전투 사이 상인·백성 이벤트에서 후방 안정 보너스가 커집니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -15321,6 +15500,9 @@ export const fortiethBattleBonds: BattleBond[] = [...thirtyNinthBattleBonds, ... export const fortyFirstBattleBonds: BattleBond[] = [...fortiethBattleBonds, ...fanCastleSiegeBonds].map( cloneBattleBondForScenario ); +export const fortySecondBattleBonds: BattleBond[] = [...fortyFirstBattleBonds, ...jingRearCrisisBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -17292,6 +17474,81 @@ function createFortyFirstBattleTerrain(): TerrainType[][] { ); } +function createFortySecondBattleTerrain(): TerrainType[][] { + return Array.from({ length: 66 }, (_, y) => + Array.from({ length: 84 }, (_, x): TerrainType => { + if (x <= 7 && y >= 58) { + return 'camp'; + } + if ( + (x >= 57 && x <= 68 && y >= 39 && y <= 51) || + (x >= 64 && x <= 76 && y >= 27 && y <= 39) || + (x >= 72 && x <= 83 && y >= 14 && y <= 29) || + (x >= 76 && x <= 83 && y >= 6 && y <= 16) + ) { + return 'fort'; + } + if ( + (x >= 21 && x <= 26 && y >= 55 && y <= 60) || + (x >= 43 && x <= 49 && y >= 49 && y <= 53) || + (x >= 58 && x <= 64 && y >= 43 && y <= 47) || + (x >= 72 && x <= 78 && y >= 35 && y <= 40) + ) { + return 'village'; + } + if ( + (x >= 16 && x <= 22 && y >= 0 && y <= 57) || + (x >= 35 && x <= 39 && y >= 9 && y <= 64) || + (x >= 54 && x <= 58 && y >= 4 && y <= 53) || + (x >= 70 && x <= 73 && y >= 18 && y <= 46) || + (y >= 34 && y <= 38 && x >= 22 && x <= 72) || + (y >= 51 && y <= 54 && x >= 36 && x <= 66) + ) { + return 'river'; + } + if ( + (y >= 60 && x >= 7 && x <= 32) || + (x >= 31 && x <= 47 && y >= 55 && y <= 60) || + (x >= 46 && x <= 62 && y >= 49 && y <= 55) || + (x >= 61 && x <= 73 && y >= 42 && y <= 49) || + (x >= 72 && x <= 82 && y >= 33 && y <= 42) || + (x >= 75 && x <= 83 && y >= 20 && y <= 33) || + (x >= 58 && x <= 81 && y >= 17 && y <= 21) || + (y >= 28 && y <= 31 && x >= 22 && x <= 75) + ) { + return 'road'; + } + if ( + (x >= 4 && x <= 30 && y >= 13 && y <= 47) || + (x >= 30 && x <= 49 && y >= 20 && y <= 52) || + (x >= 53 && x <= 70 && y >= 48 && y <= 65) || + (x >= 66 && x <= 83 && y >= 42 && y <= 64) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 15 && y <= 30) || + (x >= 28 && x <= 50 && y <= 20) || + (x >= 50 && x <= 70 && y >= 0 && y <= 22) || + (x >= 71 && x <= 83 && y <= 13) || + (x >= 33 && x <= 54 && y >= 58 && y <= 65) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 57) || + (x >= 81 && y >= 37) || + (x >= 62 && x <= 67 && y <= 10) || + (x >= 27 && x <= 34 && y >= 30 && y <= 46) || + (x >= 68 && x <= 70 && y >= 47 && y <= 58) + ) { + 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 2904eac..8d2f203 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -710,7 +710,42 @@ const unitTexture: Record = { 'fan-siege-ambusher-c': 'unit-rebel', 'fan-siege-officer-pang-de': 'unit-rebel-cavalry', 'fan-siege-officer-lu-meng': 'unit-rebel-leader', - 'fan-siege-leader-cao-ren': 'unit-rebel-leader' + 'fan-siege-leader-cao-ren': 'unit-rebel-leader', + 'jing-rear-scout-a': 'unit-rebel', + 'jing-rear-scout-b': 'unit-rebel', + 'jing-rear-scout-c': 'unit-rebel', + 'jing-rear-scout-d': 'unit-rebel', + 'jing-rear-scout-e': 'unit-rebel', + 'jing-rear-infantry-a': 'unit-rebel', + 'jing-rear-infantry-b': 'unit-rebel', + 'jing-rear-infantry-c': 'unit-rebel', + 'jing-rear-infantry-d': 'unit-rebel', + 'jing-rear-infantry-e': 'unit-rebel', + 'jing-rear-infantry-f': 'unit-rebel', + 'jing-rear-archer-a': 'unit-rebel-archer', + 'jing-rear-archer-b': 'unit-rebel-archer', + 'jing-rear-archer-c': 'unit-rebel-archer', + 'jing-rear-archer-d': 'unit-rebel-archer', + 'jing-rear-archer-e': 'unit-rebel-archer', + 'jing-rear-cavalry-a': 'unit-rebel-cavalry', + 'jing-rear-cavalry-b': 'unit-rebel-cavalry', + 'jing-rear-cavalry-c': 'unit-rebel-cavalry', + 'jing-rear-cavalry-d': 'unit-rebel-cavalry', + 'jing-rear-guard-a': 'unit-rebel', + 'jing-rear-guard-b': 'unit-rebel', + 'jing-rear-guard-c': 'unit-rebel', + 'jing-rear-guard-d': 'unit-rebel', + 'jing-rear-strategist-a': 'unit-rebel-archer', + 'jing-rear-strategist-b': 'unit-rebel-archer', + 'jing-rear-strategist-c': 'unit-rebel-archer', + 'jing-rear-ambusher-a': 'unit-rebel', + 'jing-rear-ambusher-b': 'unit-rebel', + 'jing-rear-ambusher-c': 'unit-rebel', + 'jing-rear-marine-a': 'unit-rebel', + 'jing-rear-marine-b': 'unit-rebel', + 'jing-rear-officer-lu-xun': 'unit-rebel-archer', + 'jing-rear-officer-mi-fang': 'unit-rebel-leader', + 'jing-rear-leader-lu-meng': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1878,7 +1913,42 @@ const enemyAiByUnitId: Record = { 'fan-siege-ambusher-c': 'aggressive', 'fan-siege-officer-pang-de': 'aggressive', 'fan-siege-officer-lu-meng': 'guard', - 'fan-siege-leader-cao-ren': 'guard' + 'fan-siege-leader-cao-ren': 'guard', + 'jing-rear-scout-a': 'aggressive', + 'jing-rear-scout-b': 'aggressive', + 'jing-rear-scout-c': 'aggressive', + 'jing-rear-scout-d': 'aggressive', + 'jing-rear-scout-e': 'aggressive', + 'jing-rear-infantry-a': 'guard', + 'jing-rear-infantry-b': 'guard', + 'jing-rear-infantry-c': 'guard', + 'jing-rear-infantry-d': 'guard', + 'jing-rear-infantry-e': 'guard', + 'jing-rear-infantry-f': 'guard', + 'jing-rear-archer-a': 'hold', + 'jing-rear-archer-b': 'hold', + 'jing-rear-archer-c': 'hold', + 'jing-rear-archer-d': 'hold', + 'jing-rear-archer-e': 'hold', + 'jing-rear-cavalry-a': 'aggressive', + 'jing-rear-cavalry-b': 'aggressive', + 'jing-rear-cavalry-c': 'aggressive', + 'jing-rear-cavalry-d': 'aggressive', + 'jing-rear-guard-a': 'guard', + 'jing-rear-guard-b': 'guard', + 'jing-rear-guard-c': 'guard', + 'jing-rear-guard-d': 'guard', + 'jing-rear-strategist-a': 'hold', + 'jing-rear-strategist-b': 'hold', + 'jing-rear-strategist-c': 'hold', + 'jing-rear-ambusher-a': 'aggressive', + 'jing-rear-ambusher-b': 'aggressive', + 'jing-rear-ambusher-c': 'aggressive', + 'jing-rear-marine-a': 'aggressive', + 'jing-rear-marine-b': 'guard', + 'jing-rear-officer-lu-xun': 'hold', + 'jing-rear-officer-mi-fang': 'guard', + 'jing-rear-leader-lu-meng': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 501ad0c..bd52b0f 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -7,6 +7,7 @@ import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-m import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png'; import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg'; import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg'; +import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-battle-map.svg'; import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg'; import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg'; import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg'; @@ -140,6 +141,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirty-ninth', thirtyNinthBattleMapUrl); this.load.image('battle-map-fortieth', fortiethBattleMapUrl); this.load.image('battle-map-forty-first', fortyFirstBattleMapUrl); + this.load.image('battle-map-forty-second', fortySecondBattleMapUrl); 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 1d0eb8a..f7cbb93 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -29,6 +29,7 @@ import { hanzhongScoutRecruitUnits, fanCastleBonds, fanCastleSiegeBonds, + jingRearCrisisBonds, jingDefenseBonds, jingzhouRecruitBonds, jingzhouRecruitUnits, @@ -357,7 +358,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'thirty-eighth-battle-jing-defense', 'thirty-ninth-battle-fan-castle-vanguard', 'fortieth-battle-han-river-flood', - 'forty-first-battle-fan-castle-siege' + 'forty-first-battle-fan-castle-siege', + 'forty-second-battle-jing-rear-crisis' ], nextHints: ['왕업 선언', '번성 포위', '형주 위기'] } @@ -404,7 +406,8 @@ const campBattleIds = { thirtyEighth: 'thirty-eighth-battle-jing-defense', thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard', fortieth: 'fortieth-battle-han-river-flood', - fortyFirst: 'forty-first-battle-fan-castle-siege' + fortyFirst: 'forty-first-battle-fan-castle-siege', + fortySecond: 'forty-second-battle-jing-rear-crisis' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -881,6 +884,20 @@ const sortieRulesByBattleId: Partial