From f9f0145c022523aae484d09f6208c8a00df02b06 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 18:55:38 +0900 Subject: [PATCH] Add Maicheng isolation chapter --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 224 ++++++++++++--- .../images/battle/forty-fourth-battle-map.svg | 95 +++++++ src/game/data/battles.ts | 73 ++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 263 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 96 ++++++- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 171 +++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 11 files changed, 918 insertions(+), 51 deletions(-) create mode 100644 src/assets/images/battle/forty-fourth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index ffe80c6..bbddba9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -67,16 +67,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Forty-first battle Fan Castle siege route, using a twenty-two-officer roster across an 82x64 castle battlefield to pressure Cao Ren's gate line, Pang De's desperate cavalry, and Wu's rear-shadow scouts - Forty-second battle Jing rear-crisis route, using Ma Liang and Mi Zhu's rear-guard roles across an 84x66 Jiangling ferry battlefield to repel Lu Meng's disguised scouts and expose Mi Fang's wavering rear line - 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 - 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-third 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 camp states, campaign timeline state, and camp save state +- 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 ## Next Steps -1. Build the Maicheng isolation follow-up, using Guan Yu's overextended Fan Castle front, Wu's sealed rear roads, and Wei's counterpressure as the next campaign tension -2. Expand Gongan collapse aftermath events, including officer loyalty checks, sealed gate choices, and supply decisions that affect sortie recommendations +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 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 c297658..7c21d8e 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 !== 43 || + progressTabState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postBowangProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postChangbanProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postWulingProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postChangshaProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postYizhouProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postFuPassProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postLuoProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postLuofengProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postChengduProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postJiamengProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postYangpingProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postDingjunProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 44 || 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 !== 43 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 44 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 43 || + postFoundationProgressState.campaignProgress?.totalKnown !== 44 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 43 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 44 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 43 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 44 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 43 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 44 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 43 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 44 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 43 || + postJingRearProgressState.campaignProgress?.totalKnown !== 44 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,14 +5741,172 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 43 || + postGonganProgressState.campaignProgress?.totalKnown !== 44 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || - postGonganProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { - throw new Error(`Expected post-Gongan progress tab to complete the forty-third battle and leave Maicheng isolation as the next prepared slice: ${JSON.stringify(postGonganProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Gongan progress tab to complete the forty-third battle and point to Maicheng isolation: ${JSON.stringify(postGonganProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-gongan-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const maichengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !maichengSortieState?.sortieVisible || + maichengSortieState.campaign?.step !== 'forty-third-camp' || + maichengSortieState.sortiePlan?.objectiveLine !== '맥성 고립전 · 맥성 포위망 돌파' || + maichengSortieState.sortiePlan?.maxCount !== 7 || + maichengSortieState.sortiePlan?.recommendedTotal !== 7 || + !maichengSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) || + !maichengSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + !maichengSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) + ) { + throw new Error(`Expected Maicheng isolation sortie prep to lock Guan Yu and expose a seven-officer breakout choice: ${JSON.stringify(maichengSortieState)}`); + } + assertSortieTacticalRoster(maichengSortieState, [ + '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 maichengPriorityUnits = ['wang-ping', 'zhao-yun', 'huang-zhong', 'ma-dai', 'ma-liang']; + for (const unitId of maichengPriorityUnits) { + 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 && !maichengPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const maichengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !maichengPriorityUnits.every((unitId) => + maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + !maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) || + maichengSortieReadyState.sortiePlan?.selectedCount !== 7 || + maichengSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Maicheng isolation sortie to deploy Liu Bei, Guan Yu, Wang Ping, Zhao Yun, Huang Zhong, Ma Dai, and Ma Liang: ${JSON.stringify(maichengSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-maicheng-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-maicheng-story.png', fullPage: true }); + + for (let i = 0; i < 55; i += 1) { + const enteredFortyFourthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-fourth-battle-maicheng-isolation'; + }); + if (enteredFortyFourthBattle) { + 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-fourth-battle-maicheng-isolation' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-fourth-battle.png', fullPage: true }); + + const fortyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortyFourthEnemies = fortyFourthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortyFourthAllies = fortyFourthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortyFourthEnemyBehaviors = new Set(fortyFourthEnemies.map((unit) => unit.ai)); + if ( + fortyFourthBattleState.camera?.mapWidth !== 88 || + fortyFourthBattleState.camera?.mapHeight !== 70 || + fortyFourthBattleState.victoryConditionLabel !== '맥성 포위망 돌파' || + fortyFourthEnemies.length < 45 || + !fortyFourthEnemyBehaviors.has('aggressive') || + !fortyFourthEnemyBehaviors.has('guard') || + !fortyFourthEnemyBehaviors.has('hold') || + !fortyFourthEnemies.some((unit) => unit.id === 'maicheng-leader-lu-meng') || + !fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-lu-xun') || + !fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-pan-zhang') || + !fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-ma-zhong') || + !fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-xu-huang') || + !fortyFourthAllies.some((unit) => unit.id === 'guan-yu') || + !fortyFourthAllies.some((unit) => unit.id === 'wang-ping') || + !fortyFourthAllies.some((unit) => unit.id === 'zhao-yun') || + !fortyFourthAllies.some((unit) => unit.id === 'huang-zhong') || + !fortyFourthAllies.some((unit) => unit.id === 'ma-dai') + ) { + throw new Error(`Expected forty-fourth battle to use Maicheng isolation map, breakout objective, selected support officers, and mixed Wu/Wei AI: ${JSON.stringify(fortyFourthBattleState)}`); + } + + 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 fortyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortyFourthCampState?.campBattleId !== 'forty-fourth-battle-maicheng-isolation' || + fortyFourthCampState.campTitle !== '맥성 고립전 후 군영' || + fortyFourthCampState.availableDialogueIds?.length !== 3 || + !fortyFourthCampState.availableDialogueIds.every((id) => id.includes('maicheng')) || + fortyFourthCampState.availableVisitIds?.length !== 2 || + !fortyFourthCampState.availableVisitIds.every((id) => id.includes('maicheng')) || + fortyFourthCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected forty-fourth camp to expose Maicheng dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFourthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-maicheng-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postMaichengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postMaichengProgressState?.activeTab !== 'progress' || + postMaichengProgressState.campaignProgress?.completedKnown !== 44 || + postMaichengProgressState.campaignProgress?.totalKnown !== 44 || + postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-maicheng-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -5781,10 +5939,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 Gongan collapse: ${JSON.stringify(titleContinueState)}`); + throw new Error(`Expected title continue to reopen the latest camp after Maicheng isolation: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-forty-third-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, and Gongan collapse state at ${targetUrl}`); + 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}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-fourth-battle-map.svg b/src/assets/images/battle/forty-fourth-battle-map.svg new file mode 100644 index 0000000..1be9751 --- /dev/null +++ b/src/assets/images/battle/forty-fourth-battle-map.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index a933dc3..1a47045 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -36,6 +36,10 @@ import { fortyThirdBattleMap, fortyThirdBattleUnits, fortyThirdBattleVictoryPages, + fortyFourthBattleBonds, + fortyFourthBattleMap, + fortyFourthBattleUnits, + fortyFourthBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -220,7 +224,8 @@ export type BattleScenarioId = | 'fortieth-battle-han-river-flood' | 'forty-first-battle-fan-castle-siege' | 'forty-second-battle-jing-rear-crisis' - | 'forty-third-battle-gongan-collapse'; + | 'forty-third-battle-gongan-collapse' + | 'forty-fourth-battle-maicheng-isolation'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2789,6 +2794,69 @@ export const fortyThirdBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortyFourthBattleScenario: BattleScenarioDefinition = { + id: 'forty-fourth-battle-maicheng-isolation', + title: '맥성 고립전', + victoryConditionLabel: '맥성 포위망 돌파', + defeatConditionLabel: '유비 또는 관우 퇴각', + openingObjectiveLines: [ + '공안 성문을 잠시 붙들었지만 후방 소식은 끊겼습니다. 맥성 북문으로 빠지는 좁은 길을 열어 관우 전선의 잔병을 수습하십시오.', + '강동군은 숲길과 갈대 물목을 막고, 위군 추격대는 북쪽 길을 압박합니다. 포위망의 한쪽만 밀어서는 길이 다시 닫힙니다.', + '왕평, 조운, 마대, 황충처럼 지형 판단과 기동, 고지 견제에 강한 장수를 데려가면 맥성의 좁은 퇴로를 지키기 좋습니다.' + ], + map: fortyFourthBattleMap, + units: fortyFourthBattleUnits, + bonds: fortyFourthBattleBonds, + mapTextureKey: 'battle-map-forty-fourth', + leaderUnitId: 'maicheng-leader-lu-meng', + quickVictoryTurnLimit: 72, + baseVictoryGold: 9280, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '여몽 봉쇄선 돌파', + rewardGold: 6120, + unitId: 'maicheng-leader-lu-meng' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1440, + unitId: 'liu-bei' + }, + { + id: 'guan-yu', + kind: 'keep-unit-alive', + label: '관우 생존', + rewardGold: 2260, + unitId: 'guan-yu' + }, + { + id: 'maicheng-gate', + kind: 'secure-terrain', + label: '맥성 북문 확보', + rewardGold: 2960, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '72턴 이내 승리', + rewardGold: 2120, + maxTurn: 72 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'guan-yu' } + ], + itemRewards: ['콩 44', '상처약 34', '탁주 14', '맥성 북문 지도 1', '갈대 물목 표식 1'], + victoryPages: fortyFourthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2834,7 +2902,8 @@ export const battleScenarios: Record 'fortieth-battle-han-river-flood': fortiethBattleScenario, 'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario, 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario, - 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario + 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario, + 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index fc7b91e..1746659 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -8,6 +8,7 @@ import { fortyFirstBattleScenario, fortySecondBattleScenario, fortyThirdBattleScenario, + fortyFourthBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -60,6 +61,8 @@ import { fortySecondBattleVictoryPages, fortyThirdBattleIntroPages, fortyThirdBattleVictoryPages, + fortyFourthBattleIntroPages, + fortyFourthBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -622,13 +625,24 @@ const sortieFlows: Record = { }, [fortyThirdBattleScenario.id]: { afterBattleId: fortyThirdBattleScenario.id, - eyebrow: '다음 장 준비', - title: '형주 고립 심화', + eyebrow: '다음 전장', + title: fortyFourthBattleScenario.title, description: - '공안 성문은 잠시 붙들었지만 미방과 부사인의 흔들림은 완전히 멈추지 않았습니다. 다음 장에서는 관우의 번성 전선이 후방 상실과 맞물려 고립되는 흐름을 다룹니다.', - rewardHint: '다음 장: 맥성 고립과 형주 상실 준비 중', - pages: fortyThirdBattleVictoryPages, - unavailableNotice: '맥성 고립과 형주 상실 흐름은 다음 작업에서 이어집니다. 지금은 공안 성문 변고 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' + '공안 성문은 잠시 붙들었지만 미방과 부사인의 흔들림은 완전히 멈추지 않았습니다. 다음 출진에서는 맥성으로 향하는 좁은 퇴로를 열어 관우 전선의 고립을 늦춰야 합니다.', + rewardHint: `예상 보상: ${fortyFourthBattleScenario.title} 개방 / 맥성 북문 지도`, + nextBattleId: fortyFourthBattleScenario.id, + campaignStep: 'forty-fourth-battle', + pages: [...fortyThirdBattleVictoryPages, ...fortyFourthBattleIntroPages] + }, + [fortyFourthBattleScenario.id]: { + afterBattleId: fortyFourthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '형주 상실의 대가', + description: + '맥성의 포위망을 한 차례 뚫었지만 형주의 후방은 이미 크게 갈라졌습니다. 다음 장에서는 형주 상실과 유비의 분노가 더 큰 전쟁으로 번지는 흐름을 다룹니다.', + rewardHint: '다음 장: 형주 상실과 이릉으로 이어지는 분노 준비 중', + pages: fortyFourthBattleVictoryPages, + unavailableNotice: '형주 상실과 이릉 전쟁 흐름은 다음 작업에서 이어집니다. 지금은 맥성 고립전 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 3ebd852..568f72f 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2842,6 +2842,69 @@ export const fortyThirdBattleVictoryPages: StoryPage[] = [ } ]; +export const fortyFourthBattleIntroPages: StoryPage[] = [ + { + id: 'forty-fourth-maicheng-orders', + bgm: 'story-dark', + chapter: '끊어진 후방', + background: 'story-militia', + speaker: '마량', + text: '공안 성문을 다시 잠갔으나 강릉과 나루의 소식이 끊겼습니다. 관우 장군의 번성 전선은 앞의 조조군과 뒤의 강동군 사이에서 길을 잃고 있습니다.' + }, + { + id: 'forty-fourth-lu-meng-seals-roads', + bgm: 'story-dark', + chapter: '봉쇄되는 길', + background: 'story-three-heroes', + speaker: '왕평', + text: '맥성으로 빠지는 길목마다 흰 옷을 벗은 강동 병사가 서 있습니다. 큰길은 막혔고, 숲과 물목의 작은 길도 오래 버티지 못할 것입니다.' + }, + { + id: 'forty-fourth-guan-yu-resolve', + bgm: 'battle-prep', + chapter: '남은 깃발', + background: 'story-liu-bei', + speaker: '관우', + portrait: 'guanYu', + text: '성 하나가 아니라 의리를 지키는 싸움이 되었소. 맥성으로 길을 내고 남은 병사들을 수습하겠소. 길이 좁을수록 마음이 먼저 무너지지 않게 하시오.' + }, + { + id: 'forty-fourth-sortie', + bgm: 'battle-prep', + chapter: '맥성 고립전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '운장의 깃발이 고립되었다면, 늦더라도 길을 열어야 하오. 북문과 갈대 물목을 동시에 살펴 포위망을 찢고 맥성의 퇴로를 확보하시오.' + } +]; + +export const fortyFourthBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-fourth-victory-road-opened', + bgm: 'militia-theme', + chapter: '뚫린 좁은 길', + background: 'story-sortie', + text: '맥성 북문의 좁은 길은 간신히 열렸고, 흩어진 병사들이 다시 깃발 아래로 모였습니다. 그러나 길 밖의 횃불은 여전히 더 많았습니다.' + }, + { + id: 'forty-fourth-lu-meng-tightens', + bgm: 'story-dark', + chapter: '좁아지는 포위', + background: 'story-three-heroes', + speaker: '제갈량', + text: '여몽은 물러난 것이 아니라 더 넓게 둘러섰습니다. 맥성의 작은 승리는 시간을 벌었을 뿐, 형주를 되찾는 길은 아직 멀고 험합니다.' + }, + { + id: 'forty-fourth-next-jing-loss', + bgm: 'story-dark', + chapter: '형주의 상실', + background: 'story-militia', + speaker: '마량', + text: '맥성의 포위는 풀렸으나 강릉과 공안의 마음은 이미 갈라졌습니다. 다음 장에서는 형주 상실의 대가와 유비의 분노가 더 큰 전쟁으로 번지는 흐름을 준비해야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3245,6 +3308,12 @@ export const fortyThirdBattleMap: BattleMap = { terrain: createFortyThirdBattleTerrain() }; +export const fortyFourthBattleMap: BattleMap = { + width: 88, + height: 70, + terrain: createFortyFourthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -14953,6 +15022,100 @@ export const fortyThirdBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('jing-collapse-leader-lu-meng', '여몽', '백의도강 도독', 'rebelLeader', 103, 720, 182, 83, 24) ]; +const fortyFourthBattleAllyPositions: Record = { + 'liu-bei': { x: 24, y: 45 }, + 'guan-yu': { x: 30, y: 38 }, + 'zhang-fei': { x: 24, y: 49 }, + 'jian-yong': { x: 20, y: 50 }, + 'mi-zhu': { x: 23, y: 43 }, + 'sun-qian': { x: 22, y: 53 }, + 'zhao-yun': { x: 33, y: 42 }, + 'zhuge-liang': { x: 25, y: 41 }, + 'ma-liang': { x: 31, y: 45 }, + 'yi-ji': { x: 28, y: 51 }, + 'gong-zhi': { x: 29, y: 54 }, + 'huang-zhong': { x: 35, y: 36 }, + 'wei-yan': { x: 32, y: 34 }, + 'pang-tong': { x: 27, y: 47 }, + 'fa-zheng': { x: 36, y: 46 }, + 'wu-yi': { x: 38, y: 41 }, + 'yan-yan': { x: 39, y: 44 }, + 'li-yan': { x: 35, y: 32 }, + 'huang-quan': { x: 40, y: 48 }, + 'ma-chao': { x: 41, y: 36 }, + 'ma-dai': { x: 43, y: 39 }, + 'wang-ping': { x: 37, y: 34 } +}; + +export const fortyFourthBattleUnits: 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, fortyFourthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('maicheng-wu-scout-a', '강동 척후', '북문 척후', 'bandit', 101, 426, 138, 48, 30), + createThirtyFirstEnemyUnit('maicheng-wu-scout-b', '강동 척후', '갈대 척후', 'bandit', 101, 426, 138, 53, 52), + createThirtyFirstEnemyUnit('maicheng-wu-scout-c', '강동 척후', '서림 척후', 'bandit', 102, 434, 140, 18, 41), + createThirtyFirstEnemyUnit('maicheng-wu-scout-d', '강동 척후', '남문 척후', 'bandit', 102, 434, 140, 45, 63), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-a', '강동 보병', '북문 보병', 'yellowTurban', 102, 620, 164, 55, 28), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-b', '강동 보병', '북로 보병', 'yellowTurban', 102, 620, 164, 62, 34), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-c', '강동 보병', '동문 보병', 'yellowTurban', 103, 632, 166, 70, 43), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-d', '강동 보병', '남문 보병', 'yellowTurban', 103, 632, 166, 58, 59), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-e', '강동 보병', '서림 보병', 'yellowTurban', 104, 644, 168, 22, 52), + createThirtyFirstEnemyUnit('maicheng-wu-infantry-f', '강동 보병', '포위 보병', 'yellowTurban', 104, 644, 168, 76, 28), + createThirtyFirstEnemyUnit('maicheng-wu-archer-a', '강동 궁병', '북문 궁병', 'archer', 102, 456, 146, 50, 24), + createThirtyFirstEnemyUnit('maicheng-wu-archer-b', '강동 궁병', '동림 궁병', 'archer', 103, 464, 148, 72, 38), + createThirtyFirstEnemyUnit('maicheng-wu-archer-c', '강동 궁병', '남문 궁병', 'archer', 103, 464, 148, 61, 64), + createThirtyFirstEnemyUnit('maicheng-wu-archer-d', '강동 궁병', '서림 궁병', 'archer', 104, 472, 150, 19, 47), + createThirtyFirstEnemyUnit('maicheng-wu-archer-e', '강동 궁병', '갈대 궁병', 'archer', 104, 472, 150, 43, 56), + createThirtyFirstEnemyUnit('maicheng-wu-archer-f', '강동 궁병', '산길 궁병', 'archer', 105, 480, 152, 66, 21), + createThirtyFirstEnemyUnit('maicheng-wu-cavalry-a', '강동 기병', '북로 기병', 'cavalry', 103, 632, 166, 57, 18), + createThirtyFirstEnemyUnit('maicheng-wu-cavalry-b', '강동 기병', '동로 기병', 'cavalry', 103, 632, 166, 80, 36), + createThirtyFirstEnemyUnit('maicheng-wu-cavalry-c', '강동 기병', '남로 기병', 'cavalry', 104, 644, 168, 66, 61), + createThirtyFirstEnemyUnit('maicheng-wu-cavalry-d', '강동 기병', '서로 기병', 'cavalry', 104, 644, 168, 16, 58), + createThirtyFirstEnemyUnit('maicheng-wu-guard-a', '강동 친위대', '북문 친위대', 'yellowTurban', 105, 668, 172, 63, 27), + createThirtyFirstEnemyUnit('maicheng-wu-guard-b', '강동 친위대', '동문 친위대', 'yellowTurban', 105, 668, 172, 78, 45), + createThirtyFirstEnemyUnit('maicheng-wu-guard-c', '강동 친위대', '남문 친위대', 'yellowTurban', 106, 680, 174, 54, 66), + createThirtyFirstEnemyUnit('maicheng-wu-guard-d', '강동 친위대', '서문 친위대', 'yellowTurban', 106, 680, 174, 25, 57), + createThirtyFirstEnemyUnit('maicheng-wu-ambusher-a', '백의 복병', '북문 복병', 'bandit', 102, 442, 142, 44, 28), + createThirtyFirstEnemyUnit('maicheng-wu-ambusher-b', '백의 복병', '숲길 복병', 'bandit', 103, 450, 144, 27, 33), + createThirtyFirstEnemyUnit('maicheng-wu-ambusher-c', '백의 복병', '갈대 복병', 'bandit', 103, 450, 144, 48, 58), + createThirtyFirstEnemyUnit('maicheng-wu-ambusher-d', '백의 복병', '서림 복병', 'bandit', 104, 458, 146, 12, 44), + createThirtyFirstEnemyUnit('maicheng-wu-ambusher-e', '백의 복병', '퇴로 복병', 'bandit', 104, 458, 146, 72, 55), + createThirtyFirstEnemyUnit('maicheng-wu-marine-a', '강동 수군', '갈대 수군', 'yellowTurban', 103, 636, 166, 35, 61), + createThirtyFirstEnemyUnit('maicheng-wu-marine-b', '강동 수군', '물목 수군', 'yellowTurban', 104, 648, 168, 42, 66), + createThirtyFirstEnemyUnit('maicheng-wu-marine-c', '강동 수군', '나루 수군', 'yellowTurban', 104, 648, 168, 52, 67), + createThirtyFirstEnemyUnit('maicheng-wei-cavalry-a', '위군 기병', '번성 추격대', 'cavalry', 104, 654, 170, 67, 15), + createThirtyFirstEnemyUnit('maicheng-wei-cavalry-b', '위군 기병', '북로 추격대', 'cavalry', 105, 666, 172, 74, 19), + createThirtyFirstEnemyUnit('maicheng-wei-cavalry-c', '위군 기병', '동로 추격대', 'cavalry', 105, 666, 172, 82, 25), + createThirtyFirstEnemyUnit('maicheng-wei-infantry-a', '위군 보병', '번성 보병', 'yellowTurban', 104, 650, 168, 69, 12), + createThirtyFirstEnemyUnit('maicheng-wei-infantry-b', '위군 보병', '북로 보병', 'yellowTurban', 105, 662, 170, 78, 16), + createThirtyFirstEnemyUnit('maicheng-wei-infantry-c', '위군 보병', '동로 보병', 'yellowTurban', 105, 662, 170, 84, 30), + createThirtyFirstEnemyUnit('maicheng-strategist-a', '강동 책사', '봉쇄 책사', 'strategist', 105, 492, 152, 61, 31), + createThirtyFirstEnemyUnit('maicheng-strategist-b', '강동 책사', '서찰 책사', 'strategist', 105, 492, 152, 75, 50), + createThirtyFirstEnemyUnit('maicheng-strategist-c', '위군 책사', '추격 책사', 'strategist', 106, 500, 154, 78, 22), + createThirtyFirstEnemyUnit('maicheng-officer-pan-zhang', '반장', '강동 포위장', 'rebelLeader', 106, 690, 176, 68, 53), + createThirtyFirstEnemyUnit('maicheng-officer-ma-zhong', '마충', '매복 지휘관', 'bandit', 106, 520, 156, 52, 36), + createThirtyFirstEnemyUnit('maicheng-officer-lu-xun', '육손', '강동 설객', 'strategist', 107, 512, 156, 73, 34), + createThirtyFirstEnemyUnit('maicheng-officer-xu-huang', '서황', '번성 추격장', 'cavalry', 107, 706, 180, 82, 18), + createThirtyFirstEnemyUnit('maicheng-leader-lu-meng', '여몽', '형주 봉쇄 도독', 'rebelLeader', 108, 742, 186, 80, 43) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -15685,6 +15848,35 @@ export const jingCollapseBonds: BattleBond[] = [ export const fortyThirdBattleBonds: BattleBond[] = [...fortySecondBattleBonds, ...jingCollapseBonds].map( cloneBattleBondForScenario ); +export const maichengIsolationBonds: BattleBond[] = [ + { + id: 'guan-yu__wang-ping_maicheng', + unitIds: ['guan-yu', 'wang-ping'], + title: '맥성 북문', + level: 40, + exp: 0, + description: '관우의 결단과 왕평의 지형 판단은 맥성 북문의 좁은 퇴로를 찾는 힘이 된다.' + }, + { + id: 'zhao-yun__ma-dai_maicheng', + unitIds: ['zhao-yun', 'ma-dai'], + title: '기동 차단', + level: 36, + exp: 0, + description: '조운의 빠른 판단과 마대의 안정된 기병 운용은 포위망의 틈을 빠르게 찢는다.' + }, + { + id: 'guan-yu__huang-zhong_maicheng', + unitIds: ['guan-yu', 'huang-zhong'], + title: '노장의 깃발', + level: 34, + exp: 0, + description: '관우의 정면 돌파와 황충의 고지 견제는 좁은 맥성 길목에서 포위군을 늦춘다.' + } +]; +export const fortyFourthBattleBonds: BattleBond[] = [...fortyThirdBattleBonds, ...maichengIsolationBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -17809,6 +18001,77 @@ function createFortyThirdBattleTerrain(): TerrainType[][] { ); } +function createFortyFourthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 70 }, (_, y) => + Array.from({ length: 88 }, (_, x): TerrainType => { + if (x >= 27 && x <= 39 && y >= 33 && y <= 46) { + return 'fort'; + } + if ( + (x >= 31 && x <= 35 && y >= 36 && y <= 42) || + (x >= 44 && x <= 49 && y >= 50 && y <= 56) || + (x >= 12 && x <= 18 && y >= 45 && y <= 51) || + (x >= 60 && x <= 66 && y >= 22 && y <= 28) + ) { + return 'village'; + } + if ( + (x >= 36 && x <= 42 && y >= 48 && y <= 69) || + (x >= 43 && x <= 50 && y >= 55 && y <= 69) || + (x >= 51 && x <= 57 && y >= 61 && y <= 69) || + (x >= 0 && x <= 9 && y >= 28 && y <= 69) + ) { + return 'river'; + } + if ( + (x >= 1 && x <= 31 && y >= 39 && y <= 43) || + (x >= 29 && x <= 57 && y >= 37 && y <= 41) || + (x >= 38 && x <= 82 && y >= 29 && y <= 33) || + (x >= 44 && x <= 80 && y >= 48 && y <= 52) || + (x >= 22 && x <= 32 && y >= 46 && y <= 62) || + (x >= 55 && x <= 64 && y >= 18 && y <= 36) + ) { + return 'road'; + } + if ( + (x >= 4 && x <= 26 && y >= 20 && y <= 56) || + (x >= 18 && x <= 36 && y >= 4 && y <= 31) || + (x >= 42 && x <= 66 && y >= 5 && y <= 25) || + (x >= 58 && x <= 86 && y >= 34 && y <= 64) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 25 && y <= 21) || + (x >= 34 && x <= 62 && y <= 15) || + (x >= 67 && x <= 87 && y <= 31) || + (x >= 58 && x <= 87 && y >= 60) || + (x >= 12 && x <= 31 && y >= 56) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 27) || + (x >= 85 && y >= 32) || + (x >= 38 && x <= 43 && y <= 8) || + (x >= 66 && x <= 70 && y >= 6 && y <= 31) || + (x >= 9 && x <= 13 && y >= 52 && y <= 69) || + (x >= 25 && x <= 27 && y >= 30 && y <= 49) + ) { + return 'cliff'; + } + if ( + (x >= 24 && x <= 42 && y >= 30 && y <= 49) || + (x >= 48 && x <= 60 && y >= 28 && y <= 44) || + (x >= 14 && x <= 27 && y >= 34 && y <= 53) + ) { + return 'camp'; + } + 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 a564e0f..9d5b5cc 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -782,7 +782,53 @@ const unitTexture: Record = { 'jing-collapse-officer-lu-xun': 'unit-rebel-archer', 'jing-collapse-officer-fu-shiren': 'unit-rebel-leader', 'jing-collapse-officer-mi-fang': 'unit-rebel-leader', - 'jing-collapse-leader-lu-meng': 'unit-rebel-leader' + 'jing-collapse-leader-lu-meng': 'unit-rebel-leader', + 'maicheng-wu-scout-a': 'unit-rebel', + 'maicheng-wu-scout-b': 'unit-rebel', + 'maicheng-wu-scout-c': 'unit-rebel', + 'maicheng-wu-scout-d': 'unit-rebel', + 'maicheng-wu-infantry-a': 'unit-rebel', + 'maicheng-wu-infantry-b': 'unit-rebel', + 'maicheng-wu-infantry-c': 'unit-rebel', + 'maicheng-wu-infantry-d': 'unit-rebel', + 'maicheng-wu-infantry-e': 'unit-rebel', + 'maicheng-wu-infantry-f': 'unit-rebel', + 'maicheng-wu-archer-a': 'unit-rebel-archer', + 'maicheng-wu-archer-b': 'unit-rebel-archer', + 'maicheng-wu-archer-c': 'unit-rebel-archer', + 'maicheng-wu-archer-d': 'unit-rebel-archer', + 'maicheng-wu-archer-e': 'unit-rebel-archer', + 'maicheng-wu-archer-f': 'unit-rebel-archer', + 'maicheng-wu-cavalry-a': 'unit-rebel-cavalry', + 'maicheng-wu-cavalry-b': 'unit-rebel-cavalry', + 'maicheng-wu-cavalry-c': 'unit-rebel-cavalry', + 'maicheng-wu-cavalry-d': 'unit-rebel-cavalry', + 'maicheng-wu-guard-a': 'unit-rebel', + 'maicheng-wu-guard-b': 'unit-rebel', + 'maicheng-wu-guard-c': 'unit-rebel', + 'maicheng-wu-guard-d': 'unit-rebel', + 'maicheng-wu-ambusher-a': 'unit-rebel', + 'maicheng-wu-ambusher-b': 'unit-rebel', + 'maicheng-wu-ambusher-c': 'unit-rebel', + 'maicheng-wu-ambusher-d': 'unit-rebel', + 'maicheng-wu-ambusher-e': 'unit-rebel', + 'maicheng-wu-marine-a': 'unit-rebel', + 'maicheng-wu-marine-b': 'unit-rebel', + 'maicheng-wu-marine-c': 'unit-rebel', + 'maicheng-wei-cavalry-a': 'unit-rebel-cavalry', + 'maicheng-wei-cavalry-b': 'unit-rebel-cavalry', + 'maicheng-wei-cavalry-c': 'unit-rebel-cavalry', + 'maicheng-wei-infantry-a': 'unit-rebel', + 'maicheng-wei-infantry-b': 'unit-rebel', + 'maicheng-wei-infantry-c': 'unit-rebel', + 'maicheng-strategist-a': 'unit-rebel-archer', + 'maicheng-strategist-b': 'unit-rebel-archer', + 'maicheng-strategist-c': 'unit-rebel-archer', + 'maicheng-officer-pan-zhang': 'unit-rebel-leader', + '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' }; const unitTextureByClass: Partial> = { @@ -2022,7 +2068,53 @@ const enemyAiByUnitId: Record = { 'jing-collapse-officer-lu-xun': 'hold', 'jing-collapse-officer-fu-shiren': 'guard', 'jing-collapse-officer-mi-fang': 'guard', - 'jing-collapse-leader-lu-meng': 'guard' + 'jing-collapse-leader-lu-meng': 'guard', + 'maicheng-wu-scout-a': 'aggressive', + 'maicheng-wu-scout-b': 'aggressive', + 'maicheng-wu-scout-c': 'aggressive', + 'maicheng-wu-scout-d': 'aggressive', + 'maicheng-wu-infantry-a': 'guard', + 'maicheng-wu-infantry-b': 'guard', + 'maicheng-wu-infantry-c': 'guard', + 'maicheng-wu-infantry-d': 'guard', + 'maicheng-wu-infantry-e': 'guard', + 'maicheng-wu-infantry-f': 'guard', + 'maicheng-wu-archer-a': 'hold', + 'maicheng-wu-archer-b': 'hold', + 'maicheng-wu-archer-c': 'hold', + 'maicheng-wu-archer-d': 'hold', + 'maicheng-wu-archer-e': 'hold', + 'maicheng-wu-archer-f': 'hold', + 'maicheng-wu-cavalry-a': 'aggressive', + 'maicheng-wu-cavalry-b': 'aggressive', + 'maicheng-wu-cavalry-c': 'aggressive', + 'maicheng-wu-cavalry-d': 'aggressive', + 'maicheng-wu-guard-a': 'guard', + 'maicheng-wu-guard-b': 'guard', + 'maicheng-wu-guard-c': 'guard', + 'maicheng-wu-guard-d': 'guard', + 'maicheng-wu-ambusher-a': 'aggressive', + 'maicheng-wu-ambusher-b': 'aggressive', + 'maicheng-wu-ambusher-c': 'aggressive', + 'maicheng-wu-ambusher-d': 'aggressive', + 'maicheng-wu-ambusher-e': 'aggressive', + 'maicheng-wu-marine-a': 'aggressive', + 'maicheng-wu-marine-b': 'aggressive', + 'maicheng-wu-marine-c': 'guard', + 'maicheng-wei-cavalry-a': 'aggressive', + 'maicheng-wei-cavalry-b': 'aggressive', + 'maicheng-wei-cavalry-c': 'aggressive', + 'maicheng-wei-infantry-a': 'guard', + 'maicheng-wei-infantry-b': 'guard', + 'maicheng-wei-infantry-c': 'guard', + 'maicheng-strategist-a': 'hold', + 'maicheng-strategist-b': 'hold', + 'maicheng-strategist-c': 'hold', + 'maicheng-officer-pan-zhang': 'guard', + 'maicheng-officer-ma-zhong': 'aggressive', + 'maicheng-officer-lu-xun': 'hold', + 'maicheng-officer-xu-huang': 'aggressive', + 'maicheng-leader-lu-meng': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index dd64cb3..3c690d4 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -9,6 +9,7 @@ import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg'; 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 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'; @@ -144,6 +145,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-forty-first', fortyFirstBattleMapUrl); 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('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 eda7c35..8e4d93a 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -40,6 +40,7 @@ import { luoCastleProperRecruitUnits, luoCastleRecruitBonds, luoCastleRecruitUnits, + maichengIsolationBonds, wulingRecruitBonds, wulingRecruitUnits, xuzhouRecruitBonds, @@ -396,7 +397,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'fortieth-battle-han-river-flood', 'forty-first-battle-fan-castle-siege', 'forty-second-battle-jing-rear-crisis', - 'forty-third-battle-gongan-collapse' + 'forty-third-battle-gongan-collapse', + 'forty-fourth-battle-maicheng-isolation' ], nextHints: ['왕업 선언', '번성 포위', '형주 위기'] } @@ -445,7 +447,8 @@ const campBattleIds = { fortieth: 'fortieth-battle-han-river-flood', fortyFirst: 'forty-first-battle-fan-castle-siege', fortySecond: 'forty-second-battle-jing-rear-crisis', - fortyThird: 'forty-third-battle-gongan-collapse' + fortyThird: 'forty-third-battle-gongan-collapse', + fortyFourth: 'forty-fourth-battle-maicheng-isolation' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -950,6 +953,20 @@ const sortieRulesByBattleId: Partial