From 688b51f3cc29ccbdf927053b4932c638580fc59e Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 20:59:52 +0900 Subject: [PATCH] Add Meng Huo pacification battle --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 240 +++++++++++++++--- .../images/battle/forty-eighth-battle-map.svg | 71 ++++++ src/game/data/battles.ts | 72 +++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 231 +++++++++++++++++ src/game/scenes/BattleScene.ts | 54 +++- 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, 824 insertions(+), 63 deletions(-) create mode 100644 src/assets/images/battle/forty-eighth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 210484f..95c2ef0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -72,16 +72,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Forty-sixth battle Yiling fire-attack route, using Lu Xun's delayed fire plan, stretched Shu-Han camps, a 92x74 burning river-forest battlefield, and a seven-officer retreat-route sortie choice to break through toward Baidi Castle - Baidi Castle entrustment story milestone, where Liu Bei's retreat from Yiling becomes a playable camp transition into Zhuge Liang's regency burden and post-defeat recovery planning - Forty-seventh battle Nanzhong stabilization route, where the Baidi entrustment aftermath opens a Zhuge Liang-led sortie, removes Liu Bei from the deployable roster, and uses a 94x76 southern mountain battlefield with village, warehouse, and rebel command objectives +- Forty-eighth battle Meng Huo main-force route, where Zhuge Liang leads a seven-officer pacification sortie across a 96x78 southern mountain battlefield, captures Meng Huo through a dedicated pacify-leader objective, and opens post-battle release, captive-ledger, and market-reassurance camp events - 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 Nanzhong stabilization camp, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire/Baidi entrustment camp states, campaign timeline state, and camp save state +- Flow verification script from title through the Meng Huo main-force camp, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire/Baidi entrustment/Nanzhong stabilization camp states, campaign timeline state, and camp save state ## Next Steps -1. Build Meng Huo's first main-force battle and the first explicit Nanman reconciliation mechanic after the Nanzhong stabilization route -2. Expand post-Baidi and post-Nanzhong camp consequences with grief checks, recovery policy choices, village reassurance, and regency decisions that affect sortie recommendations +1. Build the next capture-and-release Nanman battle so Meng Huo's return becomes a repeated persuasion arc rather than a one-off fight +2. Expand post-Baidi and post-Nanzhong camp consequences with grief checks, recovery policy choices, village reassurance, captive handling, and regency 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 fd3539a..fd2a884 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 !== 47 || + progressTabState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postBowangProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postChangbanProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postWulingProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postChangshaProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postYizhouProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postFuPassProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postLuoProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postLuofengProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postChengduProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postJiamengProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postYangpingProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postDingjunProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 48 || 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 !== 47 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 48 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 47 || + postFoundationProgressState.campaignProgress?.totalKnown !== 48 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 47 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 48 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 47 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 48 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 47 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 48 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 47 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 48 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 47 || + postJingRearProgressState.campaignProgress?.totalKnown !== 48 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 47 || + postGonganProgressState.campaignProgress?.totalKnown !== 48 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 47 || + postMaichengProgressState.campaignProgress?.totalKnown !== 48 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 47 || + postYilingProgressState.campaignProgress?.totalKnown !== 48 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 47 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 48 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,7 +6280,7 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 47 || + postBaidiProgressState.campaignProgress?.totalKnown !== 48 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { @@ -6441,14 +6441,176 @@ try { if ( postNanzhongProgressState?.activeTab !== 'progress' || postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || - postNanzhongProgressState.campaignProgress?.totalKnown !== 47 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 48 || postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || - postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전' ) { - throw new Error(`Expected post-Nanzhong progress tab to complete the forty-seventh battle and pause before Meng Huo's main force: ${JSON.stringify(postNanzhongProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Nanzhong progress tab to complete the forty-seventh battle and point to Meng Huo's main force: ${JSON.stringify(postNanzhongProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-nanzhong-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const mengHuoSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !mengHuoSortieState?.sortieVisible || + mengHuoSortieState.campaign?.step !== 'forty-seventh-camp' || + mengHuoSortieState.campTitle !== '남중 안정로 후 군영' || + mengHuoSortieState.sortiePlan?.objectiveLine !== '맹획 본대전 · 맹획 본대 제압과 회유' || + mengHuoSortieState.sortiePlan?.maxCount !== 7 || + mengHuoSortieState.sortiePlan?.recommendedTotal !== 7 || + !mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + !mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected Meng Huo sortie prep to lock Zhuge Liang, recommend pacification officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(mengHuoSortieState)}`); + } + assertSortieTacticalRoster(mengHuoSortieState, [ + '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 mengHuoPriorityUnits = ['ma-liang', 'wang-ping', 'huang-quan', 'zhao-yun', 'ma-chao', 'huang-zhong']; + for (const unitId of mengHuoPriorityUnits) { + 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 && !mengHuoPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const mengHuoSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !mengHuoPriorityUnits.every((unitId) => + mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + mengHuoSortieReadyState.sortiePlan?.selectedCount !== 7 || + mengHuoSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Wang Ping, Huang Quan, Zhao Yun, Ma Chao, and Huang Zhong: ${JSON.stringify(mengHuoSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-meng-huo-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-meng-huo-story.png', fullPage: true }); + + for (let i = 0; i < 60; i += 1) { + const enteredFortyEighthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-eighth-battle-meng-huo-main-force'; + }); + if (enteredFortyEighthBattle) { + 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-eighth-battle-meng-huo-main-force' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-eighth-battle.png', fullPage: true }); + + const fortyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortyEighthEnemies = fortyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortyEighthAllies = fortyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortyEighthEnemyBehaviors = new Set(fortyEighthEnemies.map((unit) => unit.ai)); + if ( + fortyEighthBattleState.camera?.mapWidth !== 96 || + fortyEighthBattleState.camera?.mapHeight !== 78 || + fortyEighthBattleState.victoryConditionLabel !== '맹획 본대 제압과 회유' || + !fortyEighthBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo' && objective.label === '맹획 생포와 회유') || + fortyEighthEnemies.length < 33 || + !fortyEighthEnemyBehaviors.has('aggressive') || + !fortyEighthEnemyBehaviors.has('guard') || + !fortyEighthEnemyBehaviors.has('hold') || + !fortyEighthEnemies.some((unit) => unit.id === 'menghuo-leader') || + !fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-ahuinan') || + !fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-dongtuna') || + !fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-mangyachang') || + !fortyEighthAllies.some((unit) => unit.id === 'zhuge-liang') || + !fortyEighthAllies.some((unit) => unit.id === 'ma-liang') || + !fortyEighthAllies.some((unit) => unit.id === 'wang-ping') || + !fortyEighthAllies.some((unit) => unit.id === 'huang-quan') || + !fortyEighthAllies.some((unit) => unit.id === 'zhao-yun') || + !fortyEighthAllies.some((unit) => unit.id === 'ma-chao') || + !fortyEighthAllies.some((unit) => unit.id === 'huang-zhong') || + fortyEighthAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected forty-eighth battle to use Meng Huo map, pacification objective, selected officers, and mixed Nanman AI: ${JSON.stringify(fortyEighthBattleState)}`); + } + + 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 fortyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortyEighthCampState?.campBattleId !== 'forty-eighth-battle-meng-huo-main-force' || + fortyEighthCampState.campTitle !== '맹획 본대전 후 군영' || + fortyEighthCampState.availableDialogueIds?.length !== 3 || + !fortyEighthCampState.availableDialogueIds.every((id) => id.includes('menghuo')) || + fortyEighthCampState.availableVisitIds?.length !== 2 || + !fortyEighthCampState.availableVisitIds.every((id) => id.includes('menghuo')) || + fortyEighthCampState.rosterCollection?.total < 21 + ) { + throw new Error(`Expected forty-eighth camp to expose Meng Huo pacification dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortyEighthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-meng-huo-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postMengHuoProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postMengHuoProgressState?.activeTab !== 'progress' || + postMengHuoProgressState.campaignProgress?.completedKnown !== 48 || + postMengHuoProgressState.campaignProgress?.totalKnown !== 48 || + postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' || + postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Meng-Huo progress tab to complete the forty-eighth battle and pause before the next capture-and-release arc: ${JSON.stringify(postMengHuoProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-meng-huo-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -6480,11 +6642,11 @@ try { activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [], camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); - if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-seventh-camp') { - throw new Error(`Expected title continue to reopen the latest Nanzhong camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-eighth-camp') { + throw new Error(`Expected title continue to reopen the latest Meng Huo camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-Nanzhong-stabilization flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, and Nanzhong stabilization state at ${targetUrl}`); + console.log(`Verified title-to-Meng-Huo-main-force flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, and Meng Huo pacification state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/forty-eighth-battle-map.svg b/src/assets/images/battle/forty-eighth-battle-map.svg new file mode 100644 index 0000000..3272f7f --- /dev/null +++ b/src/assets/images/battle/forty-eighth-battle-map.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 8029494..aefa2fe 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -44,6 +44,10 @@ import { fortyFifthBattleMap, fortyFifthBattleUnits, fortyFifthBattleVictoryPages, + fortyEighthBattleBonds, + fortyEighthBattleMap, + fortyEighthBattleUnits, + fortyEighthBattleVictoryPages, fortySixthBattleBonds, fortySixthBattleMap, fortySixthBattleUnits, @@ -240,9 +244,10 @@ export type BattleScenarioId = | 'forty-fourth-battle-maicheng-isolation' | 'forty-fifth-battle-yiling-vanguard' | 'forty-sixth-battle-yiling-fire' - | 'forty-seventh-battle-nanzhong-stabilization'; + | 'forty-seventh-battle-nanzhong-stabilization' + | 'forty-eighth-battle-meng-huo-main-force'; -export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; +export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; export type BattleObjectiveDefinition = { id: string; @@ -3052,6 +3057,66 @@ export const fortySeventhBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortyEighthBattleScenario: BattleScenarioDefinition = { + id: 'forty-eighth-battle-meng-huo-main-force', + title: '맹획 본대전', + victoryConditionLabel: '맹획 본대 제압과 회유', + defeatConditionLabel: '제갈량 퇴각', + openingObjectiveLines: [ + '남중의 첫 길은 열렸지만 맹획은 더 깊은 숲에서 호족과 맹수 기병을 모았습니다. 오늘의 승리는 적장을 베는 것이 아니라 생포해 돌려보낼 명분을 만드는 데 있습니다.', + '마을, 포로 수용소, 산채 창고를 함부로 불태우지 마십시오. 회유할 사람과 끝까지 싸울 사람을 구분해야 남중이 다시 반란으로 돌아가지 않습니다.', + '제갈량은 반드시 생존해야 합니다. 조운과 마초로 돌격을 막고, 왕평과 황권으로 포로와 산길을 정리하며, 마량으로 마을의 말을 붙잡으십시오.' + ], + map: fortyEighthBattleMap, + units: fortyEighthBattleUnits, + bonds: fortyEighthBattleBonds, + mapTextureKey: 'battle-map-forty-eighth', + leaderUnitId: 'menghuo-leader', + quickVictoryTurnLimit: 82, + baseVictoryGold: 10580, + objectives: [ + { + id: 'pacify-meng-huo', + kind: 'pacify-leader', + label: '맹획 생포와 회유', + rewardGold: 7200, + unitId: 'menghuo-leader' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 1900, + unitId: 'zhuge-liang' + }, + { + id: 'amnesty-villages', + kind: 'secure-terrain', + label: '회유 마을 보호', + rewardGold: 3580, + terrain: 'village' + }, + { + id: 'captive-camps', + kind: 'secure-terrain', + label: '포로 수용소 확보', + rewardGold: 2860, + terrain: 'camp' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '82턴 이내 생포', + rewardGold: 2440, + maxTurn: 82 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['콩 54', '상처약 42', '탁주 18', '맹획 회유 서신 1', '남중 포로 명부 1'], + victoryPages: fortyEighthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -3101,7 +3166,8 @@ export const battleScenarios: Record 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario, 'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario, 'forty-sixth-battle-yiling-fire': fortySixthBattleScenario, - 'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario + 'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario, + 'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index a9928f7..c5eea59 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -10,6 +10,7 @@ import { fortyThirdBattleScenario, fortyFourthBattleScenario, fortyFifthBattleScenario, + fortyEighthBattleScenario, fortySixthBattleScenario, fortySeventhBattleScenario, fifthBattleScenario, @@ -69,6 +70,8 @@ import { fortyFourthBattleVictoryPages, fortyFifthBattleIntroPages, fortyFifthBattleVictoryPages, + fortyEighthBattleIntroPages, + fortyEighthBattleVictoryPages, fortySixthBattleIntroPages, fortySeventhBattleIntroPages, fortySeventhBattleVictoryPages, @@ -689,13 +692,24 @@ const sortieFlows: Record = { }, [fortySeventhBattleScenario.id]: { afterBattleId: fortySeventhBattleScenario.id, - eyebrow: '다음 장 준비', - title: '맹획과 남중 본대', + eyebrow: '다음 전장', + title: fortyEighthBattleScenario.title, description: - '남중 안정로의 첫 길은 열렸지만 반란의 중심은 더 깊은 산과 마을 사이에 남아 있습니다. 다음 장에서는 맹획의 본대를 상대하며 힘과 회유를 함께 시험해야 합니다.', - rewardHint: '다음 장: 맹획 본대와 남중 회유 준비 중', - pages: fortySeventhBattleVictoryPages, - unavailableNotice: '맹획 본대와 남중 회유는 다음 작업에서 이어집니다. 지금은 남중 안정로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' + '남중 안정로의 첫 길은 열렸지만 반란의 중심은 더 깊은 산과 마을 사이에 남아 있습니다. 맹획의 본대와 맞서되, 생포와 회유의 명분을 남겨 남중을 모두 적으로 돌리지 않아야 합니다.', + rewardHint: `예상 보상: ${fortyEighthBattleScenario.title} 개방 / 맹획 회유 서신`, + nextBattleId: fortyEighthBattleScenario.id, + campaignStep: 'forty-eighth-battle', + pages: [...fortySeventhBattleVictoryPages, ...fortyEighthBattleIntroPages] + }, + [fortyEighthBattleScenario.id]: { + afterBattleId: fortyEighthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '칠종칠금의 첫 장', + description: + '맹획을 처음 생포하고 돌려보낸 뒤, 남중의 호족들은 촉한군의 방식이 단순 토벌이 아님을 보기 시작했습니다. 다음 장에서는 풀려난 맹획이 다시 세운 진영과, 그를 완전히 설득하기 위한 반복 회유 흐름을 다룹니다.', + rewardHint: '다음 장: 칠종칠금 회유전 준비 중', + pages: fortyEighthBattleVictoryPages, + unavailableNotice: '칠종칠금 회유전은 다음 작업에서 이어집니다. 지금은 맹획 본대전 뒤의 공명도, 포로 장부, 회유 선택지를 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index ba95680..a198682 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3138,6 +3138,59 @@ export const fortySeventhBattleVictoryPages: StoryPage[] = [ } ]; +export const fortyEighthBattleIntroPages: StoryPage[] = [ + { + id: 'forty-eighth-meng-huo-gathers', + bgm: 'story-dark', + chapter: '맹획의 깃발', + background: 'story-militia', + speaker: '왕평', + text: '옹개의 지휘선은 흩어졌지만 남중의 마을들은 아직 촉한군을 멀리서 바라봅니다. 깊은 숲 안쪽에서는 맹획이 여러 호족을 모아, 첫 패배를 갚겠다고 병력을 다시 세우고 있습니다.' + }, + { + id: 'forty-eighth-zhuge-amnesty', + bgm: 'battle-prep', + chapter: '힘과 회유', + background: 'story-three-heroes', + speaker: '제갈량', + text: '맹획을 쓰러뜨리는 것만으로는 남중을 얻을 수 없습니다. 반드시 생포하여 돌려보낼 길을 남기십시오. 마을과 포로 수용소를 지키면, 다음 싸움에서 칼보다 말이 먼저 닿을 것입니다.' + }, + { + id: 'forty-eighth-sortie', + bgm: 'battle-prep', + chapter: '맹획 본대전', + background: 'story-sortie', + speaker: '마량', + text: '맹획의 본대는 숲길, 늪지, 산채를 함께 쓰고 있습니다. 조운과 마초가 돌격을 막고, 왕평이 지형을 읽으며, 황권이 포로와 군량의 장부를 흐트러뜨리지 않아야 합니다.' + } +]; + +export const fortyEighthBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-eighth-victory-captured', + bgm: 'militia-theme', + chapter: '첫 생포', + background: 'story-sortie', + text: '맹획의 본대는 흩어졌고, 촉한군은 산채의 불길을 키우지 않은 채 포로와 마을 사람을 갈라 세웠습니다. 병사들은 적장을 베지 않고 생포하는 싸움이 왜 어려운지 처음으로 깨달았습니다.' + }, + { + id: 'forty-eighth-victory-release', + bgm: 'story-dark', + chapter: '돌려보내는 책략', + background: 'story-three-heroes', + speaker: '제갈량', + text: '맹획은 아직 진심으로 굴복하지 않았습니다. 그러나 억지 항복은 오래가지 않습니다. 오늘은 그를 돌려보내 남중의 사람들에게 촉한군이 약탈하러 온 군대가 아님을 보이겠습니다.' + }, + { + id: 'forty-eighth-next-capture', + bgm: 'story-dark', + chapter: '칠종칠금의 첫 장', + background: 'story-militia', + speaker: '마량', + text: '맹획은 풀려났으나 산속의 호족들은 이제 촉한군의 태도를 보았습니다. 다음 싸움부터는 힘뿐 아니라 누구를 먼저 달래고 누구를 끝까지 추격할지 선택해야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3565,6 +3618,12 @@ export const fortySeventhBattleMap: BattleMap = { terrain: createFortySeventhBattleTerrain() }; +export const fortyEighthBattleMap: BattleMap = { + width: 96, + height: 78, + terrain: createFortyEighthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -15623,6 +15682,86 @@ export const fortySeventhBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('nanzhong-leader-yong-kai', '옹개', '남중 반란 수장', 'rebelLeader', 130, 856, 212, 88, 42) ]; +const fortyEighthBattleAllyPositions: Record = { + 'guan-yu': { x: 12, y: 56 }, + 'zhang-fei': { x: 16, y: 57 }, + 'jian-yong': { x: 8, y: 61 }, + 'mi-zhu': { x: 10, y: 59 }, + 'sun-qian': { x: 13, y: 63 }, + 'zhao-yun': { x: 20, y: 53 }, + 'zhuge-liang': { x: 10, y: 54 }, + 'ma-liang': { x: 14, y: 51 }, + 'yi-ji': { x: 9, y: 66 }, + 'gong-zhi': { x: 17, y: 64 }, + 'huang-zhong': { x: 23, y: 55 }, + 'wei-yan': { x: 27, y: 58 }, + 'pang-tong': { x: 13, y: 48 }, + 'fa-zheng': { x: 18, y: 49 }, + 'wu-yi': { x: 29, y: 62 }, + 'yan-yan': { x: 31, y: 56 }, + 'li-yan': { x: 23, y: 46 }, + 'huang-quan': { x: 8, y: 52 }, + 'ma-chao': { x: 34, y: 53 }, + 'ma-dai': { x: 36, y: 57 }, + 'wang-ping': { x: 25, y: 50 } +}; + +export const fortyEighthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits, + ...hanzhongScoutRecruitUnits, + ...hanzhongMainRecruitUnits + ].map((unit) => placeScenarioUnit(unit, fortyEighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('menghuo-scout-a', '남만 척후', '산채 척후', 'bandit', 130, 552, 170, 42, 32), + createThirtyFirstEnemyUnit('menghuo-scout-b', '남만 척후', '숲길 척후', 'bandit', 130, 552, 170, 55, 28), + createThirtyFirstEnemyUnit('menghuo-scout-c', '남만 척후', '늪지 척후', 'bandit', 131, 560, 172, 70, 48), + createThirtyFirstEnemyUnit('menghuo-scout-d', '남만 척후', '동쪽 척후', 'bandit', 131, 560, 172, 86, 59), + createThirtyFirstEnemyUnit('menghuo-vine-infantry-a', '등갑병', '산채 등갑병', 'yellowTurban', 131, 812, 206, 48, 38), + createThirtyFirstEnemyUnit('menghuo-vine-infantry-b', '등갑병', '늪길 등갑병', 'yellowTurban', 132, 824, 208, 61, 42), + createThirtyFirstEnemyUnit('menghuo-vine-infantry-c', '등갑병', '마을 등갑병', 'yellowTurban', 132, 824, 208, 72, 52), + createThirtyFirstEnemyUnit('menghuo-vine-infantry-d', '등갑병', '본대 등갑병', 'yellowTurban', 133, 836, 210, 84, 45), + createThirtyFirstEnemyUnit('menghuo-vine-infantry-e', '등갑병', '수장 등갑병', 'yellowTurban', 133, 836, 210, 89, 36), + createThirtyFirstEnemyUnit('menghuo-archer-a', '남만 궁병', '나무 궁병', 'archer', 130, 604, 180, 45, 24), + createThirtyFirstEnemyUnit('menghuo-archer-b', '남만 궁병', '고지 궁병', 'archer', 131, 612, 182, 58, 21), + createThirtyFirstEnemyUnit('menghuo-archer-c', '남만 궁병', '마을 궁병', 'archer', 131, 612, 182, 68, 39), + createThirtyFirstEnemyUnit('menghuo-archer-d', '남만 궁병', '늪지 궁병', 'archer', 132, 620, 184, 80, 57), + createThirtyFirstEnemyUnit('menghuo-archer-e', '남만 궁병', '본대 궁병', 'archer', 132, 620, 184, 91, 50), + createThirtyFirstEnemyUnit('menghuo-beast-rider-a', '맹수 기병', '코끼리 선봉', 'cavalry', 132, 828, 210, 51, 31), + createThirtyFirstEnemyUnit('menghuo-beast-rider-b', '맹수 기병', '산짐승 기병', 'cavalry', 132, 828, 210, 65, 34), + createThirtyFirstEnemyUnit('menghuo-beast-rider-c', '맹수 기병', '늪지 기병', 'cavalry', 133, 840, 212, 76, 46), + createThirtyFirstEnemyUnit('menghuo-beast-rider-d', '맹수 기병', '추격 기병', 'cavalry', 133, 840, 212, 88, 54), + createThirtyFirstEnemyUnit('menghuo-ambusher-a', '남만 복병', '덩굴 복병', 'bandit', 131, 570, 174, 36, 45), + createThirtyFirstEnemyUnit('menghuo-ambusher-b', '남만 복병', '숲속 복병', 'bandit', 132, 578, 176, 52, 49), + createThirtyFirstEnemyUnit('menghuo-ambusher-c', '남만 복병', '강가 복병', 'bandit', 132, 578, 176, 64, 61), + createThirtyFirstEnemyUnit('menghuo-ambusher-d', '남만 복병', '후방 차단대', 'bandit', 133, 586, 178, 82, 68), + createThirtyFirstEnemyUnit('menghuo-ambusher-e', '남만 복병', '서쪽 차단대', 'bandit', 133, 586, 178, 31, 59), + createThirtyFirstEnemyUnit('menghuo-guard-a', '맹획 친위대', '포로장 경비대', 'yellowTurban', 134, 862, 216, 70, 36), + createThirtyFirstEnemyUnit('menghuo-guard-b', '맹획 친위대', '산채 경비대', 'yellowTurban', 134, 862, 216, 78, 44), + createThirtyFirstEnemyUnit('menghuo-guard-c', '맹획 친위대', '본영 경비대', 'yellowTurban', 135, 874, 218, 88, 40), + createThirtyFirstEnemyUnit('menghuo-shaman-a', '남만 주술사', '독안개 주술사', 'strategist', 133, 644, 190, 60, 30), + createThirtyFirstEnemyUnit('menghuo-shaman-b', '남만 주술사', '늪지 주술사', 'strategist', 134, 652, 192, 73, 43), + createThirtyFirstEnemyUnit('menghuo-shaman-c', '남만 주술사', '산채 주술사', 'strategist', 134, 652, 192, 83, 32), + createThirtyFirstEnemyUnit('menghuo-officer-ahuinan', '아회남', '남만 호족장', 'rebelLeader', 135, 890, 222, 72, 31), + createThirtyFirstEnemyUnit('menghuo-officer-dongtuna', '동도나', '남만 산채장', 'rebelLeader', 135, 890, 222, 78, 60), + createThirtyFirstEnemyUnit('menghuo-officer-mangyachang', '망아장', '맹수대장', 'rebelLeader', 136, 902, 224, 86, 48), + createThirtyFirstEnemyUnit('menghuo-leader', '맹획', '남만왕', 'rebelLeader', 138, 940, 232, 90, 38) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -16471,6 +16610,35 @@ export const nanzhongRecoveryBonds: BattleBond[] = [ export const fortySeventhBattleBonds: BattleBond[] = [...fortySixthBattleBonds, ...nanzhongRecoveryBonds].map( cloneBattleBondForScenario ); +export const mengHuoPacificationBonds: BattleBond[] = [ + { + id: 'zhuge-liang__ma-liang_menghuo', + unitIds: ['zhuge-liang', 'ma-liang'], + title: '회유의 문장', + level: 43, + exp: 0, + description: '제갈량의 큰 방침과 마량의 부드러운 문장은 맹획을 베기보다 돌려보낼 명분을 세운다.' + }, + { + id: 'zhao-yun__ma-chao_menghuo', + unitIds: ['zhao-yun', 'ma-chao'], + title: '남중 돌격 차단', + level: 38, + exp: 0, + description: '조운의 안정된 기동과 마초의 강한 돌파는 맹수 기병의 첫 충돌을 받아 내는 힘이 된다.' + }, + { + id: 'huang-quan__wang-ping_menghuo', + unitIds: ['huang-quan', 'wang-ping'], + title: '포로와 산길', + level: 37, + exp: 0, + description: '황권의 장부와 왕평의 지형 판단은 포로 수용소와 산마을을 불필요한 원한 없이 정리한다.' + } +]; +export const fortyEighthBattleBonds: BattleBond[] = [...fortySeventhBattleBonds, ...mengHuoPacificationBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -18857,6 +19025,69 @@ function createFortySeventhBattleTerrain(): TerrainType[][] { ); } +function createFortyEighthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 78 }, (_, y) => + Array.from({ length: 96 }, (_, x): TerrainType => { + if ( + (x >= 0 && x <= 95 && y >= 69) || + (x >= 43 && x <= 55 && y >= 55 && y <= 77) || + (x >= 3 && x <= 19 && y >= 60 && y <= 77) || + (x >= 72 && x <= 82 && y >= 50 && y <= 68) + ) { + return 'river'; + } + if ( + (x >= 3 && x <= 89 && y >= 53 && y <= 58) || + (x >= 21 && x <= 92 && y >= 62 && y <= 66) || + (x >= 27 && x <= 40 && y >= 37 && y <= 62) || + (x >= 60 && x <= 72 && y >= 25 && y <= 63) || + (x >= 73 && x <= 90 && y >= 35 && y <= 41) + ) { + return 'road'; + } + if ( + (x >= 24 && x <= 68 && y >= 20 && y <= 67) || + (x >= 50 && x <= 88 && y >= 27 && y <= 70) || + (x >= 2 && x <= 34 && y >= 46 && y <= 68) || + (x >= 74 && x <= 95 && y >= 38 && y <= 68) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 30 && y <= 35) || + (x >= 35 && x <= 63 && y <= 25) || + (x >= 66 && x <= 95 && y <= 31) || + (x >= 84 && x <= 95 && y >= 29 && y <= 48) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 68) || + (x >= 93 && y <= 62) || + (x >= 31 && x <= 37 && y >= 0 && y <= 35) || + (x >= 64 && x <= 70 && y >= 0 && y <= 28) || + (x >= 38 && x <= 44 && y >= 58 && y <= 77) + ) { + return 'cliff'; + } + if ( + (x >= 10 && x <= 24 && y >= 48 && y <= 60) || + (x >= 46 && x <= 58 && y >= 41 && y <= 55) || + (x >= 72 && x <= 87 && y >= 43 && y <= 55) + ) { + return 'village'; + } + if ((x >= 17 && x <= 28 && y >= 38 && y <= 47) || (x >= 77 && x <= 91 && y >= 31 && y <= 43)) { + return 'fort'; + } + if ((x >= 54 && x <= 66 && y >= 54 && y <= 64) || (x >= 31 && x <= 43 && y >= 44 && 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 63f55e4..872cc8f 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -851,7 +851,14 @@ const unitTexture: Record = { 'nanzhong-officer-gao-ding': 'unit-rebel-leader', 'nanzhong-officer-zhu-bao': 'unit-rebel-leader', 'nanzhong-officer-meng-huo-vanguard': 'unit-rebel-leader', - 'nanzhong-leader-yong-kai': 'unit-rebel-leader' + 'nanzhong-leader-yong-kai': 'unit-rebel-leader', + 'menghuo-shaman-a': 'unit-rebel-archer', + 'menghuo-shaman-b': 'unit-rebel-archer', + 'menghuo-shaman-c': 'unit-rebel-archer', + 'menghuo-officer-ahuinan': 'unit-rebel-leader', + 'menghuo-officer-dongtuna': 'unit-rebel-leader', + 'menghuo-officer-mangyachang': 'unit-rebel-leader', + 'menghuo-leader': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -2250,7 +2257,40 @@ const enemyAiByUnitId: Record = { 'nanzhong-officer-gao-ding': 'guard', 'nanzhong-officer-zhu-bao': 'guard', 'nanzhong-officer-meng-huo-vanguard': 'aggressive', - 'nanzhong-leader-yong-kai': 'hold' + 'nanzhong-leader-yong-kai': 'hold', + 'menghuo-scout-a': 'aggressive', + 'menghuo-scout-b': 'aggressive', + 'menghuo-scout-c': 'aggressive', + 'menghuo-scout-d': 'aggressive', + 'menghuo-vine-infantry-a': 'guard', + 'menghuo-vine-infantry-b': 'guard', + 'menghuo-vine-infantry-c': 'guard', + 'menghuo-vine-infantry-d': 'guard', + 'menghuo-vine-infantry-e': 'guard', + 'menghuo-archer-a': 'hold', + 'menghuo-archer-b': 'hold', + 'menghuo-archer-c': 'hold', + 'menghuo-archer-d': 'hold', + 'menghuo-archer-e': 'hold', + 'menghuo-beast-rider-a': 'aggressive', + 'menghuo-beast-rider-b': 'aggressive', + 'menghuo-beast-rider-c': 'aggressive', + 'menghuo-beast-rider-d': 'aggressive', + 'menghuo-ambusher-a': 'aggressive', + 'menghuo-ambusher-b': 'aggressive', + 'menghuo-ambusher-c': 'aggressive', + 'menghuo-ambusher-d': 'aggressive', + 'menghuo-ambusher-e': 'aggressive', + 'menghuo-guard-a': 'guard', + 'menghuo-guard-b': 'guard', + 'menghuo-guard-c': 'guard', + 'menghuo-shaman-a': 'hold', + 'menghuo-shaman-b': 'hold', + 'menghuo-shaman-c': 'hold', + 'menghuo-officer-ahuinan': 'guard', + 'menghuo-officer-dongtuna': 'guard', + 'menghuo-officer-mangyachang': 'aggressive', + 'menghuo-leader': 'hold' }; const defaultEnemyAiByClass: Partial> = { @@ -3819,7 +3859,7 @@ export class BattleScene extends Phaser.Scene { const message = outcome === 'victory' ? `승리! ${battleScenario.victoryConditionLabel}에 성공했습니다.` - : '패배... 유비가 패주했습니다.'; + : `패배... ${battleScenario.defeatConditionLabel} 조건이 발생했습니다.`; this.pushBattleLog(message); this.renderSituationPanel(message); soundDirector.playEffect(outcome === 'victory' ? 'exp-gain' : 'combat-impact', { @@ -3954,17 +3994,18 @@ export class BattleScene extends Phaser.Scene { } private objectiveState(objective: BattleObjectiveDefinition, outcome?: BattleOutcome): BattleObjectiveState { - if (objective.kind === 'defeat-leader') { + if (objective.kind === 'defeat-leader' || objective.kind === 'pacify-leader') { const targetId = objective.unitId ?? leaderUnitId; const target = battleUnits.find((unit) => unit.id === targetId); const defeated = this.isUnitDefeated(targetId); + const doneText = objective.kind === 'pacify-leader' ? '생포' : '격파'; const achieved = outcome ? outcome === 'victory' && defeated : defeated; return { id: objective.id, label: objective.label, achieved, status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active', - detail: defeated ? `${this.unitName(targetId)} 격파` : `${this.unitName(targetId)} 병력 ${target?.hp ?? 0}/${target?.maxHp ?? 0}`, + detail: defeated ? `${this.unitName(targetId)} ${doneText}` : `${this.unitName(targetId)} 병력 ${target?.hp ?? 0}/${target?.maxHp ?? 0}`, rewardGold: objective.rewardGold }; } @@ -3988,13 +4029,14 @@ export class BattleScene extends Phaser.Scene { terrain === 'village' ? !this.hasEnemyThreateningTerrain(terrain, 2) : !this.hasEnemyOnTerrain(terrain); + const terrainLabel = terrain === 'village' ? '마을 주변' : terrain === 'camp' ? '군영/수용소' : terrain === 'fort' ? '요새' : '목표 지형'; const achieved = outcome ? outcome === 'victory' && secured : secured; return { id: objective.id, label: objective.label, achieved, status: achieved ? 'done' : outcome === 'defeat' ? 'failed' : 'active', - detail: secured ? '마을 주변 안전' : '마을 주변 교전 중', + detail: secured ? `${terrainLabel} 안전` : `${terrainLabel} 교전 중`, rewardGold: objective.rewardGold }; } diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index a43756c..e733afc 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -11,6 +11,7 @@ import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-bat import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battle-map.svg'; import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg'; import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg'; +import fortyEighthBattleMapUrl from '../../assets/images/battle/forty-eighth-battle-map.svg'; import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg'; import fortySeventhBattleMapUrl from '../../assets/images/battle/forty-seventh-battle-map.svg'; import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg'; @@ -152,6 +153,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-forty-fifth', fortyFifthBattleMapUrl); this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl); this.load.image('battle-map-forty-seventh', fortySeventhBattleMapUrl); + this.load.image('battle-map-forty-eighth', fortyEighthBattleMapUrl); 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 825cdce..334f5d3 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -41,6 +41,7 @@ import { luoCastleRecruitBonds, luoCastleRecruitUnits, maichengIsolationBonds, + mengHuoPacificationBonds, nanzhongRecoveryBonds, wulingRecruitBonds, wulingRecruitUnits, @@ -407,9 +408,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'forty-fourth-battle-maicheng-isolation', 'forty-fifth-battle-yiling-vanguard', 'forty-sixth-battle-yiling-fire', - 'forty-seventh-battle-nanzhong-stabilization' + 'forty-seventh-battle-nanzhong-stabilization', + 'forty-eighth-battle-meng-huo-main-force' ], - nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정'] + nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유'] } ]; @@ -460,7 +462,8 @@ const campBattleIds = { fortyFourth: 'forty-fourth-battle-maicheng-isolation', fortyFifth: 'forty-fifth-battle-yiling-vanguard', fortySixth: 'forty-sixth-battle-yiling-fire', - fortySeventh: 'forty-seventh-battle-nanzhong-stabilization' + fortySeventh: 'forty-seventh-battle-nanzhong-stabilization', + fortyEighth: 'forty-eighth-battle-meng-huo-main-force' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1021,6 +1024,20 @@ const sortieRulesByBattleId: Partial