From f9689c0149b48ce7d12d251d58967c3c5b53f24b Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 24 Jun 2026 10:05:38 +0900 Subject: [PATCH] Add Qishan retreat campaign chapter --- docs/roadmap.md | 6 +- scripts/verify-flow.mjs | 269 ++++++++++++++---- .../images/battle/fifty-eighth-battle-map.svg | 62 ++++ src/game/data/battleMapAssets.ts | 2 + src/game/data/battles.ts | 91 +++++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 266 +++++++++++++++++ src/game/scenes/CampScene.ts | 177 +++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 10 files changed, 839 insertions(+), 72 deletions(-) create mode 100644 src/assets/images/battle/fifty-eighth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 64a8553..5ab9ea5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -86,15 +86,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Post-Tianshui camp events with Zhuge Liang, Wang Ping, Zhao Yun, Ma Dai, Ma Liang, Fa Zheng, and Huang Quan focusing on Jieting terrain judgment, flank discipline, Jiang Wei's talent, waterline logistics, and the next northern-front risk - Fifty-seventh battle Jieting placement crisis, where Zhuge Liang leads a seven-officer sortie across a 114x96 battlefield, Wang Ping's waterline judgment becomes a survival objective, Zhang He pressures the ridge, and Jiang Wei shifts from rival hook into a recruitable Shu-Han officer after victory - Post-Jieting camp events with Zhuge Liang, Wang Ping, Ma Liang, Jiang Wei, Zhao Yun, and Ma Dai focusing on waterline doctrine, Jiang Wei's first role, cavalry retreat discipline, and the harder sortie choices created by one more strong officer joining the roster +- Fifty-eighth battle Qishan retreat route, where Jiang Wei becomes selectable for his first Shu-Han sortie, Zhuge Liang must preserve carts and waterlines against Sima Yi and Zhang He, and the 116x98 battlefield closes this slice as a northern-front regrouping point rather than an ending +- 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 - 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 Jieting placement crisis and post-Jieting camp with Jiang Wei joining, 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 Qishan retreat and post-retreat 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 Jiang Wei joins, making his first selectable sortie 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 the Qishan retreat, preserving the regrouping tone and making Jiang Wei's growing role compete with Zhao Yun, Ma Dai, Wang Ping, and the strategists instead of simply increasing raw party strength 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 4563c6e..dc7f9ef 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 !== 57 || + progressTabState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postBowangProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postChangbanProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postWulingProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postChangshaProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postYizhouProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postFuPassProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postLuoProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postLuofengProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postChengduProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postJiamengProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postYangpingProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postDingjunProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 58 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 57 || + postFoundationProgressState.campaignProgress?.totalKnown !== 58 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 57 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 58 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 57 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 58 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 57 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 58 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 57 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 58 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 57 || + postJingRearProgressState.campaignProgress?.totalKnown !== 58 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 57 || + postGonganProgressState.campaignProgress?.totalKnown !== 58 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 57 || + postMaichengProgressState.campaignProgress?.totalKnown !== 58 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 57 || + postYilingProgressState.campaignProgress?.totalKnown !== 58 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 57 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 58 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,7 +6280,7 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 57 || + postBaidiProgressState.campaignProgress?.totalKnown !== 58 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { @@ -6441,7 +6441,7 @@ try { if ( postNanzhongProgressState?.activeTab !== 'progress' || postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || - postNanzhongProgressState.campaignProgress?.totalKnown !== 57 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 58 || postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전' ) { @@ -6603,7 +6603,7 @@ try { if ( postMengHuoProgressState?.activeTab !== 'progress' || postMengHuoProgressState.campaignProgress?.completedKnown !== 48 || - postMengHuoProgressState.campaignProgress?.totalKnown !== 57 || + postMengHuoProgressState.campaignProgress?.totalKnown !== 58 || postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' || postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전' ) { @@ -6765,7 +6765,7 @@ try { if ( postSecondCaptureProgressState?.activeTab !== 'progress' || postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 || - postSecondCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postSecondCaptureProgressState.campaignProgress?.totalKnown !== 58 || postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' || postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전' ) { @@ -6930,7 +6930,7 @@ try { if ( postThirdCaptureProgressState?.activeTab !== 'progress' || postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 || - postThirdCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postThirdCaptureProgressState.campaignProgress?.totalKnown !== 58 || postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' || postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전' ) { @@ -7096,7 +7096,7 @@ try { if ( postFourthCaptureProgressState?.activeTab !== 'progress' || postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 || - postFourthCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postFourthCaptureProgressState.campaignProgress?.totalKnown !== 58 || postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' || postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전' ) { @@ -7262,7 +7262,7 @@ try { if ( postFifthCaptureProgressState?.activeTab !== 'progress' || postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 || - postFifthCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postFifthCaptureProgressState.campaignProgress?.totalKnown !== 58 || postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' || postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전' ) { @@ -7427,7 +7427,7 @@ try { if ( postSixthCaptureProgressState?.activeTab !== 'progress' || postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 || - postSixthCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postSixthCaptureProgressState.campaignProgress?.totalKnown !== 58 || postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' || postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전' ) { @@ -7593,7 +7593,7 @@ try { if ( postFinalCaptureProgressState?.activeTab !== 'progress' || postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 || - postFinalCaptureProgressState.campaignProgress?.totalKnown !== 57 || + postFinalCaptureProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postNorthernPrepProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postFiftyFifthProgressState.campaignProgress?.totalKnown !== 58 || 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 !== 57 || + postFiftySixthProgressState.campaignProgress?.totalKnown !== 58 || postFiftySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftySixthProgressState.campaignProgress?.latestBattleTitle !== '천수 진군로' || postFiftySixthProgressState.campaignProgress?.nextBattleTitle !== '가정 배치 위기' @@ -8121,14 +8121,166 @@ try { if ( postFiftySeventhProgressState?.activeTab !== 'progress' || postFiftySeventhProgressState.campaignProgress?.completedKnown !== 57 || - postFiftySeventhProgressState.campaignProgress?.totalKnown !== 57 || + postFiftySeventhProgressState.campaignProgress?.totalKnown !== 58 || postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || - postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' + postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' || + postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로' ) { - throw new Error(`Expected post-fifty-seventh progress tab to complete the Jieting crisis and count Jiang Wei's join point: ${JSON.stringify(postFiftySeventhProgressState?.campaignProgress)}`); + throw new Error(`Expected post-fifty-seventh progress tab to complete the Jieting crisis and point toward the Qishan retreat: ${JSON.stringify(postFiftySeventhProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fifty-seventh-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const qishanRetreatSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !qishanRetreatSortieState?.sortieVisible || + qishanRetreatSortieState.campaign?.step !== 'fifty-seventh-camp' || + qishanRetreatSortieState.campTitle !== '가정 배치 위기 후 군영' || + !String(qishanRetreatSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 후퇴로') || + !String(qishanRetreatSortieState.sortiePlan?.objectiveLine ?? '').includes('사마의') || + qishanRetreatSortieState.sortiePlan?.maxCount !== 7 || + qishanRetreatSortieState.sortiePlan?.recommendedTotal !== 7 || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected Qishan retreat sortie to require Zhuge Liang, exclude Liu Bei, and recommend Jiang Wei's first selectable sortie role: ${JSON.stringify(qishanRetreatSortieState)}`); + } + assertSortieTacticalRoster(qishanRetreatSortieState, [ + '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 qishanPriorityUnits = ['jiang-wei', 'wang-ping', 'zhao-yun', 'ma-dai', 'huang-quan', 'ma-liang']; + for (const unitId of qishanPriorityUnits) { + 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 && !qishanPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const qishanRetreatSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !qishanPriorityUnits.every((unitId) => + qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + qishanRetreatSortieReadyState.sortiePlan?.selectedCount !== 7 || + qishanRetreatSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Qishan retreat sortie to deploy Zhuge Liang, Jiang Wei, Wang Ping, Zhao Yun, Ma Dai, Huang Quan, and Ma Liang: ${JSON.stringify(qishanRetreatSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-fifty-eighth-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-fifty-eighth-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredFiftyEighthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-eighth-battle-qishan-retreat'; + }); + if (enteredFiftyEighthBattle) { + 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 === 'fifty-eighth-battle-qishan-retreat' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-fifty-eighth-battle.png', fullPage: true }); + + const fiftyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fiftyEighthEnemies = fiftyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fiftyEighthAllies = fiftyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fiftyEighthEnemyBehaviors = new Set(fiftyEighthEnemies.map((unit) => unit.ai)); + if ( + fiftyEighthBattleState.camera?.mapWidth !== 116 || + fiftyEighthBattleState.camera?.mapHeight !== 98 || + fiftyEighthBattleState.victoryConditionLabel !== '기산 퇴로와 군량 수레 확보 및 사마의 본대 저지' || + !fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'repel-sima-yi' && objective.label === '사마의 본대 저지') || + !fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'halt-zhang-he' && objective.label === '장합 추격 차단') || + !fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'qishan-cart-road' && objective.label === '기산 군량 수레 길 확보') || + !fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'qishan-waterline' && objective.label === '퇴각 물길 확보') || + fiftyEighthEnemies.length < 42 || + !fiftyEighthEnemyBehaviors.has('aggressive') || + !fiftyEighthEnemyBehaviors.has('guard') || + !fiftyEighthEnemyBehaviors.has('hold') || + !fiftyEighthEnemies.some((unit) => unit.id === 'northern-fourth-leader-sima-yi') || + !fiftyEighthEnemies.some((unit) => unit.id === 'northern-fourth-officer-zhang-he') || + !fiftyEighthAllies.some((unit) => unit.id === 'zhuge-liang') || + !fiftyEighthAllies.some((unit) => unit.id === 'jiang-wei') || + !fiftyEighthAllies.some((unit) => unit.id === 'wang-ping') || + !fiftyEighthAllies.some((unit) => unit.id === 'zhao-yun') || + !fiftyEighthAllies.some((unit) => unit.id === 'ma-dai') || + !fiftyEighthAllies.some((unit) => unit.id === 'huang-quan') || + !fiftyEighthAllies.some((unit) => unit.id === 'ma-liang') || + fiftyEighthAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected fifty-eighth battle to use the Qishan retreat map, Sima Yi/Zhang He objectives, selected Jiang Wei sortie, and mixed Wei AI: ${JSON.stringify(fiftyEighthBattleState)}`); + } + + 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 fiftyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fiftyEighthCampState?.campBattleId !== 'fifty-eighth-battle-qishan-retreat' || + fiftyEighthCampState.campaign?.step !== 'fifty-eighth-camp' || + fiftyEighthCampState.campTitle !== '기산 후퇴로 후 군영' || + fiftyEighthCampState.availableDialogueIds?.length !== 3 || + !fiftyEighthCampState.availableDialogueIds.every((id) => id.startsWith('northern-fourth-')) || + fiftyEighthCampState.availableVisitIds?.length !== 1 || + !fiftyEighthCampState.availableVisitIds.every((id) => id.startsWith('northern-fourth-')) || + fiftyEighthCampState.rosterCollection?.total < 22 || + !fiftyEighthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__jiang-wei_northern-fourth') + ) { + throw new Error(`Expected Qishan retreat camp to expose Jiang Wei first-sortie aftermath events and northern-fourth bonds: ${JSON.stringify(fiftyEighthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-fifty-eighth-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postFiftyEighthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postFiftyEighthProgressState?.activeTab !== 'progress' || + postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 || + postFiftyEighthProgressState.campaignProgress?.totalKnown !== 58 || + postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || + postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' || + postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-fifty-eighth progress tab to complete the Qishan retreat and leave the next northern step pending: ${JSON.stringify(postFiftyEighthProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-fifty-eighth-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -8156,15 +8308,32 @@ try { break; } } + for (let attempt = 0; attempt < 24; attempt += 1) { + const continuedToQishanCamp = await page.evaluate(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const camp = window.__HEROS_DEBUG__?.camp?.() ?? null; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-eighth-camp'; + }); + if (continuedToQishanCamp) { + break; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(320); + } + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const camp = window.__HEROS_DEBUG__?.camp?.() ?? null; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-eighth-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-seventh-camp') { - throw new Error(`Expected title continue to reopen the Jieting aftermath camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fifty-eighth-camp') { + throw new Error(`Expected title continue to reopen the Qishan retreat aftermath camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-Jieting-crisis 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, and Jiang Wei joining state at ${targetUrl}`); + console.log(`Verified title-to-Qishan-retreat 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, and pending next northern step state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/fifty-eighth-battle-map.svg b/src/assets/images/battle/fifty-eighth-battle-map.svg new file mode 100644 index 0000000..a30bce4 --- /dev/null +++ b/src/assets/images/battle/fifty-eighth-battle-map.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts index 792c4b7..858dde9 100644 --- a/src/game/data/battleMapAssets.ts +++ b/src/game/data/battleMapAssets.ts @@ -7,6 +7,7 @@ import fiftySecondBattleMapUrl from '../../assets/images/battle/fifty-second-bat import fiftyThirdBattleMapUrl from '../../assets/images/battle/fifty-third-battle-map.svg'; import fiftyFourthBattleMapUrl from '../../assets/images/battle/fifty-fourth-battle-map.svg'; import fiftyFifthBattleMapUrl from '../../assets/images/battle/fifty-fifth-battle-map.svg'; +import fiftyEighthBattleMapUrl from '../../assets/images/battle/fifty-eighth-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'; @@ -112,6 +113,7 @@ export const battleMapAssets: Record = { 'battle-map-fifty-third': fiftyThirdBattleMapUrl, 'battle-map-fifty-fourth': fiftyFourthBattleMapUrl, 'battle-map-fifty-fifth': fiftyFifthBattleMapUrl, + 'battle-map-fifty-eighth': fiftyEighthBattleMapUrl, '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 8ef18f1..0a5fd54 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -19,6 +19,10 @@ import { fiftyFourthBattleMap, fiftyFourthBattleUnits, fiftyFourthBattleVictoryPages, + fiftyEighthBattleBonds, + fiftyEighthBattleMap, + fiftyEighthBattleUnits, + fiftyEighthBattleVictoryPages, fiftyFifthBattleBonds, fiftyFifthBattleMap, fiftyFifthBattleUnits, @@ -290,7 +294,8 @@ export type BattleScenarioId = | 'fifty-fourth-battle-meng-huo-final-capture' | 'fifty-fifth-battle-northern-qishan-road' | 'fifty-sixth-battle-tianshui-advance' - | 'fifty-seventh-battle-jieting-crisis'; + | 'fifty-seventh-battle-jieting-crisis' + | 'fifty-eighth-battle-qishan-retreat'; export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -3775,6 +3780,87 @@ export const fiftySeventhBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fiftyEighthBattleScenario: BattleScenarioDefinition = { + id: 'fifty-eighth-battle-qishan-retreat', + title: '기산 후퇴로', + victoryConditionLabel: '기산 퇴로와 군량 수레 확보 및 사마의 본대 저지', + defeatConditionLabel: '제갈량 퇴각 또는 군량 퇴로 상실', + openingObjectiveLines: [ + '가정의 위기는 넘겼지만 북벌군의 보급선은 이미 길게 늘어졌습니다. 무리한 추격보다 기산으로 돌아가는 군량 수레 길을 먼저 열어 병사를 살려야 합니다.', + '강유가 촉한의 장수로 처음 출전 후보에 들어왔습니다. 천수의 낮은 길을 아는 강유, 물길을 보는 왕평, 회수 기동의 조운과 마대 중 누구에게 자리를 줄지 결정하십시오.', + '장합의 빠른 추격을 끊고 사마의의 본대를 저지하십시오. 물길과 수레 길, 후방 진영을 함께 확보하면 다음 북벌을 준비할 병력과 장부를 지킬 수 있습니다.' + ], + map: fiftyEighthBattleMap, + units: fiftyEighthBattleUnits, + bonds: fiftyEighthBattleBonds, + mapTextureKey: 'battle-map-fifty-eighth', + leaderUnitId: 'northern-fourth-leader-sima-yi', + quickVictoryTurnLimit: 116, + baseVictoryGold: 16680, + objectives: [ + { + id: 'repel-sima-yi', + kind: 'defeat-leader', + label: '사마의 본대 저지', + rewardGold: 11400, + unitId: 'northern-fourth-leader-sima-yi' + }, + { + id: 'halt-zhang-he', + kind: 'defeat-leader', + label: '장합 추격 차단', + rewardGold: 5600, + unitId: 'northern-fourth-officer-zhang-he' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 2980, + unitId: 'zhuge-liang' + }, + { + id: 'qishan-cart-road', + kind: 'secure-terrain', + label: '기산 군량 수레 길 확보', + rewardGold: 5900, + terrain: 'road' + }, + { + id: 'qishan-waterline', + kind: 'secure-terrain', + label: '퇴각 물길 확보', + rewardGold: 5780, + terrain: 'river' + }, + { + id: 'rear-camps', + kind: 'secure-terrain', + label: '후방 진영 수습', + rewardGold: 5480, + terrain: 'camp' + }, + { + id: 'ridge-forts', + kind: 'secure-terrain', + label: '위군 능선 거점 제압', + rewardGold: 5320, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '116턴 이내 기산 후퇴 완료', + rewardGold: 3720, + maxTurn: 116 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['금 98', '상처약 82', '군주 40', '기산 퇴각 지도 1', '강유 첫 출전 기록 1'], + victoryPages: fiftyEighthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -3834,7 +3920,8 @@ export const battleScenarios: Record 'fifty-fourth-battle-meng-huo-final-capture': fiftyFourthBattleScenario, 'fifty-fifth-battle-northern-qishan-road': fiftyFifthBattleScenario, 'fifty-sixth-battle-tianshui-advance': fiftySixthBattleScenario, - 'fifty-seventh-battle-jieting-crisis': fiftySeventhBattleScenario + 'fifty-seventh-battle-jieting-crisis': fiftySeventhBattleScenario, + 'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 0c1f9a1..07d82ac 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -5,6 +5,7 @@ import { eighteenthBattleScenario, eleventhBattleScenario, fiftiethBattleScenario, + fiftyEighthBattleScenario, fiftyFirstBattleScenario, fiftySecondBattleScenario, fiftyThirdBattleScenario, @@ -69,6 +70,8 @@ import { eleventhBattleVictoryPages, fiftiethBattleIntroPages, fiftiethBattleVictoryPages, + fiftyEighthBattleIntroPages, + fiftyEighthBattleVictoryPages, fiftyFirstBattleIntroPages, fiftyFirstBattleVictoryPages, fiftySecondBattleIntroPages, @@ -841,13 +844,24 @@ const sortieFlows: Record = { }, [fiftySeventhBattleScenario.id]: { afterBattleId: fiftySeventhBattleScenario.id, - eyebrow: '다음 장 준비', - title: '가정 배치 위기 이후', + eyebrow: '다음 전장', + title: fiftyEighthBattleScenario.title, description: - '가정의 물길과 고지는 가까스로 지켰고 강유가 촉한의 새 장수로 합류했습니다. 다음 북벌에서는 새로 늘어난 장수진 안에서 더 엄격한 출전 선택과 보급 판단이 필요합니다.', - rewardHint: '다음 목표: 강유 편성 훈련 / 다음 북벌 전선 준비', - pages: fiftySeventhBattleVictoryPages, - unavailableNotice: '가정 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 강유 합류 후 군영에서 무장 대화, 보급 정비, 출전 편성 방향을 확인할 수 있습니다.' + '가정의 물길과 고지는 가까스로 지켰고 강유가 촉한의 새 장수로 합류했습니다. 이제 장합의 추격과 사마의의 시선을 끊으며 기산으로 돌아갈 군량 수레 길을 열어야 합니다.', + rewardHint: `예상 보상: ${fiftyEighthBattleScenario.title} 개방 / 강유 첫 출전 기록`, + nextBattleId: fiftyEighthBattleScenario.id, + campaignStep: 'fifty-eighth-battle', + pages: [...fiftySeventhBattleVictoryPages, ...fiftyEighthBattleIntroPages] + }, + [fiftyEighthBattleScenario.id]: { + afterBattleId: fiftyEighthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '기산 후퇴로 이후', + description: + '기산 후퇴로에서 병사와 장부를 지켜 냈습니다. 그러나 이것은 북벌의 끝이 아니라 다음 출정을 위한 정비입니다. 강유의 첫 출전, 왕평의 물길 판단, 조운과 마대의 귀환선을 군영에서 다시 정리합니다.', + rewardHint: '다음 목표: 북벌 재정비 / 강유 첫 출전 평가', + pages: fiftyEighthBattleVictoryPages, + unavailableNotice: '기산 후퇴로 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 강유 첫 출전 후 군영 대화, 보급 정비, 출전 편성 방향을 확인할 수 있습니다.' }, 'northern-campaign-prep-camp-legacy': { afterBattleId: fiftyFourthBattleScenario.id, diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 06d8f58..12f9b3b 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3695,6 +3695,59 @@ export const fiftySeventhBattleVictoryPages: StoryPage[] = [ } ]; +export const fiftyEighthBattleIntroPages: StoryPage[] = [ + { + id: 'fifty-eighth-qishan-retreat-council', + bgm: 'story-dark', + chapter: '기산 퇴각 명령', + background: 'story-militia', + speaker: '제갈량', + text: '가정의 물길은 가까스로 지켰지만 북방의 군량 길은 이미 오래 늘어졌습니다. 지금 억지로 깊게 밀고 들어가면 승리가 아니라 고립을 얻게 됩니다. 기산으로 돌아가는 수레 길을 열고, 병사와 장부를 먼저 살려야 합니다.' + }, + { + id: 'fifty-eighth-jiang-wei-first-sortie', + bgm: 'battle-prep', + chapter: '강유의 첫 출전', + background: 'story-sortie', + speaker: '강유', + text: '제가 알던 천수의 낮은 길이 이제 촉한의 퇴로가 되었습니다. 아직 모두가 저를 믿지는 못하겠지만, 오늘은 말보다 길로 증명하겠습니다. 조운과 마대가 회수 길을 지키고, 왕평이 물길을 보면 위군의 추격을 끊을 수 있습니다.' + }, + { + id: 'fifty-eighth-sima-yi-shadow', + bgm: 'story-dark', + chapter: '새로운 그림자', + background: 'story-three-heroes', + speaker: '마량', + text: '장합의 추격만 막으면 끝나는 전장이 아닙니다. 위군 본영 뒤에서 사마의가 군량 수레가 어디로 빠지는지 보고 있습니다. 강유를 내보내면 길은 넓어지지만, 누군가는 조운이나 왕평의 자리를 양보해야 합니다.' + } +]; + +export const fiftyEighthBattleVictoryPages: StoryPage[] = [ + { + id: 'fifty-eighth-victory-retreat-road', + bgm: 'militia-theme', + chapter: '기산 퇴로 확보', + background: 'story-sortie', + text: '촉한군은 군량 수레를 잃지 않고 기산의 낮은 길로 물러났습니다. 장합의 추격은 물길 앞에서 꺾였고, 사마의의 본대는 더 깊게 들어오지 못했습니다. 패배를 승리로 꾸밀 수는 없지만, 다음 북벌을 위한 병사와 장부는 지켜냈습니다.' + }, + { + id: 'fifty-eighth-victory-jiang-wei-reviewed', + bgm: 'story-dark', + chapter: '강유의 첫 평가', + background: 'story-militia', + speaker: '제갈량', + text: '강유는 천수의 길을 숨기지 않았고, 조운과 마대는 병사들이 돌아올 시간을 벌었습니다. 왕평의 물길 판단도 여전히 군의 목숨줄입니다. 이제부터 출전 명단은 단순히 강한 장수를 고르는 일이 아니라, 어떤 길을 택할지 정하는 일이 됩니다.' + }, + { + id: 'fifty-eighth-next-northern-plan', + bgm: 'story-dark', + chapter: '다음 북벌을 위하여', + background: 'story-three-heroes', + speaker: '강유', + text: '이번 후퇴로 저는 촉한의 군율과 병사들의 숨을 배웠습니다. 다음에는 물러나는 길이 아니라 나아가는 길을 열겠습니다. 승상께서 다시 북쪽을 보실 때, 제가 먼저 그 길을 살피겠습니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -4182,6 +4235,12 @@ export const fiftySeventhBattleMap: BattleMap = { terrain: createFiftySeventhBattleTerrain() }; +export const fiftyEighthBattleMap: BattleMap = { + width: 116, + height: 98, + terrain: createFiftyEighthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -17126,6 +17185,97 @@ export const fiftySeventhBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-third-leader-zhang-he', '장합', '위군 역습 총대장', 'rebelLeader', 187, 1400, 332, 109, 24) ]; +const fiftyEighthBattleAllyPositions: Record = { + 'guan-yu': { x: 20, y: 85 }, + 'zhang-fei': { x: 26, y: 86 }, + 'jian-yong': { x: 10, y: 92 }, + 'mi-zhu': { x: 14, y: 93 }, + 'sun-qian': { x: 19, y: 93 }, + 'zhao-yun': { x: 32, y: 82 }, + 'zhuge-liang': { x: 18, y: 81 }, + 'ma-liang': { x: 23, y: 78 }, + 'yi-ji': { x: 9, y: 95 }, + 'gong-zhi': { x: 27, y: 93 }, + 'huang-zhong': { x: 38, y: 81 }, + 'wei-yan': { x: 44, y: 84 }, + 'pang-tong': { x: 15, y: 77 }, + 'fa-zheng': { x: 27, y: 76 }, + 'wu-yi': { x: 45, y: 91 }, + 'yan-yan': { x: 49, y: 84 }, + 'li-yan': { x: 35, y: 75 }, + 'huang-quan': { x: 12, y: 80 }, + 'ma-chao': { x: 52, y: 80 }, + 'ma-dai': { x: 56, y: 87 }, + 'wang-ping': { x: 40, y: 75 }, + 'jiang-wei': { x: 33, y: 78 } +}; + +export const fiftyEighthBattleUnits: 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, fiftyEighthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('northern-fourth-scout-a', '위군 퇴로 정찰병', '서쪽 물길 정찰', 'bandit', 186, 920, 262, 47, 76), + createThirtyFirstEnemyUnit('northern-fourth-scout-b', '위군 퇴로 정찰병', '기산 수레 길 정찰', 'bandit', 186, 920, 262, 58, 73), + createThirtyFirstEnemyUnit('northern-fourth-scout-c', '위군 퇴로 정찰병', '중앙 낮은 길 정찰', 'bandit', 187, 932, 264, 70, 66), + createThirtyFirstEnemyUnit('northern-fourth-scout-d', '위군 퇴로 정찰병', '동쪽 강변 정찰', 'bandit', 187, 932, 264, 83, 58), + createThirtyFirstEnemyUnit('northern-fourth-scout-e', '위군 퇴로 정찰병', '사마의 본영 정찰', 'bandit', 188, 944, 266, 99, 44), + createThirtyFirstEnemyUnit('northern-fourth-scout-f', '위군 퇴로 정찰병', '북쪽 능선 정찰', 'bandit', 188, 944, 266, 111, 29), + createThirtyFirstEnemyUnit('northern-fourth-infantry-a', '위군 퇴로 봉쇄병', '서쪽 길목 봉쇄', 'yellowTurban', 187, 1280, 312, 52, 72), + createThirtyFirstEnemyUnit('northern-fourth-infantry-b', '위군 퇴로 봉쇄병', '수레 길 봉쇄', 'yellowTurban', 187, 1280, 312, 62, 68), + createThirtyFirstEnemyUnit('northern-fourth-infantry-c', '위군 퇴로 봉쇄병', '중앙 낮은 길 봉쇄', 'yellowTurban', 188, 1300, 316, 74, 61), + createThirtyFirstEnemyUnit('northern-fourth-infantry-d', '위군 퇴로 봉쇄병', '물길 앞 봉쇄', 'yellowTurban', 188, 1300, 316, 86, 55), + createThirtyFirstEnemyUnit('northern-fourth-infantry-e', '위군 퇴로 봉쇄병', '동쪽 고개 봉쇄', 'yellowTurban', 189, 1320, 320, 98, 48), + createThirtyFirstEnemyUnit('northern-fourth-infantry-f', '위군 퇴로 봉쇄병', '사마의 본영 보병', 'yellowTurban', 189, 1320, 320, 108, 34), + createThirtyFirstEnemyUnit('northern-fourth-infantry-g', '위군 퇴로 봉쇄병', '군량 차단 보병', 'yellowTurban', 190, 1340, 324, 93, 73), + createThirtyFirstEnemyUnit('northern-fourth-infantry-h', '위군 퇴로 봉쇄병', '북쪽 능선 보병', 'yellowTurban', 190, 1340, 324, 111, 21), + createThirtyFirstEnemyUnit('northern-fourth-archer-a', '위군 추격 궁병', '서쪽 물길 궁병', 'archer', 187, 968, 274, 56, 64), + createThirtyFirstEnemyUnit('northern-fourth-archer-b', '위군 추격 궁병', '수레 길 궁병', 'archer', 187, 968, 274, 68, 59), + createThirtyFirstEnemyUnit('northern-fourth-archer-c', '위군 추격 궁병', '중앙 언덕 궁병', 'archer', 188, 980, 276, 82, 51), + createThirtyFirstEnemyUnit('northern-fourth-archer-d', '위군 추격 궁병', '강변 궁병', 'archer', 189, 992, 278, 94, 43), + createThirtyFirstEnemyUnit('northern-fourth-archer-e', '위군 추격 궁병', '사마의 본영 궁병', 'archer', 189, 992, 278, 106, 31), + createThirtyFirstEnemyUnit('northern-fourth-archer-f', '위군 추격 궁병', '북쪽 능선 궁병', 'archer', 190, 1004, 280, 112, 18), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-a', '장합 추격 기병', '서쪽 추격 기병', 'cavalry', 188, 1328, 326, 48, 66), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-b', '장합 추격 기병', '수레 길 추격 기병', 'cavalry', 188, 1328, 326, 62, 63), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-c', '장합 추격 기병', '중앙 돌파 기병', 'cavalry', 189, 1350, 330, 76, 56), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-d', '장합 추격 기병', '동쪽 우회 기병', 'cavalry', 190, 1372, 334, 90, 49), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-e', '장합 추격 기병', '사마의 본영 기병', 'cavalry', 190, 1372, 334, 103, 41), + createThirtyFirstEnemyUnit('northern-fourth-cavalry-f', '장합 추격 기병', '북쪽 추격 기병', 'cavalry', 191, 1394, 338, 112, 25), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-a', '기산 능선 수비대', '서쪽 능선 수비', 'yellowTurban', 188, 1300, 318, 43, 52), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-b', '기산 능선 수비대', '중앙 능선 수비', 'yellowTurban', 188, 1300, 318, 57, 45), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-c', '기산 능선 수비대', '물길 위 수비', 'yellowTurban', 189, 1320, 322, 73, 37), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-d', '기산 능선 수비대', '동쪽 능선 수비', 'yellowTurban', 189, 1320, 322, 88, 30), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-e', '기산 능선 수비대', '본영 앞 수비', 'yellowTurban', 190, 1340, 326, 101, 23), + createThirtyFirstEnemyUnit('northern-fourth-ridge-guard-f', '기산 능선 수비대', '북쪽 절벽 수비', 'yellowTurban', 190, 1340, 326, 113, 16), + createThirtyFirstEnemyUnit('northern-fourth-strategist-a', '위군 퇴로 군리', '수레 길 계산 군리', 'strategist', 188, 1032, 286, 70, 53), + createThirtyFirstEnemyUnit('northern-fourth-strategist-b', '위군 퇴로 군리', '물길 기록 군리', 'strategist', 189, 1044, 288, 84, 44), + createThirtyFirstEnemyUnit('northern-fourth-strategist-c', '위군 퇴로 군리', '사마의 본영 군리', 'strategist', 189, 1044, 288, 98, 36), + createThirtyFirstEnemyUnit('northern-fourth-strategist-d', '위군 퇴로 군리', '군량 차단 군리', 'strategist', 190, 1056, 290, 104, 61), + createThirtyFirstEnemyUnit('northern-fourth-officer-zhang-he', '장합', '퇴로 추격 선봉장', 'rebelLeader', 192, 1440, 342, 94, 38), + createThirtyFirstEnemyUnit('northern-fourth-officer-fei-yao', '비요', '수레 길 압박 장수', 'rebelLeader', 190, 1400, 334, 102, 54), + createThirtyFirstEnemyUnit('northern-fourth-officer-guo-huai', '곽회', '가정 방면 장수', 'rebelLeader', 190, 1400, 334, 108, 66), + createThirtyFirstEnemyUnit('northern-fourth-officer-supply', '위군 보급 차단장', '군량 수레 차단 장수', 'rebelLeader', 189, 1380, 330, 78, 72), + createThirtyFirstEnemyUnit('northern-fourth-officer-ridge', '위군 능선 장수', '북쪽 능선 장수', 'rebelLeader', 190, 1400, 334, 110, 19), + createThirtyFirstEnemyUnit('northern-fourth-leader-sima-yi', '사마의', '위군 본대 총지휘관', 'rebelLeader', 193, 1500, 352, 112, 27) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -18323,6 +18473,46 @@ export const fiftySeventhBattleBonds: BattleBond[] = [ ...northernThirdBattleBonds ].map(cloneBattleBondForScenario); +export const northernFourthBattleBonds: BattleBond[] = [ + { + id: 'zhuge-liang__jiang-wei_northern-fourth', + unitIds: ['zhuge-liang', 'jiang-wei'], + title: '새 길을 맡긴 마음', + level: 48, + exp: 0, + description: '제갈량의 큰 계산과 강유의 천수 길 지식이 맞물리면 퇴각전에서도 군량 수레와 병사를 함께 살릴 수 있습니다.' + }, + { + id: 'jiang-wei__wang-ping_northern-fourth', + unitIds: ['jiang-wei', 'wang-ping'], + title: '낮은 길과 물길', + level: 44, + exp: 0, + description: '강유가 아는 낮은 길과 왕평의 물길 판단이 합쳐지면 추격을 끊고 안전한 퇴로를 열 확률이 높아집니다.' + }, + { + id: 'jiang-wei__zhao-yun_northern-fourth', + unitIds: ['jiang-wei', 'zhao-yun'], + title: '새 장수와 백마', + level: 42, + exp: 0, + description: '강유의 길 안내와 조운의 안정된 회수 기동이 어울리면 빠르게 들어온 위군 기병을 늦출 수 있습니다.' + }, + { + id: 'ma-dai__jiang-wei_northern-fourth', + unitIds: ['ma-dai', 'jiang-wei'], + title: '서량 말과 천수 길', + level: 40, + exp: 0, + description: '마대의 서량 기병 감각과 강유의 천수 지형 지식이 맞물리면 좁은 길에서도 추격을 흩어 놓을 수 있습니다.' + } +]; + +export const fiftyEighthBattleBonds: BattleBond[] = [ + ...fiftySeventhBattleBonds, + ...northernFourthBattleBonds +].map(cloneBattleBondForScenario); + function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => Array.from({ length: 26 }, (_, x): TerrainType => { @@ -21415,6 +21605,82 @@ function createFiftySeventhBattleTerrain(): TerrainType[][] { ); } +function createFiftyEighthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 98 }, (_, y) => + Array.from({ length: 116 }, (_, x): TerrainType => { + if ( + (x >= 36 && x <= 43 && y >= 0 && y <= 97) || + (x >= 64 && x <= 71 && y >= 18 && y <= 97) || + (x >= 0 && x <= 115 && y >= 88) + ) { + return 'river'; + } + if ( + (x >= 6 && x <= 76 && y >= 82 && y <= 87) || + (x >= 18 && x <= 88 && y >= 70 && y <= 78) || + (x >= 52 && x <= 102 && y >= 58 && y <= 67) || + (x >= 72 && x <= 110 && y >= 43 && y <= 53) || + (x >= 88 && x <= 115 && y >= 29 && y <= 39) || + (x >= 24 && x <= 32 && y >= 64 && y <= 92) || + (x >= 48 && x <= 56 && y >= 50 && y <= 84) || + (x >= 78 && x <= 86 && y >= 36 && y <= 72) + ) { + return 'road'; + } + if ( + (x >= 0 && x <= 38 && y >= 55 && y <= 86) || + (x >= 46 && x <= 78 && y >= 67 && y <= 88) || + (x >= 88 && x <= 115 && y >= 59 && y <= 84) || + (x >= 12 && x <= 34 && y >= 38 && y <= 56) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 44 && y <= 34) || + (x >= 44 && x <= 86 && y <= 28) || + (x >= 84 && x <= 115 && y <= 45) || + (x >= 96 && x <= 115 && y >= 38 && y <= 64) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 88) || + (x >= 113 && y <= 82) || + (x >= 32 && x <= 37 && y >= 0 && y <= 58) || + (x >= 72 && x <= 78 && y >= 0 && y <= 40) || + (x >= 58 && x <= 64 && y >= 78 && y <= 97) + ) { + return 'cliff'; + } + if ( + (x >= 9 && x <= 24 && y >= 78 && y <= 88) || + (x >= 42 && x <= 58 && y >= 66 && y <= 77) || + (x >= 75 && x <= 91 && y >= 51 && y <= 63) || + (x >= 96 && x <= 110 && y >= 36 && y <= 48) + ) { + return 'village'; + } + if ( + (x >= 18 && x <= 40 && y >= 72 && y <= 84) || + (x >= 52 && x <= 74 && y >= 58 && y <= 70) || + (x >= 84 && x <= 104 && y >= 43 && y <= 56) || + (x >= 92 && x <= 112 && y >= 26 && y <= 38) + ) { + return 'camp'; + } + if ( + (x >= 47 && x <= 63 && y >= 40 && y <= 54) || + (x >= 70 && x <= 90 && y >= 30 && y <= 44) || + (x >= 92 && x <= 110 && y >= 44 && y <= 58) || + (x >= 99 && x <= 114 && y >= 14 && y <= 28) + ) { + 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 3708d00..a6edc4e 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -51,6 +51,7 @@ import { mengHuoPacificationBonds, mengHuoSecondCaptureBonds, nanzhongRecoveryBonds, + northernFourthBattleBonds, northernFirstBattleBonds, northernSecondBattleBonds, northernThirdBattleBonds, @@ -439,9 +440,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ battleIds: [ 'fifty-fifth-battle-northern-qishan-road', 'fifty-sixth-battle-tianshui-advance', - 'fifty-seventh-battle-jieting-crisis' + 'fifty-seventh-battle-jieting-crisis', + 'fifty-eighth-battle-qishan-retreat' ], - nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류'] + nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '다음 북벌 재정비'] } ]; @@ -502,7 +504,8 @@ const campBattleIds = { fiftyFourth: 'fifty-fourth-battle-meng-huo-final-capture', fiftyFifth: 'fifty-fifth-battle-northern-qishan-road', fiftySixth: 'fifty-sixth-battle-tianshui-advance', - fiftySeventh: 'fifty-seventh-battle-jieting-crisis' + fiftySeventh: 'fifty-seventh-battle-jieting-crisis', + fiftyEighth: 'fifty-eighth-battle-qishan-retreat' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1206,6 +1209,21 @@ const sortieRulesByBattleId: Partial { + this.addSortieButton(isBattleSortie ? '출진' : '진행', x + width - 152, y + height - 42, 124, () => { soundDirector.playSelect(); this.startVictoryStory(); }, depth + 3); @@ -10147,7 +10302,7 @@ export class CampScene extends Phaser.Scene { private sortieTerrainLine(unit: UnitData) { const scenario = this.nextSortieScenario(); if (!scenario) { - return `${getUnitClass(unit.classKey).role}`; + return `지형 의정 · ${getUnitClass(unit.classKey).role}`; } const terrainCounts = this.passableTerrainCounts(scenario); diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index 9a87af7..ee09441 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -13,6 +13,7 @@ import { fiftyFifthBattleScenario, fiftySixthBattleScenario, fiftySeventhBattleScenario, + fiftyEighthBattleScenario, fortiethBattleScenario, fortyFirstBattleScenario, fortySecondBattleScenario, @@ -414,6 +415,7 @@ export class TitleScene extends Phaser.Scene { campaign.step === 'fifty-fifth-camp' || campaign.step === 'fifty-sixth-camp' || campaign.step === 'fifty-seventh-camp' || + campaign.step === 'fifty-eighth-camp' || campaign.step === 'hanzhong-king-camp' || campaign.step === 'shu-han-foundation-camp' || campaign.step === 'baidi-entrustment-camp' || @@ -708,6 +710,11 @@ export class TitleScene extends Phaser.Scene { return; } + if (campaign.step === 'fifty-eighth-battle') { + this.scene.start('BattleScene', { battleId: fiftyEighthBattleScenario.id }); + return; + } + if (campaign.step === 'first-victory-story') { this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' }); return; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 7d84a83..86357b8 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -156,6 +156,8 @@ export type CampaignStep = | 'fifty-sixth-camp' | 'fifty-seventh-battle' | 'fifty-seventh-camp' + | 'fifty-eighth-battle' + | 'fifty-eighth-camp' | 'hanzhong-king-camp' | 'shu-han-foundation-camp' | 'baidi-entrustment-camp' @@ -322,7 +324,8 @@ const campaignBattleSteps: Record