From ef20c6460ba71910a41505b196a77d562f4086ab Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 19:18:14 +0900 Subject: [PATCH] Add Yiling vanguard chapter --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 225 +++++++++++++--- .../images/battle/forty-fifth-battle-map.svg | 73 +++++ src/game/data/battles.ts | 70 ++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 255 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 51 +++- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 173 +++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 11 files changed, 841 insertions(+), 53 deletions(-) create mode 100644 src/assets/images/battle/forty-fifth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index bbddba9..915ab02 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -68,16 +68,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 - 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 - 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-fourth 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 camp states, campaign timeline state, and camp save state +- 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 ## Next Steps -1. Build the Jing Province loss and Yiling revenge bridge, using Maicheng's aftermath, Liu Bei's grief, and Wu's sealed routes as the next campaign tension -2. Expand Maicheng aftermath events, including officer loyalty checks, escape-route choices, and supply decisions that affect sortie recommendations +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 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 7c21d8e..fdca748 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 !== 44 || + progressTabState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postBowangProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postChangbanProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postWulingProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postChangshaProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postYizhouProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postFuPassProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postLuoProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postLuofengProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postChengduProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postJiamengProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postYangpingProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postDingjunProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 45 || 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 !== 44 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 45 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 44 || + postFoundationProgressState.campaignProgress?.totalKnown !== 45 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 44 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 45 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 44 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 45 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 44 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 45 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 44 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 45 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 44 || + postJingRearProgressState.campaignProgress?.totalKnown !== 45 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 44 || + postGonganProgressState.campaignProgress?.totalKnown !== 45 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,14 +5899,171 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 44 || + postMaichengProgressState.campaignProgress?.totalKnown !== 45 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || - postMaichengProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { - throw new Error(`Expected post-Maicheng progress tab to complete the forty-fourth battle and pause before the next prepared slice: ${JSON.stringify(postMaichengProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Maicheng progress tab to complete the forty-fourth battle and point to Yiling vanguard: ${JSON.stringify(postMaichengProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-maicheng-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const yilingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yilingSortieState?.sortieVisible || + yilingSortieState.campaign?.step !== 'forty-fourth-camp' || + yilingSortieState.sortiePlan?.objectiveLine !== '이릉 진격로 · 육손 전초선 격파' || + yilingSortieState.sortiePlan?.maxCount !== 7 || + yilingSortieState.sortiePlan?.recommendedTotal !== 7 || + !yilingSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + !yilingSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || + !yilingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recommended) + ) { + throw new Error(`Expected Yiling vanguard sortie prep to lock Liu Bei and expose a seven-officer revenge-route choice: ${JSON.stringify(yilingSortieState)}`); + } + assertSortieTacticalRoster(yilingSortieState, [ + '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 yilingPriorityUnits = ['zhang-fei', 'zhao-yun', 'ma-chao', 'zhuge-liang', 'huang-quan', 'ma-liang']; + for (const unitId of yilingPriorityUnits) { + 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 && !yilingPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const yilingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yilingPriorityUnits.every((unitId) => + yilingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !yilingSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + yilingSortieReadyState.sortiePlan?.selectedCount !== 7 || + yilingSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Yiling vanguard sortie to deploy Liu Bei, Zhang Fei, Zhao Yun, Ma Chao, Zhuge Liang, Huang Quan, and Ma Liang: ${JSON.stringify(yilingSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-yiling-vanguard-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-vanguard-story.png', fullPage: true }); + + for (let i = 0; i < 55; i += 1) { + const enteredFortyFifthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-fifth-battle-yiling-vanguard'; + }); + if (enteredFortyFifthBattle) { + 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-fifth-battle-yiling-vanguard' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-fifth-battle.png', fullPage: true }); + + const fortyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortyFifthEnemies = fortyFifthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortyFifthAllies = fortyFifthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortyFifthEnemyBehaviors = new Set(fortyFifthEnemies.map((unit) => unit.ai)); + if ( + fortyFifthBattleState.camera?.mapWidth !== 90 || + fortyFifthBattleState.camera?.mapHeight !== 72 || + fortyFifthBattleState.victoryConditionLabel !== '육손 전초선 격파' || + fortyFifthEnemies.length < 38 || + !fortyFifthEnemyBehaviors.has('aggressive') || + !fortyFifthEnemyBehaviors.has('guard') || + !fortyFifthEnemyBehaviors.has('hold') || + !fortyFifthEnemies.some((unit) => unit.id === 'yiling-leader-lu-xun') || + !fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-zhu-ran') || + !fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-pan-zhang') || + !fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-ma-zhong') || + !fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-gan-ning-remnant') || + !fortyFifthAllies.some((unit) => unit.id === 'zhang-fei') || + !fortyFifthAllies.some((unit) => unit.id === 'zhao-yun') || + !fortyFifthAllies.some((unit) => unit.id === 'ma-chao') || + !fortyFifthAllies.some((unit) => unit.id === 'zhuge-liang') || + !fortyFifthAllies.some((unit) => unit.id === 'huang-quan') + ) { + throw new Error(`Expected forty-fifth battle to use Yiling vanguard map, Lu Xun objective, selected revenge-route officers, and mixed Wu AI: ${JSON.stringify(fortyFifthBattleState)}`); + } + + 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 fortyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortyFifthCampState?.campBattleId !== 'forty-fifth-battle-yiling-vanguard' || + fortyFifthCampState.campTitle !== '이릉 진격로 후 군영' || + fortyFifthCampState.availableDialogueIds?.length !== 3 || + !fortyFifthCampState.availableDialogueIds.every((id) => id.includes('yiling')) || + fortyFifthCampState.availableVisitIds?.length !== 2 || + !fortyFifthCampState.availableVisitIds.every((id) => id.includes('yiling')) || + fortyFifthCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected forty-fifth camp to expose Yiling dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFifthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-yiling-vanguard-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postYilingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postYilingProgressState?.activeTab !== 'progress' || + postYilingProgressState.campaignProgress?.completedKnown !== 45 || + postYilingProgressState.campaignProgress?.totalKnown !== 45 || + postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-yiling-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -5939,10 +6096,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 Maicheng isolation: ${JSON.stringify(titleContinueState)}`); + throw new Error(`Expected title continue to reopen the latest camp after Yiling vanguard: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-forty-fourth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, 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, and Maicheng isolation state at ${targetUrl}`); + 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}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-fifth-battle-map.svg b/src/assets/images/battle/forty-fifth-battle-map.svg new file mode 100644 index 0000000..eb71b20 --- /dev/null +++ b/src/assets/images/battle/forty-fifth-battle-map.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 1a47045..2f3a553 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -40,6 +40,10 @@ import { fortyFourthBattleMap, fortyFourthBattleUnits, fortyFourthBattleVictoryPages, + fortyFifthBattleBonds, + fortyFifthBattleMap, + fortyFifthBattleUnits, + fortyFifthBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -225,7 +229,8 @@ export type BattleScenarioId = | 'forty-first-battle-fan-castle-siege' | 'forty-second-battle-jing-rear-crisis' | 'forty-third-battle-gongan-collapse' - | 'forty-fourth-battle-maicheng-isolation'; + | 'forty-fourth-battle-maicheng-isolation' + | 'forty-fifth-battle-yiling-vanguard'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2857,6 +2862,66 @@ export const fortyFourthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortyFifthBattleScenario: BattleScenarioDefinition = { + id: 'forty-fifth-battle-yiling-vanguard', + title: '이릉 진격로', + victoryConditionLabel: '육손 전초선 격파', + defeatConditionLabel: '유비 퇴각', + openingObjectiveLines: [ + '형주 상실의 비보가 촉한 군영을 뒤흔들었습니다. 유비는 오군을 향해 동진하며 이릉으로 향하는 첫 길을 열고자 합니다.', + '오군은 정면 결전을 피하고 숲길, 강변, 나루에 전초선을 깔았습니다. 길게 늘어선 진영이 끊기지 않게 강변 보급로를 확보하십시오.', + '장비, 조운, 마초처럼 돌파와 기동에 강한 장수로 선봉을 밀고, 제갈량과 황권처럼 보급과 화계를 읽는 참모를 곁에 두면 안정적입니다.' + ], + map: fortyFifthBattleMap, + units: fortyFifthBattleUnits, + bonds: fortyFifthBattleBonds, + mapTextureKey: 'battle-map-forty-fifth', + leaderUnitId: 'yiling-leader-lu-xun', + quickVictoryTurnLimit: 74, + baseVictoryGold: 9560, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '육손 전초선 격파', + rewardGold: 6360, + unitId: 'yiling-leader-lu-xun' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1600, + unitId: 'liu-bei' + }, + { + id: 'river-camp', + kind: 'secure-terrain', + label: '강변 진영 확보', + rewardGold: 3120, + terrain: 'camp' + }, + { + id: 'yiling-road', + kind: 'secure-terrain', + label: '동진로 보급점 확보', + rewardGold: 2480, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '74턴 이내 승리', + rewardGold: 2200, + maxTurn: 74 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], + itemRewards: ['콩 46', '상처약 36', '탁주 15', '이릉 동진 지도 1', '강변 진영 말뚝 1'], + victoryPages: fortyFifthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2903,7 +2968,8 @@ export const battleScenarios: Record 'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario, 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario, 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario, - 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario + 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario, + 'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 1746659..06ead29 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -9,6 +9,7 @@ import { fortySecondBattleScenario, fortyThirdBattleScenario, fortyFourthBattleScenario, + fortyFifthBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -63,6 +64,8 @@ import { fortyThirdBattleVictoryPages, fortyFourthBattleIntroPages, fortyFourthBattleVictoryPages, + fortyFifthBattleIntroPages, + fortyFifthBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -636,13 +639,24 @@ const sortieFlows: Record = { }, [fortyFourthBattleScenario.id]: { afterBattleId: fortyFourthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '형주 상실의 대가', + eyebrow: '다음 전장', + title: fortyFifthBattleScenario.title, description: - '맥성의 포위망을 한 차례 뚫었지만 형주의 후방은 이미 크게 갈라졌습니다. 다음 장에서는 형주 상실과 유비의 분노가 더 큰 전쟁으로 번지는 흐름을 다룹니다.', - rewardHint: '다음 장: 형주 상실과 이릉으로 이어지는 분노 준비 중', - pages: fortyFourthBattleVictoryPages, - unavailableNotice: '형주 상실과 이릉 전쟁 흐름은 다음 작업에서 이어집니다. 지금은 맥성 고립전 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' + '맥성의 포위망을 한 차례 뚫었지만 형주의 후방은 이미 크게 갈라졌습니다. 유비의 분노는 이릉으로 향하고, 먼저 강변과 숲길의 동진로를 열어 긴 복수전의 첫 진영을 세워야 합니다.', + rewardHint: `예상 보상: ${fortyFifthBattleScenario.title} 개방 / 이릉 동진 지도`, + nextBattleId: fortyFifthBattleScenario.id, + campaignStep: 'forty-fifth-battle', + pages: [...fortyFourthBattleVictoryPages, ...fortyFifthBattleIntroPages] + }, + [fortyFifthBattleScenario.id]: { + afterBattleId: fortyFifthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '이릉 본전의 불씨', + description: + '이릉으로 향하는 첫 진영은 세웠지만 오군은 아직 본격적으로 맞붙지 않았습니다. 다음 장에서는 길게 늘어진 촉한 진영과 육손의 기다림, 화공의 위험을 본전으로 확장합니다.', + rewardHint: '다음 장: 이릉 본전과 화공 위기 준비 중', + pages: fortyFifthBattleVictoryPages, + unavailableNotice: '이릉 본전과 화공 위기는 다음 작업에서 이어집니다. 지금은 이릉 진격로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 568f72f..b88f065 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2905,6 +2905,70 @@ export const fortyFourthBattleVictoryPages: StoryPage[] = [ } ]; +export const fortyFifthBattleIntroPages: StoryPage[] = [ + { + id: 'forty-fifth-jing-loss-news', + bgm: 'story-dark', + chapter: '형주 상실의 비보', + background: 'story-militia', + speaker: '마량', + text: '맥성의 길은 열렸지만 형주의 큰 성들은 이미 강동의 깃발 아래로 넘어갔습니다. 돌아온 전령들은 흩어진 잔병과 끊긴 나루, 그리고 다시 모이지 못한 깃발의 소식을 전했습니다.' + }, + { + id: 'forty-fifth-liu-bei-grief', + bgm: 'story-dark', + chapter: '복수의 칼끝', + background: 'story-three-heroes', + speaker: '유비', + portrait: 'liuBei', + text: '형주의 상실은 땅 하나를 잃은 일이 아니오. 함께 세운 뜻이 강동의 말과 불길에 찢긴 일이오. 이릉으로 가는 길을 열어, 무너진 의리를 다시 묻겠소.' + }, + { + id: 'forty-fifth-zhuge-warning', + bgm: 'story-dark', + chapter: '분노와 군략', + background: 'story-resolve', + speaker: '제갈량', + text: '분노는 군을 움직이지만, 오래 버티게 하지는 못합니다. 오군은 산림과 물길을 이용해 긴 보급로를 태우려 할 것입니다. 먼저 이릉으로 향하는 동진로를 열고 진영을 흩어지지 않게 해야 합니다.' + }, + { + id: 'forty-fifth-sortie', + bgm: 'battle-prep', + chapter: '이릉 진격로', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '장강을 따라 깊이 들어가면 돌아올 길도 길어지오. 동진로의 숲과 나루를 안정시키고, 오군 선봉을 밀어 이릉 전쟁의 첫 진영을 세우시오.' + } +]; + +export const fortyFifthBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-fifth-victory-road-camp', + bgm: 'militia-theme', + chapter: '세워진 복수의 진', + background: 'story-sortie', + text: '이릉으로 향하는 첫 진격로가 열렸습니다. 유비군은 강가와 숲 사이에 진영을 세웠지만, 긴 보급로와 뜨거운 여름 바람은 이미 다음 위기를 예고했습니다.' + }, + { + id: 'forty-fifth-lu-xun-watches', + bgm: 'story-dark', + chapter: '육손의 침묵', + background: 'story-three-heroes', + speaker: '제갈량', + text: '오군은 크게 맞붙지 않았습니다. 육손은 물러난 것이 아니라 기다리고 있습니다. 진영이 길게 늘어설수록, 불길 하나가 군 전체를 흔들 수 있습니다.' + }, + { + id: 'forty-fifth-next-yiling-main', + bgm: 'story-dark', + chapter: '이릉의 불씨', + background: 'story-militia', + speaker: '유비', + portrait: 'liuBei', + text: '첫 길은 열렸으나 전쟁은 이제 시작이오. 다음에는 흩어진 진영을 붙들고, 강동의 기다림이 무엇을 노리는지 확인해야 하오.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3314,6 +3378,12 @@ export const fortyFourthBattleMap: BattleMap = { terrain: createFortyFourthBattleTerrain() }; +export const fortyFifthBattleMap: BattleMap = { + width: 90, + height: 72, + terrain: createFortyFifthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -15116,6 +15186,93 @@ export const fortyFourthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('maicheng-leader-lu-meng', '여몽', '형주 봉쇄 도독', 'rebelLeader', 108, 742, 186, 80, 43) ]; +const fortyFifthBattleAllyPositions: Record = { + 'liu-bei': { x: 14, y: 36 }, + 'guan-yu': { x: 10, y: 41 }, + 'zhang-fei': { x: 16, y: 39 }, + 'jian-yong': { x: 9, y: 44 }, + 'mi-zhu': { x: 11, y: 35 }, + 'sun-qian': { x: 12, y: 47 }, + 'zhao-yun': { x: 18, y: 34 }, + 'zhuge-liang': { x: 8, y: 38 }, + 'ma-liang': { x: 13, y: 32 }, + 'yi-ji': { x: 10, y: 50 }, + 'gong-zhi': { x: 17, y: 49 }, + 'huang-zhong': { x: 20, y: 37 }, + 'wei-yan': { x: 19, y: 42 }, + 'pang-tong': { x: 12, y: 29 }, + 'fa-zheng': { x: 15, y: 30 }, + 'wu-yi': { x: 21, y: 45 }, + 'yan-yan': { x: 22, y: 39 }, + 'li-yan': { x: 17, y: 27 }, + 'huang-quan': { x: 8, y: 33 }, + 'ma-chao': { x: 23, y: 34 }, + 'ma-dai': { x: 24, y: 38 }, + 'wang-ping': { x: 19, y: 31 } +}; + +export const fortyFifthBattleUnits: 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, fortyFifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('yiling-wu-scout-a', '오군 척후', '강변 척후', 'bandit', 108, 448, 144, 36, 23), + createThirtyFirstEnemyUnit('yiling-wu-scout-b', '오군 척후', '숲길 척후', 'bandit', 108, 448, 144, 45, 31), + createThirtyFirstEnemyUnit('yiling-wu-scout-c', '오군 척후', '나루 척후', 'bandit', 109, 456, 146, 57, 49), + createThirtyFirstEnemyUnit('yiling-wu-scout-d', '오군 척후', '남안 척후', 'bandit', 109, 456, 146, 73, 58), + createThirtyFirstEnemyUnit('yiling-wu-infantry-a', '오군 보병', '동진로 보병', 'yellowTurban', 109, 654, 170, 42, 28), + createThirtyFirstEnemyUnit('yiling-wu-infantry-b', '오군 보병', '숲길 보병', 'yellowTurban', 109, 654, 170, 49, 35), + createThirtyFirstEnemyUnit('yiling-wu-infantry-c', '오군 보병', '강변 보병', 'yellowTurban', 110, 666, 172, 56, 44), + createThirtyFirstEnemyUnit('yiling-wu-infantry-d', '오군 보병', '남로 보병', 'yellowTurban', 110, 666, 172, 65, 52), + createThirtyFirstEnemyUnit('yiling-wu-infantry-e', '오군 보병', '협곡 보병', 'yellowTurban', 111, 678, 174, 74, 39), + createThirtyFirstEnemyUnit('yiling-wu-infantry-f', '오군 보병', '후위 보병', 'yellowTurban', 111, 678, 174, 82, 47), + createThirtyFirstEnemyUnit('yiling-wu-archer-a', '오군 궁병', '숲그늘 궁병', 'archer', 109, 492, 150, 38, 20), + createThirtyFirstEnemyUnit('yiling-wu-archer-b', '오군 궁병', '동진 궁병', 'archer', 110, 500, 152, 52, 29), + createThirtyFirstEnemyUnit('yiling-wu-archer-c', '오군 궁병', '강안 궁병', 'archer', 110, 500, 152, 61, 42), + createThirtyFirstEnemyUnit('yiling-wu-archer-d', '오군 궁병', '남안 궁병', 'archer', 111, 508, 154, 71, 55), + createThirtyFirstEnemyUnit('yiling-wu-archer-e', '오군 궁병', '고지 궁병', 'archer', 111, 508, 154, 78, 34), + createThirtyFirstEnemyUnit('yiling-wu-archer-f', '오군 궁병', '화살 진지', 'archer', 112, 516, 156, 84, 50), + createThirtyFirstEnemyUnit('yiling-wu-cavalry-a', '오군 기병', '강북 기병', 'cavalry', 110, 668, 172, 43, 18), + createThirtyFirstEnemyUnit('yiling-wu-cavalry-b', '오군 기병', '숲끝 기병', 'cavalry', 110, 668, 172, 58, 24), + createThirtyFirstEnemyUnit('yiling-wu-cavalry-c', '오군 기병', '남로 기병', 'cavalry', 111, 680, 174, 69, 45), + createThirtyFirstEnemyUnit('yiling-wu-cavalry-d', '오군 기병', '추격 기병', 'cavalry', 111, 680, 174, 84, 39), + createThirtyFirstEnemyUnit('yiling-wu-marine-a', '오군 수군', '강변 수군', 'yellowTurban', 110, 672, 172, 39, 60), + createThirtyFirstEnemyUnit('yiling-wu-marine-b', '오군 수군', '나루 수군', 'yellowTurban', 111, 684, 174, 51, 63), + createThirtyFirstEnemyUnit('yiling-wu-marine-c', '오군 수군', '동쪽 수군', 'yellowTurban', 112, 696, 176, 67, 66), + createThirtyFirstEnemyUnit('yiling-wu-ambusher-a', '오군 복병', '숲속 복병', 'bandit', 109, 468, 148, 34, 35), + createThirtyFirstEnemyUnit('yiling-wu-ambusher-b', '오군 복병', '협곡 복병', 'bandit', 110, 476, 150, 46, 42), + createThirtyFirstEnemyUnit('yiling-wu-ambusher-c', '오군 복병', '강안 복병', 'bandit', 110, 476, 150, 59, 55), + createThirtyFirstEnemyUnit('yiling-wu-ambusher-d', '오군 복병', '후방 복병', 'bandit', 111, 484, 152, 76, 61), + createThirtyFirstEnemyUnit('yiling-wu-ambusher-e', '오군 복병', '화공 준비대', 'bandit', 112, 492, 154, 80, 27), + createThirtyFirstEnemyUnit('yiling-wu-guard-a', '오군 친위대', '전초 친위대', 'yellowTurban', 112, 704, 178, 64, 32), + createThirtyFirstEnemyUnit('yiling-wu-guard-b', '오군 친위대', '강안 친위대', 'yellowTurban', 112, 704, 178, 72, 43), + createThirtyFirstEnemyUnit('yiling-wu-guard-c', '오군 친위대', '후위 친위대', 'yellowTurban', 113, 716, 180, 85, 57), + createThirtyFirstEnemyUnit('yiling-strategist-a', '오군 책사', '화계 책사', 'strategist', 112, 532, 160, 62, 26), + createThirtyFirstEnemyUnit('yiling-strategist-b', '오군 책사', '진영 책사', 'strategist', 113, 540, 162, 74, 51), + createThirtyFirstEnemyUnit('yiling-strategist-c', '오군 책사', '수로 책사', 'strategist', 113, 540, 162, 58, 64), + createThirtyFirstEnemyUnit('yiling-officer-gan-ning-remnant', '감녕 잔부', '강안 돌격장', 'rebelLeader', 112, 712, 180, 53, 58), + createThirtyFirstEnemyUnit('yiling-officer-zhu-ran', '주연', '이릉 전초장', 'rebelLeader', 114, 736, 184, 76, 36), + createThirtyFirstEnemyUnit('yiling-officer-pan-zhang', '반장', '형주 추격장', 'rebelLeader', 113, 728, 182, 70, 24), + createThirtyFirstEnemyUnit('yiling-officer-ma-zhong', '마충', '밀림 매복장', 'bandit', 113, 548, 164, 49, 48), + createThirtyFirstEnemyUnit('yiling-leader-lu-xun', '육손', '오군 대도독', 'rebelLeader', 115, 768, 190, 84, 32) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -15877,6 +16034,35 @@ export const maichengIsolationBonds: BattleBond[] = [ export const fortyFourthBattleBonds: BattleBond[] = [...fortyThirdBattleBonds, ...maichengIsolationBonds].map( cloneBattleBondForScenario ); +export const yilingVanguardBonds: BattleBond[] = [ + { + id: 'liu-bei__zhang-fei_yiling', + unitIds: ['liu-bei', 'zhang-fei'], + title: '복수의 맹세', + level: 46, + exp: 0, + description: '유비의 분노와 장비의 돌파력은 이릉 진격로에서 전열을 빠르게 밀어붙인다.' + }, + { + id: 'zhuge-liang__huang-quan_yiling', + unitIds: ['zhuge-liang', 'huang-quan'], + title: '분노를 재는 책략', + level: 40, + exp: 0, + description: '제갈량과 황권의 신중한 판단은 긴 보급로와 오군의 기다림을 읽는 힘이 된다.' + }, + { + id: 'zhao-yun__ma-chao_yiling', + unitIds: ['zhao-yun', 'ma-chao'], + title: '동진 기동대', + level: 38, + exp: 0, + description: '조운과 마초의 빠른 기동은 강변과 숲길 사이의 오군 선봉을 끊는다.' + } +]; +export const fortyFifthBattleBonds: BattleBond[] = [...fortyFourthBattleBonds, ...yilingVanguardBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -18072,6 +18258,75 @@ function createFortyFourthBattleTerrain(): TerrainType[][] { ); } +function createFortyFifthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 72 }, (_, y) => + Array.from({ length: 90 }, (_, x): TerrainType => { + if ( + (x >= 0 && x <= 89 && y >= 60) || + (x >= 36 && x <= 48 && y >= 55 && y <= 71) || + (x >= 58 && x <= 70 && y >= 63 && y <= 71) + ) { + return 'river'; + } + if ( + (x >= 8 && x <= 18 && y >= 34 && y <= 45) || + (x >= 48 && x <= 56 && y >= 38 && y <= 47) || + (x >= 70 && x <= 78 && y >= 30 && y <= 39) + ) { + return 'village'; + } + if ( + (x >= 1 && x <= 83 && y >= 34 && y <= 39) || + (x >= 12 && x <= 87 && y >= 45 && y <= 49) || + (x >= 21 && x <= 32 && y >= 28 && y <= 45) || + (x >= 54 && x <= 63 && y >= 21 && y <= 45) || + (x >= 68 && x <= 83 && y >= 25 && y <= 34) + ) { + return 'road'; + } + if ( + (x >= 22 && x <= 52 && y >= 18 && y <= 53) || + (x >= 42 && x <= 76 && y >= 24 && y <= 58) || + (x >= 4 && x <= 27 && y >= 45 && y <= 59) || + (x >= 70 && x <= 89 && y >= 40 && y <= 59) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 24 && y <= 28) || + (x >= 32 && x <= 58 && y <= 20) || + (x >= 64 && x <= 89 && y <= 26) || + (x >= 78 && x <= 89 && y >= 27 && y <= 43) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 59) || + (x >= 87 && y <= 55) || + (x >= 29 && x <= 34 && y >= 0 && y <= 27) || + (x >= 60 && x <= 64 && y >= 0 && y <= 24) || + (x >= 31 && x <= 35 && y >= 50 && y <= 71) + ) { + return 'cliff'; + } + if ( + (x >= 6 && x <= 21 && y >= 31 && y <= 50) || + (x >= 47 && x <= 64 && y >= 32 && y <= 52) || + (x >= 73 && x <= 87 && y >= 26 && y <= 42) + ) { + return 'camp'; + } + if ( + (x >= 15 && x <= 22 && y >= 25 && y <= 31) || + (x >= 78 && x <= 86 && y >= 44 && y <= 55) + ) { + return 'fort'; + } + 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 9d5b5cc..57cbc1e 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -828,7 +828,15 @@ const unitTexture: Record = { 'maicheng-officer-ma-zhong': 'unit-rebel', 'maicheng-officer-lu-xun': 'unit-rebel-archer', 'maicheng-officer-xu-huang': 'unit-rebel-cavalry', - 'maicheng-leader-lu-meng': 'unit-rebel-leader' + 'maicheng-leader-lu-meng': 'unit-rebel-leader', + 'yiling-strategist-a': 'unit-rebel-archer', + 'yiling-strategist-b': 'unit-rebel-archer', + 'yiling-strategist-c': 'unit-rebel-archer', + 'yiling-officer-gan-ning-remnant': 'unit-rebel-leader', + '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' }; const unitTextureByClass: Partial> = { @@ -2114,7 +2122,46 @@ const enemyAiByUnitId: Record = { 'maicheng-officer-ma-zhong': 'aggressive', 'maicheng-officer-lu-xun': 'hold', 'maicheng-officer-xu-huang': 'aggressive', - 'maicheng-leader-lu-meng': 'guard' + 'maicheng-leader-lu-meng': 'guard', + 'yiling-wu-scout-a': 'aggressive', + 'yiling-wu-scout-b': 'aggressive', + 'yiling-wu-scout-c': 'aggressive', + 'yiling-wu-scout-d': 'aggressive', + 'yiling-wu-infantry-a': 'guard', + 'yiling-wu-infantry-b': 'guard', + 'yiling-wu-infantry-c': 'guard', + 'yiling-wu-infantry-d': 'guard', + 'yiling-wu-infantry-e': 'guard', + 'yiling-wu-infantry-f': 'guard', + 'yiling-wu-archer-a': 'hold', + 'yiling-wu-archer-b': 'hold', + 'yiling-wu-archer-c': 'hold', + 'yiling-wu-archer-d': 'hold', + 'yiling-wu-archer-e': 'hold', + 'yiling-wu-archer-f': 'hold', + 'yiling-wu-cavalry-a': 'aggressive', + 'yiling-wu-cavalry-b': 'aggressive', + 'yiling-wu-cavalry-c': 'aggressive', + 'yiling-wu-cavalry-d': 'aggressive', + 'yiling-wu-marine-a': 'aggressive', + 'yiling-wu-marine-b': 'guard', + 'yiling-wu-marine-c': 'guard', + 'yiling-wu-ambusher-a': 'aggressive', + 'yiling-wu-ambusher-b': 'aggressive', + 'yiling-wu-ambusher-c': 'aggressive', + 'yiling-wu-ambusher-d': 'aggressive', + 'yiling-wu-ambusher-e': 'aggressive', + 'yiling-wu-guard-a': 'guard', + 'yiling-wu-guard-b': 'guard', + 'yiling-wu-guard-c': 'guard', + 'yiling-strategist-a': 'hold', + 'yiling-strategist-b': 'hold', + 'yiling-strategist-c': 'hold', + 'yiling-officer-gan-ning-remnant': 'aggressive', + 'yiling-officer-zhu-ran': 'guard', + 'yiling-officer-pan-zhang': 'aggressive', + 'yiling-officer-ma-zhong': 'aggressive', + 'yiling-leader-lu-xun': 'hold' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 3c690d4..ab63221 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -10,6 +10,7 @@ import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battl import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-battle-map.svg'; 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 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'; @@ -146,6 +147,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-forty-second', fortySecondBattleMapUrl); 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('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 8e4d93a..d67f0be 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -47,6 +47,7 @@ import { xuzhouRecruitUnits, yizhouRecruitBonds, yizhouRecruitUnits, + yilingVanguardBonds, zhugeRecruitBonds, zhugeRecruitUnits, type PortraitKey, @@ -398,9 +399,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'forty-first-battle-fan-castle-siege', 'forty-second-battle-jing-rear-crisis', 'forty-third-battle-gongan-collapse', - 'forty-fourth-battle-maicheng-isolation' + 'forty-fourth-battle-maicheng-isolation', + 'forty-fifth-battle-yiling-vanguard' ], - nextHints: ['왕업 선언', '번성 포위', '형주 위기'] + nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 복수전'] } ]; @@ -448,7 +450,8 @@ const campBattleIds = { fortyFirst: 'forty-first-battle-fan-castle-siege', fortySecond: 'forty-second-battle-jing-rear-crisis', fortyThird: 'forty-third-battle-gongan-collapse', - fortyFourth: 'forty-fourth-battle-maicheng-isolation' + fortyFourth: 'forty-fourth-battle-maicheng-isolation', + fortyFifth: 'forty-fifth-battle-yiling-vanguard' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -967,6 +970,20 @@ const sortieRulesByBattleId: Partial