From 6a55313492cc166f9a306c482dafba0af8350388 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 16:42:33 +0900 Subject: [PATCH] Add Fan Castle siege chapter --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 216 ++++++++++++--- .../images/battle/forty-first-battle-map.svg | 110 ++++++++ src/game/data/battles.ts | 73 ++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 260 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 74 ++++- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 172 +++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 11 files changed, 902 insertions(+), 50 deletions(-) create mode 100644 src/assets/images/battle/forty-first-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index bba3112..59bc1d5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -64,16 +64,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-eighth battle Jing Province defense vanguard route, using a twenty-two-officer roster but limiting sortie to seven officers so Guan Yu's front, Ma Liang's local judgment, Mi Zhu's supply role, and Wu/Wei pressure become a deliberate pre-battle choice - Thirty-ninth battle Fan Castle outer-front route, locking Liu Bei and Guan Yu as required sortie officers while the player chooses five support officers from the twenty-two-officer roster to pressure Pang De, Cao Ren, and Yu Jin - Fortieth battle Han River flood route, using Guan Yu's water-control plan across an 80x62 flooded battlefield to break Yu Jin's seven armies and open the Fan Castle siege setup +- Forty-first battle Fan Castle siege route, using a twenty-two-officer roster across an 82x64 castle battlefield to pressure Cao Ren's gate line, Pang De's desperate cavalry, and Wu's rear-shadow scouts - Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter - Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer - Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers - Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses -- Flow verification script from title through the fortieth 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 camp states, campaign timeline state, and camp save state +- Flow verification script from title through the forty-first battle victory, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the Fan Castle siege follow-up, using the aftermath of Yu Jin's defeat, Pang De's resolve, Cao Ren's defense, and Wu's growing shadow as the next campaign tension -2. Expand Han River aftermath events, including prisoner handling, dike repair, siege logistics, bond choices, and supply decisions that affect sortie recommendations +1. Build the Jing Province rear-crisis follow-up, using Fan Castle's pressure, Wu's river scouts, and the first signs of rear-line collapse as the next campaign tension +2. Expand Fan Castle aftermath events, including rear guard assignments, river signal choices, siege fatigue, and supply decisions that affect sortie recommendations 3. Expand sortie selection into richer pre-battle officer planning, including explicit reserve drills, class practice, and bond-focused camp assignments 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 0141028..7ebbb7d 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1,4 +1,4 @@ -import { spawn } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/'; @@ -1736,7 +1736,7 @@ try { if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || - progressTabState.campaignProgress?.totalKnown !== 40 || + progressTabState.campaignProgress?.totalKnown !== 41 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' @@ -1888,7 +1888,7 @@ try { if ( postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState.campaignProgress?.completedKnown !== 18 || - postBowangProgressState.campaignProgress?.totalKnown !== 40 || + postBowangProgressState.campaignProgress?.totalKnown !== 41 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' @@ -2026,7 +2026,7 @@ try { if ( postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState.campaignProgress?.completedKnown !== 19 || - postChangbanProgressState.campaignProgress?.totalKnown !== 40 || + postChangbanProgressState.campaignProgress?.totalKnown !== 41 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' @@ -2164,7 +2164,7 @@ try { if ( postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || - postJiangdongProgressState.campaignProgress?.totalKnown !== 40 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 41 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' @@ -2304,7 +2304,7 @@ try { if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || - postRedCliffsProgressState.campaignProgress?.totalKnown !== 40 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 41 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' @@ -2444,7 +2444,7 @@ try { if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || - postFireAttackProgressState.campaignProgress?.totalKnown !== 40 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 41 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2587,7 +2587,7 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 40 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 41 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2729,7 +2729,7 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 40 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 41 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2872,7 +2872,7 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 40 || + postWulingProgressState.campaignProgress?.totalKnown !== 41 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' @@ -3021,7 +3021,7 @@ try { if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || - postChangshaProgressState.campaignProgress?.totalKnown !== 40 || + postChangshaProgressState.campaignProgress?.totalKnown !== 41 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' @@ -3166,7 +3166,7 @@ try { if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || - postYizhouProgressState.campaignProgress?.totalKnown !== 40 || + postYizhouProgressState.campaignProgress?.totalKnown !== 41 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' @@ -3313,7 +3313,7 @@ try { if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || - postFuPassProgressState.campaignProgress?.totalKnown !== 40 || + postFuPassProgressState.campaignProgress?.totalKnown !== 41 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3462,7 +3462,7 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 40 || + postLuoProgressState.campaignProgress?.totalKnown !== 41 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' @@ -3608,7 +3608,7 @@ try { if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || - postLuofengProgressState.campaignProgress?.totalKnown !== 40 || + postLuofengProgressState.campaignProgress?.totalKnown !== 41 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' @@ -3760,7 +3760,7 @@ try { if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || - postLuoMainProgressState.campaignProgress?.totalKnown !== 40 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 41 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' @@ -3913,7 +3913,7 @@ try { if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || - postMianzhuProgressState.campaignProgress?.totalKnown !== 40 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 41 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' @@ -4067,7 +4067,7 @@ try { if ( postChengduProgressState?.activeTab !== 'progress' || postChengduProgressState.campaignProgress?.completedKnown !== 33 || - postChengduProgressState.campaignProgress?.totalKnown !== 40 || + postChengduProgressState.campaignProgress?.totalKnown !== 41 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' @@ -4222,7 +4222,7 @@ try { if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || - postJiamengProgressState.campaignProgress?.totalKnown !== 40 || + postJiamengProgressState.campaignProgress?.totalKnown !== 41 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' @@ -4378,7 +4378,7 @@ try { if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || - postYangpingProgressState.campaignProgress?.totalKnown !== 40 || + postYangpingProgressState.campaignProgress?.totalKnown !== 41 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' @@ -4535,7 +4535,7 @@ try { if ( postDingjunProgressState?.activeTab !== 'progress' || postDingjunProgressState.campaignProgress?.completedKnown !== 36 || - postDingjunProgressState.campaignProgress?.totalKnown !== 40 || + postDingjunProgressState.campaignProgress?.totalKnown !== 41 || postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전' @@ -4688,7 +4688,7 @@ try { if ( postHanzhongProgressState?.activeTab !== 'progress' || postHanzhongProgressState.campaignProgress?.completedKnown !== 37 || - postHanzhongProgressState.campaignProgress?.totalKnown !== 40 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 41 || postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비' @@ -4749,7 +4749,7 @@ try { if ( postKingCouncilProgressState?.activeTab !== 'progress' || postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || - postKingCouncilProgressState.campaignProgress?.totalKnown !== 40 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 41 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4809,7 +4809,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 40 || + postFoundationProgressState.campaignProgress?.totalKnown !== 41 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4960,7 +4960,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 40 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 41 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5113,7 +5113,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 40 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 41 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5267,14 +5267,168 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 40 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 41 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || - postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { - throw new Error(`Expected post-Han River progress tab to complete the fortieth battle and leave Fan Castle siege as the next prepared slice: ${JSON.stringify(postHanRiverProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Han River progress tab to complete the fortieth battle and point to Fan Castle siege: ${JSON.stringify(postHanRiverProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-han-river-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const fanSiegeSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fanSiegeSortieState?.sortieVisible || + fanSiegeSortieState.campaign?.step !== 'fortieth-camp' || + fanSiegeSortieState.sortiePlan?.objectiveLine !== '번성 공성전 · 조인 성문 방어선 격파' || + fanSiegeSortieState.sortiePlan?.maxCount !== 7 || + fanSiegeSortieState.sortiePlan?.recommendedTotal !== 7 || + !fanSiegeSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) + ) { + throw new Error(`Expected Fan Castle siege sortie prep to lock Guan Yu and expose a seven-officer castle choice: ${JSON.stringify(fanSiegeSortieState)}`); + } + assertSortieTacticalRoster(fanSiegeSortieState, [ + '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 fanSiegePriorityUnits = ['huang-zhong', 'ma-liang', 'fa-zheng', 'zhao-yun', 'ma-chao']; + for (const unitId of fanSiegePriorityUnits) { + 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 && !fanSiegePriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const fanSiegeSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fanSiegePriorityUnits.every((unitId) => + fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) || + !fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) || + fanSiegeSortieReadyState.sortiePlan?.selectedCount !== 7 || + fanSiegeSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Fan Castle siege sortie to deploy Liu Bei, Guan Yu, Huang Zhong, Ma Liang, Fa Zheng, Zhao Yun, and Ma Chao: ${JSON.stringify(fanSiegeSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-fan-siege-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-fan-siege-story.png', fullPage: true }); + + for (let i = 0; i < 45; i += 1) { + const enteredFortyFirstBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-first-battle-fan-castle-siege'; + }); + if (enteredFortyFirstBattle) { + 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-first-battle-fan-castle-siege' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-first-battle.png', fullPage: true }); + + const fortyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortyFirstEnemies = fortyFirstBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortyFirstAllies = fortyFirstBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortyFirstEnemyBehaviors = new Set(fortyFirstEnemies.map((unit) => unit.ai)); + if ( + fortyFirstBattleState.camera?.mapWidth !== 82 || + fortyFirstBattleState.camera?.mapHeight !== 64 || + fortyFirstBattleState.victoryConditionLabel !== '조인 성문 방어선 격파' || + fortyFirstEnemies.length < 35 || + !fortyFirstEnemyBehaviors.has('aggressive') || + !fortyFirstEnemyBehaviors.has('guard') || + !fortyFirstEnemyBehaviors.has('hold') || + !fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-leader-cao-ren') || + !fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-officer-pang-de') || + !fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-officer-lu-meng') || + !fortyFirstAllies.some((unit) => unit.id === 'guan-yu') || + !fortyFirstAllies.some((unit) => unit.id === 'huang-zhong') || + !fortyFirstAllies.some((unit) => unit.id === 'ma-liang') || + !fortyFirstAllies.some((unit) => unit.id === 'fa-zheng') || + !fortyFirstAllies.some((unit) => unit.id === 'zhao-yun') + ) { + throw new Error(`Expected forty-first battle to use Fan Castle siege map, Cao Ren objective, selected allied officers, and mixed siege AI: ${JSON.stringify(fortyFirstBattleState)}`); + } + + 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 fortyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortyFirstCampState?.campBattleId !== 'forty-first-battle-fan-castle-siege' || + fortyFirstCampState.campTitle !== '번성 공성전 후 군영' || + fortyFirstCampState.availableDialogueIds?.length !== 3 || + !fortyFirstCampState.availableDialogueIds.every((id) => id.endsWith('fan-siege')) || + fortyFirstCampState.availableVisitIds?.length !== 2 || + !fortyFirstCampState.availableVisitIds.every((id) => id.includes('fan-siege')) || + fortyFirstCampState.rosterCollection?.total < 22 + ) { + throw new Error(`Expected forty-first camp to expose Fan Castle siege dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFirstCampState)}`); + } + await page.screenshot({ path: 'dist/verification-fan-siege-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postFanSiegeProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postFanSiegeProgressState?.activeTab !== 'progress' || + postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 41 || + postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || + postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Fan Castle siege progress tab to complete the forty-first battle and leave Jing Province crisis as the next prepared slice: ${JSON.stringify(postFanSiegeProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-fan-siege-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -5307,10 +5461,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 Han River flood: ${JSON.stringify(titleContinueState)}`); + throw new Error(`Expected title continue to reopen the latest camp after Fan Castle siege: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-fortieth-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, and Han River flood state at ${targetUrl}`); + console.log(`Verified title-to-forty-first-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, and Fan Castle siege state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-first-battle-map.svg b/src/assets/images/battle/forty-first-battle-map.svg new file mode 100644 index 0000000..801f42b --- /dev/null +++ b/src/assets/images/battle/forty-first-battle-map.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index c1e9d30..3d0a9ca 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -24,6 +24,10 @@ import { fortiethBattleMap, fortiethBattleUnits, fortiethBattleVictoryPages, + fortyFirstBattleBonds, + fortyFirstBattleMap, + fortyFirstBattleUnits, + fortyFirstBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -205,7 +209,8 @@ export type BattleScenarioId = | 'thirty-seventh-battle-hanzhong-decisive' | 'thirty-eighth-battle-jing-defense' | 'thirty-ninth-battle-fan-castle-vanguard' - | 'fortieth-battle-han-river-flood'; + | 'fortieth-battle-han-river-flood' + | 'forty-first-battle-fan-castle-siege'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2585,6 +2590,69 @@ export const fortiethBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortyFirstBattleScenario: BattleScenarioDefinition = { + id: 'forty-first-battle-fan-castle-siege', + title: '번성 공성전', + victoryConditionLabel: '조인 성문 방어선 격파', + defeatConditionLabel: '유비 또는 관우 퇴각', + openingObjectiveLines: [ + '한수 수공으로 칠군은 무너졌지만 번성 성문은 아직 버티고 있습니다. 외성 보루와 수문을 끊어 조인의 방어선을 흔드십시오.', + '관우는 성문 압박의 핵심입니다. 관우가 무너지면 번성 포위의 명분과 형주 전선의 사기가 함께 꺾입니다.', + '방덕의 결사 기병은 정면 돌파를 노리고, 강동 정찰대는 뒤의 나루를 살핍니다. 성벽 압박과 후방 경계를 함께 맡을 무장을 고르십시오.' + ], + map: fortyFirstBattleMap, + units: fortyFirstBattleUnits, + bonds: fortyFirstBattleBonds, + mapTextureKey: 'battle-map-forty-first', + leaderUnitId: 'fan-siege-leader-cao-ren', + quickVictoryTurnLimit: 66, + baseVictoryGold: 8240, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '조인 성문 방어선 격파', + rewardGold: 5320, + unitId: 'fan-siege-leader-cao-ren' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 1280, + unitId: 'liu-bei' + }, + { + id: 'guan-yu', + kind: 'keep-unit-alive', + label: '관우 생존', + rewardGold: 2020, + unitId: 'guan-yu' + }, + { + id: 'outer-wall', + kind: 'secure-terrain', + label: '번성 외성 보루 확보', + rewardGold: 2560, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '66턴 이내 승리', + rewardGold: 1880, + maxTurn: 66 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'guan-yu' } + ], + itemRewards: ['콩 38', '상처약 28', '탁주 12', '번성 성문도 1', '방덕 결사문 1'], + victoryPages: fortyFirstBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -2627,7 +2695,8 @@ export const battleScenarios: Record 'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario, 'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario, 'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario, - 'fortieth-battle-han-river-flood': fortiethBattleScenario + 'fortieth-battle-han-river-flood': fortiethBattleScenario, + 'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index b36bd50..c59d536 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -5,6 +5,7 @@ import { eighteenthBattleScenario, eleventhBattleScenario, fortiethBattleScenario, + fortyFirstBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -51,6 +52,8 @@ import { eleventhBattleVictoryPages, fortiethBattleIntroPages, fortiethBattleVictoryPages, + fortyFirstBattleIntroPages, + fortyFirstBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -580,13 +583,24 @@ const sortieFlows: Record = { }, [fortiethBattleScenario.id]: { afterBattleId: fortiethBattleScenario.id, - eyebrow: '다음 장 준비', - title: '번성 포위 준비', + eyebrow: '다음 전장', + title: fortyFirstBattleScenario.title, description: - '한수의 물길이 조조군을 무너뜨렸지만 번성의 성문은 아직 열리지 않았습니다. 다음 장에서는 수공 이후의 포위전, 방덕의 결말, 오와 위의 시선이 더 날카롭게 이어집니다.', - rewardHint: '다음 장: 번성 포위와 형주 위기 준비 중', - pages: fortiethBattleVictoryPages, - unavailableNotice: '번성 포위전과 형주 위기는 다음 작업에서 이어집니다. 지금은 군영에서 물길 전투의 성장, 공명도, 보급을 정비할 수 있습니다.' + '한수의 물길이 조조군을 무너뜨렸지만 번성의 성문은 아직 열리지 않았습니다. 다음 출진에서는 조인의 성문 방어선과 방덕의 결사대를 눌러 포위전을 실제 공성전으로 밀어붙입니다.', + rewardHint: `예상 보상: ${fortyFirstBattleScenario.title} 개방 / 번성 성문도`, + nextBattleId: fortyFirstBattleScenario.id, + campaignStep: 'forty-first-battle', + pages: [...fortiethBattleVictoryPages, ...fortyFirstBattleIntroPages] + }, + [fortyFirstBattleScenario.id]: { + afterBattleId: fortyFirstBattleScenario.id, + eyebrow: '다음 장 준비', + title: '형주 후방 경계', + description: + '번성의 외성 방어선은 흔들렸지만 강동의 그림자가 더 가까워졌습니다. 다음 장에서는 번성 승세와 형주 후방의 균열을 함께 다루며 관우 전선의 위기를 키웁니다.', + rewardHint: '다음 장: 형주 후방 위기와 오군 침투 준비 중', + pages: fortyFirstBattleVictoryPages, + unavailableNotice: '형주 후방 위기와 오군 침투는 다음 작업에서 이어집니다. 지금은 군영에서 공성전 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index e21304c..dd8b856 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2653,6 +2653,70 @@ export const fortiethBattleVictoryPages: StoryPage[] = [ } ]; +export const fortyFirstBattleIntroPages: StoryPage[] = [ + { + id: 'forty-first-fan-wall', + bgm: 'story-dark', + chapter: '성벽 앞의 밤', + background: 'story-militia', + speaker: '제갈량', + text: '한수의 물길은 조조군의 칠군을 무너뜨렸지만, 번성의 성벽은 아직 닫혀 있습니다. 조인은 성문을 걸어 잠그고, 방덕은 마지막 돌격을 준비합니다.' + }, + { + id: 'forty-first-siege-plan', + bgm: 'battle-prep', + chapter: '포위의 순서', + background: 'story-liu-bei', + speaker: '마량', + text: '성벽을 바로 두드리면 병사만 지칩니다. 외성 보루와 수문, 강가의 낮은 마을을 먼저 붙잡아야 성 안의 사기가 흔들립니다.' + }, + { + id: 'forty-first-wu-shadow', + bgm: 'story-dark', + chapter: '강 건너의 시선', + background: 'story-three-heroes', + speaker: '관우', + portrait: 'guanYu', + text: '북쪽 성문만 보아서는 안 되오. 강동의 배가 멀리서 움직이고 있으니, 번성을 치는 동안 뒤의 나루도 비워 두지 마시오.' + }, + { + id: 'forty-first-sortie', + bgm: 'battle-prep', + chapter: '번성 공성전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '관우가 성문을 압박하고, 남은 무장들은 보루와 나루를 나누어 맡아라. 이번 싸움은 성을 치면서도 뒤의 그림자를 놓치지 않는 싸움이다.' + } +]; + +export const fortyFirstBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-first-victory-gate', + bgm: 'militia-theme', + chapter: '성문이 흔들리다', + background: 'story-sortie', + text: '번성의 외성 보루가 무너지고 조인의 방어선은 성 안쪽으로 밀려났습니다. 방덕의 결사대도 더는 성문 밖에서 버티지 못했습니다.' + }, + { + id: 'forty-first-pang-de-end', + bgm: 'battle-prep', + chapter: '방덕의 결말', + background: 'story-three-heroes', + speaker: '관우', + portrait: 'guanYu', + text: '방덕은 끝까지 물러서지 않았소. 적이라 하나 그 기개는 가벼이 볼 수 없으니, 병사들에게 전하되 오만해지지 말라 이르시오.' + }, + { + id: 'forty-first-jing-warning', + bgm: 'story-dark', + chapter: '형주의 불안', + background: 'story-militia', + speaker: '제갈량', + text: '번성의 성벽은 흔들렸으나, 강 건너 오의 움직임은 더 가까워졌습니다. 다음 장에서는 포위의 승세와 형주 후방의 균열을 함께 다루어야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3038,6 +3102,12 @@ export const fortiethBattleMap: BattleMap = { terrain: createFortiethBattleTerrain() }; +export const fortyFirstBattleMap: BattleMap = { + width: 82, + height: 64, + terrain: createFortyFirstBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -14495,6 +14565,89 @@ export const fortiethBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('han-river-leader-yu-jin', '우금', '칠군 총대장', 'rebelLeader', 95, 612, 158, 73, 26) ]; +const fortyFirstBattleAllyPositions: Record = { + 'liu-bei': { x: 5, y: 58 }, + 'guan-yu': { x: 12, y: 53 }, + 'zhang-fei': { x: 6, y: 60 }, + 'jian-yong': { x: 4, y: 61 }, + 'mi-zhu': { x: 8, y: 59 }, + 'sun-qian': { x: 9, y: 62 }, + 'zhao-yun': { x: 15, y: 56 }, + 'zhuge-liang': { x: 9, y: 55 }, + 'ma-liang': { x: 14, y: 58 }, + 'yi-ji': { x: 16, y: 61 }, + 'gong-zhi': { x: 17, y: 59 }, + 'huang-zhong': { x: 18, y: 54 }, + 'wei-yan': { x: 16, y: 52 }, + 'pang-tong': { x: 13, y: 56 }, + 'fa-zheng': { x: 19, y: 58 }, + 'wu-yi': { x: 20, y: 55 }, + 'yan-yan': { x: 21, y: 57 }, + 'li-yan': { x: 19, y: 52 }, + 'huang-quan': { x: 22, y: 59 }, + 'ma-chao': { x: 23, y: 53 }, + 'ma-dai': { x: 24, y: 55 }, + 'wang-ping': { x: 22, y: 51 } +}; + +export const fortyFirstBattleUnits: 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, fortyFirstBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('fan-siege-scout-a', '번성 척후', '남문 척후', 'bandit', 91, 354, 116, 31, 54), + createThirtyFirstEnemyUnit('fan-siege-scout-b', '번성 척후', '수문 척후', 'bandit', 91, 354, 116, 42, 50), + createThirtyFirstEnemyUnit('fan-siege-scout-c', '번성 척후', '동문 척후', 'bandit', 92, 362, 118, 55, 43), + createThirtyFirstEnemyUnit('fan-siege-scout-d', '강동 척후', '나루 척후', 'bandit', 92, 362, 118, 26, 56), + createThirtyFirstEnemyUnit('fan-siege-infantry-a', '번성 보병', '하류 보병', 'yellowTurban', 92, 528, 146, 44, 48), + createThirtyFirstEnemyUnit('fan-siege-infantry-b', '번성 보병', '외성 보병', 'yellowTurban', 92, 528, 146, 53, 43), + createThirtyFirstEnemyUnit('fan-siege-infantry-c', '번성 보병', '남문 보병', 'yellowTurban', 93, 540, 148, 62, 37), + createThirtyFirstEnemyUnit('fan-siege-infantry-d', '번성 보병', '동문 보병', 'yellowTurban', 93, 540, 148, 69, 31), + createThirtyFirstEnemyUnit('fan-siege-infantry-e', '번성 보병', '내성 보병', 'yellowTurban', 94, 552, 150, 75, 24), + createThirtyFirstEnemyUnit('fan-siege-infantry-f', '번성 보병', '북문 보병', 'yellowTurban', 94, 552, 150, 78, 16), + createThirtyFirstEnemyUnit('fan-siege-archer-a', '번성 궁병', '제방 궁병', 'archer', 92, 378, 124, 46, 50), + createThirtyFirstEnemyUnit('fan-siege-archer-b', '번성 궁병', '외성 궁병', 'archer', 93, 386, 126, 56, 41), + createThirtyFirstEnemyUnit('fan-siege-archer-c', '번성 궁병', '성벽 궁병', 'archer', 93, 386, 126, 66, 34), + createThirtyFirstEnemyUnit('fan-siege-archer-d', '번성 궁병', '내성 궁병', 'archer', 94, 394, 128, 74, 28), + createThirtyFirstEnemyUnit('fan-siege-archer-e', '번성 궁병', '북문 궁병', 'archer', 94, 394, 128, 79, 20), + createThirtyFirstEnemyUnit('fan-siege-archer-f', '강동 궁병', '강안 궁병', 'archer', 93, 386, 126, 32, 57), + createThirtyFirstEnemyUnit('fan-siege-cavalry-a', '방덕 기병', '결사 기병', 'cavalry', 94, 552, 150, 38, 55), + createThirtyFirstEnemyUnit('fan-siege-cavalry-b', '방덕 기병', '중앙 기병', 'cavalry', 94, 552, 150, 51, 48), + createThirtyFirstEnemyUnit('fan-siege-cavalry-c', '방덕 기병', '동문 기병', 'cavalry', 95, 564, 152, 64, 40), + createThirtyFirstEnemyUnit('fan-siege-cavalry-d', '조조군 기병', '북로 기병', 'cavalry', 95, 564, 152, 73, 34), + createThirtyFirstEnemyUnit('fan-siege-cavalry-e', '강동 기병', '나루 기병', 'cavalry', 94, 552, 150, 35, 59), + createThirtyFirstEnemyUnit('fan-siege-guard-a', '번성 친위대', '외성 친위대', 'yellowTurban', 96, 596, 158, 63, 35), + createThirtyFirstEnemyUnit('fan-siege-guard-b', '번성 친위대', '성문 친위대', 'yellowTurban', 96, 596, 158, 70, 30), + createThirtyFirstEnemyUnit('fan-siege-guard-c', '번성 친위대', '수문 친위대', 'yellowTurban', 97, 608, 160, 74, 22), + createThirtyFirstEnemyUnit('fan-siege-guard-d', '번성 친위대', '내성 친위대', 'yellowTurban', 97, 608, 160, 79, 17), + createThirtyFirstEnemyUnit('fan-siege-guard-e', '번성 친위대', '조인 친위대', 'yellowTurban', 98, 620, 162, 77, 12), + createThirtyFirstEnemyUnit('fan-siege-strategist-a', '번성 책사', '수문 책사', 'strategist', 94, 394, 130, 58, 39), + createThirtyFirstEnemyUnit('fan-siege-strategist-b', '번성 책사', '성문 책사', 'strategist', 95, 402, 132, 69, 26), + createThirtyFirstEnemyUnit('fan-siege-strategist-c', '강동 책사', '나루 책사', 'strategist', 94, 394, 130, 30, 53), + createThirtyFirstEnemyUnit('fan-siege-ambusher-a', '성벽 복병', '무너진 담 복병', 'bandit', 92, 364, 120, 47, 45), + createThirtyFirstEnemyUnit('fan-siege-ambusher-b', '성벽 복병', '마른 해자 복병', 'bandit', 93, 372, 122, 60, 38), + createThirtyFirstEnemyUnit('fan-siege-ambusher-c', '강동 복병', '강안 복병', 'bandit', 93, 372, 122, 27, 60), + createThirtyFirstEnemyUnit('fan-siege-officer-pang-de', '방덕', '결사 선봉장', 'cavalry', 98, 648, 168, 61, 32), + createThirtyFirstEnemyUnit('fan-siege-officer-lu-meng', '여몽', '강동 정찰장', 'rebelLeader', 96, 612, 160, 33, 61), + createThirtyFirstEnemyUnit('fan-siege-leader-cao-ren', '조인', '번성 수비장', 'rebelLeader', 99, 676, 172, 78, 18) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -15073,6 +15226,33 @@ export const hanRiverFloodBonds: BattleBond[] = [ } ]; +export const fanCastleSiegeBonds: BattleBond[] = [ + { + id: 'guan-yu__huang-zhong', + unitIds: ['guan-yu', 'huang-zhong'], + title: '성벽을 겨누는 노장', + level: 27, + exp: 0, + description: '관우의 정면 압박과 황충의 성벽 견제가 맞물리면, 성문을 지키는 궁병과 친위대에 대한 연계 공격 확률이 높아집니다.' + }, + { + id: 'guan-yu__ma-liang', + unitIds: ['guan-yu', 'ma-liang'], + title: '형주 민심의 군령', + level: 26, + exp: 0, + description: '관우의 포위전 지휘와 마량의 형주 민심 판단이 이어지면, 마을과 나루를 해치지 않는 장기전 준비가 단단해집니다.' + }, + { + id: 'liu-bei__guan-yu_fan-siege', + unitIds: ['liu-bei', 'guan-yu'], + title: '왕업과 의형제', + level: 28, + exp: 0, + description: '유비의 큰 명분과 관우의 번성 전선이 맞물리면, 성문 공략 중 사기와 공명 피해 보너스가 더 크게 살아납니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -15138,6 +15318,9 @@ export const thirtyNinthBattleBonds: BattleBond[] = [...thirtyEighthBattleBonds, export const fortiethBattleBonds: BattleBond[] = [...thirtyNinthBattleBonds, ...hanRiverFloodBonds].map( cloneBattleBondForScenario ); +export const fortyFirstBattleBonds: BattleBond[] = [...fortiethBattleBonds, ...fanCastleSiegeBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -17032,6 +17215,83 @@ function createFortiethBattleTerrain(): TerrainType[][] { ); } +function createFortyFirstBattleTerrain(): TerrainType[][] { + return Array.from({ length: 64 }, (_, y) => + Array.from({ length: 82 }, (_, x): TerrainType => { + if (x <= 6 && y >= 57) { + return 'camp'; + } + if ( + (x >= 60 && x <= 81 && y >= 5 && y <= 24) || + (x >= 54 && x <= 70 && y >= 12 && y <= 27) || + (x >= 68 && x <= 81 && y >= 23 && y <= 41) || + (x >= 73 && x <= 80 && y >= 37 && y <= 47) || + (x >= 75 && x <= 81 && y >= 8 && y <= 17) + ) { + return 'fort'; + } + if ( + (x >= 22 && x <= 26 && y >= 54 && y <= 58) || + (x >= 45 && x <= 49 && y >= 47 && y <= 50) || + (x >= 58 && x <= 62 && y >= 40 && y <= 44) || + (x >= 72 && x <= 76 && y >= 44 && y <= 48) + ) { + return 'village'; + } + if ( + (x >= 18 && x <= 22 && y >= 0 && y <= 55) || + (x >= 34 && x <= 36 && y >= 12 && y <= 60) || + (x >= 51 && x <= 54 && y >= 5 && y <= 49) || + (x >= 63 && x <= 65 && y >= 41 && y <= 63) || + (y >= 31 && y <= 34 && x >= 21 && x <= 65) || + (x >= 70 && x <= 72 && y >= 16 && y <= 44) + ) { + return 'river'; + } + if ( + (y >= 59 && x >= 6 && x <= 31) || + (x >= 30 && x <= 46 && y >= 54 && y <= 59) || + (x >= 45 && x <= 60 && y >= 48 && y <= 54) || + (x >= 59 && x <= 72 && y >= 42 && y <= 48) || + (x >= 71 && x <= 80 && y >= 34 && y <= 42) || + (x >= 74 && x <= 81 && y >= 19 && y <= 34) || + (x >= 58 && x <= 79 && y >= 17 && y <= 21) || + (y >= 27 && y <= 30 && x >= 22 && x <= 74) || + (x >= 37 && x <= 56 && y >= 37 && y <= 40) + ) { + return 'road'; + } + if ( + (x >= 4 && x <= 29 && y >= 12 && y <= 45) || + (x >= 30 && x <= 47 && y >= 20 && y <= 50) || + (x >= 52 && x <= 68 && y >= 47 && y <= 63) || + (x >= 64 && x <= 81 && y >= 43 && y <= 62) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 17 && y <= 29) || + (x >= 28 && x <= 49 && y <= 19) || + (x >= 50 && x <= 69 && y >= 1 && y <= 21) || + (x >= 70 && x <= 81 && y <= 16) || + (x >= 33 && x <= 53 && y >= 56 && y <= 63) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 56) || + (x >= 79 && y >= 38) || + (x >= 61 && x <= 66 && y <= 9) || + (x >= 27 && x <= 33 && y >= 29 && y <= 45) || + (x >= 68 && x <= 70 && y >= 45 && y <= 57) + ) { + return 'cliff'; + } + return 'plain'; + }) + ); +} + function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData { return { ...cloneUnitForScenario(unit), diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 66a4c36..2904eac 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -675,7 +675,42 @@ const unitTexture: Record = { 'han-river-ambusher-c': 'unit-rebel', 'han-river-officer-pang-de': 'unit-rebel-cavalry', 'han-river-officer-cao-ren': 'unit-rebel-leader', - 'han-river-leader-yu-jin': 'unit-rebel-leader' + 'han-river-leader-yu-jin': 'unit-rebel-leader', + 'fan-siege-scout-a': 'unit-rebel', + 'fan-siege-scout-b': 'unit-rebel', + 'fan-siege-scout-c': 'unit-rebel', + 'fan-siege-scout-d': 'unit-rebel', + 'fan-siege-infantry-a': 'unit-rebel', + 'fan-siege-infantry-b': 'unit-rebel', + 'fan-siege-infantry-c': 'unit-rebel', + 'fan-siege-infantry-d': 'unit-rebel', + 'fan-siege-infantry-e': 'unit-rebel', + 'fan-siege-infantry-f': 'unit-rebel', + 'fan-siege-archer-a': 'unit-rebel-archer', + 'fan-siege-archer-b': 'unit-rebel-archer', + 'fan-siege-archer-c': 'unit-rebel-archer', + 'fan-siege-archer-d': 'unit-rebel-archer', + 'fan-siege-archer-e': 'unit-rebel-archer', + 'fan-siege-archer-f': 'unit-rebel-archer', + 'fan-siege-cavalry-a': 'unit-rebel-cavalry', + 'fan-siege-cavalry-b': 'unit-rebel-cavalry', + 'fan-siege-cavalry-c': 'unit-rebel-cavalry', + 'fan-siege-cavalry-d': 'unit-rebel-cavalry', + 'fan-siege-cavalry-e': 'unit-rebel-cavalry', + 'fan-siege-guard-a': 'unit-rebel', + 'fan-siege-guard-b': 'unit-rebel', + 'fan-siege-guard-c': 'unit-rebel', + 'fan-siege-guard-d': 'unit-rebel', + 'fan-siege-guard-e': 'unit-rebel', + 'fan-siege-strategist-a': 'unit-rebel-archer', + 'fan-siege-strategist-b': 'unit-rebel-archer', + 'fan-siege-strategist-c': 'unit-rebel-archer', + 'fan-siege-ambusher-a': 'unit-rebel', + 'fan-siege-ambusher-b': 'unit-rebel', + 'fan-siege-ambusher-c': 'unit-rebel', + 'fan-siege-officer-pang-de': 'unit-rebel-cavalry', + 'fan-siege-officer-lu-meng': 'unit-rebel-leader', + 'fan-siege-leader-cao-ren': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -1808,7 +1843,42 @@ const enemyAiByUnitId: Record = { 'han-river-ambusher-c': 'aggressive', 'han-river-officer-pang-de': 'aggressive', 'han-river-officer-cao-ren': 'guard', - 'han-river-leader-yu-jin': 'guard' + 'han-river-leader-yu-jin': 'guard', + 'fan-siege-scout-a': 'aggressive', + 'fan-siege-scout-b': 'aggressive', + 'fan-siege-scout-c': 'aggressive', + 'fan-siege-scout-d': 'aggressive', + 'fan-siege-infantry-a': 'guard', + 'fan-siege-infantry-b': 'guard', + 'fan-siege-infantry-c': 'guard', + 'fan-siege-infantry-d': 'guard', + 'fan-siege-infantry-e': 'guard', + 'fan-siege-infantry-f': 'guard', + 'fan-siege-archer-a': 'hold', + 'fan-siege-archer-b': 'hold', + 'fan-siege-archer-c': 'hold', + 'fan-siege-archer-d': 'hold', + 'fan-siege-archer-e': 'hold', + 'fan-siege-archer-f': 'hold', + 'fan-siege-cavalry-a': 'aggressive', + 'fan-siege-cavalry-b': 'aggressive', + 'fan-siege-cavalry-c': 'aggressive', + 'fan-siege-cavalry-d': 'aggressive', + 'fan-siege-cavalry-e': 'aggressive', + 'fan-siege-guard-a': 'guard', + 'fan-siege-guard-b': 'guard', + 'fan-siege-guard-c': 'guard', + 'fan-siege-guard-d': 'guard', + 'fan-siege-guard-e': 'guard', + 'fan-siege-strategist-a': 'hold', + 'fan-siege-strategist-b': 'hold', + 'fan-siege-strategist-c': 'hold', + 'fan-siege-ambusher-a': 'aggressive', + 'fan-siege-ambusher-b': 'aggressive', + 'fan-siege-ambusher-c': 'aggressive', + 'fan-siege-officer-pang-de': 'aggressive', + 'fan-siege-officer-lu-meng': 'guard', + 'fan-siege-leader-cao-ren': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index f0aa74e..501ad0c 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -6,6 +6,7 @@ import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg'; import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-map.svg'; import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png'; import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg'; +import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg'; import 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'; @@ -138,6 +139,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirty-eighth', thirtyEighthBattleMapUrl); this.load.image('battle-map-thirty-ninth', thirtyNinthBattleMapUrl); this.load.image('battle-map-fortieth', fortiethBattleMapUrl); + this.load.image('battle-map-forty-first', fortyFirstBattleMapUrl); this.load.image('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 22e4ae1..1d0eb8a 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -28,6 +28,7 @@ import { hanzhongScoutRecruitBonds, hanzhongScoutRecruitUnits, fanCastleBonds, + fanCastleSiegeBonds, jingDefenseBonds, jingzhouRecruitBonds, jingzhouRecruitUnits, @@ -355,9 +356,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'thirty-seventh-battle-hanzhong-decisive', 'thirty-eighth-battle-jing-defense', 'thirty-ninth-battle-fan-castle-vanguard', - 'fortieth-battle-han-river-flood' + 'fortieth-battle-han-river-flood', + 'forty-first-battle-fan-castle-siege' ], - nextHints: ['왕업 선언', '오호대장군', '북벌 준비'] + nextHints: ['왕업 선언', '번성 포위', '형주 위기'] } ]; @@ -401,7 +403,8 @@ const campBattleIds = { thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive', thirtyEighth: 'thirty-eighth-battle-jing-defense', thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard', - fortieth: 'fortieth-battle-han-river-flood' + fortieth: 'fortieth-battle-han-river-flood', + fortyFirst: 'forty-first-battle-fan-castle-siege' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -864,6 +867,20 @@ const sortieRulesByBattleId: Partial