From aa152d827c7edc81285c94423ec312f768d01bd4 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 22:01:50 +0900 Subject: [PATCH] Add third Meng Huo capture battle --- docs/roadmap.md | 6 +- scripts/verify-flow.mjs | 247 +++++++++++++++--- .../images/battle/fiftieth-battle-map.svg | 77 ++++++ src/game/data/battles.ts | 70 ++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 231 ++++++++++++++++ src/game/scenes/BattleScene.ts | 46 +++- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 170 +++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 11 files changed, 829 insertions(+), 58 deletions(-) create mode 100644 src/assets/images/battle/fiftieth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index d807991..79a0ef4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -74,15 +74,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 - Forty-ninth battle Meng Huo second-capture route, turning the first release into a repeated Seven Captures arc with a 98x80 marsh-jungle battlefield, renewed pacify-leader objective, second-release camp dialogues, and village/captive reassurance visits +- Fiftieth battle Meng Huo third-capture route, making the growing recruited roster matter through a Zhuge Liang-led seven-officer sortie choice, a 100x82 clan-and-marsh battlefield, named southern clan officers, and post-battle clan-persuasion 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 second Meng Huo capture 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/Meng Huo pacification camp states, campaign timeline state, and camp save state +- Flow verification script from title through the third Meng Huo capture 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/Meng Huo pacification camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the third Seven Captures Nanman battle so Meng Huo's repeated release begins to sway named southern clans and unlock stronger persuasion choices +1. Build the fourth Seven Captures Nanman battle so the previous clan persuasion choices begin changing objectives, enemy morale, and recommended sortie officers 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 @@ -93,4 +94,5 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Use original names, portraits, maps, and dialogue inspired by historical tactical RPGs. - Keep copyrighted scripts, sprites, portraits, music, and maps out of the repository. - Prefer data-driven scenarios so new chapters and battlefields can be added without rewriting scene code. +- Treat progressive officer recruitment, battle-specific required/recommended slots, and meaningful reserve choices as a core growth loop rather than a side menu. - Follow Liu Bei's long-form campaign arc: Yellow Turban suppression -> anti-Dong Zhuo era -> service under Gongsun Zan -> inheritance of Xu Province from Tao Qian -> loss of Xu Province to Lu Bu -> service under Cao Cao -> break from Cao Cao -> service under Yuan Shao -> refuge under Liu Biao -> recruitment of Zhuge Liang -> Red Cliffs -> securing Jing and Yi Provinces -> founding of Shu Han. diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 7d049ec..a268d53 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 !== 49 || + progressTabState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postBowangProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postChangbanProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postWulingProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postChangshaProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postYizhouProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postFuPassProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postLuoProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postLuofengProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postChengduProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postJiamengProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postYangpingProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postDingjunProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 50 || 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 !== 49 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 50 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 49 || + postFoundationProgressState.campaignProgress?.totalKnown !== 50 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 49 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 50 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 49 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 50 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 49 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 50 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 49 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 50 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 49 || + postJingRearProgressState.campaignProgress?.totalKnown !== 50 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 49 || + postGonganProgressState.campaignProgress?.totalKnown !== 50 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 49 || + postMaichengProgressState.campaignProgress?.totalKnown !== 50 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 49 || + postYilingProgressState.campaignProgress?.totalKnown !== 50 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 49 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 50 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,7 +6280,7 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 49 || + postBaidiProgressState.campaignProgress?.totalKnown !== 50 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { @@ -6441,7 +6441,7 @@ try { if ( postNanzhongProgressState?.activeTab !== 'progress' || postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || - postNanzhongProgressState.campaignProgress?.totalKnown !== 49 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 50 || postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전' ) { @@ -6603,7 +6603,7 @@ try { if ( postMengHuoProgressState?.activeTab !== 'progress' || postMengHuoProgressState.campaignProgress?.completedKnown !== 48 || - postMengHuoProgressState.campaignProgress?.totalKnown !== 49 || + postMengHuoProgressState.campaignProgress?.totalKnown !== 50 || postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' || postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전' ) { @@ -6765,14 +6765,179 @@ try { if ( postSecondCaptureProgressState?.activeTab !== 'progress' || postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 || - postSecondCaptureProgressState.campaignProgress?.totalKnown !== 49 || + postSecondCaptureProgressState.campaignProgress?.totalKnown !== 50 || postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' || - postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전' ) { - throw new Error(`Expected post-second-capture progress tab to complete the forty-ninth battle and pause before the third persuasion arc: ${JSON.stringify(postSecondCaptureProgressState?.campaignProgress)}`); + throw new Error(`Expected post-second-capture progress tab to complete the forty-ninth battle and point to the third capture arc: ${JSON.stringify(postSecondCaptureProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-second-capture-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const thirdCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !thirdCaptureSortieState?.sortieVisible || + thirdCaptureSortieState.campaign?.step !== 'forty-ninth-camp' || + thirdCaptureSortieState.campTitle !== '칠종칠금 2차전 후 군영' || + thirdCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 3차전 · 맹획 세 번째 생포와 호족 회유' || + thirdCaptureSortieState.sortiePlan?.maxCount !== 7 || + thirdCaptureSortieState.sortiePlan?.recommendedTotal !== 7 || + !thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + !thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + !thirdCaptureSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') || + thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected third Meng Huo sortie prep to expose the expanded recruited roster, Zhuge Liang lock, and clan-persuasion recommendations: ${JSON.stringify(thirdCaptureSortieState)}`); + } + assertSortieTacticalRoster(thirdCaptureSortieState, [ + '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 thirdCapturePriorityUnits = ['ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'ma-chao', 'huang-zhong']; + for (const unitId of thirdCapturePriorityUnits) { + 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 && !thirdCapturePriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const thirdCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !thirdCapturePriorityUnits.every((unitId) => + thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + thirdCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 || + thirdCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected third Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Huang Quan, Wang Ping, Zhao Yun, Ma Chao, and Huang Zhong: ${JSON.stringify(thirdCaptureSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-third-capture-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-third-capture-story.png', fullPage: true }); + + for (let i = 0; i < 60; i += 1) { + const enteredFiftiethBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'fiftieth-battle-meng-huo-third-capture'; + }); + if (enteredFiftiethBattle) { + 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 === 'fiftieth-battle-meng-huo-third-capture' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-fiftieth-battle.png', fullPage: true }); + + const fiftiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fiftiethEnemies = fiftiethBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fiftiethAllies = fiftiethBattleState.units.filter((unit) => unit.faction === 'ally'); + const fiftiethEnemyBehaviors = new Set(fiftiethEnemies.map((unit) => unit.ai)); + if ( + fiftiethBattleState.camera?.mapWidth !== 100 || + fiftiethBattleState.camera?.mapHeight !== 82 || + fiftiethBattleState.victoryConditionLabel !== '맹획 세 번째 생포와 호족 회유' || + !fiftiethBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-third' && objective.label === '맹획 세 번째 생포') || + fiftiethEnemies.length < 34 || + !fiftiethEnemyBehaviors.has('aggressive') || + !fiftiethEnemyBehaviors.has('guard') || + !fiftiethEnemyBehaviors.has('hold') || + !fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-leader') || + !fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-dailai') || + !fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-zhurong-vanguard') || + !fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-jinhuan') || + !fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-ahuinan') || + !fiftiethAllies.some((unit) => unit.id === 'zhuge-liang') || + !fiftiethAllies.some((unit) => unit.id === 'ma-liang') || + !fiftiethAllies.some((unit) => unit.id === 'huang-quan') || + !fiftiethAllies.some((unit) => unit.id === 'wang-ping') || + !fiftiethAllies.some((unit) => unit.id === 'zhao-yun') || + !fiftiethAllies.some((unit) => unit.id === 'ma-chao') || + !fiftiethAllies.some((unit) => unit.id === 'huang-zhong') || + fiftiethAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected fiftieth battle to use the third Meng Huo map, pacification objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftiethBattleState)}`); + } + + 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 fiftiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fiftiethCampState?.campBattleId !== 'fiftieth-battle-meng-huo-third-capture' || + fiftiethCampState.campTitle !== '칠종칠금 3차전 후 군영' || + fiftiethCampState.availableDialogueIds?.length !== 3 || + !fiftiethCampState.availableDialogueIds.every((id) => id.includes('third-capture')) || + fiftiethCampState.availableVisitIds?.length !== 2 || + !fiftiethCampState.availableVisitIds.every((id) => id.includes('third-capture')) || + fiftiethCampState.rosterCollection?.total < 21 + ) { + throw new Error(`Expected fiftieth camp to expose third capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftiethCampState)}`); + } + await page.screenshot({ path: 'dist/verification-third-capture-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postThirdCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postThirdCaptureProgressState?.activeTab !== 'progress' || + postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 || + postThirdCaptureProgressState.campaignProgress?.totalKnown !== 50 || + postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' || + postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-third-capture progress tab to complete the fiftieth battle and pause before the fourth capture arc: ${JSON.stringify(postThirdCaptureProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-third-capture-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -6804,11 +6969,11 @@ try { activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [], camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); - if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-ninth-camp') { - throw new Error(`Expected title continue to reopen the latest second Meng Huo capture camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fiftieth-camp') { + throw new Error(`Expected title continue to reopen the latest third Meng Huo capture camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-second-Meng-Huo-capture 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, Meng Huo pacification, and second capture state at ${targetUrl}`); + console.log(`Verified title-to-third-Meng-Huo-capture 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, Meng Huo pacification, second capture, and third capture state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/fiftieth-battle-map.svg b/src/assets/images/battle/fiftieth-battle-map.svg new file mode 100644 index 0000000..6792bd7 --- /dev/null +++ b/src/assets/images/battle/fiftieth-battle-map.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 3e84a41..478e5cc 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -1,4 +1,8 @@ import { + fiftiethBattleBonds, + fiftiethBattleMap, + fiftiethBattleUnits, + fiftiethBattleVictoryPages, firstBattleBonds, fifteenthBattleBonds, fifteenthBattleMap, @@ -250,7 +254,8 @@ export type BattleScenarioId = | 'forty-sixth-battle-yiling-fire' | 'forty-seventh-battle-nanzhong-stabilization' | 'forty-eighth-battle-meng-huo-main-force' - | 'forty-ninth-battle-meng-huo-second-capture'; + | 'forty-ninth-battle-meng-huo-second-capture' + | 'fiftieth-battle-meng-huo-third-capture'; export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -3182,6 +3187,66 @@ export const fortyNinthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fiftiethBattleScenario: BattleScenarioDefinition = { + id: 'fiftieth-battle-meng-huo-third-capture', + title: '칠종칠금 3차전', + victoryConditionLabel: '맹획 세 번째 생포와 호족 회유', + defeatConditionLabel: '제갈량 퇴각', + openingObjectiveLines: [ + '맹획이 세 번째로 군세를 모으자 남중 호족들의 마음이 갈라지고 있습니다. 이번 승리는 적장을 잡는 동시에, 지켜본 호족들이 촉한군의 질서를 인정하게 만드는 싸움입니다.', + '산채와 마을, 포로장을 함부로 짓밟지 마십시오. 회유를 받아들일 호족에게는 길을 남기고, 끝까지 선동하는 자만 생포망 안으로 몰아야 합니다.', + '제갈량은 반드시 생존해야 합니다. 마량으로 호족의 말을 붙잡고, 황권과 왕평으로 장부와 길목을 지키며, 조운과 마초로 맹수 돌파를 봉쇄하십시오.' + ], + map: fiftiethBattleMap, + units: fiftiethBattleUnits, + bonds: fiftiethBattleBonds, + mapTextureKey: 'battle-map-fiftieth', + leaderUnitId: 'menghuo-third-leader', + quickVictoryTurnLimit: 90, + baseVictoryGold: 11720, + objectives: [ + { + id: 'pacify-meng-huo-third', + kind: 'pacify-leader', + label: '맹획 세 번째 생포', + rewardGold: 8000, + unitId: 'menghuo-third-leader' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 2060, + unitId: 'zhuge-liang' + }, + { + id: 'clan-villages', + kind: 'secure-terrain', + label: '호족 마을 보호', + rewardGold: 3920, + terrain: 'village' + }, + { + id: 'captive-camps', + kind: 'secure-terrain', + label: '포로장 신뢰 유지', + rewardGold: 3180, + terrain: 'camp' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '90턴 이내 세 번째 생포', + rewardGold: 2600, + maxTurn: 90 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['콩 62', '상처약 46', '탁주 20', '남중 호족 설득문 1', '세 번째 회유 장부 1'], + victoryPages: fiftiethBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -3233,7 +3298,8 @@ export const battleScenarios: Record 'forty-sixth-battle-yiling-fire': fortySixthBattleScenario, 'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario, 'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario, - 'forty-ninth-battle-meng-huo-second-capture': fortyNinthBattleScenario + 'forty-ninth-battle-meng-huo-second-capture': fortyNinthBattleScenario, + 'fiftieth-battle-meng-huo-third-capture': fiftiethBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 34e9037..17a7d80 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -4,6 +4,7 @@ import { eighthBattleScenario, eighteenthBattleScenario, eleventhBattleScenario, + fiftiethBattleScenario, fortiethBattleScenario, fortyFirstBattleScenario, fortySecondBattleScenario, @@ -59,6 +60,8 @@ import { eighteenthBattleVictoryPages, eleventhBattleIntroPages, eleventhBattleVictoryPages, + fiftiethBattleIntroPages, + fiftiethBattleVictoryPages, fortiethBattleIntroPages, fortiethBattleVictoryPages, fortyFirstBattleIntroPages, @@ -717,13 +720,24 @@ const sortieFlows: Record = { }, [fortyNinthBattleScenario.id]: { afterBattleId: fortyNinthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '남중 회유의 확장', + eyebrow: '다음 전장', + title: fiftiethBattleScenario.title, description: - '맹획은 두 번째로 붙잡혔지만 아직 남중 전체가 굴복한 것은 아닙니다. 이제는 맹획 개인의 고집을 꺾는 것과 동시에, 호족과 마을이 어떤 조건에서 촉한의 질서를 받아들일지 다루어야 합니다.', - rewardHint: '다음 장: 칠종칠금 3차전과 남중 호족 선택지 준비 중', - pages: fortyNinthBattleVictoryPages, - unavailableNotice: '칠종칠금 3차전은 다음 작업에서 이어집니다. 지금은 두 번째 생포 뒤의 회유 공명, 포로 안심 장부, 남중 마을 선택지를 정비할 수 있습니다.' + '맹획은 두 번째로 붙잡혔지만 아직 남중 전체가 굴복한 것은 아닙니다. 이제는 맹획 개인의 고집을 꺾는 것과 동시에, 호족과 마을이 어떤 조건에서 촉한의 질서를 받아들일지 전장에서 보여야 합니다.', + rewardHint: `예상 보상: ${fiftiethBattleScenario.title} 개방 / 남중 호족 설득문`, + nextBattleId: fiftiethBattleScenario.id, + campaignStep: 'fiftieth-battle', + pages: [...fortyNinthBattleVictoryPages, ...fiftiethBattleIntroPages] + }, + [fiftiethBattleScenario.id]: { + afterBattleId: fiftiethBattleScenario.id, + eyebrow: '다음 장 준비', + title: '호족 회유 회의', + description: + '맹획은 세 번째로 붙잡혔고, 그를 따르던 호족들의 말도 흔들리기 시작했습니다. 다음 장에서는 전투 전 회의에서 어떤 호족을 먼저 달랠지 고르고, 그 선택이 다음 남중 전장의 목표와 추천 출전에 영향을 주는 흐름을 준비합니다.', + rewardHint: '다음 장: 남중 호족 회유 회의와 칠종칠금 4차전 준비 중', + pages: fiftiethBattleVictoryPages, + unavailableNotice: '칠종칠금 4차전은 다음 작업에서 이어집니다. 지금은 세 번째 생포 뒤의 호족 회유 선택지와 출전 추천 변화를 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 0f84fac..fe8de6b 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3244,6 +3244,59 @@ export const fortyNinthBattleVictoryPages: StoryPage[] = [ } ]; +export const fiftiethBattleIntroPages: StoryPage[] = [ + { + id: 'fiftieth-southern-clans-waver', + bgm: 'story-dark', + chapter: '흔들리는 호족들', + background: 'story-militia', + speaker: '황권', + text: '맹획이 두 번 붙잡혔다가 풀려나자 남중의 호족들은 서로 다른 말을 하기 시작했습니다. 어떤 이는 촉한군의 군율을 두려워했고, 어떤 이는 맹획이 아직 꺾이지 않았다고 외쳤습니다.' + }, + { + id: 'fiftieth-third-capture-plan', + bgm: 'battle-prep', + chapter: '세 번째 회유', + background: 'story-three-heroes', + speaker: '제갈량', + text: '이번 싸움에서는 맹획만 다시 잡아서는 부족합니다. 그를 따르는 호족들이 보는 앞에서 마을과 포로장을 지키고, 회유를 받아들일 길을 전장 안에 남겨야 합니다.' + }, + { + id: 'fiftieth-sortie', + bgm: 'battle-prep', + chapter: '칠종칠금 3차전', + background: 'story-sortie', + speaker: '왕평', + text: '산채는 더 깊고 늪지는 더 넓습니다. 마량이 호족의 말을 붙잡고, 황권이 장부를 흔들리지 않게 하며, 조운과 마초가 돌격을 막아야 맹획을 세 번째로 생포할 수 있습니다.' + } +]; + +export const fiftiethBattleVictoryPages: StoryPage[] = [ + { + id: 'fiftieth-victory-third-capture', + bgm: 'militia-theme', + chapter: '세 번째 생포', + background: 'story-sortie', + text: '맹획은 세 번째로 붙잡혔고, 그의 곁에 있던 호족들은 촉한군이 이번에도 마을을 태우지 않았다는 사실을 직접 보았습니다. 전장의 승리는 이제 남중 사람들의 말 속으로 번지기 시작했습니다.' + }, + { + id: 'fiftieth-victory-clan-choice', + bgm: 'story-dark', + chapter: '호족의 흔들림', + background: 'story-militia', + speaker: '마량', + text: '몇몇 촌장은 더 싸워도 마을이 남지 않는다고 말하기 시작했습니다. 그러나 맹획은 아직 고개를 숙이지 않았습니다. 그를 다시 풀어 보내면, 이번에는 남중의 호족들이 먼저 서로를 설득할 것입니다.' + }, + { + id: 'fiftieth-next-persuasive-camp', + bgm: 'story-dark', + chapter: '전장 밖의 회유', + background: 'story-three-heroes', + speaker: '제갈량', + text: '남중을 얻는 길은 점점 분명해지고 있습니다. 다음 장에서는 맹획을 따르던 호족들과 직접 이야기를 나누고, 전투 전 선택이 전장 목표에 영향을 주는 흐름을 준비해야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3683,6 +3736,12 @@ export const fortyNinthBattleMap: BattleMap = { terrain: createFortyNinthBattleTerrain() }; +export const fiftiethBattleMap: BattleMap = { + width: 100, + height: 82, + terrain: createFiftiethBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -15901,6 +15960,87 @@ export const fortyNinthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('menghuo-second-leader', '맹획', '남만왕', 'rebelLeader', 144, 974, 240, 92, 39) ]; +const fiftiethBattleAllyPositions: Record = { + 'guan-yu': { x: 13, y: 61 }, + 'zhang-fei': { x: 18, y: 63 }, + 'jian-yong': { x: 9, y: 68 }, + 'mi-zhu': { x: 11, y: 66 }, + 'sun-qian': { x: 15, y: 70 }, + 'zhao-yun': { x: 23, y: 58 }, + 'zhuge-liang': { x: 12, y: 58 }, + 'ma-liang': { x: 16, y: 55 }, + 'yi-ji': { x: 9, y: 73 }, + 'gong-zhi': { x: 19, y: 71 }, + 'huang-zhong': { x: 26, y: 59 }, + 'wei-yan': { x: 31, y: 63 }, + 'pang-tong': { x: 14, y: 52 }, + 'fa-zheng': { x: 20, y: 53 }, + 'wu-yi': { x: 33, y: 68 }, + 'yan-yan': { x: 36, y: 61 }, + 'li-yan': { x: 25, y: 51 }, + 'huang-quan': { x: 9, y: 56 }, + 'ma-chao': { x: 38, y: 58 }, + 'ma-dai': { x: 41, y: 63 }, + 'wang-ping': { x: 28, y: 54 } +}; + +export const fiftiethBattleUnits: 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, fiftiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('menghuo-third-scout-a', '남만 호족 척후', '서쪽 촌락 척후', 'bandit', 142, 620, 188, 45, 35), + createThirtyFirstEnemyUnit('menghuo-third-scout-b', '남만 호족 척후', '늪지 척후', 'bandit', 142, 620, 188, 60, 33), + createThirtyFirstEnemyUnit('menghuo-third-scout-c', '남만 호족 척후', '장터 척후', 'bandit', 143, 628, 190, 75, 50), + createThirtyFirstEnemyUnit('menghuo-third-scout-d', '남만 호족 척후', '동쪽 산길 척후', 'bandit', 143, 628, 190, 90, 64), + createThirtyFirstEnemyUnit('menghuo-third-vine-a', '남만 등갑 정예', '늪지 등갑병', 'yellowTurban', 143, 880, 222, 50, 43), + createThirtyFirstEnemyUnit('menghuo-third-vine-b', '남만 등갑 정예', '산채 등갑병', 'yellowTurban', 144, 892, 224, 62, 46), + createThirtyFirstEnemyUnit('menghuo-third-vine-c', '남만 등갑 정예', '마을 등갑병', 'yellowTurban', 144, 892, 224, 74, 56), + createThirtyFirstEnemyUnit('menghuo-third-vine-d', '남만 등갑 정예', '본대 등갑병', 'yellowTurban', 145, 904, 226, 86, 48), + createThirtyFirstEnemyUnit('menghuo-third-vine-e', '남만 등갑 정예', '후위 등갑병', 'yellowTurban', 145, 904, 226, 94, 40), + createThirtyFirstEnemyUnit('menghuo-third-archer-a', '남만 산림 궁병', '강변 궁병', 'archer', 142, 668, 198, 46, 27), + createThirtyFirstEnemyUnit('menghuo-third-archer-b', '남만 산림 궁병', '절벽 궁병', 'archer', 143, 676, 200, 61, 25), + createThirtyFirstEnemyUnit('menghuo-third-archer-c', '남만 산림 궁병', '마을 궁병', 'archer', 143, 676, 200, 72, 43), + createThirtyFirstEnemyUnit('menghuo-third-archer-d', '남만 산림 궁병', '늪지 궁병', 'archer', 144, 684, 202, 84, 60), + createThirtyFirstEnemyUnit('menghuo-third-archer-e', '남만 산림 궁병', '본영 궁병', 'archer', 144, 684, 202, 93, 53), + createThirtyFirstEnemyUnit('menghuo-third-beast-a', '남만 맹수대', '강가 맹수대', 'cavalry', 144, 896, 226, 55, 36), + createThirtyFirstEnemyUnit('menghuo-third-beast-b', '남만 맹수대', '늪지 맹수대', 'cavalry', 144, 896, 226, 68, 38), + createThirtyFirstEnemyUnit('menghuo-third-beast-c', '남만 맹수대', '산채 맹수대', 'cavalry', 145, 908, 228, 79, 51), + createThirtyFirstEnemyUnit('menghuo-third-beast-d', '남만 맹수대', '추격 맹수대', 'cavalry', 145, 908, 228, 91, 57), + createThirtyFirstEnemyUnit('menghuo-third-ambusher-a', '호족 매복병', '덩굴 매복병', 'bandit', 143, 636, 192, 37, 49), + createThirtyFirstEnemyUnit('menghuo-third-ambusher-b', '호족 매복병', '숲속 매복병', 'bandit', 144, 644, 194, 53, 52), + createThirtyFirstEnemyUnit('menghuo-third-ambusher-c', '호족 매복병', '강가 매복병', 'bandit', 144, 644, 194, 66, 65), + createThirtyFirstEnemyUnit('menghuo-third-ambusher-d', '호족 매복병', '후방 차단대', 'bandit', 145, 652, 196, 84, 72), + createThirtyFirstEnemyUnit('menghuo-third-ambusher-e', '호족 매복병', '서쪽 차단대', 'bandit', 145, 652, 196, 32, 62), + createThirtyFirstEnemyUnit('menghuo-third-guard-a', '맹획 호족 친위', '포로장 친위대', 'yellowTurban', 146, 930, 232, 70, 40), + createThirtyFirstEnemyUnit('menghuo-third-guard-b', '맹획 호족 친위', '산채 친위대', 'yellowTurban', 146, 930, 232, 82, 46), + createThirtyFirstEnemyUnit('menghuo-third-guard-c', '맹획 호족 친위', '본영 친위대', 'yellowTurban', 147, 942, 234, 92, 43), + createThirtyFirstEnemyUnit('menghuo-third-shaman-a', '남만 독천 주술사', '늪지 주술사', 'strategist', 145, 708, 208, 62, 34), + createThirtyFirstEnemyUnit('menghuo-third-shaman-b', '남만 독천 주술사', '강가 주술사', 'strategist', 146, 716, 210, 76, 47), + createThirtyFirstEnemyUnit('menghuo-third-shaman-c', '남만 독천 주술사', '산채 주술사', 'strategist', 146, 716, 210, 86, 36), + createThirtyFirstEnemyUnit('menghuo-third-officer-dailai', '대래동주', '남중 호족장', 'rebelLeader', 147, 956, 238, 74, 35), + createThirtyFirstEnemyUnit('menghuo-third-officer-zhurong-vanguard', '축융 선봉', '불창 선봉장', 'rebelLeader', 147, 956, 238, 80, 63), + createThirtyFirstEnemyUnit('menghuo-third-officer-jinhuan', '금환삼결', '산채 연합장', 'rebelLeader', 148, 968, 240, 88, 52), + createThirtyFirstEnemyUnit('menghuo-third-officer-ahuinan', '아회남', '흔들리는 호족장', 'rebelLeader', 146, 936, 234, 68, 58), + createThirtyFirstEnemyUnit('menghuo-third-leader', '맹획', '남만왕', 'rebelLeader', 150, 1010, 248, 94, 41) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -16807,6 +16947,35 @@ export const mengHuoSecondCaptureBonds: BattleBond[] = [ export const fortyNinthBattleBonds: BattleBond[] = [...fortyEighthBattleBonds, ...mengHuoSecondCaptureBonds].map( cloneBattleBondForScenario ); +export const mengHuoThirdCaptureBonds: BattleBond[] = [ + { + id: 'zhuge-liang__ma-liang_third-capture', + unitIds: ['zhuge-liang', 'ma-liang'], + title: '호족 설득문', + level: 48, + exp: 0, + description: '제갈량의 회유 방침과 마량의 문장은 흔들리는 호족들이 항복할 명분을 전장 안에 남긴다.' + }, + { + id: 'huang-quan__wang-ping_third-capture', + unitIds: ['huang-quan', 'wang-ping'], + title: '장부와 길목', + level: 44, + exp: 0, + description: '황권은 포로와 군량의 기준을 지키고 왕평은 마을 길목을 열어, 남중 호족들이 촉한군의 질서를 직접 보게 한다.' + }, + { + id: 'zhao-yun__ma-chao_third-capture', + unitIds: ['zhao-yun', 'ma-chao'], + title: '맹수 돌파 봉쇄', + level: 42, + exp: 0, + description: '조운과 마초는 맹획의 거친 재돌격을 막되 깊이 베지 않아, 세 번째 생포망을 유지한다.' + } +]; +export const fiftiethBattleBonds: BattleBond[] = [...fortyNinthBattleBonds, ...mengHuoThirdCaptureBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -19318,6 +19487,68 @@ function createFortyNinthBattleTerrain(): TerrainType[][] { ); } +function createFiftiethBattleTerrain(): TerrainType[][] { + return Array.from({ length: 82 }, (_, y) => + Array.from({ length: 100 }, (_, x): TerrainType => { + if ( + (x >= 0 && x <= 99 && y >= 74) || + (x >= 58 && x <= 64 && y >= 25 && y <= 74) || + (x >= 4 && x <= 27 && y >= 66 && y <= 81) || + (x >= 74 && x <= 90 && y >= 56 && y <= 74) + ) { + return 'river'; + } + if ( + (x >= 8 && x <= 94 && y >= 61 && y <= 67) || + (x >= 26 && x <= 72 && y >= 40 && y <= 47) || + (x >= 68 && x <= 94 && y >= 32 && y <= 38) || + (x >= 35 && x <= 43 && y >= 37 && y <= 70) + ) { + return 'road'; + } + if ( + (x >= 19 && x <= 70 && y >= 19 && y <= 72) || + (x >= 54 && x <= 94 && y >= 26 && y <= 68) || + (x >= 2 && x <= 36 && y >= 48 && y <= 72) || + (x >= 76 && x <= 99 && y >= 39 && y <= 71) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 29 && y <= 36) || + (x >= 32 && x <= 67 && y <= 26) || + (x >= 69 && x <= 99 && y <= 35) || + (x >= 85 && x <= 99 && y >= 32 && y <= 53) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 72) || + (x >= 97 && y <= 68) || + (x >= 29 && x <= 35 && y >= 0 && y <= 38) || + (x >= 66 && x <= 72 && y >= 0 && y <= 31) || + (x >= 41 && x <= 47 && y >= 62 && y <= 81) + ) { + return 'cliff'; + } + if ( + (x >= 9 && x <= 24 && y >= 57 && y <= 69) || + (x >= 37 && x <= 52 && y >= 41 && y <= 54) || + (x >= 74 && x <= 89 && y >= 47 && y <= 59) + ) { + return 'village'; + } + if ((x >= 22 && x <= 36 && y >= 49 && y <= 60) || (x >= 61 && x <= 76 && y >= 54 && y <= 66)) { + return 'camp'; + } + if ((x >= 46 && x <= 61 && y >= 29 && y <= 40) || (x >= 71 && x <= 90 && y >= 30 && y <= 43)) { + return 'fort'; + } + return 'plain'; + }) + ); +} + function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData { return { ...cloneUnitForScenario(unit), diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 38133a4..6462fe3 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -865,7 +865,15 @@ const unitTexture: Record = { 'menghuo-return-officer-meng-you': 'unit-rebel-leader', 'menghuo-return-officer-duosi': 'unit-rebel-leader', 'menghuo-return-officer-mulu': 'unit-rebel-leader', - 'menghuo-second-leader': 'unit-rebel-leader' + 'menghuo-second-leader': 'unit-rebel-leader', + 'menghuo-third-shaman-a': 'unit-rebel-archer', + 'menghuo-third-shaman-b': 'unit-rebel-archer', + 'menghuo-third-shaman-c': 'unit-rebel-archer', + 'menghuo-third-officer-dailai': 'unit-rebel-leader', + 'menghuo-third-officer-zhurong-vanguard': 'unit-rebel-leader', + 'menghuo-third-officer-jinhuan': 'unit-rebel-leader', + 'menghuo-third-officer-ahuinan': 'unit-rebel-leader', + 'menghuo-third-leader': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -2330,7 +2338,41 @@ const enemyAiByUnitId: Record = { 'menghuo-return-officer-meng-you': 'aggressive', 'menghuo-return-officer-duosi': 'guard', 'menghuo-return-officer-mulu': 'aggressive', - 'menghuo-second-leader': 'hold' + 'menghuo-second-leader': 'hold', + 'menghuo-third-scout-a': 'aggressive', + 'menghuo-third-scout-b': 'aggressive', + 'menghuo-third-scout-c': 'aggressive', + 'menghuo-third-scout-d': 'aggressive', + 'menghuo-third-vine-a': 'guard', + 'menghuo-third-vine-b': 'guard', + 'menghuo-third-vine-c': 'guard', + 'menghuo-third-vine-d': 'guard', + 'menghuo-third-vine-e': 'guard', + 'menghuo-third-archer-a': 'hold', + 'menghuo-third-archer-b': 'hold', + 'menghuo-third-archer-c': 'hold', + 'menghuo-third-archer-d': 'hold', + 'menghuo-third-archer-e': 'hold', + 'menghuo-third-beast-a': 'aggressive', + 'menghuo-third-beast-b': 'aggressive', + 'menghuo-third-beast-c': 'aggressive', + 'menghuo-third-beast-d': 'aggressive', + 'menghuo-third-ambusher-a': 'aggressive', + 'menghuo-third-ambusher-b': 'aggressive', + 'menghuo-third-ambusher-c': 'aggressive', + 'menghuo-third-ambusher-d': 'aggressive', + 'menghuo-third-ambusher-e': 'aggressive', + 'menghuo-third-guard-a': 'guard', + 'menghuo-third-guard-b': 'guard', + 'menghuo-third-guard-c': 'guard', + 'menghuo-third-shaman-a': 'hold', + 'menghuo-third-shaman-b': 'hold', + 'menghuo-third-shaman-c': 'hold', + 'menghuo-third-officer-dailai': 'guard', + 'menghuo-third-officer-zhurong-vanguard': 'aggressive', + 'menghuo-third-officer-jinhuan': 'aggressive', + 'menghuo-third-officer-ahuinan': 'guard', + 'menghuo-third-leader': 'hold' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 4d21fa8..3770cb1 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -2,6 +2,7 @@ import Phaser from 'phaser'; import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg'; import eighteenthBattleMapUrl from '../../assets/images/battle/eighteenth-battle-map.svg'; import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg'; +import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-battle-map.svg'; 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'; @@ -118,6 +119,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-ninth', ninthBattleMapUrl); this.load.image('battle-map-tenth', tenthBattleMapUrl); this.load.image('battle-map-eleventh', eleventhBattleMapUrl); + this.load.image('battle-map-fiftieth', fiftiethBattleMapUrl); this.load.image('battle-map-twelfth', twelfthBattleMapUrl); this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl); this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl); diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 0fff0da..5a75bcc 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -27,6 +27,7 @@ import { hanzhongMainRecruitUnits, hanzhongScoutRecruitBonds, hanzhongScoutRecruitUnits, + mengHuoThirdCaptureBonds, fanCastleBonds, fanCastleSiegeBonds, jingCollapseBonds, @@ -411,7 +412,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'forty-sixth-battle-yiling-fire', 'forty-seventh-battle-nanzhong-stabilization', 'forty-eighth-battle-meng-huo-main-force', - 'forty-ninth-battle-meng-huo-second-capture' + 'forty-ninth-battle-meng-huo-second-capture', + 'fiftieth-battle-meng-huo-third-capture' ], nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금'] } @@ -466,7 +468,8 @@ const campBattleIds = { fortySixth: 'forty-sixth-battle-yiling-fire', fortySeventh: 'forty-seventh-battle-nanzhong-stabilization', fortyEighth: 'forty-eighth-battle-meng-huo-main-force', - fortyNinth: 'forty-ninth-battle-meng-huo-second-capture' + fortyNinth: 'forty-ninth-battle-meng-huo-second-capture', + fiftieth: 'fiftieth-battle-meng-huo-third-capture' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1055,6 +1058,20 @@ const sortieRulesByBattleId: Partial