From f3d23a13be0bd3f4dfc509f843a6a5d1eea5c4c3 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 24 Jun 2026 11:28:46 +0900 Subject: [PATCH] Add Wudu Yinping campaign chapter --- docs/roadmap.md | 6 +- scripts/verify-flow.mjs | 269 ++++++++++++++---- .../images/battle/sixtieth-battle-map.svg | 69 +++++ src/game/data/battleMapAssets.ts | 2 + src/game/data/battles.ts | 91 +++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 265 +++++++++++++++++ src/game/scenes/CampScene.ts | 169 ++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 10 files changed, 833 insertions(+), 76 deletions(-) create mode 100644 src/assets/images/battle/sixtieth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 78cee11..77b0f42 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -90,15 +90,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Post-Qishan-retreat camp events with Zhuge Liang, Jiang Wei, Wang Ping, Zhao Yun, Ma Dai, Huang Quan, and Ma Liang focusing on first-sortie review, low-road logistics, return-line discipline, and the next northern campaign preparation - Fifty-ninth battle Chencang siege route, where Jiang Wei's role continues but Wei Yan's breakthrough pressure, Wang Ping's camp markers, Ma Dai's cavalry response, Huang Quan's supply limits, and Fa Zheng's siege reading compete inside a seven-officer sortie choice across a 118x100 fortress battlefield - Post-Chencang camp events with Jiang Wei, Fa Zheng, Wei Yan, Huang Quan, Ma Liang, Wang Ping, and Ma Dai focusing on siege timing, restrained breakthrough choices, supply ledgers, and the next northern-front recalculation +- Sixtieth battle Wudu/Yinping commanderies route, where the post-Chencang plan shifts from siege pressure to split mountain-road control, using eight recommended officers for seven slots so Jiang Wei, Wei Yan, Zhao Yun, Ma Dai, Wang Ping, Huang Quan, and Ma Liang compete inside a 120x102 battlefield against Guo Huai's relief line +- Post-Wudu/Yinping camp events with Jiang Wei, Wei Yan, Zhao Yun, Ma Dai, Wang Ping, Huang Quan, and Ma Liang focusing on breakthrough return lines, cavalry recall signals, two-commandery supply ledgers, and the next northern-front responsibility - 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, required/excluded/recommended officers, expanding recruited-roster selection pressure, 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 Chencang siege and post-siege camp, with Jiang Wei's first selectable sortie, 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 Wudu/Yinping commanderies and post-commandery camp, with Jiang Wei's first selectable sortie, 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 next northern-front step after the Chencang siege, preserving the regrouping tone and making Jiang Wei and Wei Yan's growing roles compete with Zhao Yun, Ma Dai, Wang Ping, and the strategists instead of simply increasing raw party strength +1. Build the next northern-front step after securing Wudu and Yinping, preserving the broadened-responsibility tone and making the newly held commanderies create more supply, defense, and sortie tradeoffs instead of treating the extra territory as a simple victory lap 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 diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index a01bc8b..70b1e4a 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 !== 59 || + progressTabState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postBowangProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postChangbanProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postWulingProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postChangshaProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postYizhouProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postFuPassProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postLuoProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postLuofengProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postChengduProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postJiamengProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postYangpingProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postDingjunProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 60 || 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 !== 59 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 60 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 59 || + postFoundationProgressState.campaignProgress?.totalKnown !== 60 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 59 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 60 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 59 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 60 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 59 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 60 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 59 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 60 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 59 || + postJingRearProgressState.campaignProgress?.totalKnown !== 60 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 59 || + postGonganProgressState.campaignProgress?.totalKnown !== 60 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 59 || + postMaichengProgressState.campaignProgress?.totalKnown !== 60 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 59 || + postYilingProgressState.campaignProgress?.totalKnown !== 60 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 59 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 60 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,7 +6280,7 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 59 || + postBaidiProgressState.campaignProgress?.totalKnown !== 60 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { @@ -6441,7 +6441,7 @@ try { if ( postNanzhongProgressState?.activeTab !== 'progress' || postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || - postNanzhongProgressState.campaignProgress?.totalKnown !== 59 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 60 || postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전' ) { @@ -6603,7 +6603,7 @@ try { if ( postMengHuoProgressState?.activeTab !== 'progress' || postMengHuoProgressState.campaignProgress?.completedKnown !== 48 || - postMengHuoProgressState.campaignProgress?.totalKnown !== 59 || + postMengHuoProgressState.campaignProgress?.totalKnown !== 60 || postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' || postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전' ) { @@ -6765,7 +6765,7 @@ try { if ( postSecondCaptureProgressState?.activeTab !== 'progress' || postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 || - postSecondCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postSecondCaptureProgressState.campaignProgress?.totalKnown !== 60 || postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' || postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전' ) { @@ -6930,7 +6930,7 @@ try { if ( postThirdCaptureProgressState?.activeTab !== 'progress' || postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 || - postThirdCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postThirdCaptureProgressState.campaignProgress?.totalKnown !== 60 || postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' || postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전' ) { @@ -7096,7 +7096,7 @@ try { if ( postFourthCaptureProgressState?.activeTab !== 'progress' || postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 || - postFourthCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postFourthCaptureProgressState.campaignProgress?.totalKnown !== 60 || postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' || postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전' ) { @@ -7262,7 +7262,7 @@ try { if ( postFifthCaptureProgressState?.activeTab !== 'progress' || postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 || - postFifthCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postFifthCaptureProgressState.campaignProgress?.totalKnown !== 60 || postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' || postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전' ) { @@ -7427,7 +7427,7 @@ try { if ( postSixthCaptureProgressState?.activeTab !== 'progress' || postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 || - postSixthCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postSixthCaptureProgressState.campaignProgress?.totalKnown !== 60 || postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' || postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전' ) { @@ -7593,7 +7593,7 @@ try { if ( postFinalCaptureProgressState?.activeTab !== 'progress' || postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 || - postFinalCaptureProgressState.campaignProgress?.totalKnown !== 59 || + postFinalCaptureProgressState.campaignProgress?.totalKnown !== 60 || postFinalCaptureProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFinalCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' || !String(postFinalCaptureProgressState.campaignProgress?.nextBattleTitle ?? '').includes('북벌 준비') @@ -7666,7 +7666,7 @@ try { if ( postNorthernPrepProgressState?.activeTab !== 'progress' || postNorthernPrepProgressState.campaignProgress?.completedKnown !== 54 || - postNorthernPrepProgressState.campaignProgress?.totalKnown !== 59 || + postNorthernPrepProgressState.campaignProgress?.totalKnown !== 60 || postNorthernPrepProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postNorthernPrepProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' || postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진로' @@ -7816,7 +7816,7 @@ try { if ( postFiftyFifthProgressState?.activeTab !== 'progress' || postFiftyFifthProgressState.campaignProgress?.completedKnown !== 55 || - postFiftyFifthProgressState.campaignProgress?.totalKnown !== 59 || + postFiftyFifthProgressState.campaignProgress?.totalKnown !== 60 || postFiftyFifthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftyFifthProgressState.campaignProgress?.latestBattleTitle !== '제1차 북벌 출진로' || postFiftyFifthProgressState.campaignProgress?.nextBattleTitle !== '천수 진군로' @@ -7966,7 +7966,7 @@ try { if ( postFiftySixthProgressState?.activeTab !== 'progress' || postFiftySixthProgressState.campaignProgress?.completedKnown !== 56 || - postFiftySixthProgressState.campaignProgress?.totalKnown !== 59 || + postFiftySixthProgressState.campaignProgress?.totalKnown !== 60 || postFiftySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftySixthProgressState.campaignProgress?.latestBattleTitle !== '천수 진군로' || postFiftySixthProgressState.campaignProgress?.nextBattleTitle !== '가정 배치 위기' @@ -8121,7 +8121,7 @@ try { if ( postFiftySeventhProgressState?.activeTab !== 'progress' || postFiftySeventhProgressState.campaignProgress?.completedKnown !== 57 || - postFiftySeventhProgressState.campaignProgress?.totalKnown !== 59 || + postFiftySeventhProgressState.campaignProgress?.totalKnown !== 60 || postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' || postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로' @@ -8272,7 +8272,7 @@ try { if ( postFiftyEighthProgressState?.activeTab !== 'progress' || postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 || - postFiftyEighthProgressState.campaignProgress?.totalKnown !== 59 || + postFiftyEighthProgressState.campaignProgress?.totalKnown !== 60 || postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' || postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '진창 공성전' @@ -8423,15 +8423,172 @@ try { if ( postFiftyNinthProgressState?.activeTab !== 'progress' || postFiftyNinthProgressState.campaignProgress?.completedKnown !== 59 || - postFiftyNinthProgressState.campaignProgress?.totalKnown !== 59 || + postFiftyNinthProgressState.campaignProgress?.totalKnown !== 60 || postFiftyNinthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftyNinthProgressState.campaignProgress?.latestBattleTitle !== '진창 공성전' || - postFiftyNinthProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postFiftyNinthProgressState.campaignProgress?.nextBattleTitle !== '무도·음평 확보전' ) { - throw new Error(`Expected post-fifty-ninth progress tab to complete Chencang and leave the next northern step pending: ${JSON.stringify(postFiftyNinthProgressState?.campaignProgress)}`); + throw new Error(`Expected post-fifty-ninth progress tab to complete Chencang and point toward Wudu/Yinping: ${JSON.stringify(postFiftyNinthProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fifty-ninth-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const wuduSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !wuduSortieState?.sortieVisible || + wuduSortieState.campaign?.step !== 'fifty-ninth-camp' || + wuduSortieState.campTitle !== '진창 공성전 후 군영' || + !String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('무도·음평 확보전') || + !String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('곽회') || + wuduSortieState.sortiePlan?.maxCount !== 7 || + wuduSortieState.sortiePlan?.recommendedTotal !== 8 || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + wuduSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected Wudu/Yinping sortie to require Zhuge Liang, exclude Liu Bei, and recommend eight competing northern roles: ${JSON.stringify(wuduSortieState)}`); + } + assertSortieTacticalRoster(wuduSortieState, [ + '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', 'jiang-wei' + ]); + + const wuduPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang']; + for (const unitId of wuduPriorityUnits) { + 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 && !wuduPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const wuduSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !wuduPriorityUnits.every((unitId) => + wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) || + wuduSortieReadyState.sortiePlan?.selectedCount !== 7 || + wuduSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || + wuduSortieReadyState.sortiePlan?.recommendedTotal !== 8 + ) { + throw new Error(`Expected Wudu/Yinping sortie to deploy Zhuge Liang, Jiang Wei, Wei Yan, Zhao Yun, Wang Ping, Huang Quan, and Ma Liang while benching one recommended cavalry role: ${JSON.stringify(wuduSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-sixtieth-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-sixtieth-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredSixtiethBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'sixtieth-battle-wudu-yinping'; + }); + if (enteredSixtiethBattle) { + 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 === 'sixtieth-battle-wudu-yinping' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-sixtieth-battle.png', fullPage: true }); + + const sixtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const sixtiethEnemies = sixtiethBattleState.units.filter((unit) => unit.faction === 'enemy'); + const sixtiethAllies = sixtiethBattleState.units.filter((unit) => unit.faction === 'ally'); + const sixtiethEnemyBehaviors = new Set(sixtiethEnemies.map((unit) => unit.ai)); + if ( + sixtiethBattleState.camera?.mapWidth !== 120 || + sixtiethBattleState.camera?.mapHeight !== 102 || + sixtiethBattleState.victoryConditionLabel !== '곽회 구원로 차단과 무도·음평 확보' || + !sixtiethBattleState.objectives?.some((objective) => objective.id === 'repel-guo-huai-relief' && objective.label === '곽회 구원로 차단') || + !sixtiethBattleState.objectives?.some((objective) => objective.id === 'secure-wudu-guard' && objective.label === '무도 고개 수비 제압') || + !sixtiethBattleState.objectives?.some((objective) => objective.id === 'secure-yinping-guard' && objective.label === '음평 물목 수비 제압') || + !sixtiethBattleState.objectives?.some((objective) => objective.id === 'two-commandery-road' && objective.label === '두 고을 수레 길 확보') || + !sixtiethBattleState.objectives?.some((objective) => objective.id === 'wudu-yinping-villages' && objective.label === '무도·음평 마을 안정') || + sixtiethEnemies.length < 42 || + !sixtiethEnemyBehaviors.has('aggressive') || + !sixtiethEnemyBehaviors.has('guard') || + !sixtiethEnemyBehaviors.has('hold') || + !sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-leader-guo-huai') || + !sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-officer-wudu') || + !sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-officer-yinping') || + !sixtiethAllies.some((unit) => unit.id === 'zhuge-liang') || + !sixtiethAllies.some((unit) => unit.id === 'jiang-wei') || + !sixtiethAllies.some((unit) => unit.id === 'wei-yan') || + !sixtiethAllies.some((unit) => unit.id === 'zhao-yun') || + !sixtiethAllies.some((unit) => unit.id === 'wang-ping') || + !sixtiethAllies.some((unit) => unit.id === 'huang-quan') || + !sixtiethAllies.some((unit) => unit.id === 'ma-liang') || + sixtiethAllies.some((unit) => unit.id === 'ma-dai') || + sixtiethAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected sixtieth battle to use the Wudu/Yinping map, Guo Huai objectives, a deliberately benched recommended cavalry role, and mixed Wei AI: ${JSON.stringify(sixtiethBattleState)}`); + } + + 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 sixtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + sixtiethCampState?.campBattleId !== 'sixtieth-battle-wudu-yinping' || + sixtiethCampState.campaign?.step !== 'sixtieth-camp' || + sixtiethCampState.campTitle !== '무도·음평 확보 후 군영' || + sixtiethCampState.availableDialogueIds?.length !== 3 || + !sixtiethCampState.availableDialogueIds.every((id) => id.startsWith('northern-sixth-')) || + sixtiethCampState.availableVisitIds?.length !== 1 || + !sixtiethCampState.availableVisitIds.every((id) => id.startsWith('northern-sixth-')) || + sixtiethCampState.rosterCollection?.total < 22 || + !sixtiethCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__wei-yan_northern-sixth') + ) { + throw new Error(`Expected Wudu/Yinping aftermath camp to expose sixth northern campaign events and bonds: ${JSON.stringify(sixtiethCampState)}`); + } + await page.screenshot({ path: 'dist/verification-sixtieth-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postSixtiethProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postSixtiethProgressState?.activeTab !== 'progress' || + postSixtiethProgressState.campaignProgress?.completedKnown !== 60 || + postSixtiethProgressState.campaignProgress?.totalKnown !== 60 || + postSixtiethProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || + postSixtiethProgressState.campaignProgress?.latestBattleTitle !== '무도·음평 확보전' || + postSixtiethProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-sixtieth progress tab to complete Wudu/Yinping and leave the next northern step pending: ${JSON.stringify(postSixtiethProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-sixtieth-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -8460,12 +8617,12 @@ try { } } for (let attempt = 0; attempt < 24; attempt += 1) { - const continuedToChencangCamp = await page.evaluate(() => { + const continuedToWuduCamp = await page.evaluate(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; const camp = window.__HEROS_DEBUG__?.camp?.() ?? null; - return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-ninth-camp'; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixtieth-camp'; }); - if (continuedToChencangCamp) { + if (continuedToWuduCamp) { break; } await page.keyboard.press('Space'); @@ -8474,17 +8631,17 @@ try { await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; const camp = window.__HEROS_DEBUG__?.camp?.() ?? null; - return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-ninth-camp'; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixtieth-camp'; }); const titleContinueState = await page.evaluate(() => ({ activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [], camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); - if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fifty-ninth-camp') { - throw new Error(`Expected title continue to reopen the Chencang aftermath camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'sixtieth-camp') { + throw new Error(`Expected title continue to reopen the Wudu/Yinping aftermath camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-Chencang 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 siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, northern campaign preparation, Qishan road first northern expedition, Tianshui advance, Jieting crisis, Jiang Wei joining, Qishan retreat, Chencang siege, and pending next northern step state at ${targetUrl}`); + console.log(`Verified title-to-Wudu/Yinping 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 siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, northern campaign preparation, Qishan road first northern expedition, Tianshui advance, Jieting crisis, Jiang Wei joining, Qishan retreat, Chencang siege, Wudu/Yinping commanderies, and pending next northern step state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/sixtieth-battle-map.svg b/src/assets/images/battle/sixtieth-battle-map.svg new file mode 100644 index 0000000..1607d05 --- /dev/null +++ b/src/assets/images/battle/sixtieth-battle-map.svg @@ -0,0 +1,69 @@ + + Wudu and Yinping mountain campaign map + Original tactical battlefield map with twin northern valleys, mountain roads, river bends, Shu camps, Wei forts, and commandery towns. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 촉 진영 + 음평로 + 무도로 + 곽회 방어선 + 위 본영 + + diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts index 7c62047..e0e1588 100644 --- a/src/game/data/battleMapAssets.ts +++ b/src/game/data/battleMapAssets.ts @@ -9,6 +9,7 @@ import fiftyFourthBattleMapUrl from '../../assets/images/battle/fifty-fourth-bat import fiftyFifthBattleMapUrl from '../../assets/images/battle/fifty-fifth-battle-map.svg'; import fiftyEighthBattleMapUrl from '../../assets/images/battle/fifty-eighth-battle-map.svg'; import fiftyNinthBattleMapUrl from '../../assets/images/battle/fifty-ninth-battle-map.svg'; +import sixtiethBattleMapUrl from '../../assets/images/battle/sixtieth-battle-map.svg'; import fiftySeventhBattleMapUrl from '../../assets/images/battle/fifty-seventh-battle-map.svg'; import fiftySixthBattleMapUrl from '../../assets/images/battle/fifty-sixth-battle-map.svg'; import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg'; @@ -116,6 +117,7 @@ export const battleMapAssets: Record = { 'battle-map-fifty-fifth': fiftyFifthBattleMapUrl, 'battle-map-fifty-eighth': fiftyEighthBattleMapUrl, 'battle-map-fifty-ninth': fiftyNinthBattleMapUrl, + 'battle-map-sixtieth': sixtiethBattleMapUrl, 'battle-map-fifty-seventh': fiftySeventhBattleMapUrl, 'battle-map-fifty-sixth': fiftySixthBattleMapUrl }; diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 523ca79..a01429e 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -35,6 +35,10 @@ import { fiftyNinthBattleMap, fiftyNinthBattleUnits, fiftyNinthBattleVictoryPages, + sixtiethBattleBonds, + sixtiethBattleMap, + sixtiethBattleUnits, + sixtiethBattleVictoryPages, fiftySeventhBattleBonds, fiftySeventhBattleMap, fiftySeventhBattleUnits, @@ -300,7 +304,8 @@ export type BattleScenarioId = | 'fifty-sixth-battle-tianshui-advance' | 'fifty-seventh-battle-jieting-crisis' | 'fifty-eighth-battle-qishan-retreat' - | 'fifty-ninth-battle-chencang-siege'; + | 'fifty-ninth-battle-chencang-siege' + | 'sixtieth-battle-wudu-yinping'; export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -3947,6 +3952,87 @@ export const fiftyNinthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const sixtiethBattleScenario: BattleScenarioDefinition = { + id: 'sixtieth-battle-wudu-yinping', + title: '무도·음평 확보전', + victoryConditionLabel: '곽회 구원로 차단과 무도·음평 확보', + defeatConditionLabel: '제갈량 퇴각 또는 두 고을 보급선 붕괴', + openingObjectiveLines: [ + '진창에서 물러난 뒤 촉한군은 무도와 음평의 산길을 동시에 압박합니다. 성 하나를 오래 치는 대신 두 고을의 길목을 얻어 북벌의 숨통을 넓히려 합니다.', + '곽회는 구원 기병과 산길 수비대를 보내 두 길을 끊으려 합니다. 강유가 길을 읽고 위연이 돌파하되, 조운·마대·왕평·황권·마량 중 누구를 데려갈지 선택이 전투의 모양을 바꿉니다.', + '곽회의 본대를 늦추고 무도와 음평의 마을, 수레 길, 산길 진영을 함께 안정시키십시오. 두 고을을 얻어도 보급이 끊기면 다음 북벌은 다시 좁아집니다.' + ], + map: sixtiethBattleMap, + units: sixtiethBattleUnits, + bonds: sixtiethBattleBonds, + mapTextureKey: 'battle-map-sixtieth', + leaderUnitId: 'northern-sixth-leader-guo-huai', + quickVictoryTurnLimit: 120, + baseVictoryGold: 17920, + objectives: [ + { + id: 'repel-guo-huai-relief', + kind: 'defeat-leader', + label: '곽회 구원로 차단', + rewardGold: 12600, + unitId: 'northern-sixth-leader-guo-huai' + }, + { + id: 'secure-wudu-guard', + kind: 'defeat-leader', + label: '무도 고개 수비 제압', + rewardGold: 6280, + unitId: 'northern-sixth-officer-wudu' + }, + { + id: 'secure-yinping-guard', + kind: 'defeat-leader', + label: '음평 물목 수비 제압', + rewardGold: 6240, + unitId: 'northern-sixth-officer-yinping' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 3200, + unitId: 'zhuge-liang' + }, + { + id: 'two-commandery-road', + kind: 'secure-terrain', + label: '두 고을 수레 길 확보', + rewardGold: 6040, + terrain: 'road' + }, + { + id: 'wudu-yinping-villages', + kind: 'secure-terrain', + label: '무도·음평 마을 안정', + rewardGold: 5900, + terrain: 'village' + }, + { + id: 'mountain-camps', + kind: 'secure-terrain', + label: '산길 진영 유지', + rewardGold: 5760, + terrain: 'camp' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '120턴 이내 두 고을 확보', + rewardGold: 3920, + maxTurn: 120 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['금 110', '상처약 90', '군주 44', '무도·음평 장부 1', '곽회 구원로 기록 1'], + victoryPages: sixtiethBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -4008,7 +4094,8 @@ export const battleScenarios: Record 'fifty-sixth-battle-tianshui-advance': fiftySixthBattleScenario, 'fifty-seventh-battle-jieting-crisis': fiftySeventhBattleScenario, 'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario, - 'fifty-ninth-battle-chencang-siege': fiftyNinthBattleScenario + 'fifty-ninth-battle-chencang-siege': fiftyNinthBattleScenario, + 'sixtieth-battle-wudu-yinping': sixtiethBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index fcc8b6c..dcb600e 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -14,6 +14,7 @@ import { fiftySixthBattleScenario, fiftyNinthBattleScenario, fiftySeventhBattleScenario, + sixtiethBattleScenario, fortiethBattleScenario, fortyFirstBattleScenario, fortySecondBattleScenario, @@ -75,6 +76,8 @@ import { fiftyEighthBattleVictoryPages, fiftyNinthBattleIntroPages, fiftyNinthBattleVictoryPages, + sixtiethBattleIntroPages, + sixtiethBattleVictoryPages, fiftyFirstBattleIntroPages, fiftyFirstBattleVictoryPages, fiftySecondBattleIntroPages, @@ -869,13 +872,24 @@ const sortieFlows: Record = { }, [fiftyNinthBattleScenario.id]: { afterBattleId: fiftyNinthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '진창 공성전 이후', + eyebrow: '다음 전장', + title: sixtiethBattleScenario.title, description: - '진창 성채를 압박하고 왕쌍의 추격을 끊었지만, 성을 오래 물고 늘어지는 북벌은 군량의 시간을 갉아먹습니다. 이것은 결말이 아니라 다음 북방 계산을 위한 경계선입니다.', - rewardHint: '다음 목표: 북벌 보급 재계산 / 진창 공성 평가', - pages: fiftyNinthBattleVictoryPages, - unavailableNotice: '진창 공성전 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 진창 공성 평가, 보급 재계산, 강유와 위연의 편성 방향을 확인할 수 있습니다.' + '진창 성채를 오래 물고 늘어지는 대신 무도와 음평의 산길을 나누어 확보합니다. 강유와 위연의 전진성, 조운과 마대의 기동 회수, 왕평과 황권의 제동 장치를 7명 안에서 다시 골라야 합니다.', + rewardHint: `예상 보상: ${sixtiethBattleScenario.title} 개방 / 무도·음평 장부`, + nextBattleId: sixtiethBattleScenario.id, + campaignStep: 'sixtieth-battle', + pages: [...fiftyNinthBattleVictoryPages, ...sixtiethBattleIntroPages] + }, + [sixtiethBattleScenario.id]: { + afterBattleId: sixtiethBattleScenario.id, + eyebrow: '다음 장 준비', + title: '무도·음평 확보 이후', + description: + '무도와 음평의 산길을 확보했지만, 얻은 두 고을은 북벌의 끝이 아니라 더 넓어진 책임입니다. 다음 북방 계산에서는 새 고을을 지키는 보급과 다시 나아갈 장수의 고집을 함께 다루어야 합니다.', + rewardHint: '다음 목표: 두 고을 안정 / 다음 북벌 보급 재계산', + pages: sixtiethBattleVictoryPages, + unavailableNotice: '무도·음평 확보 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 두 고을 안정, 산길 표식, 강유와 위연의 전진 기준을 확인할 수 있습니다.' }, 'northern-campaign-prep-camp-legacy': { afterBattleId: fiftyFourthBattleScenario.id, diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index c23fb45..fc08c71 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3801,6 +3801,59 @@ export const fiftyNinthBattleVictoryPages: StoryPage[] = [ } ]; +export const sixtiethBattleIntroPages: StoryPage[] = [ + { + id: 'sixtieth-wudu-yinping-order', + bgm: 'story-dark', + chapter: '무도와 음평', + background: 'story-militia', + speaker: '제갈량', + text: '진창의 성벽은 오래 물고 늘어질 곳이 아니었습니다. 이번에는 성을 치기보다 무도와 음평의 산길을 나누어 잡아, 북방의 두 고을을 촉한의 숨통으로 만들겠습니다.' + }, + { + id: 'sixtieth-guo-huai-relief', + bgm: 'battle-prep', + chapter: '곽회의 구원로', + background: 'story-sortie', + speaker: '강유', + text: '곽회는 성을 지키는 장수가 아니라 길을 끊는 장수입니다. 제가 아는 천수 길로 앞을 보겠습니다. 다만 위연 장군의 돌파, 조운과 마대의 기병, 왕평의 산길 판단을 모두 한꺼번에 쓸 수는 없습니다.' + }, + { + id: 'sixtieth-two-commandery-supply', + bgm: 'story-dark', + chapter: '둘로 갈라진 보급', + background: 'story-three-heroes', + speaker: '황권', + text: '무도와 음평을 동시에 안정시키려면 장부가 둘로 갈라집니다. 마량이 마을을 달래고, 왕평이 산길 표식을 세우며, 누가 빠르게 치고 누가 멈출지 출전 전에 정해야 합니다.' + } +]; + +export const sixtiethBattleVictoryPages: StoryPage[] = [ + { + id: 'sixtieth-victory-commanderies-secured', + bgm: 'militia-theme', + chapter: '두 고을 확보', + background: 'story-sortie', + text: '촉한군은 무도와 음평의 길목을 차례로 눌러 곽회의 구원로를 늦췄습니다. 큰 성 하나를 무너뜨린 승리가 아니라, 북벌이 다시 숨 쉴 수 있는 산길을 확보한 승리였습니다.' + }, + { + id: 'sixtieth-victory-jiangwei-weiyan-choice', + bgm: 'story-dark', + chapter: '돌파 뒤의 지도', + background: 'story-militia', + speaker: '제갈량', + text: '강유는 길을 읽었고 위연은 길이 열릴 때 망설이지 않았습니다. 하지만 둘이 함께 앞서갈수록 조운과 마대의 회수선, 왕평과 황권의 제동 장치가 더 중요해졌습니다.' + }, + { + id: 'sixtieth-next-northern-plan', + bgm: 'story-dark', + chapter: '다음 북방 계산', + background: 'story-three-heroes', + speaker: '마량', + text: '무도와 음평의 백성에게는 오늘의 승리가 끝이 아니라 약속의 시작입니다. 다음 북벌에서는 얻은 땅을 지키는 장부와 다시 나아갈 장수의 마음을 함께 보아야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -4300,6 +4353,12 @@ export const fiftyNinthBattleMap: BattleMap = { terrain: createFiftyNinthBattleTerrain() }; +export const sixtiethBattleMap: BattleMap = { + width: 120, + height: 102, + terrain: createSixtiethBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -17426,6 +17485,97 @@ export const fiftyNinthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-fifth-leader-hao-zhao', '학소', '진창 수비장', 'rebelLeader', 197, 1540, 362, 109, 35) ]; +const sixtiethBattleAllyPositions: Record = { + 'guan-yu': { x: 18, y: 90 }, + 'zhang-fei': { x: 24, y: 92 }, + 'jian-yong': { x: 10, y: 97 }, + 'mi-zhu': { x: 14, y: 98 }, + 'sun-qian': { x: 19, y: 98 }, + 'zhao-yun': { x: 31, y: 88 }, + 'zhuge-liang': { x: 17, y: 87 }, + 'ma-liang': { x: 22, y: 84 }, + 'yi-ji': { x: 9, y: 100 }, + 'gong-zhi': { x: 27, y: 98 }, + 'huang-zhong': { x: 37, y: 86 }, + 'wei-yan': { x: 43, y: 88 }, + 'pang-tong': { x: 15, y: 83 }, + 'fa-zheng': { x: 27, y: 82 }, + 'wu-yi': { x: 45, y: 96 }, + 'yan-yan': { x: 49, y: 89 }, + 'li-yan': { x: 35, y: 81 }, + 'huang-quan': { x: 12, y: 86 }, + 'ma-chao': { x: 52, y: 85 }, + 'ma-dai': { x: 56, y: 92 }, + 'wang-ping': { x: 40, y: 81 }, + 'jiang-wei': { x: 33, y: 84 } +}; + +export const sixtiethBattleUnits: 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, + ...jiangWeiRecruitUnits + ].map((unit) => placeScenarioUnit(unit, sixtiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('northern-sixth-scout-a', '무도 산길 척후병', '서쪽 고개 정찰', 'bandit', 198, 1000, 278, 50, 80), + createThirtyFirstEnemyUnit('northern-sixth-scout-b', '음평 하곡 척후병', '하곡 정찰', 'bandit', 198, 1000, 278, 61, 76), + createThirtyFirstEnemyUnit('northern-sixth-scout-c', '무도 산길 척후병', '중앙 갈림길 정찰', 'bandit', 199, 1012, 280, 74, 69), + createThirtyFirstEnemyUnit('northern-sixth-scout-d', '음평 하곡 척후병', '동쪽 하천 정찰', 'bandit', 199, 1012, 280, 87, 61), + createThirtyFirstEnemyUnit('northern-sixth-scout-e', '곽회 전초 척후병', '곽회 본영 정찰', 'bandit', 200, 1024, 282, 103, 47), + createThirtyFirstEnemyUnit('northern-sixth-scout-f', '북쪽 봉화 척후병', '북쪽 봉화 정찰', 'bandit', 200, 1024, 282, 116, 30), + createThirtyFirstEnemyUnit('northern-sixth-infantry-a', '위군 무도 보병', '서쪽 고개 보병', 'yellowTurban', 199, 1400, 336, 55, 75), + createThirtyFirstEnemyUnit('northern-sixth-infantry-b', '위군 음평 보병', '하곡 보병', 'yellowTurban', 199, 1400, 336, 65, 70), + createThirtyFirstEnemyUnit('northern-sixth-infantry-c', '위군 무도 보병', '중앙 보병', 'yellowTurban', 200, 1420, 340, 77, 64), + createThirtyFirstEnemyUnit('northern-sixth-infantry-d', '위군 음평 보병', '동쪽 물목 보병', 'yellowTurban', 200, 1420, 340, 90, 57), + createThirtyFirstEnemyUnit('northern-sixth-infantry-e', '위군 무도 보병', '방어선 보병', 'yellowTurban', 201, 1440, 344, 102, 50), + createThirtyFirstEnemyUnit('northern-sixth-infantry-f', '위군 음평 보병', '곽회 본영 보병', 'yellowTurban', 201, 1440, 344, 112, 38), + createThirtyFirstEnemyUnit('northern-sixth-infantry-g', '위군 보급 차단병', '군량로 차단 보병', 'yellowTurban', 202, 1460, 348, 95, 78), + createThirtyFirstEnemyUnit('northern-sixth-infantry-h', '북쪽 능선 보병', '북쪽 능선 보병', 'yellowTurban', 202, 1460, 348, 117, 20), + createThirtyFirstEnemyUnit('northern-sixth-archer-a', '무도 능선 궁병', '서쪽 능선 궁병', 'archer', 199, 1050, 290, 60, 65), + createThirtyFirstEnemyUnit('northern-sixth-archer-b', '음평 물목 궁병', '하곡 궁병', 'archer', 199, 1050, 290, 72, 60), + createThirtyFirstEnemyUnit('northern-sixth-archer-c', '무도 능선 궁병', '중앙 능선 궁병', 'archer', 200, 1062, 292, 86, 53), + createThirtyFirstEnemyUnit('northern-sixth-archer-d', '음평 물목 궁병', '동쪽 물목 궁병', 'archer', 201, 1074, 294, 98, 45), + createThirtyFirstEnemyUnit('northern-sixth-archer-e', '곽회 본영 궁병', '곽회 본영 궁병', 'archer', 201, 1074, 294, 110, 33), + createThirtyFirstEnemyUnit('northern-sixth-archer-f', '북쪽 봉화 궁병', '봉화대 궁병', 'archer', 202, 1086, 296, 118, 17), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-a', '곽회 구원 기병', '서쪽 구원 기병', 'cavalry', 200, 1458, 354, 51, 68), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-b', '곽회 구원 기병', '하곡 구원 기병', 'cavalry', 200, 1458, 354, 64, 65), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-c', '곽회 구원 기병', '중앙 우회 기병', 'cavalry', 201, 1480, 358, 78, 58), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-d', '곽회 구원 기병', '동쪽 우회 기병', 'cavalry', 202, 1502, 362, 92, 51), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-e', '곽회 구원 기병', '본영 지원 기병', 'cavalry', 202, 1502, 362, 105, 43), + createThirtyFirstEnemyUnit('northern-sixth-cavalry-f', '곽회 구원 기병', '북쪽 차단 기병', 'cavalry', 203, 1524, 366, 116, 27), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-a', '무도 고개 수비대', '서쪽 고개 수비', 'yellowTurban', 200, 1420, 342, 46, 55), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-b', '음평 물길 수비대', '하곡 물길 수비', 'yellowTurban', 200, 1420, 342, 60, 49), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-c', '무도 고개 수비대', '중앙 고개 수비', 'yellowTurban', 201, 1440, 346, 76, 41), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-d', '음평 물길 수비대', '동쪽 물목 수비', 'yellowTurban', 201, 1440, 346, 92, 34), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-e', '곽회 본영 수비대', '곽회 본영 수비', 'yellowTurban', 202, 1460, 350, 106, 26), + createThirtyFirstEnemyUnit('northern-sixth-ridge-guard-f', '북쪽 절벽 수비대', '북쪽 절벽 수비', 'yellowTurban', 202, 1460, 350, 118, 14), + createThirtyFirstEnemyUnit('northern-sixth-strategist-a', '위군 무도 군리', '무도 장부 군리', 'strategist', 200, 1120, 304, 74, 56), + createThirtyFirstEnemyUnit('northern-sixth-strategist-b', '위군 음평 군리', '음평 장부 군리', 'strategist', 201, 1132, 306, 88, 47), + createThirtyFirstEnemyUnit('northern-sixth-strategist-c', '곽회 본영 군리', '곽회 본영 군리', 'strategist', 201, 1132, 306, 102, 38), + createThirtyFirstEnemyUnit('northern-sixth-strategist-d', '위군 보급 군리', '군량 차단 군리', 'strategist', 202, 1144, 308, 108, 66), + createThirtyFirstEnemyUnit('northern-sixth-officer-wudu', '무도 수비장', '무도 고개 장수', 'rebelLeader', 203, 1500, 360, 86, 72), + createThirtyFirstEnemyUnit('northern-sixth-officer-yinping', '음평 수비장', '음평 물목 장수', 'rebelLeader', 203, 1500, 360, 101, 60), + createThirtyFirstEnemyUnit('northern-sixth-officer-fei-yao', '비요', '곽회 지원 장수', 'rebelLeader', 202, 1480, 356, 109, 70), + createThirtyFirstEnemyUnit('northern-sixth-officer-supply', '위군 군량 차단장', '두 고을 보급 차단 장수', 'rebelLeader', 201, 1460, 352, 80, 80), + createThirtyFirstEnemyUnit('northern-sixth-officer-relay', '위군 봉화 장수', '북쪽 봉화 장수', 'rebelLeader', 202, 1480, 356, 115, 25), + createThirtyFirstEnemyUnit('northern-sixth-leader-guo-huai', '곽회', '위군 북방 구원 총대장', 'rebelLeader', 204, 1580, 372, 112, 36) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -18703,6 +18853,46 @@ export const fiftyNinthBattleBonds: BattleBond[] = [ ...northernFifthBattleBonds ].map(cloneBattleBondForScenario); +export const northernSixthBattleBonds: BattleBond[] = [ + { + id: 'jiang-wei__wei-yan_northern-sixth', + unitIds: ['jiang-wei', 'wei-yan'], + title: '길을 여는 돌파', + level: 48, + exp: 0, + description: '강유의 산길 판단과 위연의 돌파력이 맞물리면 무도와 음평의 좁은 갈림길에서도 빠르게 압박할 수 있습니다.' + }, + { + id: 'zhao-yun__ma-dai_northern-sixth', + unitIds: ['zhao-yun', 'ma-dai'], + title: '두 기병의 회수선', + level: 56, + exp: 0, + description: '조운의 안정된 회수 기동과 마대의 서량 우회 감각이 함께하면 곽회의 구원 기병을 늦추면서도 본대와 끊어지지 않습니다.' + }, + { + id: 'wang-ping__huang-quan_northern-sixth', + unitIds: ['wang-ping', 'huang-quan'], + title: '산길과 장부', + level: 50, + exp: 0, + description: '왕평의 산길 표식과 황권의 보급 장부가 맞물리면 두 고을을 얻은 뒤에도 군량선이 무너지지 않습니다.' + }, + { + id: 'ma-liang__jiang-wei_northern-sixth', + unitIds: ['ma-liang', 'jiang-wei'], + title: '새 고을의 약속', + level: 42, + exp: 0, + description: '마량의 민심 문서와 강유의 천수 사람 이해가 함께하면 무도와 음평의 백성이 새 전선을 두려움만으로 보지 않게 됩니다.' + } +]; + +export const sixtiethBattleBonds: BattleBond[] = [ + ...fiftyNinthBattleBonds, + ...northernSixthBattleBonds +].map(cloneBattleBondForScenario); + function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => Array.from({ length: 26 }, (_, x): TerrainType => { @@ -21946,6 +22136,81 @@ function createFiftyNinthBattleTerrain(): TerrainType[][] { ); } +function createSixtiethBattleTerrain(): TerrainType[][] { + return Array.from({ length: 102 }, (_, y) => + Array.from({ length: 120 }, (_, x): TerrainType => { + if ( + (x >= 5 && x <= 11 && y >= 0 && y <= 101) || + (x >= 62 && x <= 68 && y >= 16 && y <= 101) || + (x >= 0 && x <= 119 && y >= 94) + ) { + return 'river'; + } + if ( + (x >= 9 && x <= 80 && y >= 86 && y <= 92) || + (x >= 23 && x <= 98 && y >= 75 && y <= 83) || + (x >= 48 && x <= 108 && y >= 63 && y <= 73) || + (x >= 76 && x <= 116 && y >= 49 && y <= 61) || + (x >= 92 && x <= 119 && y >= 35 && y <= 46) || + (x >= 30 && x <= 38 && y >= 68 && y <= 96) || + (x >= 54 && x <= 62 && y >= 55 && y <= 88) || + (x >= 84 && x <= 93 && y >= 41 && y <= 78) + ) { + return 'road'; + } + if ( + (x >= 0 && x <= 35 && y >= 58 && y <= 90) || + (x >= 42 && x <= 78 && y >= 72 && y <= 93) || + (x >= 88 && x <= 119 && y >= 66 && y <= 91) || + (x >= 14 && x <= 38 && y >= 42 && y <= 60) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 49 && y <= 38) || + (x >= 46 && x <= 90 && y <= 32) || + (x >= 86 && x <= 119 && y <= 50) || + (x >= 98 && x <= 119 && y >= 44 && y <= 70) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 92) || + (x >= 117 && y <= 86) || + (x >= 38 && x <= 44 && y >= 0 && y <= 62) || + (x >= 75 && x <= 81 && y >= 0 && y <= 44) || + (x >= 59 && x <= 66 && y >= 84 && y <= 101) + ) { + return 'cliff'; + } + if ( + (x >= 12 && x <= 28 && y >= 82 && y <= 92) || + (x >= 43 && x <= 62 && y >= 70 && y <= 82) || + (x >= 78 && x <= 96 && y >= 56 && y <= 68) || + (x >= 100 && x <= 116 && y >= 41 && y <= 54) + ) { + return 'village'; + } + if ( + (x >= 20 && x <= 45 && y >= 77 && y <= 90) || + (x >= 54 && x <= 79 && y >= 64 && y <= 76) || + (x >= 87 && x <= 110 && y >= 50 && y <= 63) + ) { + return 'camp'; + } + if ( + (x >= 48 && x <= 67 && y >= 45 && y <= 60) || + (x >= 72 && x <= 96 && y >= 35 && y <= 50) || + (x >= 94 && x <= 116 && y >= 44 && y <= 60) || + (x >= 102 && x <= 119 && y >= 14 && y <= 33) + ) { + return 'fort'; + } + return 'plain'; + }) + ); +} + function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData { return { ...cloneUnitForScenario(unit), diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index c6fcf48..9fe39d7 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -55,6 +55,7 @@ import { northernFourthBattleBonds, northernFirstBattleBonds, northernSecondBattleBonds, + northernSixthBattleBonds, northernThirdBattleBonds, wulingRecruitBonds, wulingRecruitUnits, @@ -443,9 +444,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'fifty-sixth-battle-tianshui-advance', 'fifty-seventh-battle-jieting-crisis', 'fifty-eighth-battle-qishan-retreat', - 'fifty-ninth-battle-chencang-siege' + 'fifty-ninth-battle-chencang-siege', + 'sixtieth-battle-wudu-yinping' ], - nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산'] + nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보'] } ]; @@ -508,7 +510,8 @@ const campBattleIds = { fiftySixth: 'fifty-sixth-battle-tianshui-advance', fiftySeventh: 'fifty-seventh-battle-jieting-crisis', fiftyEighth: 'fifty-eighth-battle-qishan-retreat', - fiftyNinth: 'fifty-ninth-battle-chencang-siege' + fiftyNinth: 'fifty-ninth-battle-chencang-siege', + sixtieth: 'sixtieth-battle-wudu-yinping' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1242,6 +1245,22 @@ const sortieRulesByBattleId: Partial