diff --git a/docs/roadmap.md b/docs/roadmap.md index 915ab02..1841a72 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -69,16 +69,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Forty-third battle Gongan gate-collapse route, using a twenty-two-officer roster across an 86x68 Gongan gate battlefield to delay Lu Meng's main infiltration, Fu Shiren's wavering guard, and Mi Fang's silent ledger crisis - Forty-fourth battle Maicheng isolation route, using Guan Yu's sealed retreat, Wu's reed-ford ambushes, Wei cavalry pressure, and a seven-officer sortie choice across an 88x70 battlefield to open one more road out of the encirclement - Forty-fifth battle Yiling vanguard route, turning Jing Province's loss into Liu Bei's revenge march and using a 90x72 river-forest battlefield to establish the first Yiling camp against Lu Xun's waiting line +- Forty-sixth battle Yiling fire-attack route, using Lu Xun's delayed fire plan, stretched Shu-Han camps, a 92x74 burning river-forest battlefield, and a seven-officer retreat-route sortie choice to break through toward Baidi Castle - 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, focused officer analysis, 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, selectable reserve drill focus, 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-fifth 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/Gongan collapse/Maicheng isolation/Yiling vanguard camp states, campaign timeline state, and camp save state +- Flow verification script from title through the forty-sixth 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/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the Yiling main battle and fire-attack crisis, using Lu Xun's patience, the stretched Shu-Han camp line, and summer wind as the next campaign tension -2. Expand Yiling aftermath events, including grief checks, overheated revenge choices, and camp-spacing decisions that affect sortie recommendations +1. Build the Baidi Castle entrustment chapter, using Liu Bei's retreat, final counsel, and Zhuge Liang's regency burden as the next story milestone +2. Expand post-Yiling camp consequences with grief checks, overheated revenge choices, and recovery decisions that affect sortie recommendations 3. Expand reserve planning into individual officer assignments, including targeted class practice, paired bond training, and camp task slots 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 fdca748..94007ad 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1739,7 +1739,7 @@ try { if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || - progressTabState.campaignProgress?.totalKnown !== 45 || + progressTabState.campaignProgress?.totalKnown !== 46 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' @@ -1891,7 +1891,7 @@ try { if ( postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState.campaignProgress?.completedKnown !== 18 || - postBowangProgressState.campaignProgress?.totalKnown !== 45 || + postBowangProgressState.campaignProgress?.totalKnown !== 46 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' @@ -2029,7 +2029,7 @@ try { if ( postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState.campaignProgress?.completedKnown !== 19 || - postChangbanProgressState.campaignProgress?.totalKnown !== 45 || + postChangbanProgressState.campaignProgress?.totalKnown !== 46 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' @@ -2167,7 +2167,7 @@ try { if ( postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || - postJiangdongProgressState.campaignProgress?.totalKnown !== 45 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 46 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' @@ -2307,7 +2307,7 @@ try { if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || - postRedCliffsProgressState.campaignProgress?.totalKnown !== 45 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 46 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' @@ -2447,7 +2447,7 @@ try { if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || - postFireAttackProgressState.campaignProgress?.totalKnown !== 45 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 46 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2590,7 +2590,7 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 45 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 46 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2732,7 +2732,7 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 45 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 46 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2875,7 +2875,7 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 45 || + postWulingProgressState.campaignProgress?.totalKnown !== 46 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' @@ -3024,7 +3024,7 @@ try { if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || - postChangshaProgressState.campaignProgress?.totalKnown !== 45 || + postChangshaProgressState.campaignProgress?.totalKnown !== 46 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' @@ -3169,7 +3169,7 @@ try { if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || - postYizhouProgressState.campaignProgress?.totalKnown !== 45 || + postYizhouProgressState.campaignProgress?.totalKnown !== 46 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' @@ -3316,7 +3316,7 @@ try { if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || - postFuPassProgressState.campaignProgress?.totalKnown !== 45 || + postFuPassProgressState.campaignProgress?.totalKnown !== 46 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3465,7 +3465,7 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 45 || + postLuoProgressState.campaignProgress?.totalKnown !== 46 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' @@ -3611,7 +3611,7 @@ try { if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || - postLuofengProgressState.campaignProgress?.totalKnown !== 45 || + postLuofengProgressState.campaignProgress?.totalKnown !== 46 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' @@ -3763,7 +3763,7 @@ try { if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || - postLuoMainProgressState.campaignProgress?.totalKnown !== 45 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 46 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' @@ -3916,7 +3916,7 @@ try { if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || - postMianzhuProgressState.campaignProgress?.totalKnown !== 45 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 46 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' @@ -4070,7 +4070,7 @@ try { if ( postChengduProgressState?.activeTab !== 'progress' || postChengduProgressState.campaignProgress?.completedKnown !== 33 || - postChengduProgressState.campaignProgress?.totalKnown !== 45 || + postChengduProgressState.campaignProgress?.totalKnown !== 46 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' @@ -4225,7 +4225,7 @@ try { if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || - postJiamengProgressState.campaignProgress?.totalKnown !== 45 || + postJiamengProgressState.campaignProgress?.totalKnown !== 46 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' @@ -4381,7 +4381,7 @@ try { if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || - postYangpingProgressState.campaignProgress?.totalKnown !== 45 || + postYangpingProgressState.campaignProgress?.totalKnown !== 46 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' @@ -4538,7 +4538,7 @@ try { if ( postDingjunProgressState?.activeTab !== 'progress' || postDingjunProgressState.campaignProgress?.completedKnown !== 36 || - postDingjunProgressState.campaignProgress?.totalKnown !== 45 || + postDingjunProgressState.campaignProgress?.totalKnown !== 46 || postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전' @@ -4691,7 +4691,7 @@ try { if ( postHanzhongProgressState?.activeTab !== 'progress' || postHanzhongProgressState.campaignProgress?.completedKnown !== 37 || - postHanzhongProgressState.campaignProgress?.totalKnown !== 45 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 46 || postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비' @@ -4752,7 +4752,7 @@ try { if ( postKingCouncilProgressState?.activeTab !== 'progress' || postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || - postKingCouncilProgressState.campaignProgress?.totalKnown !== 45 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 46 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 45 || + postFoundationProgressState.campaignProgress?.totalKnown !== 46 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 45 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 46 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 45 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 46 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 45 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 46 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 45 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 46 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 45 || + postJingRearProgressState.campaignProgress?.totalKnown !== 46 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 45 || + postGonganProgressState.campaignProgress?.totalKnown !== 46 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 45 || + postMaichengProgressState.campaignProgress?.totalKnown !== 46 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,14 +6056,172 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 45 || + postYilingProgressState.campaignProgress?.totalKnown !== 46 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || - postYilingProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { - throw new Error(`Expected post-Yiling progress tab to complete the forty-fifth battle and pause before the next prepared slice: ${JSON.stringify(postYilingProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Yiling progress tab to complete the forty-fifth battle and point to the Yiling fire attack: ${JSON.stringify(postYilingProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-yiling-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const yilingFireSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yilingFireSortieState?.sortieVisible || + yilingFireSortieState.campaign?.step !== 'forty-fifth-camp' || + yilingFireSortieState.sortiePlan?.objectiveLine !== '이릉 화공전 · 오군 화공망 돌파' || + yilingFireSortieState.sortiePlan?.maxCount !== 7 || + yilingFireSortieState.sortiePlan?.recommendedTotal !== 7 || + !yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + !yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) + ) { + throw new Error(`Expected Yiling fire sortie prep to lock Liu Bei and expose a seven-officer firebreak choice: ${JSON.stringify(yilingFireSortieState)}`); + } + assertSortieTacticalRoster(yilingFireSortieState, [ + '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 yilingFirePriorityUnits = ['huang-quan', 'zhuge-liang', 'ma-liang', 'zhao-yun', 'wang-ping', 'ma-chao']; + for (const unitId of yilingFirePriorityUnits) { + 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 && !yilingFirePriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const yilingFireSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yilingFirePriorityUnits.every((unitId) => + yilingFireSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !yilingFireSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + yilingFireSortieReadyState.sortiePlan?.selectedCount !== 7 || + yilingFireSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Yiling fire sortie to deploy Liu Bei, Huang Quan, Zhuge Liang, Ma Liang, Zhao Yun, Wang Ping, and Ma Chao: ${JSON.stringify(yilingFireSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-yiling-fire-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-yiling-fire-story.png', fullPage: true }); + + for (let i = 0; i < 55; i += 1) { + const enteredFortySixthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-sixth-battle-yiling-fire'; + }); + if (enteredFortySixthBattle) { + 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-sixth-battle-yiling-fire' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-sixth-battle.png', fullPage: true }); + + const fortySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortySixthEnemies = fortySixthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortySixthAllies = fortySixthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortySixthEnemyBehaviors = new Set(fortySixthEnemies.map((unit) => unit.ai)); + if ( + fortySixthBattleState.camera?.mapWidth !== 92 || + fortySixthBattleState.camera?.mapHeight !== 74 || + fortySixthBattleState.victoryConditionLabel !== '오군 화공망 돌파' || + fortySixthEnemies.length < 38 || + !fortySixthEnemyBehaviors.has('aggressive') || + !fortySixthEnemyBehaviors.has('guard') || + !fortySixthEnemyBehaviors.has('hold') || + !fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-leader-lu-xun') || + !fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-zhu-ran') || + !fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-han-dang') || + !fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-pan-zhang') || + !fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-ma-zhong') || + !fortySixthAllies.some((unit) => unit.id === 'huang-quan') || + !fortySixthAllies.some((unit) => unit.id === 'zhuge-liang') || + !fortySixthAllies.some((unit) => unit.id === 'ma-liang') || + !fortySixthAllies.some((unit) => unit.id === 'zhao-yun') || + !fortySixthAllies.some((unit) => unit.id === 'wang-ping') || + !fortySixthAllies.some((unit) => unit.id === 'ma-chao') + ) { + throw new Error(`Expected forty-sixth battle to use Yiling fire map, Lu Xun firebreak objective, selected retreat-route officers, and mixed Wu AI: ${JSON.stringify(fortySixthBattleState)}`); + } + + 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 fortySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortySixthCampState?.campBattleId !== 'forty-sixth-battle-yiling-fire' || + fortySixthCampState.campTitle !== '이릉 화공전 후 군영' || + fortySixthCampState.availableDialogueIds?.length !== 3 || + !fortySixthCampState.availableDialogueIds.every((id) => id.includes('yiling-fire')) || + fortySixthCampState.availableVisitIds?.length !== 2 || + !fortySixthCampState.availableVisitIds.every((id) => id.includes('yiling-fire')) || + fortySixthCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected forty-sixth camp to expose Yiling fire dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortySixthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-yiling-fire-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postYilingFireProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postYilingFireProgressState?.activeTab !== 'progress' || + postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 46 || + postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || + postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Yiling-fire progress tab to complete the forty-sixth battle and pause before Baidi Castle: ${JSON.stringify(postYilingFireProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-yiling-fire-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -6096,10 +6254,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 Yiling vanguard: ${JSON.stringify(titleContinueState)}`); + throw new Error(`Expected title continue to reopen the latest camp after Yiling fire: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-forty-fifth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, 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, Jing rear crisis, Gongan collapse, Maicheng isolation, and Yiling vanguard state at ${targetUrl}`); + console.log(`Verified title-to-forty-sixth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, 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, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, and Yiling fire attack state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-sixth-battle-map.svg b/src/assets/images/battle/forty-sixth-battle-map.svg new file mode 100644 index 0000000..9542261 --- /dev/null +++ b/src/assets/images/battle/forty-sixth-battle-map.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 2f3a553..fe4a0d9 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -44,6 +44,10 @@ import { fortyFifthBattleMap, fortyFifthBattleUnits, fortyFifthBattleVictoryPages, + fortySixthBattleBonds, + fortySixthBattleMap, + fortySixthBattleUnits, + fortySixthBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -230,7 +234,8 @@ export type BattleScenarioId = | 'forty-second-battle-jing-rear-crisis' | 'forty-third-battle-gongan-collapse' | 'forty-fourth-battle-maicheng-isolation' - | 'forty-fifth-battle-yiling-vanguard'; + | 'forty-fifth-battle-yiling-vanguard' + | 'forty-sixth-battle-yiling-fire'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2922,6 +2927,66 @@ export const fortyFifthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortySixthBattleScenario: BattleScenarioDefinition = { + id: 'forty-sixth-battle-yiling-fire', + title: '이릉 화공전', + victoryConditionLabel: '오군 화공망 돌파', + defeatConditionLabel: '유비 퇴각', + openingObjectiveLines: [ + '육손은 긴 촉한 진영이 마른 숲과 강변 군막 사이로 늘어진 순간을 기다렸습니다. 오군의 화공이 시작되며 진영 곳곳이 끊어집니다.', + '불타는 군막을 모두 지키려 하기보다, 서쪽 협곡과 강변 물길을 열어 병사를 모아야 합니다. 마을과 군막 지점을 확보하면 퇴로가 안정됩니다.', + '황권과 제갈량은 화공의 번짐을 읽고, 조운과 왕평은 흩어진 길을 찾습니다. 누가 퇴로를 맡고 누가 불길을 뚫을지 출진 편성에서 결정하십시오.' + ], + map: fortySixthBattleMap, + units: fortySixthBattleUnits, + bonds: fortySixthBattleBonds, + mapTextureKey: 'battle-map-forty-sixth', + leaderUnitId: 'yiling-fire-leader-lu-xun', + quickVictoryTurnLimit: 76, + baseVictoryGold: 9840, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '육손 화공 지휘선 돌파', + rewardGold: 6600, + unitId: 'yiling-fire-leader-lu-xun' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1700, + unitId: 'liu-bei' + }, + { + id: 'fire-camps', + kind: 'secure-terrain', + label: '불타는 군막 수습', + rewardGold: 3300, + terrain: 'camp' + }, + { + id: 'retreat-road', + kind: 'secure-terrain', + label: '서쪽 퇴로 확보', + rewardGold: 2600, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '76턴 이내 돌파', + rewardGold: 2280, + maxTurn: 76 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], + itemRewards: ['콩 48', '상처약 38', '탁주 16', '화공 방화선 표식 1', '백제성 퇴로 지도 1'], + victoryPages: fortySixthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2969,7 +3034,8 @@ export const battleScenarios: Record 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario, 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario, 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario, - 'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario + 'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario, + 'forty-sixth-battle-yiling-fire': fortySixthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 06ead29..3aad6f2 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -10,6 +10,7 @@ import { fortyThirdBattleScenario, fortyFourthBattleScenario, fortyFifthBattleScenario, + fortySixthBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -66,6 +67,8 @@ import { fortyFourthBattleVictoryPages, fortyFifthBattleIntroPages, fortyFifthBattleVictoryPages, + fortySixthBattleIntroPages, + fortySixthBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -650,13 +653,24 @@ const sortieFlows: Record = { }, [fortyFifthBattleScenario.id]: { afterBattleId: fortyFifthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '이릉 본전의 불씨', + eyebrow: '다음 전장', + title: fortySixthBattleScenario.title, description: - '이릉으로 향하는 첫 진영은 세웠지만 오군은 아직 본격적으로 맞붙지 않았습니다. 다음 장에서는 길게 늘어진 촉한 진영과 육손의 기다림, 화공의 위험을 본전으로 확장합니다.', - rewardHint: '다음 장: 이릉 본전과 화공 위기 준비 중', - pages: fortyFifthBattleVictoryPages, - unavailableNotice: '이릉 본전과 화공 위기는 다음 작업에서 이어집니다. 지금은 이릉 진격로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' + '이릉으로 향하는 첫 진영은 세웠지만 오군은 아직 본격적으로 맞붙지 않았습니다. 길게 늘어진 촉한 군막과 마른 숲길이 육손의 기다림 속에서 화공의 위기로 바뀝니다.', + rewardHint: `예상 보상: ${fortySixthBattleScenario.title} 개방 / 화공 방화선 표식`, + nextBattleId: fortySixthBattleScenario.id, + campaignStep: 'forty-sixth-battle', + pages: [...fortyFifthBattleVictoryPages, ...fortySixthBattleIntroPages] + }, + [fortySixthBattleScenario.id]: { + afterBattleId: fortySixthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '백제성의 남은 뜻', + description: + '이릉의 불길을 뚫고 퇴로는 열었지만 복수전은 꺾였습니다. 다음 장에서는 백제성으로 물러난 유비가 남은 나라의 길과 후계를 정하는 이야기를 다룹니다.', + rewardHint: '다음 장: 백제성 유탁과 촉한 정비 준비 중', + pages: fortySixthBattleVictoryPages, + unavailableNotice: '백제성 유탁과 촉한 정비는 다음 작업에서 이어집니다. 지금은 이릉 화공전 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index b88f065..a899913 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2969,6 +2969,70 @@ export const fortyFifthBattleVictoryPages: StoryPage[] = [ } ]; +export const fortySixthBattleIntroPages: StoryPage[] = [ + { + id: 'forty-sixth-camps-stretch', + bgm: 'story-dark', + chapter: '길어진 진영', + background: 'story-militia', + speaker: '황권', + text: '이릉의 첫 진영은 세웠지만, 촉한의 군막은 강변과 숲길을 따라 너무 길게 늘어졌습니다. 물길은 멀고, 마른 풀은 높으며, 오군은 여전히 큰 싸움을 피하고 있습니다.' + }, + { + id: 'forty-sixth-lu-xun-fire', + bgm: 'story-dark', + chapter: '기다린 불씨', + background: 'story-three-heroes', + speaker: '제갈량', + text: '육손의 침묵은 물러남이 아니었습니다. 바람이 동쪽에서 불고 군막이 말랐을 때, 오군의 작은 횃불이 긴 진영 전체를 흔드는 칼이 되었습니다.' + }, + { + id: 'forty-sixth-liu-bei-orders', + bgm: 'story-dark', + chapter: '불길 속의 명령', + background: 'story-resolve', + speaker: '유비', + portrait: 'liuBei', + text: '불길이 먼저 달려든다면, 사람의 길을 먼저 열어야 하오. 진영을 버려도 병사를 버릴 수는 없소. 서쪽 퇴로와 강변 물길을 붙들어 백제성으로 물러날 길을 만드시오.' + }, + { + id: 'forty-sixth-sortie', + bgm: 'battle-prep', + chapter: '이릉 화공전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '육손의 화공망이 진영을 갈라 놓았소. 불타는 군막 사이로 퇴로를 열고, 흩어진 병사를 모아 서쪽 협곡으로 빠져나가시오.' + } +]; + +export const fortySixthBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-sixth-victory-fire-broken', + bgm: 'story-dark', + chapter: '불길을 뚫은 길', + background: 'story-sortie', + text: '불타는 군막 사이로 좁은 퇴로가 열렸습니다. 승리라 부르기에는 잃은 것이 많았지만, 유비의 깃발은 완전히 꺼지지 않았습니다.' + }, + { + id: 'forty-sixth-retreat-baidi', + bgm: 'story-dark', + chapter: '백제성으로', + background: 'story-militia', + speaker: '제갈량', + text: '이릉의 불길은 복수의 속도를 꺾었습니다. 남은 병사들은 백제성으로 물러나며, 촉한의 다음 길이 전장에서 조정과 후계로 옮겨가고 있음을 느꼈습니다.' + }, + { + id: 'forty-sixth-next-baidi', + bgm: 'story-dark', + chapter: '남은 뜻', + background: 'story-three-heroes', + speaker: '유비', + portrait: 'liuBei', + text: '뜻을 세운 날은 멀어졌고, 손에 남은 것은 불탄 깃발뿐이오. 그러나 남은 이들이 있다면 뜻도 아직 끝나지 않았소. 다음 장에서는 백제성에서 남은 나라의 길을 정해야 하오.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3384,6 +3448,12 @@ export const fortyFifthBattleMap: BattleMap = { terrain: createFortyFifthBattleTerrain() }; +export const fortySixthBattleMap: BattleMap = { + width: 92, + height: 74, + terrain: createFortySixthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -15273,6 +15343,93 @@ export const fortyFifthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('yiling-leader-lu-xun', '육손', '오군 대도독', 'rebelLeader', 115, 768, 190, 84, 32) ]; +const fortySixthBattleAllyPositions: Record = { + 'liu-bei': { x: 18, y: 41 }, + 'guan-yu': { x: 13, y: 46 }, + 'zhang-fei': { x: 20, y: 44 }, + 'jian-yong': { x: 15, y: 51 }, + 'mi-zhu': { x: 13, y: 39 }, + 'sun-qian': { x: 16, y: 54 }, + 'zhao-yun': { x: 24, y: 38 }, + 'zhuge-liang': { x: 12, y: 43 }, + 'ma-liang': { x: 17, y: 35 }, + 'yi-ji': { x: 11, y: 56 }, + 'gong-zhi': { x: 21, y: 56 }, + 'huang-zhong': { x: 26, y: 42 }, + 'wei-yan': { x: 27, y: 47 }, + 'pang-tong': { x: 14, y: 32 }, + 'fa-zheng': { x: 19, y: 33 }, + 'wu-yi': { x: 28, y: 50 }, + 'yan-yan': { x: 30, y: 45 }, + 'li-yan': { x: 22, y: 30 }, + 'huang-quan': { x: 10, y: 37 }, + 'ma-chao': { x: 31, y: 39 }, + 'ma-dai': { x: 32, y: 43 }, + 'wang-ping': { x: 24, y: 34 } +}; + +export const fortySixthBattleUnits: 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, fortySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('yiling-fire-scout-a', '오군 척후', '불길 척후', 'bandit', 114, 480, 150, 36, 26), + createThirtyFirstEnemyUnit('yiling-fire-scout-b', '오군 척후', '강변 척후', 'bandit', 114, 480, 150, 48, 57), + createThirtyFirstEnemyUnit('yiling-fire-scout-c', '오군 척후', '서쪽 척후', 'bandit', 115, 488, 152, 30, 34), + createThirtyFirstEnemyUnit('yiling-fire-scout-d', '오군 척후', '협곡 척후', 'bandit', 115, 488, 152, 64, 33), + createThirtyFirstEnemyUnit('yiling-fire-infantry-a', '오군 보병', '불길 보병', 'yellowTurban', 115, 690, 178, 42, 31), + createThirtyFirstEnemyUnit('yiling-fire-infantry-b', '오군 보병', '진영 보병', 'yellowTurban', 115, 690, 178, 51, 38), + createThirtyFirstEnemyUnit('yiling-fire-infantry-c', '오군 보병', '강안 보병', 'yellowTurban', 116, 702, 180, 58, 53), + createThirtyFirstEnemyUnit('yiling-fire-infantry-d', '오군 보병', '남안 보병', 'yellowTurban', 116, 702, 180, 70, 61), + createThirtyFirstEnemyUnit('yiling-fire-infantry-e', '오군 보병', '협곡 보병', 'yellowTurban', 117, 714, 182, 76, 37), + createThirtyFirstEnemyUnit('yiling-fire-infantry-f', '오군 보병', '후위 보병', 'yellowTurban', 117, 714, 182, 86, 48), + createThirtyFirstEnemyUnit('yiling-fire-archer-a', '오군 궁병', '화살 불씨', 'archer', 115, 530, 160, 39, 22), + createThirtyFirstEnemyUnit('yiling-fire-archer-b', '오군 궁병', '숲그늘 궁병', 'archer', 116, 538, 162, 54, 29), + createThirtyFirstEnemyUnit('yiling-fire-archer-c', '오군 궁병', '강안 궁병', 'archer', 116, 538, 162, 61, 48), + createThirtyFirstEnemyUnit('yiling-fire-archer-d', '오군 궁병', '남안 궁병', 'archer', 117, 546, 164, 75, 58), + createThirtyFirstEnemyUnit('yiling-fire-archer-e', '오군 궁병', '고지 궁병', 'archer', 117, 546, 164, 82, 30), + createThirtyFirstEnemyUnit('yiling-fire-archer-f', '오군 궁병', '퇴로 궁병', 'archer', 118, 554, 166, 88, 43), + createThirtyFirstEnemyUnit('yiling-fire-cavalry-a', '오군 기병', '불길 추격대', 'cavalry', 116, 704, 180, 44, 19), + createThirtyFirstEnemyUnit('yiling-fire-cavalry-b', '오군 기병', '숲끝 기병', 'cavalry', 116, 704, 180, 62, 24), + createThirtyFirstEnemyUnit('yiling-fire-cavalry-c', '오군 기병', '남로 기병', 'cavalry', 117, 716, 182, 73, 45), + createThirtyFirstEnemyUnit('yiling-fire-cavalry-d', '오군 기병', '퇴로 추격대', 'cavalry', 117, 716, 182, 88, 35), + createThirtyFirstEnemyUnit('yiling-fire-marine-a', '오군 수군', '강변 수군', 'yellowTurban', 116, 708, 180, 42, 64), + createThirtyFirstEnemyUnit('yiling-fire-marine-b', '오군 수군', '물길 차단대', 'yellowTurban', 117, 720, 182, 56, 67), + createThirtyFirstEnemyUnit('yiling-fire-marine-c', '오군 수군', '동쪽 수군', 'yellowTurban', 118, 732, 184, 72, 69), + createThirtyFirstEnemyUnit('yiling-fire-ambusher-a', '오군 복병', '불붙임 복병', 'bandit', 115, 500, 154, 35, 39), + createThirtyFirstEnemyUnit('yiling-fire-ambusher-b', '오군 복병', '군막 복병', 'bandit', 116, 508, 156, 47, 44), + createThirtyFirstEnemyUnit('yiling-fire-ambusher-c', '오군 복병', '강안 복병', 'bandit', 116, 508, 156, 59, 59), + createThirtyFirstEnemyUnit('yiling-fire-ambusher-d', '오군 복병', '후방 복병', 'bandit', 117, 516, 158, 80, 62), + createThirtyFirstEnemyUnit('yiling-fire-ambusher-e', '오군 복병', '서쪽 차단대', 'bandit', 117, 516, 158, 28, 50), + createThirtyFirstEnemyUnit('yiling-fire-guard-a', '오군 친위대', '화공 친위대', 'yellowTurban', 118, 742, 186, 66, 34), + createThirtyFirstEnemyUnit('yiling-fire-guard-b', '오군 친위대', '강안 친위대', 'yellowTurban', 118, 742, 186, 78, 51), + createThirtyFirstEnemyUnit('yiling-fire-guard-c', '오군 친위대', '대도독 친위대', 'yellowTurban', 119, 754, 188, 87, 56), + createThirtyFirstEnemyUnit('yiling-fire-strategist-a', '오군 책사', '화계 책사', 'strategist', 118, 570, 168, 63, 28), + createThirtyFirstEnemyUnit('yiling-fire-strategist-b', '오군 책사', '연소 책사', 'strategist', 119, 578, 170, 72, 47), + createThirtyFirstEnemyUnit('yiling-fire-strategist-c', '오군 책사', '수로 책사', 'strategist', 119, 578, 170, 57, 65), + createThirtyFirstEnemyUnit('yiling-fire-officer-zhu-ran', '주연', '화공 선봉장', 'rebelLeader', 120, 776, 192, 78, 40), + createThirtyFirstEnemyUnit('yiling-fire-officer-han-dang', '한당', '강안 노장', 'rebelLeader', 119, 762, 190, 66, 58), + createThirtyFirstEnemyUnit('yiling-fire-officer-pan-zhang', '반장', '추격 지휘관', 'rebelLeader', 119, 762, 190, 72, 24), + createThirtyFirstEnemyUnit('yiling-fire-officer-ma-zhong', '마충', '밀림 차단장', 'bandit', 119, 578, 170, 49, 50), + createThirtyFirstEnemyUnit('yiling-fire-leader-lu-xun', '육손', '화공 대도독', 'rebelLeader', 122, 812, 198, 86, 31) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -16063,6 +16220,35 @@ export const yilingVanguardBonds: BattleBond[] = [ export const fortyFifthBattleBonds: BattleBond[] = [...fortyFourthBattleBonds, ...yilingVanguardBonds].map( cloneBattleBondForScenario ); +export const yilingFireBonds: BattleBond[] = [ + { + id: 'liu-bei__huang-quan_yiling-fire', + unitIds: ['liu-bei', 'huang-quan'], + title: '퇴로의 장부', + level: 44, + exp: 0, + description: '유비의 결단과 황권의 신중함은 불타는 진영에서 퇴로와 보급을 지키는 힘이 된다.' + }, + { + id: 'zhao-yun__wang-ping_yiling-fire', + unitIds: ['zhao-yun', 'wang-ping'], + title: '불길 속 길잡이', + level: 40, + exp: 0, + description: '조운의 기동과 왕평의 지형 판단은 불길이 번지는 숲길에서 흩어진 병사를 모은다.' + }, + { + id: 'zhuge-liang__ma-liang_yiling-fire', + unitIds: ['zhuge-liang', 'ma-liang'], + title: '남은 뜻의 기록', + level: 38, + exp: 0, + description: '제갈량과 마량은 이릉의 패착을 기록하고 다음 국정으로 이어질 판단을 남긴다.' + } +]; +export const fortySixthBattleBonds: BattleBond[] = [...fortyFifthBattleBonds, ...yilingFireBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -18327,6 +18513,67 @@ function createFortyFifthBattleTerrain(): TerrainType[][] { ); } +function createFortySixthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 74 }, (_, y) => + Array.from({ length: 92 }, (_, x): TerrainType => { + if ( + (x >= 0 && x <= 91 && y >= 62) || + (x >= 44 && x <= 58 && y >= 56 && y <= 73) || + (x >= 66 && x <= 78 && y >= 64 && y <= 73) + ) { + return 'river'; + } + if ( + (x >= 11 && x <= 22 && y >= 38 && y <= 50) || + (x >= 42 && x <= 55 && y >= 35 && y <= 49) || + (x >= 68 && x <= 82 && y >= 27 && y <= 42) + ) { + return 'camp'; + } + if ( + (x >= 1 && x <= 84 && y >= 38 && y <= 43) || + (x >= 15 && x <= 88 && y >= 50 && y <= 54) || + (x >= 24 && x <= 37 && y >= 31 && y <= 50) || + (x >= 57 && x <= 67 && y >= 23 && y <= 50) + ) { + return 'road'; + } + if ( + (x >= 25 && x <= 60 && y >= 18 && y <= 55) || + (x >= 48 && x <= 78 && y >= 25 && y <= 60) || + (x >= 4 && x <= 30 && y >= 48 && y <= 61) || + (x >= 72 && x <= 91 && y >= 42 && y <= 61) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 26 && y <= 29) || + (x >= 34 && x <= 60 && y <= 22) || + (x >= 66 && x <= 91 && y <= 28) || + (x >= 80 && x <= 91 && y >= 29 && y <= 44) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 61) || + (x >= 89 && y <= 57) || + (x >= 30 && x <= 35 && y >= 0 && y <= 30) || + (x >= 62 && x <= 66 && y >= 0 && y <= 25) || + (x >= 35 && x <= 39 && y >= 52 && y <= 73) + ) { + return 'cliff'; + } + if ((x >= 16 && x <= 23 && y >= 31 && y <= 37) || (x >= 76 && x <= 87 && y >= 45 && y <= 57)) { + return 'fort'; + } + if ((x >= 9 && x <= 18 && y >= 52 && y <= 58) || (x >= 56 && x <= 63 && y >= 53 && y <= 59)) { + return 'village'; + } + 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 57cbc1e..3efa792 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -836,7 +836,15 @@ const unitTexture: Record = { 'yiling-officer-zhu-ran': 'unit-rebel-leader', 'yiling-officer-pan-zhang': 'unit-rebel-leader', 'yiling-officer-ma-zhong': 'unit-rebel', - 'yiling-leader-lu-xun': 'unit-rebel-leader' + 'yiling-leader-lu-xun': 'unit-rebel-leader', + 'yiling-fire-strategist-a': 'unit-rebel-archer', + 'yiling-fire-strategist-b': 'unit-rebel-archer', + 'yiling-fire-strategist-c': 'unit-rebel-archer', + 'yiling-fire-officer-zhu-ran': 'unit-rebel-leader', + 'yiling-fire-officer-han-dang': 'unit-rebel-leader', + 'yiling-fire-officer-pan-zhang': 'unit-rebel-leader', + 'yiling-fire-officer-ma-zhong': 'unit-rebel', + 'yiling-fire-leader-lu-xun': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -2161,7 +2169,46 @@ const enemyAiByUnitId: Record = { 'yiling-officer-zhu-ran': 'guard', 'yiling-officer-pan-zhang': 'aggressive', 'yiling-officer-ma-zhong': 'aggressive', - 'yiling-leader-lu-xun': 'hold' + 'yiling-leader-lu-xun': 'hold', + 'yiling-fire-scout-a': 'aggressive', + 'yiling-fire-scout-b': 'aggressive', + 'yiling-fire-scout-c': 'aggressive', + 'yiling-fire-scout-d': 'aggressive', + 'yiling-fire-infantry-a': 'guard', + 'yiling-fire-infantry-b': 'guard', + 'yiling-fire-infantry-c': 'guard', + 'yiling-fire-infantry-d': 'guard', + 'yiling-fire-infantry-e': 'guard', + 'yiling-fire-infantry-f': 'guard', + 'yiling-fire-archer-a': 'hold', + 'yiling-fire-archer-b': 'hold', + 'yiling-fire-archer-c': 'hold', + 'yiling-fire-archer-d': 'hold', + 'yiling-fire-archer-e': 'hold', + 'yiling-fire-archer-f': 'hold', + 'yiling-fire-cavalry-a': 'aggressive', + 'yiling-fire-cavalry-b': 'aggressive', + 'yiling-fire-cavalry-c': 'aggressive', + 'yiling-fire-cavalry-d': 'aggressive', + 'yiling-fire-marine-a': 'aggressive', + 'yiling-fire-marine-b': 'guard', + 'yiling-fire-marine-c': 'guard', + 'yiling-fire-ambusher-a': 'aggressive', + 'yiling-fire-ambusher-b': 'aggressive', + 'yiling-fire-ambusher-c': 'aggressive', + 'yiling-fire-ambusher-d': 'aggressive', + 'yiling-fire-ambusher-e': 'aggressive', + 'yiling-fire-guard-a': 'guard', + 'yiling-fire-guard-b': 'guard', + 'yiling-fire-guard-c': 'guard', + 'yiling-fire-strategist-a': 'hold', + 'yiling-fire-strategist-b': 'hold', + 'yiling-fire-strategist-c': 'hold', + 'yiling-fire-officer-zhu-ran': 'aggressive', + 'yiling-fire-officer-han-dang': 'guard', + 'yiling-fire-officer-pan-zhang': 'aggressive', + 'yiling-fire-officer-ma-zhong': 'aggressive', + 'yiling-fire-leader-lu-xun': 'hold' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index ab63221..401c764 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -11,6 +11,7 @@ import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-bat import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battle-map.svg'; import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg'; import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg'; +import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-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'; @@ -148,6 +149,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-forty-third', fortyThirdBattleMapUrl); this.load.image('battle-map-forty-fourth', fortyFourthBattleMapUrl); this.load.image('battle-map-forty-fifth', fortyFifthBattleMapUrl); + this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl); 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 d67f0be..628b551 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -47,6 +47,7 @@ import { xuzhouRecruitUnits, yizhouRecruitBonds, yizhouRecruitUnits, + yilingFireBonds, yilingVanguardBonds, zhugeRecruitBonds, zhugeRecruitUnits, @@ -400,9 +401,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'forty-second-battle-jing-rear-crisis', 'forty-third-battle-gongan-collapse', 'forty-fourth-battle-maicheng-isolation', - 'forty-fifth-battle-yiling-vanguard' + 'forty-fifth-battle-yiling-vanguard', + 'forty-sixth-battle-yiling-fire' ], - nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 복수전'] + nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공'] } ]; @@ -451,7 +453,8 @@ const campBattleIds = { fortySecond: 'forty-second-battle-jing-rear-crisis', fortyThird: 'forty-third-battle-gongan-collapse', fortyFourth: 'forty-fourth-battle-maicheng-isolation', - fortyFifth: 'forty-fifth-battle-yiling-vanguard' + fortyFifth: 'forty-fifth-battle-yiling-vanguard', + fortySixth: 'forty-sixth-battle-yiling-fire' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -984,6 +987,20 @@ const sortieRulesByBattleId: Partial