From 779b639bdfb714676a0f15f295864036948b9ddb Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 24 Jun 2026 11:03:12 +0900 Subject: [PATCH] Add Chencang siege campaign chapter --- docs/roadmap.md | 6 +- scripts/verify-flow.mjs | 263 +++++++++++++---- .../images/battle/fifty-ninth-battle-map.svg | 79 ++++++ 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 | 165 ++++++++++- src/game/scenes/TitleScene.ts | 7 + src/game/state/campaignState.ts | 5 +- 10 files changed, 834 insertions(+), 75 deletions(-) create mode 100644 src/assets/images/battle/fifty-ninth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 5ab9ea5..78cee11 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -88,15 +88,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 +- 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 - 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 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 +- 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 ## Next Steps -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 +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 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 dc7f9ef..a01bc8b 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1,4 +1,4 @@ -import { spawn } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/'; @@ -1739,7 +1739,7 @@ try { if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || - progressTabState.campaignProgress?.totalKnown !== 58 || + progressTabState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postBowangProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postChangbanProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postWulingProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postChangshaProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postYizhouProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postFuPassProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postLuoProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postLuofengProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postChengduProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postJiamengProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postYangpingProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postDingjunProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 59 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 58 || + postFoundationProgressState.campaignProgress?.totalKnown !== 59 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 58 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 59 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 58 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 59 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 58 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 59 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 58 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 59 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 58 || + postJingRearProgressState.campaignProgress?.totalKnown !== 59 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 58 || + postGonganProgressState.campaignProgress?.totalKnown !== 59 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 58 || + postMaichengProgressState.campaignProgress?.totalKnown !== 59 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 58 || + postYilingProgressState.campaignProgress?.totalKnown !== 59 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 58 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 59 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,7 +6280,7 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 58 || + postBaidiProgressState.campaignProgress?.totalKnown !== 59 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { @@ -6441,7 +6441,7 @@ try { if ( postNanzhongProgressState?.activeTab !== 'progress' || postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || - postNanzhongProgressState.campaignProgress?.totalKnown !== 58 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 59 || postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전' ) { @@ -6603,7 +6603,7 @@ try { if ( postMengHuoProgressState?.activeTab !== 'progress' || postMengHuoProgressState.campaignProgress?.completedKnown !== 48 || - postMengHuoProgressState.campaignProgress?.totalKnown !== 58 || + postMengHuoProgressState.campaignProgress?.totalKnown !== 59 || postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' || postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전' ) { @@ -6765,7 +6765,7 @@ try { if ( postSecondCaptureProgressState?.activeTab !== 'progress' || postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 || - postSecondCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postSecondCaptureProgressState.campaignProgress?.totalKnown !== 59 || postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' || postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전' ) { @@ -6930,7 +6930,7 @@ try { if ( postThirdCaptureProgressState?.activeTab !== 'progress' || postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 || - postThirdCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postThirdCaptureProgressState.campaignProgress?.totalKnown !== 59 || postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' || postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전' ) { @@ -7096,7 +7096,7 @@ try { if ( postFourthCaptureProgressState?.activeTab !== 'progress' || postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 || - postFourthCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postFourthCaptureProgressState.campaignProgress?.totalKnown !== 59 || postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' || postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전' ) { @@ -7262,7 +7262,7 @@ try { if ( postFifthCaptureProgressState?.activeTab !== 'progress' || postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 || - postFifthCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postFifthCaptureProgressState.campaignProgress?.totalKnown !== 59 || postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' || postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전' ) { @@ -7427,7 +7427,7 @@ try { if ( postSixthCaptureProgressState?.activeTab !== 'progress' || postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 || - postSixthCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postSixthCaptureProgressState.campaignProgress?.totalKnown !== 59 || postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' || postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전' ) { @@ -7593,7 +7593,7 @@ try { if ( postFinalCaptureProgressState?.activeTab !== 'progress' || postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 || - postFinalCaptureProgressState.campaignProgress?.totalKnown !== 58 || + postFinalCaptureProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postNorthernPrepProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postFiftyFifthProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postFiftySixthProgressState.campaignProgress?.totalKnown !== 59 || 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 !== 58 || + postFiftySeventhProgressState.campaignProgress?.totalKnown !== 59 || postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' || postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로' @@ -8272,15 +8272,166 @@ try { if ( postFiftyEighthProgressState?.activeTab !== 'progress' || postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 || - postFiftyEighthProgressState.campaignProgress?.totalKnown !== 58 || + postFiftyEighthProgressState.campaignProgress?.totalKnown !== 59 || postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' || - postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + 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)}`); + throw new Error(`Expected post-fifty-eighth progress tab to complete the Qishan retreat and point toward Chencang: ${JSON.stringify(postFiftyEighthProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fifty-eighth-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const chencangSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !chencangSortieState?.sortieVisible || + chencangSortieState.campaign?.step !== 'fifty-eighth-camp' || + chencangSortieState.campTitle !== '기산 후퇴로 후 군영' || + !String(chencangSortieState.sortiePlan?.objectiveLine ?? '').includes('진창 공성전') || + !String(chencangSortieState.sortiePlan?.objectiveLine ?? '').includes('학소') || + chencangSortieState.sortiePlan?.maxCount !== 7 || + chencangSortieState.sortiePlan?.recommendedTotal !== 7 || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !chencangSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recommended) || + chencangSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected Chencang sortie to require Zhuge Liang, exclude Liu Bei, and recommend Jiang Wei, Wei Yan, terrain, cavalry, and logistics roles: ${JSON.stringify(chencangSortieState)}`); + } + assertSortieTacticalRoster(chencangSortieState, [ + '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 chencangPriorityUnits = ['jiang-wei', 'wang-ping', 'ma-dai', 'wei-yan', 'huang-quan', 'fa-zheng']; + for (const unitId of chencangPriorityUnits) { + 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 && !chencangPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const chencangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !chencangPriorityUnits.every((unitId) => + chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + chencangSortieReadyState.sortiePlan?.selectedCount !== 7 || + chencangSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Chencang sortie to deploy Zhuge Liang, Jiang Wei, Wang Ping, Ma Dai, Wei Yan, Huang Quan, and Fa Zheng: ${JSON.stringify(chencangSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-fifty-ninth-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-ninth-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredFiftyNinthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-ninth-battle-chencang-siege'; + }); + if (enteredFiftyNinthBattle) { + 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-ninth-battle-chencang-siege' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-fifty-ninth-battle.png', fullPage: true }); + + const fiftyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fiftyNinthEnemies = fiftyNinthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fiftyNinthAllies = fiftyNinthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fiftyNinthEnemyBehaviors = new Set(fiftyNinthEnemies.map((unit) => unit.ai)); + if ( + fiftyNinthBattleState.camera?.mapWidth !== 118 || + fiftyNinthBattleState.camera?.mapHeight !== 100 || + fiftyNinthBattleState.victoryConditionLabel !== '학소 진창 수비 압박과 왕쌍 추격 차단' || + !fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'pressure-hao-zhao' && objective.label === '학소 진창 수비 압박') || + !fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'halt-wang-shuang' && objective.label === '왕쌍 추격 차단') || + !fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'chencang-gate' && objective.label === '진창 성문 압박') || + !fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'siege-camps' && objective.label === '공성 진영 유지') || + fiftyNinthEnemies.length < 42 || + !fiftyNinthEnemyBehaviors.has('aggressive') || + !fiftyNinthEnemyBehaviors.has('guard') || + !fiftyNinthEnemyBehaviors.has('hold') || + !fiftyNinthEnemies.some((unit) => unit.id === 'northern-fifth-leader-hao-zhao') || + !fiftyNinthEnemies.some((unit) => unit.id === 'northern-fifth-officer-wang-shuang') || + !fiftyNinthAllies.some((unit) => unit.id === 'zhuge-liang') || + !fiftyNinthAllies.some((unit) => unit.id === 'jiang-wei') || + !fiftyNinthAllies.some((unit) => unit.id === 'wang-ping') || + !fiftyNinthAllies.some((unit) => unit.id === 'ma-dai') || + !fiftyNinthAllies.some((unit) => unit.id === 'wei-yan') || + !fiftyNinthAllies.some((unit) => unit.id === 'huang-quan') || + !fiftyNinthAllies.some((unit) => unit.id === 'fa-zheng') || + fiftyNinthAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected fifty-ninth battle to use the Chencang siege map, Hao Zhao/Wang Shuang objectives, selected Jiang Wei sortie, and mixed Wei AI: ${JSON.stringify(fiftyNinthBattleState)}`); + } + + 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 fiftyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fiftyNinthCampState?.campBattleId !== 'fifty-ninth-battle-chencang-siege' || + fiftyNinthCampState.campaign?.step !== 'fifty-ninth-camp' || + fiftyNinthCampState.campTitle !== '진창 공성전 후 군영' || + fiftyNinthCampState.availableDialogueIds?.length !== 3 || + !fiftyNinthCampState.availableDialogueIds.every((id) => id.startsWith('northern-fifth-')) || + fiftyNinthCampState.availableVisitIds?.length !== 1 || + !fiftyNinthCampState.availableVisitIds.every((id) => id.startsWith('northern-fifth-')) || + fiftyNinthCampState.rosterCollection?.total < 22 || + !fiftyNinthCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__fa-zheng_northern-fifth') + ) { + throw new Error(`Expected Chencang aftermath camp to expose fifth northern campaign events and bonds: ${JSON.stringify(fiftyNinthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-fifty-ninth-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postFiftyNinthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postFiftyNinthProgressState?.activeTab !== 'progress' || + postFiftyNinthProgressState.campaignProgress?.completedKnown !== 59 || + postFiftyNinthProgressState.campaignProgress?.totalKnown !== 59 || + postFiftyNinthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || + postFiftyNinthProgressState.campaignProgress?.latestBattleTitle !== '진창 공성전' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-fifty-ninth-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -8309,12 +8460,12 @@ try { } } for (let attempt = 0; attempt < 24; attempt += 1) { - const continuedToQishanCamp = await page.evaluate(() => { + const continuedToChencangCamp = 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'; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-ninth-camp'; }); - if (continuedToQishanCamp) { + if (continuedToChencangCamp) { break; } await page.keyboard.press('Space'); @@ -8323,17 +8474,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-eighth-camp'; + return activeScenes.includes('CampScene') && camp?.campaign?.step === 'fifty-ninth-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-eighth-camp') { - throw new Error(`Expected title continue to reopen the Qishan retreat aftermath camp: ${JSON.stringify(titleContinueState)}`); + 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)}`); } - 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}`); + 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}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/fifty-ninth-battle-map.svg b/src/assets/images/battle/fifty-ninth-battle-map.svg new file mode 100644 index 0000000..4b4d266 --- /dev/null +++ b/src/assets/images/battle/fifty-ninth-battle-map.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts index 858dde9..7c62047 100644 --- a/src/game/data/battleMapAssets.ts +++ b/src/game/data/battleMapAssets.ts @@ -8,6 +8,7 @@ import fiftyThirdBattleMapUrl from '../../assets/images/battle/fifty-third-battl 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 fiftyNinthBattleMapUrl from '../../assets/images/battle/fifty-ninth-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'; @@ -114,6 +115,7 @@ export const battleMapAssets: Record = { 'battle-map-fifty-fourth': fiftyFourthBattleMapUrl, 'battle-map-fifty-fifth': fiftyFifthBattleMapUrl, 'battle-map-fifty-eighth': fiftyEighthBattleMapUrl, + 'battle-map-fifty-ninth': fiftyNinthBattleMapUrl, '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 0a5fd54..523ca79 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -31,6 +31,10 @@ import { fiftySixthBattleMap, fiftySixthBattleUnits, fiftySixthBattleVictoryPages, + fiftyNinthBattleBonds, + fiftyNinthBattleMap, + fiftyNinthBattleUnits, + fiftyNinthBattleVictoryPages, fiftySeventhBattleBonds, fiftySeventhBattleMap, fiftySeventhBattleUnits, @@ -295,7 +299,8 @@ export type BattleScenarioId = | 'fifty-fifth-battle-northern-qishan-road' | 'fifty-sixth-battle-tianshui-advance' | 'fifty-seventh-battle-jieting-crisis' - | 'fifty-eighth-battle-qishan-retreat'; + | 'fifty-eighth-battle-qishan-retreat' + | 'fifty-ninth-battle-chencang-siege'; export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -3861,6 +3866,87 @@ export const fiftyEighthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fiftyNinthBattleScenario: BattleScenarioDefinition = { + id: 'fifty-ninth-battle-chencang-siege', + title: '진창 공성전', + victoryConditionLabel: '학소 진창 수비 압박과 왕쌍 추격 차단', + defeatConditionLabel: '제갈량 퇴각 또는 공성 진영 붕괴', + openingObjectiveLines: [ + '기산에서 후퇴한 뒤 촉한군은 병사와 장부를 다시 묶어 진창 성채를 압박합니다. 학소가 성벽을 굳게 지키고 있어 성문 하나만 바라보는 공격은 오래 버티지 못합니다.', + '강유는 길을 알고, 왕평은 진영 표식을 보며, 위연은 돌파를 원합니다. 여기에 황권과 마량, 법정의 장부와 계책을 어떻게 넣을지에 따라 공성전의 무게가 달라집니다.', + '학소의 수비를 흔들고 왕쌍의 추격을 차단하십시오. 공성 진영과 군량 수레 길을 함께 지키면 다음 북방 계산으로 이어갈 수 있습니다.' + ], + map: fiftyNinthBattleMap, + units: fiftyNinthBattleUnits, + bonds: fiftyNinthBattleBonds, + mapTextureKey: 'battle-map-fifty-ninth', + leaderUnitId: 'northern-fifth-leader-hao-zhao', + quickVictoryTurnLimit: 118, + baseVictoryGold: 17280, + objectives: [ + { + id: 'pressure-hao-zhao', + kind: 'defeat-leader', + label: '학소 진창 수비 압박', + rewardGold: 12040, + unitId: 'northern-fifth-leader-hao-zhao' + }, + { + id: 'halt-wang-shuang', + kind: 'defeat-leader', + label: '왕쌍 추격 차단', + rewardGold: 6000, + unitId: 'northern-fifth-officer-wang-shuang' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 3100, + unitId: 'zhuge-liang' + }, + { + id: 'chencang-gate', + kind: 'secure-terrain', + label: '진창 성문 압박', + rewardGold: 6200, + terrain: 'fort' + }, + { + id: 'siege-camps', + kind: 'secure-terrain', + label: '공성 진영 유지', + rewardGold: 5940, + terrain: 'camp' + }, + { + id: 'grain-road', + kind: 'secure-terrain', + label: '군량 수레 길 확보', + rewardGold: 5820, + terrain: 'road' + }, + { + id: 'rear-villages', + kind: 'secure-terrain', + label: '후방 마을 안정', + rewardGold: 5480, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '118턴 이내 진창 압박 완료', + rewardGold: 3840, + maxTurn: 118 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['금 104', '상처약 86', '군주 42', '진창 공성 장부 1', '왕쌍 추격 기록 1'], + victoryPages: fiftyNinthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -3921,7 +4007,8 @@ export const battleScenarios: Record 'fifty-fifth-battle-northern-qishan-road': fiftyFifthBattleScenario, 'fifty-sixth-battle-tianshui-advance': fiftySixthBattleScenario, 'fifty-seventh-battle-jieting-crisis': fiftySeventhBattleScenario, - 'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario + 'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario, + 'fifty-ninth-battle-chencang-siege': fiftyNinthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 07d82ac..fcc8b6c 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -12,6 +12,7 @@ import { fiftyFourthBattleScenario, fiftyFifthBattleScenario, fiftySixthBattleScenario, + fiftyNinthBattleScenario, fiftySeventhBattleScenario, fortiethBattleScenario, fortyFirstBattleScenario, @@ -72,6 +73,8 @@ import { fiftiethBattleVictoryPages, fiftyEighthBattleIntroPages, fiftyEighthBattleVictoryPages, + fiftyNinthBattleIntroPages, + fiftyNinthBattleVictoryPages, fiftyFirstBattleIntroPages, fiftyFirstBattleVictoryPages, fiftySecondBattleIntroPages, @@ -855,13 +858,24 @@ const sortieFlows: Record = { }, [fiftyEighthBattleScenario.id]: { afterBattleId: fiftyEighthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '기산 후퇴로 이후', + eyebrow: '다음 전장', + title: fiftyNinthBattleScenario.title, description: - '기산 후퇴로에서 병사와 장부를 지켜 냈습니다. 그러나 이것은 북벌의 끝이 아니라 다음 출정을 위한 정비입니다. 강유의 첫 출전, 왕평의 물길 판단, 조운과 마대의 귀환선을 군영에서 다시 정리합니다.', - rewardHint: '다음 목표: 북벌 재정비 / 강유 첫 출전 평가', - pages: fiftyEighthBattleVictoryPages, - unavailableNotice: '기산 후퇴로 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 강유 첫 출전 후 군영 대화, 보급 정비, 출전 편성 방향을 확인할 수 있습니다.' + '기산 후퇴로에서 병사와 장부를 지켜 냈습니다. 이제 다시 북쪽을 보되, 진창 성채의 학소와 왕쌍의 추격을 상대해야 합니다. 강유의 길 판단과 공성 진영, 군량 수레를 함께 계산하십시오.', + rewardHint: `예상 보상: ${fiftyNinthBattleScenario.title} 개방 / 진창 공성 장부`, + nextBattleId: fiftyNinthBattleScenario.id, + campaignStep: 'fifty-ninth-battle', + pages: [...fiftyEighthBattleVictoryPages, ...fiftyNinthBattleIntroPages] + }, + [fiftyNinthBattleScenario.id]: { + afterBattleId: fiftyNinthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '진창 공성전 이후', + description: + '진창 성채를 압박하고 왕쌍의 추격을 끊었지만, 성을 오래 물고 늘어지는 북벌은 군량의 시간을 갉아먹습니다. 이것은 결말이 아니라 다음 북방 계산을 위한 경계선입니다.', + rewardHint: '다음 목표: 북벌 보급 재계산 / 진창 공성 평가', + pages: fiftyNinthBattleVictoryPages, + unavailableNotice: '진창 공성전 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 진창 공성 평가, 보급 재계산, 강유와 위연의 편성 방향을 확인할 수 있습니다.' }, 'northern-campaign-prep-camp-legacy': { afterBattleId: fiftyFourthBattleScenario.id, diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 12f9b3b..c23fb45 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3748,6 +3748,59 @@ export const fiftyEighthBattleVictoryPages: StoryPage[] = [ } ]; +export const fiftyNinthBattleIntroPages: StoryPage[] = [ + { + id: 'fifty-ninth-chencang-order', + bgm: 'story-dark', + chapter: '진창 성채', + background: 'story-militia', + speaker: '제갈량', + text: '기산에서 병사를 살렸기에 다시 북쪽을 볼 수 있습니다. 그러나 이번 길은 넓은 들이 아니라 진창의 굳은 성채입니다. 성문을 무리하게 깨기보다 공성 진영, 군량 수레, 퇴로를 함께 보아야 합니다.' + }, + { + id: 'fifty-ninth-hao-zhao-wall', + bgm: 'battle-prep', + chapter: '학소의 성벽', + background: 'story-sortie', + speaker: '강유', + text: '진창의 수비장 학소는 성을 버리지 않을 사람입니다. 제가 앞길을 살피겠습니다만, 성벽만 바라보면 왕쌍의 기병이 뒤를 물 것입니다. 조운, 마대, 왕평 중 누구를 앞으로 세우느냐가 이번 편성의 무게입니다.' + }, + { + id: 'fifty-ninth-supply-limit', + bgm: 'story-dark', + chapter: '군량의 시간', + background: 'story-three-heroes', + speaker: '황권', + text: '성채 하나에 오래 묶이면 북벌 전체가 굶습니다. 법정의 계책, 마량의 문서, 황권의 장부가 함께 있어야 공성전이 단순한 힘겨루기로 흐르지 않습니다. 진창을 압박하되 돌아갈 수 있는 시간을 남기십시오.' + } +]; + +export const fiftyNinthBattleVictoryPages: StoryPage[] = [ + { + id: 'fifty-ninth-victory-chencang-pressure', + bgm: 'militia-theme', + chapter: '진창 압박', + background: 'story-sortie', + text: '촉한군은 진창의 외곽 진영과 보급로를 흔들었지만, 학소의 성벽은 끝내 쉽게 열리지 않았습니다. 승리라 부르기에는 조심스럽고 패배라 덮기에는 얻은 길이 분명한 전투였습니다.' + }, + { + id: 'fifty-ninth-victory-wang-shuang-cut', + bgm: 'story-dark', + chapter: '왕쌍 추격 차단', + background: 'story-militia', + speaker: '조운', + text: '왕쌍의 기병은 공성 진영 뒤를 물려 했으나, 강유의 길 안내와 마대의 우회, 왕평의 진영 표식이 먼저 움직였습니다. 오래 버티는 성을 상대로도 물러날 때를 정하는 장수가 필요합니다.' + }, + { + id: 'fifty-ninth-next-northern-plan', + bgm: 'story-dark', + chapter: '다음 북방 계산', + background: 'story-three-heroes', + speaker: '제갈량', + text: '진창은 북벌의 끝이 아니라, 무리한 공세가 어디서 멈춰야 하는지를 보여 준 성입니다. 다음 출정에서는 성을 치는 힘뿐 아니라 군량이 버틸 날, 돌아올 길, 장수의 고집을 모두 함께 계산해야 합니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -4241,6 +4294,12 @@ export const fiftyEighthBattleMap: BattleMap = { terrain: createFiftyEighthBattleTerrain() }; +export const fiftyNinthBattleMap: BattleMap = { + width: 118, + height: 100, + terrain: createFiftyNinthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -17276,6 +17335,97 @@ export const fiftyEighthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('northern-fourth-leader-sima-yi', '사마의', '위군 본대 총지휘관', 'rebelLeader', 193, 1500, 352, 112, 27) ]; +const fiftyNinthBattleAllyPositions: Record = { + 'guan-yu': { x: 18, y: 88 }, + 'zhang-fei': { x: 24, y: 90 }, + 'jian-yong': { x: 10, y: 95 }, + 'mi-zhu': { x: 14, y: 96 }, + 'sun-qian': { x: 19, y: 96 }, + 'zhao-yun': { x: 31, y: 86 }, + 'zhuge-liang': { x: 17, y: 85 }, + 'ma-liang': { x: 22, y: 82 }, + 'yi-ji': { x: 9, y: 98 }, + 'gong-zhi': { x: 27, y: 96 }, + 'huang-zhong': { x: 37, y: 84 }, + 'wei-yan': { x: 43, y: 86 }, + 'pang-tong': { x: 15, y: 81 }, + 'fa-zheng': { x: 27, y: 80 }, + 'wu-yi': { x: 45, y: 94 }, + 'yan-yan': { x: 49, y: 87 }, + 'li-yan': { x: 35, y: 79 }, + 'huang-quan': { x: 12, y: 84 }, + 'ma-chao': { x: 52, y: 83 }, + 'ma-dai': { x: 56, y: 90 }, + 'wang-ping': { x: 40, y: 79 }, + 'jiang-wei': { x: 33, y: 82 } +}; + +export const fiftyNinthBattleUnits: 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, fiftyNinthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('northern-fifth-scout-a', '진창 외곽 척후병', '서쪽 수레길 정찰', 'bandit', 190, 960, 270, 49, 78), + createThirtyFirstEnemyUnit('northern-fifth-scout-b', '진창 외곽 척후병', '공성 진영 정찰', 'bandit', 190, 960, 270, 60, 74), + createThirtyFirstEnemyUnit('northern-fifth-scout-c', '진창 외곽 척후병', '중앙 낮은 길 정찰', 'bandit', 191, 972, 272, 72, 67), + createThirtyFirstEnemyUnit('northern-fifth-scout-d', '진창 외곽 척후병', '동쪽 성문 정찰', 'bandit', 191, 972, 272, 84, 60), + createThirtyFirstEnemyUnit('northern-fifth-scout-e', '진창 외곽 척후병', '학소 성벽 정찰', 'bandit', 192, 984, 274, 101, 42), + createThirtyFirstEnemyUnit('northern-fifth-scout-f', '진창 외곽 척후병', '북쪽 봉화 정찰', 'bandit', 192, 984, 274, 114, 25), + createThirtyFirstEnemyUnit('northern-fifth-infantry-a', '위군 진창 보병', '서쪽 공성로 보병', 'yellowTurban', 191, 1340, 324, 54, 73), + createThirtyFirstEnemyUnit('northern-fifth-infantry-b', '위군 진창 보병', '수레길 보병', 'yellowTurban', 191, 1340, 324, 64, 68), + createThirtyFirstEnemyUnit('northern-fifth-infantry-c', '위군 진창 보병', '중앙 보병', 'yellowTurban', 192, 1360, 328, 76, 62), + createThirtyFirstEnemyUnit('northern-fifth-infantry-d', '위군 진창 보병', '동문 앞 보병', 'yellowTurban', 192, 1360, 328, 88, 55), + createThirtyFirstEnemyUnit('northern-fifth-infantry-e', '위군 진창 보병', '성벽 아래 보병', 'yellowTurban', 193, 1380, 332, 100, 48), + createThirtyFirstEnemyUnit('northern-fifth-infantry-f', '위군 진창 보병', '학소 본영 보병', 'yellowTurban', 193, 1380, 332, 110, 36), + createThirtyFirstEnemyUnit('northern-fifth-infantry-g', '위군 진창 보병', '보급 차단 보병', 'yellowTurban', 194, 1400, 336, 92, 76), + createThirtyFirstEnemyUnit('northern-fifth-infantry-h', '위군 진창 보병', '북쪽 성벽 보병', 'yellowTurban', 194, 1400, 336, 115, 18), + createThirtyFirstEnemyUnit('northern-fifth-archer-a', '진창 성벽 궁병', '서쪽 성벽 궁병', 'archer', 191, 1010, 282, 60, 63), + createThirtyFirstEnemyUnit('northern-fifth-archer-b', '진창 성벽 궁병', '공성로 궁병', 'archer', 191, 1010, 282, 71, 58), + createThirtyFirstEnemyUnit('northern-fifth-archer-c', '진창 성벽 궁병', '중앙 성벽 궁병', 'archer', 192, 1022, 284, 84, 51), + createThirtyFirstEnemyUnit('northern-fifth-archer-d', '진창 성벽 궁병', '동문 궁병', 'archer', 193, 1034, 286, 96, 44), + createThirtyFirstEnemyUnit('northern-fifth-archer-e', '진창 성벽 궁병', '학소 본영 궁병', 'archer', 193, 1034, 286, 108, 31), + createThirtyFirstEnemyUnit('northern-fifth-archer-f', '진창 성벽 궁병', '봉화대 궁병', 'archer', 194, 1046, 288, 116, 16), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-a', '왕쌍 추격 기병', '서쪽 추격 기병', 'cavalry', 192, 1388, 338, 50, 66), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-b', '왕쌍 추격 기병', '수레길 추격 기병', 'cavalry', 192, 1388, 338, 63, 63), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-c', '왕쌍 추격 기병', '중앙 돌파 기병', 'cavalry', 193, 1410, 342, 77, 56), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-d', '왕쌍 추격 기병', '동쪽 우회 기병', 'cavalry', 194, 1432, 346, 91, 49), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-e', '왕쌍 추격 기병', '성문 지원 기병', 'cavalry', 194, 1432, 346, 103, 41), + createThirtyFirstEnemyUnit('northern-fifth-cavalry-f', '왕쌍 추격 기병', '북쪽 추격 기병', 'cavalry', 195, 1454, 350, 114, 27), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-a', '진창 성벽 수비대', '서쪽 성벽 수비', 'yellowTurban', 192, 1360, 330, 44, 53), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-b', '진창 성벽 수비대', '중앙 성벽 수비', 'yellowTurban', 192, 1360, 330, 58, 47), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-c', '진창 성벽 수비대', '동문 수비', 'yellowTurban', 193, 1380, 334, 74, 39), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-d', '진창 성벽 수비대', '내성 수비', 'yellowTurban', 193, 1380, 334, 90, 32), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-e', '진창 성벽 수비대', '학소 본영 수비', 'yellowTurban', 194, 1400, 338, 104, 24), + createThirtyFirstEnemyUnit('northern-fifth-wall-guard-f', '진창 성벽 수비대', '북쪽 절벽 수비', 'yellowTurban', 194, 1400, 338, 116, 13), + createThirtyFirstEnemyUnit('northern-fifth-strategist-a', '위군 공성 대응 군리', '성벽 수리 군리', 'strategist', 192, 1080, 296, 72, 54), + createThirtyFirstEnemyUnit('northern-fifth-strategist-b', '위군 공성 대응 군리', '봉화 연락 군리', 'strategist', 193, 1092, 298, 86, 45), + createThirtyFirstEnemyUnit('northern-fifth-strategist-c', '위군 공성 대응 군리', '학소 본영 군리', 'strategist', 193, 1092, 298, 100, 36), + createThirtyFirstEnemyUnit('northern-fifth-strategist-d', '위군 공성 대응 군리', '군량 차단 군리', 'strategist', 194, 1104, 300, 106, 63), + createThirtyFirstEnemyUnit('northern-fifth-officer-wang-shuang', '왕쌍', '위군 추격 장수', 'rebelLeader', 196, 1480, 356, 87, 72), + createThirtyFirstEnemyUnit('northern-fifth-officer-fei-yao', '비요', '진창 지원 장수', 'rebelLeader', 194, 1440, 348, 101, 58), + createThirtyFirstEnemyUnit('northern-fifth-officer-guo-huai', '곽회', '북방 지원 장수', 'rebelLeader', 194, 1440, 348, 110, 68), + createThirtyFirstEnemyUnit('northern-fifth-officer-supply', '위군 군량 차단장', '공성 진영 압박 장수', 'rebelLeader', 193, 1420, 344, 78, 76), + createThirtyFirstEnemyUnit('northern-fifth-officer-wall', '진창 성벽 장수', '성문 수비 장수', 'rebelLeader', 194, 1440, 348, 112, 27), + createThirtyFirstEnemyUnit('northern-fifth-leader-hao-zhao', '학소', '진창 수비장', 'rebelLeader', 197, 1540, 362, 109, 35) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -18513,6 +18663,46 @@ export const fiftyEighthBattleBonds: BattleBond[] = [ ...northernFourthBattleBonds ].map(cloneBattleBondForScenario); +export const northernFifthBattleBonds: BattleBond[] = [ + { + id: 'jiang-wei__fa-zheng_northern-fifth', + unitIds: ['jiang-wei', 'fa-zheng'], + title: '새 길과 오래된 계책', + level: 46, + exp: 0, + description: '강유의 진창 길 판단과 법정의 공성 계책이 맞물리면 성벽을 무리하게 치기보다 수비의 빈틈을 재는 힘이 생깁니다.' + }, + { + id: 'wei-yan__jiang-wei_northern-fifth', + unitIds: ['wei-yan', 'jiang-wei'], + title: '돌파와 절제', + level: 42, + exp: 0, + description: '위연의 돌파 욕심과 강유의 길 계산을 함께 묶으면 성채 앞에서 밀어붙일 곳과 멈출 곳을 구분할 수 있습니다.' + }, + { + id: 'huang-quan__ma-liang_northern-fifth', + unitIds: ['huang-quan', 'ma-liang'], + title: '군량과 문서', + level: 52, + exp: 0, + description: '황권의 군량 장부와 마량의 명분 문서가 함께 움직이면 진창 공성전이 오래 끌릴 때도 후방의 약속이 흔들리지 않습니다.' + }, + { + id: 'wang-ping__ma-dai_northern-fifth', + unitIds: ['wang-ping', 'ma-dai'], + title: '진영 표식과 우회 기병', + level: 44, + exp: 0, + description: '왕평의 진영 표식과 마대의 우회 기병이 어울리면 왕쌍의 추격을 받기 전에 공성 진영을 접을 시간을 벌 수 있습니다.' + } +]; + +export const fiftyNinthBattleBonds: BattleBond[] = [ + ...fiftyEighthBattleBonds, + ...northernFifthBattleBonds +].map(cloneBattleBondForScenario); + function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => Array.from({ length: 26 }, (_, x): TerrainType => { @@ -21681,6 +21871,81 @@ function createFiftyEighthBattleTerrain(): TerrainType[][] { ); } +function createFiftyNinthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 100 }, (_, y) => + Array.from({ length: 118 }, (_, x): TerrainType => { + if ( + (x >= 4 && x <= 10 && y >= 0 && y <= 99) || + (x >= 60 && x <= 67 && y >= 20 && y <= 99) || + (x >= 0 && x <= 117 && y >= 92) + ) { + return 'river'; + } + if ( + (x >= 10 && x <= 78 && y >= 84 && y <= 90) || + (x >= 24 && x <= 94 && y >= 72 && y <= 80) || + (x >= 48 && x <= 106 && y >= 60 && y <= 70) || + (x >= 72 && x <= 114 && y >= 47 && y <= 58) || + (x >= 92 && x <= 117 && y >= 33 && y <= 44) || + (x >= 30 && x <= 38 && y >= 66 && y <= 94) || + (x >= 54 && x <= 62 && y >= 54 && y <= 86) || + (x >= 84 && x <= 92 && y >= 39 && y <= 76) + ) { + return 'road'; + } + if ( + (x >= 0 && x <= 34 && y >= 56 && y <= 88) || + (x >= 42 && x <= 76 && y >= 70 && y <= 91) || + (x >= 88 && x <= 117 && y >= 63 && y <= 88) || + (x >= 14 && x <= 36 && y >= 40 && y <= 58) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 48 && y <= 36) || + (x >= 46 && x <= 88 && y <= 30) || + (x >= 86 && x <= 117 && y <= 48) || + (x >= 98 && x <= 117 && y >= 42 && y <= 68) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 90) || + (x >= 115 && y <= 84) || + (x >= 37 && x <= 43 && y >= 0 && y <= 60) || + (x >= 74 && x <= 80 && y >= 0 && y <= 42) || + (x >= 58 && x <= 65 && y >= 82 && y <= 99) + ) { + return 'cliff'; + } + if ( + (x >= 12 && x <= 27 && y >= 80 && y <= 90) || + (x >= 44 && x <= 61 && y >= 68 && y <= 79) || + (x >= 77 && x <= 94 && y >= 54 && y <= 66) || + (x >= 98 && x <= 114 && y >= 39 && y <= 52) + ) { + return 'village'; + } + if ( + (x >= 20 && x <= 44 && y >= 75 && y <= 88) || + (x >= 54 && x <= 78 && y >= 62 && y <= 74) || + (x >= 86 && x <= 108 && y >= 48 && y <= 61) + ) { + return 'camp'; + } + if ( + (x >= 48 && x <= 66 && y >= 43 && y <= 58) || + (x >= 72 && x <= 94 && y >= 33 && y <= 48) || + (x >= 92 && x <= 114 && y >= 42 && y <= 58) || + (x >= 100 && x <= 117 && y >= 12 && y <= 31) + ) { + 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 a6edc4e..c6fcf48 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -51,6 +51,7 @@ import { mengHuoPacificationBonds, mengHuoSecondCaptureBonds, nanzhongRecoveryBonds, + northernFifthBattleBonds, northernFourthBattleBonds, northernFirstBattleBonds, northernSecondBattleBonds, @@ -441,9 +442,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'fifty-fifth-battle-northern-qishan-road', 'fifty-sixth-battle-tianshui-advance', 'fifty-seventh-battle-jieting-crisis', - 'fifty-eighth-battle-qishan-retreat' + 'fifty-eighth-battle-qishan-retreat', + 'fifty-ninth-battle-chencang-siege' ], - nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '다음 북벌 재정비'] + nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산'] } ]; @@ -505,7 +507,8 @@ const campBattleIds = { fiftyFifth: 'fifty-fifth-battle-northern-qishan-road', fiftySixth: 'fifty-sixth-battle-tianshui-advance', fiftySeventh: 'fifty-seventh-battle-jieting-crisis', - fiftyEighth: 'fifty-eighth-battle-qishan-retreat' + fiftyEighth: 'fifty-eighth-battle-qishan-retreat', + fiftyNinth: 'fifty-ninth-battle-chencang-siege' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1224,6 +1227,21 @@ const sortieRulesByBattleId: Partial