diff --git a/docs/roadmap.md b/docs/roadmap.md
index 77b0f42..64bc4e5 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -92,15 +92,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Post-Chencang camp events with Jiang Wei, Fa Zheng, Wei Yan, Huang Quan, Ma Liang, Wang Ping, and Ma Dai focusing on siege timing, restrained breakthrough choices, supply ledgers, and the next northern-front recalculation
- Sixtieth battle Wudu/Yinping commanderies route, where the post-Chencang plan shifts from siege pressure to split mountain-road control, using eight recommended officers for seven slots so Jiang Wei, Wei Yan, Zhao Yun, Ma Dai, Wang Ping, Huang Quan, and Ma Liang compete inside a 120x102 battlefield against Guo Huai's relief line
- Post-Wudu/Yinping camp events with Jiang Wei, Wei Yan, Zhao Yun, Ma Dai, Wang Ping, Huang Quan, and Ma Liang focusing on breakthrough return lines, cavalry recall signals, two-commandery supply ledgers, and the next northern-front responsibility
+- Sixty-first battle Hanzhong rainy-pass defense route, where Cao Zhen's counteroffensive turns the newly held Wudu/Yinping commanderies into a defensive burden, forcing nine recommended officers into seven slots across a 122x104 battlefield while Zhuge Liang protects Hanzhong granaries, rain-soaked roads, and split supply ledgers
+- Post-Hanzhong-rain camp events with Li Yan, Huang Quan, Jiang Wei, Wang Ping, Zhuge Liang, Wei Yan, and Ma Liang focusing on rain-delay ledgers, return-first road markings, restrained counterattack limits, 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 Wudu/Yinping commanderies and post-commandery camp, with Jiang Wei's first selectable sortie, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire/Baidi entrustment/Nanzhong stabilization/Meng Huo pacification camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the Wudu/Yinping commanderies, Hanzhong rainy-pass defense, and post-rain-defense 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 securing Wudu and Yinping, preserving the broadened-responsibility tone and making the newly held commanderies create more supply, defense, and sortie tradeoffs instead of treating the extra territory as a simple victory lap
+1. Build the next northern-front step after Hanzhong rainy-pass defense, turning the preserved granaries and Wudu/Yinping supply roads back into an offensive decision without treating the successful defense as an ending
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 70b1e4a..a3acd44 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 !== 60 ||
+ progressTabState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 61 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 61 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 60 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 61 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 61 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 60 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 61 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 61 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,7 +5586,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 60 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 61 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5741,7 +5741,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
- postGonganProgressState.campaignProgress?.totalKnown !== 60 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 61 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5899,7 +5899,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
- postMaichengProgressState.campaignProgress?.totalKnown !== 60 ||
+ postMaichengProgressState.campaignProgress?.totalKnown !== 61 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6056,7 +6056,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
- postYilingProgressState.campaignProgress?.totalKnown !== 60 ||
+ postYilingProgressState.campaignProgress?.totalKnown !== 61 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6214,7 +6214,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
- postYilingFireProgressState.campaignProgress?.totalKnown !== 60 ||
+ postYilingFireProgressState.campaignProgress?.totalKnown !== 61 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6280,7 +6280,7 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
- postBaidiProgressState.campaignProgress?.totalKnown !== 60 ||
+ postBaidiProgressState.campaignProgress?.totalKnown !== 61 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
@@ -6441,7 +6441,7 @@ try {
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
- postNanzhongProgressState.campaignProgress?.totalKnown !== 60 ||
+ postNanzhongProgressState.campaignProgress?.totalKnown !== 61 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
) {
@@ -6603,7 +6603,7 @@ try {
if (
postMengHuoProgressState?.activeTab !== 'progress' ||
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
- postMengHuoProgressState.campaignProgress?.totalKnown !== 60 ||
+ postMengHuoProgressState.campaignProgress?.totalKnown !== 61 ||
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
) {
@@ -6765,7 +6765,7 @@ try {
if (
postSecondCaptureProgressState?.activeTab !== 'progress' ||
postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
- postSecondCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postSecondCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전'
) {
@@ -6930,7 +6930,7 @@ try {
if (
postThirdCaptureProgressState?.activeTab !== 'progress' ||
postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 ||
- postThirdCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postThirdCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' ||
postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전'
) {
@@ -7096,7 +7096,7 @@ try {
if (
postFourthCaptureProgressState?.activeTab !== 'progress' ||
postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 ||
- postFourthCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFourthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' ||
postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전'
) {
@@ -7262,7 +7262,7 @@ try {
if (
postFifthCaptureProgressState?.activeTab !== 'progress' ||
postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 ||
- postFifthCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFifthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' ||
postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전'
) {
@@ -7427,7 +7427,7 @@ try {
if (
postSixthCaptureProgressState?.activeTab !== 'progress' ||
postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 ||
- postSixthCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postSixthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' ||
postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전'
) {
@@ -7593,7 +7593,7 @@ try {
if (
postFinalCaptureProgressState?.activeTab !== 'progress' ||
postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 ||
- postFinalCaptureProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFinalCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postNorthernPrepProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postFiftyFifthProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postFiftySixthProgressState.campaignProgress?.totalKnown !== 61 ||
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 !== 60 ||
+ postFiftySeventhProgressState.campaignProgress?.totalKnown !== 61 ||
postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' ||
postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로'
@@ -8272,7 +8272,7 @@ try {
if (
postFiftyEighthProgressState?.activeTab !== 'progress' ||
postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 ||
- postFiftyEighthProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFiftyEighthProgressState.campaignProgress?.totalKnown !== 61 ||
postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' ||
postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '진창 공성전'
@@ -8423,7 +8423,7 @@ try {
if (
postFiftyNinthProgressState?.activeTab !== 'progress' ||
postFiftyNinthProgressState.campaignProgress?.completedKnown !== 59 ||
- postFiftyNinthProgressState.campaignProgress?.totalKnown !== 60 ||
+ postFiftyNinthProgressState.campaignProgress?.totalKnown !== 61 ||
postFiftyNinthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftyNinthProgressState.campaignProgress?.latestBattleTitle !== '진창 공성전' ||
postFiftyNinthProgressState.campaignProgress?.nextBattleTitle !== '무도·음평 확보전'
@@ -8580,15 +8580,175 @@ try {
if (
postSixtiethProgressState?.activeTab !== 'progress' ||
postSixtiethProgressState.campaignProgress?.completedKnown !== 60 ||
- postSixtiethProgressState.campaignProgress?.totalKnown !== 60 ||
+ postSixtiethProgressState.campaignProgress?.totalKnown !== 61 ||
postSixtiethProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postSixtiethProgressState.campaignProgress?.latestBattleTitle !== '무도·음평 확보전' ||
- postSixtiethProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postSixtiethProgressState.campaignProgress?.nextBattleTitle !== '한중 우로 방어전'
) {
- throw new Error(`Expected post-sixtieth progress tab to complete Wudu/Yinping and leave the next northern step pending: ${JSON.stringify(postSixtiethProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-sixtieth progress tab to complete Wudu/Yinping and point toward Hanzhong rainy-pass defense: ${JSON.stringify(postSixtiethProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-sixtieth-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const hanzhongRainSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanzhongRainSortieState?.sortieVisible ||
+ hanzhongRainSortieState.campaign?.step !== 'sixtieth-camp' ||
+ hanzhongRainSortieState.campTitle !== '무도·음평 확보 후 군영' ||
+ !String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('한중 우로 방어전') ||
+ !String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('보급선') ||
+ hanzhongRainSortieState.sortiePlan?.maxCount !== 7 ||
+ hanzhongRainSortieState.sortiePlan?.recommendedTotal !== 9 ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
+ !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
+ hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected Hanzhong rainy-pass sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing defense/logistics roles: ${JSON.stringify(hanzhongRainSortieState)}`);
+ }
+ assertSortieTacticalRoster(hanzhongRainSortieState, [
+ '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 hanzhongRainPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'ma-liang', 'wei-yan'];
+ for (const unitId of hanzhongRainPriorityUnits) {
+ 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 && !hanzhongRainPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const hanzhongRainSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanzhongRainPriorityUnits.every((unitId) =>
+ hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
+ hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) ||
+ hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
+ hanzhongRainSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ hanzhongRainSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
+ hanzhongRainSortieReadyState.sortiePlan?.recommendedTotal !== 9
+ ) {
+ throw new Error(`Expected Hanzhong rainy-pass sortie to deploy Zhuge Liang plus six logistics/road officers while benching two recommended cavalry roles: ${JSON.stringify(hanzhongRainSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-sixty-first-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-sixty-first-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredSixtyFirstBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'sixty-first-battle-hanzhong-rain-defense';
+ });
+ if (enteredSixtyFirstBattle) {
+ 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 === 'sixty-first-battle-hanzhong-rain-defense' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-sixty-first-battle.png', fullPage: true });
+
+ const sixtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const sixtyFirstEnemies = sixtyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const sixtyFirstAllies = sixtyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
+ const sixtyFirstEnemyBehaviors = new Set(sixtyFirstEnemies.map((unit) => unit.ai));
+ if (
+ sixtyFirstBattleState.camera?.mapWidth !== 122 ||
+ sixtyFirstBattleState.camera?.mapHeight !== 104 ||
+ sixtyFirstBattleState.victoryConditionLabel !== '한중 북문과 두 고을 보급선 방어' ||
+ !sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'repel-cao-zhen-main' && objective.label === '조진 본대 격퇴') ||
+ !sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'check-sima-yi-column' && objective.label === '사마의 별동대 견제') ||
+ !sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'hold-wudu-yinping-supply' && objective.label === '무도·음평 보급선 방어') ||
+ !sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'hanzhong-granary-camps' && objective.label === '한중 창고 진영 유지') ||
+ !sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'rain-pass-roads' && objective.label === '빗길 수레길 확보') ||
+ sixtyFirstEnemies.length < 42 ||
+ !sixtyFirstEnemyBehaviors.has('aggressive') ||
+ !sixtyFirstEnemyBehaviors.has('guard') ||
+ !sixtyFirstEnemyBehaviors.has('hold') ||
+ !sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-leader-cao-zhen') ||
+ !sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-officer-sima-yi') ||
+ !sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-officer-supply') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'jiang-wei') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'wang-ping') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'huang-quan') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'li-yan') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'ma-liang') ||
+ !sixtyFirstAllies.some((unit) => unit.id === 'wei-yan') ||
+ sixtyFirstAllies.some((unit) => unit.id === 'zhao-yun') ||
+ sixtyFirstAllies.some((unit) => unit.id === 'ma-dai') ||
+ sixtyFirstAllies.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected sixty-first battle to use the Hanzhong rainy-pass map, Cao Zhen/Sima Yi objectives, logistics-heavy selected sortie, and mixed Wei AI: ${JSON.stringify(sixtyFirstBattleState)}`);
+ }
+
+ 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 sixtyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ sixtyFirstCampState?.campBattleId !== 'sixty-first-battle-hanzhong-rain-defense' ||
+ sixtyFirstCampState.campaign?.step !== 'sixty-first-camp' ||
+ sixtyFirstCampState.campTitle !== '한중 우로 방어 후 군영' ||
+ sixtyFirstCampState.availableDialogueIds?.length !== 3 ||
+ !sixtyFirstCampState.availableDialogueIds.every((id) => id.startsWith('northern-seventh-')) ||
+ sixtyFirstCampState.availableVisitIds?.length !== 1 ||
+ !sixtyFirstCampState.availableVisitIds.every((id) => id.startsWith('northern-seventh-')) ||
+ sixtyFirstCampState.rosterCollection?.total < 22 ||
+ !sixtyFirstCampState.report?.bonds?.some((bond) => bond.id === 'li-yan__huang-quan_northern-seventh')
+ ) {
+ throw new Error(`Expected Hanzhong rainy-pass aftermath camp to expose seventh northern campaign events and bonds: ${JSON.stringify(sixtyFirstCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-sixty-first-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postSixtyFirstProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postSixtyFirstProgressState?.activeTab !== 'progress' ||
+ postSixtyFirstProgressState.campaignProgress?.completedKnown !== 61 ||
+ postSixtyFirstProgressState.campaignProgress?.totalKnown !== 61 ||
+ postSixtyFirstProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
+ postSixtyFirstProgressState.campaignProgress?.latestBattleTitle !== '한중 우로 방어전' ||
+ postSixtyFirstProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-sixty-first progress tab to complete Hanzhong rainy-pass defense and leave the next northern step pending: ${JSON.stringify(postSixtyFirstProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-sixty-first-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -8617,12 +8777,12 @@ try {
}
}
for (let attempt = 0; attempt < 24; attempt += 1) {
- const continuedToWuduCamp = await page.evaluate(() => {
+ const continuedToRainDefenseCamp = await page.evaluate(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const camp = window.__HEROS_DEBUG__?.camp?.() ?? null;
- return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixtieth-camp';
+ return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixty-first-camp';
});
- if (continuedToWuduCamp) {
+ if (continuedToRainDefenseCamp) {
break;
}
await page.keyboard.press('Space');
@@ -8631,17 +8791,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 === 'sixtieth-camp';
+ return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixty-first-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 !== 'sixtieth-camp') {
- throw new Error(`Expected title continue to reopen the Wudu/Yinping aftermath camp: ${JSON.stringify(titleContinueState)}`);
+ if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'sixty-first-camp') {
+ throw new Error(`Expected title continue to reopen the Hanzhong rainy-pass aftermath camp: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-Wudu/Yinping flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, northern campaign preparation, Qishan road first northern expedition, Tianshui advance, Jieting crisis, Jiang Wei joining, Qishan retreat, Chencang siege, Wudu/Yinping commanderies, and pending next northern step state at ${targetUrl}`);
+ console.log(`Verified title-to-Hanzhong-rain-defense flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, northern campaign preparation, Qishan road first northern expedition, Tianshui advance, Jieting crisis, Jiang Wei joining, Qishan retreat, Chencang siege, Wudu/Yinping commanderies, Hanzhong rainy-pass defense, and pending next northern step state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/sixty-first-battle-map.svg b/src/assets/images/battle/sixty-first-battle-map.svg
new file mode 100644
index 0000000..58d9fc4
--- /dev/null
+++ b/src/assets/images/battle/sixty-first-battle-map.svg
@@ -0,0 +1,85 @@
+
diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts
index e0e1588..b1281e3 100644
--- a/src/game/data/battleMapAssets.ts
+++ b/src/game/data/battleMapAssets.ts
@@ -10,6 +10,7 @@ import fiftyFifthBattleMapUrl from '../../assets/images/battle/fifty-fifth-battl
import fiftyEighthBattleMapUrl from '../../assets/images/battle/fifty-eighth-battle-map.svg';
import fiftyNinthBattleMapUrl from '../../assets/images/battle/fifty-ninth-battle-map.svg';
import sixtiethBattleMapUrl from '../../assets/images/battle/sixtieth-battle-map.svg';
+import sixtyFirstBattleMapUrl from '../../assets/images/battle/sixty-first-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';
@@ -118,6 +119,7 @@ export const battleMapAssets: Record = {
'battle-map-fifty-eighth': fiftyEighthBattleMapUrl,
'battle-map-fifty-ninth': fiftyNinthBattleMapUrl,
'battle-map-sixtieth': sixtiethBattleMapUrl,
+ 'battle-map-sixty-first': sixtyFirstBattleMapUrl,
'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 a01429e..354da4f 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -39,6 +39,10 @@ import {
sixtiethBattleMap,
sixtiethBattleUnits,
sixtiethBattleVictoryPages,
+ sixtyFirstBattleBonds,
+ sixtyFirstBattleMap,
+ sixtyFirstBattleUnits,
+ sixtyFirstBattleVictoryPages,
fiftySeventhBattleBonds,
fiftySeventhBattleMap,
fiftySeventhBattleUnits,
@@ -305,7 +309,8 @@ export type BattleScenarioId =
| 'fifty-seventh-battle-jieting-crisis'
| 'fifty-eighth-battle-qishan-retreat'
| 'fifty-ninth-battle-chencang-siege'
- | 'sixtieth-battle-wudu-yinping';
+ | 'sixtieth-battle-wudu-yinping'
+ | 'sixty-first-battle-hanzhong-rain-defense';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -4033,6 +4038,87 @@ export const sixtiethBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const sixtyFirstBattleScenario: BattleScenarioDefinition = {
+ id: 'sixty-first-battle-hanzhong-rain-defense',
+ title: '한중 우로 방어전',
+ victoryConditionLabel: '한중 북문과 두 고을 보급선 방어',
+ defeatConditionLabel: '제갈량 퇴각 또는 보급 방어선 붕괴',
+ openingObjectiveLines: [
+ '무도와 음평을 확보한 뒤 위군은 조진을 앞세워 한중 북문의 빗길을 압박합니다. 새로 얻은 두 고을은 이제 전리품이 아니라 지켜야 할 보급 책임입니다.',
+ '조진의 본대를 늦추고 사마의 별동대가 봉화로 길을 잇기 전에 끊으십시오. 강유와 왕평은 길을 보고, 이엄과 황권은 군량 흐름을 맞추며, 위연은 반격할 순간을 기다립니다.',
+ '한중 창고 진영, 무도·음평 수레 길, 전방 마을을 함께 지키십시오. 어느 하나가 무너지면 다음 북벌은 출발하기 전에 숨이 막힙니다.'
+ ],
+ map: sixtyFirstBattleMap,
+ units: sixtyFirstBattleUnits,
+ bonds: sixtyFirstBattleBonds,
+ mapTextureKey: 'battle-map-sixty-first',
+ leaderUnitId: 'northern-seventh-leader-cao-zhen',
+ quickVictoryTurnLimit: 122,
+ baseVictoryGold: 18480,
+ objectives: [
+ {
+ id: 'repel-cao-zhen-main',
+ kind: 'defeat-leader',
+ label: '조진 본대 격퇴',
+ rewardGold: 13000,
+ unitId: 'northern-seventh-leader-cao-zhen'
+ },
+ {
+ id: 'check-sima-yi-column',
+ kind: 'defeat-leader',
+ label: '사마의 별동대 견제',
+ rewardGold: 6680,
+ unitId: 'northern-seventh-officer-sima-yi'
+ },
+ {
+ id: 'hold-wudu-yinping-supply',
+ kind: 'defeat-leader',
+ label: '무도·음평 보급선 방어',
+ rewardGold: 6540,
+ unitId: 'northern-seventh-officer-supply'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 3300,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'hanzhong-granary-camps',
+ kind: 'secure-terrain',
+ label: '한중 창고 진영 유지',
+ rewardGold: 6260,
+ terrain: 'camp'
+ },
+ {
+ id: 'rain-pass-roads',
+ kind: 'secure-terrain',
+ label: '빗길 수레길 확보',
+ rewardGold: 6120,
+ terrain: 'road'
+ },
+ {
+ id: 'front-villages',
+ kind: 'secure-terrain',
+ label: '전방 마을 안심',
+ rewardGold: 5960,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '122턴 이내 방어선 안정',
+ rewardGold: 4040,
+ maxTurn: 122
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
+ itemRewards: ['금 118', '상처약 94', '군주 46', '한중 우로 장부 1', '조진 역공 기록 1'],
+ victoryPages: sixtyFirstBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -4095,7 +4181,8 @@ export const battleScenarios: Record
'fifty-seventh-battle-jieting-crisis': fiftySeventhBattleScenario,
'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario,
'fifty-ninth-battle-chencang-siege': fiftyNinthBattleScenario,
- 'sixtieth-battle-wudu-yinping': sixtiethBattleScenario
+ 'sixtieth-battle-wudu-yinping': sixtiethBattleScenario,
+ 'sixty-first-battle-hanzhong-rain-defense': sixtyFirstBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index dcb600e..6ed8d7a 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -15,6 +15,7 @@ import {
fiftyNinthBattleScenario,
fiftySeventhBattleScenario,
sixtiethBattleScenario,
+ sixtyFirstBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -78,6 +79,8 @@ import {
fiftyNinthBattleVictoryPages,
sixtiethBattleIntroPages,
sixtiethBattleVictoryPages,
+ sixtyFirstBattleIntroPages,
+ sixtyFirstBattleVictoryPages,
fiftyFirstBattleIntroPages,
fiftyFirstBattleVictoryPages,
fiftySecondBattleIntroPages,
@@ -883,13 +886,24 @@ const sortieFlows: Record = {
},
[sixtiethBattleScenario.id]: {
afterBattleId: sixtiethBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '무도·음평 확보 이후',
+ eyebrow: '다음 전장',
+ title: sixtyFirstBattleScenario.title,
description:
- '무도와 음평의 산길을 확보했지만, 얻은 두 고을은 북벌의 끝이 아니라 더 넓어진 책임입니다. 다음 북방 계산에서는 새 고을을 지키는 보급과 다시 나아갈 장수의 고집을 함께 다루어야 합니다.',
- rewardHint: '다음 목표: 두 고을 안정 / 다음 북벌 보급 재계산',
- pages: sixtiethBattleVictoryPages,
- unavailableNotice: '무도·음평 확보 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 두 고을 안정, 산길 표식, 강유와 위연의 전진 기준을 확인할 수 있습니다.'
+ '무도와 음평의 산길을 확보하자 위군은 조진의 역공으로 한중 북문과 새 보급선을 동시에 흔듭니다. 이번에는 전진보다 방어, 돌파보다 장부, 새 고을을 지키는 책임을 7명 안에서 다시 골라야 합니다.',
+ rewardHint: `예상 보상: ${sixtyFirstBattleScenario.title} 개방 / 한중 우로 장부`,
+ nextBattleId: sixtyFirstBattleScenario.id,
+ campaignStep: 'sixty-first-battle',
+ pages: [...sixtiethBattleVictoryPages, ...sixtyFirstBattleIntroPages]
+ },
+ [sixtyFirstBattleScenario.id]: {
+ afterBattleId: sixtyFirstBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '한중 우로 방어 이후',
+ description:
+ '조진의 역공을 막아 냈지만 한중 북문, 무도, 음평을 함께 지키는 부담은 그대로 남았습니다. 다음 북방 계산에서는 지켜 낸 길을 다시 공격로로 바꾸는 일과 후방에 남길 장수를 함께 정해야 합니다.',
+ rewardHint: '다음 목표: 한중 방어선 재정비 / 다음 북벌 공격로 검토',
+ pages: sixtyFirstBattleVictoryPages,
+ unavailableNotice: '한중 우로 방어 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 조진 역공 방어, 한중 창고 유지, 무도·음평 보급 책임을 확인할 수 있습니다.'
},
'northern-campaign-prep-camp-legacy': {
afterBattleId: fiftyFourthBattleScenario.id,
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index fc08c71..318823f 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -3854,6 +3854,59 @@ export const sixtiethBattleVictoryPages: StoryPage[] = [
}
];
+export const sixtyFirstBattleIntroPages: StoryPage[] = [
+ {
+ id: 'sixty-first-hanzhong-rain-warning',
+ bgm: 'story-dark',
+ chapter: '한중의 빗길',
+ background: 'story-three-heroes',
+ speaker: '황권',
+ text: '무도와 음평을 얻은 뒤부터 한중의 장부는 더 두꺼워졌습니다. 조진이 북문으로 내려오고, 사마의가 빗길을 따라 별동대를 움직이면 새 고을은 승리의 이름이 아니라 지켜야 할 짐이 됩니다.'
+ },
+ {
+ id: 'sixty-first-cao-zhen-counteroffensive',
+ bgm: 'battle-prep',
+ chapter: '조진의 역공',
+ background: 'story-sortie',
+ speaker: '제갈량',
+ text: '이번에는 성을 치지 않습니다. 한중 북문의 창고, 무도와 음평으로 이어지는 수레 길, 그리고 비에 무너지는 고개를 함께 지켜야 합니다. 위연의 돌파와 강유의 길눈, 이엄과 황권의 보급 장부 중 무엇을 앞세울지 정하십시오.'
+ },
+ {
+ id: 'sixty-first-two-commandery-burden',
+ bgm: 'story-dark',
+ chapter: '넓어진 책임',
+ background: 'story-militia',
+ speaker: '마량',
+ text: '백성은 촉한의 깃발만 보고 안심하지 않습니다. 비가 오면 수레는 늦고, 군문이 흔들리면 새 고을은 먼저 두려워합니다. 출전 명단에서 누가 길을 잡고 누가 장부를 지킬지 분명히 해야 합니다.'
+ }
+];
+
+export const sixtyFirstBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'sixty-first-victory-rain-defense',
+ bgm: 'militia-theme',
+ chapter: '빗길을 막다',
+ background: 'story-sortie',
+ text: '촉한군은 한중 북문의 창고를 비워 내지 않고 지켜 냈고, 무도와 음평으로 향하는 수레 길도 끊기지 않게 붙들었습니다. 조진의 역공은 빗길과 산길 사이에서 힘을 잃었습니다.'
+ },
+ {
+ id: 'sixty-first-victory-supply-responsibility',
+ bgm: 'story-dark',
+ chapter: '장부의 무게',
+ background: 'story-three-heroes',
+ speaker: '이엄',
+ text: '고을을 얻는 일보다 고을을 먹여 살리는 일이 더 오래 갑니다. 황권의 장부와 마량의 문서가 늦어지면, 강유와 위연이 아무리 앞에서 길을 열어도 뒤의 비명은 전선으로 돌아옵니다.'
+ },
+ {
+ id: 'sixty-first-next-northern-plan',
+ bgm: 'story-dark',
+ chapter: '다시 북쪽을 보다',
+ background: 'story-militia',
+ speaker: '강유',
+ text: '조진을 막아 냈지만 위의 북방은 한 번 물러난 것으로 끝나지 않습니다. 다음 출정에서는 지켜 낸 길을 다시 공격의 길로 바꿀 수 있는지, 그리고 그때 누구를 뒤에 남길지까지 따져야 합니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -4359,6 +4412,12 @@ export const sixtiethBattleMap: BattleMap = {
terrain: createSixtiethBattleTerrain()
};
+export const sixtyFirstBattleMap: BattleMap = {
+ width: 122,
+ height: 104,
+ terrain: createSixtyFirstBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -17576,6 +17635,97 @@ export const sixtiethBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('northern-sixth-leader-guo-huai', '곽회', '위군 북방 구원 총대장', 'rebelLeader', 204, 1580, 372, 112, 36)
];
+const sixtyFirstBattleAllyPositions: Record = {
+ 'guan-yu': { x: 17, y: 92 },
+ 'zhang-fei': { x: 23, y: 94 },
+ 'jian-yong': { x: 9, y: 99 },
+ 'mi-zhu': { x: 13, y: 100 },
+ 'sun-qian': { x: 18, y: 100 },
+ 'zhao-yun': { x: 30, y: 91 },
+ 'zhuge-liang': { x: 18, y: 88 },
+ 'ma-liang': { x: 24, y: 85 },
+ 'yi-ji': { x: 8, y: 102 },
+ 'gong-zhi': { x: 28, y: 100 },
+ 'huang-zhong': { x: 38, y: 88 },
+ 'wei-yan': { x: 45, y: 90 },
+ 'pang-tong': { x: 15, y: 84 },
+ 'fa-zheng': { x: 28, y: 83 },
+ 'wu-yi': { x: 46, y: 98 },
+ 'yan-yan': { x: 50, y: 91 },
+ 'li-yan': { x: 34, y: 82 },
+ 'huang-quan': { x: 12, y: 87 },
+ 'ma-chao': { x: 53, y: 87 },
+ 'ma-dai': { x: 58, y: 94 },
+ 'wang-ping': { x: 41, y: 82 },
+ 'jiang-wei': { x: 34, y: 85 }
+};
+
+export const sixtyFirstBattleUnits: 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, sixtyFirstBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-a', '자오곡 우중 척후병', '서쪽 빗길 정찰', 'bandit', 205, 1030, 286, 50, 82),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-b', '무도 보급로 척후병', '보급로 정찰', 'bandit', 205, 1030, 286, 62, 79),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-c', '한중 북문 척후병', '중앙 고개 정찰', 'bandit', 206, 1042, 288, 76, 72),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-d', '음평 물목 척후병', '동쪽 물목 정찰', 'bandit', 206, 1042, 288, 90, 64),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-e', '조진 전초 척후병', '조진 본영 정찰', 'bandit', 207, 1054, 290, 106, 49),
+ createThirtyFirstEnemyUnit('northern-seventh-scout-f', '북문 봉화 척후병', '북쪽 봉화 정찰', 'bandit', 207, 1054, 290, 118, 30),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-a', '위군 자오곡 보병', '서쪽 고개 보병', 'yellowTurban', 206, 1460, 352, 54, 76),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-b', '위군 무도 보병', '보급로 보병', 'yellowTurban', 206, 1460, 352, 66, 72),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-c', '위군 한중 보병', '중앙 보병', 'yellowTurban', 207, 1480, 356, 79, 66),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-d', '위군 음평 보병', '동쪽 물목 보병', 'yellowTurban', 207, 1480, 356, 92, 59),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-e', '위군 북문 보병', '방어선 보병', 'yellowTurban', 208, 1500, 360, 104, 52),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-f', '조진 본영 보병', '조진 본영 보병', 'yellowTurban', 208, 1500, 360, 114, 40),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-g', '위군 보급 차단병', '수레길 차단 보병', 'yellowTurban', 209, 1520, 364, 98, 80),
+ createThirtyFirstEnemyUnit('northern-seventh-infantry-h', '사마의 별동 보병', '별동대 보병', 'yellowTurban', 209, 1520, 364, 119, 22),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-a', '자오곡 우중 궁병', '서쪽 능선 궁병', 'archer', 206, 1090, 300, 61, 67),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-b', '무도 보급로 궁병', '보급로 궁병', 'archer', 206, 1090, 300, 73, 62),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-c', '한중 북문 궁병', '중앙 능선 궁병', 'archer', 207, 1102, 302, 88, 55),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-d', '음평 물목 궁병', '동쪽 물목 궁병', 'archer', 208, 1114, 304, 100, 47),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-e', '조진 본영 궁병', '조진 본영 궁병', 'archer', 208, 1114, 304, 112, 35),
+ createThirtyFirstEnemyUnit('northern-seventh-archer-f', '사마의 봉화 궁병', '봉화대 궁병', 'archer', 209, 1126, 306, 120, 18),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-a', '조진 역공 기병', '서쪽 역공 기병', 'cavalry', 207, 1520, 370, 52, 70),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-b', '조진 역공 기병', '무도 역공 기병', 'cavalry', 207, 1520, 370, 65, 67),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-c', '조진 역공 기병', '중앙 역공 기병', 'cavalry', 208, 1542, 374, 80, 60),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-d', '조진 역공 기병', '음평 역공 기병', 'cavalry', 209, 1564, 378, 94, 53),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-e', '사마의 별동 기병', '별동대 기병', 'cavalry', 209, 1564, 378, 108, 45),
+ createThirtyFirstEnemyUnit('northern-seventh-cavalry-f', '북문 차단 기병', '북쪽 차단 기병', 'cavalry', 210, 1586, 382, 119, 28),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-a', '자오곡 빗길 수비대', '서쪽 빗길 수비', 'yellowTurban', 207, 1480, 358, 47, 56),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-b', '무도 보급로 수비대', '보급로 수비', 'yellowTurban', 207, 1480, 358, 62, 51),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-c', '한중 북문 수비대', '중앙 고개 수비', 'yellowTurban', 208, 1500, 362, 78, 43),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-d', '음평 물목 수비대', '동쪽 물목 수비', 'yellowTurban', 208, 1500, 362, 94, 36),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-e', '조진 본영 수비대', '조진 본영 수비', 'yellowTurban', 209, 1520, 366, 108, 28),
+ createThirtyFirstEnemyUnit('northern-seventh-rain-guard-f', '북쪽 봉화 수비대', '북쪽 봉화 수비', 'yellowTurban', 209, 1520, 366, 120, 14),
+ createThirtyFirstEnemyUnit('northern-seventh-strategist-a', '위군 우로 군리', '빗길 장부 군리', 'strategist', 207, 1160, 316, 76, 58),
+ createThirtyFirstEnemyUnit('northern-seventh-strategist-b', '위군 보급 군리', '보급 차단 군리', 'strategist', 208, 1172, 318, 90, 49),
+ createThirtyFirstEnemyUnit('northern-seventh-strategist-c', '조진 본영 군리', '조진 본영 군리', 'strategist', 208, 1172, 318, 104, 40),
+ createThirtyFirstEnemyUnit('northern-seventh-strategist-d', '사마의 별동 군리', '별동대 장부 군리', 'strategist', 209, 1184, 320, 110, 68),
+ createThirtyFirstEnemyUnit('northern-seventh-officer-hanzhong', '한중 북문 장수', '한중 북문 압박 장수', 'rebelLeader', 211, 1580, 378, 88, 74),
+ createThirtyFirstEnemyUnit('northern-seventh-officer-supply', '보급 차단장', '무도·음평 보급 차단 장수', 'rebelLeader', 211, 1580, 378, 101, 62),
+ createThirtyFirstEnemyUnit('northern-seventh-officer-guo-huai', '곽회', '북방 보조 장수', 'rebelLeader', 210, 1560, 374, 110, 72),
+ createThirtyFirstEnemyUnit('northern-seventh-officer-rain-road', '우로 감군', '빗길 감군 장수', 'rebelLeader', 210, 1560, 374, 82, 82),
+ createThirtyFirstEnemyUnit('northern-seventh-officer-sima-yi', '사마의', '별동대 총지휘', 'rebelLeader', 212, 1620, 386, 116, 24),
+ createThirtyFirstEnemyUnit('northern-seventh-leader-cao-zhen', '조진', '위군 역공 총대장', 'rebelLeader', 213, 1660, 392, 114, 38)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -18893,6 +19043,46 @@ export const sixtiethBattleBonds: BattleBond[] = [
...northernSixthBattleBonds
].map(cloneBattleBondForScenario);
+export const northernSeventhBattleBonds: BattleBond[] = [
+ {
+ id: 'li-yan__huang-quan_northern-seventh',
+ unitIds: ['li-yan', 'huang-quan'],
+ title: '빗길 장부',
+ level: 46,
+ exp: 0,
+ description: '이엄의 운송 책임과 황권의 보급 장부가 맞물리면 한중 북문과 무도·음평으로 갈라지는 군량 흐름을 늦더라도 끊기지 않게 붙들 수 있습니다.'
+ },
+ {
+ id: 'jiang-wei__wang-ping_northern-seventh',
+ unitIds: ['jiang-wei', 'wang-ping'],
+ title: '새 고을을 지키는 길눈',
+ level: 50,
+ exp: 0,
+ description: '강유의 전방 길눈과 왕평의 진영 표식이 함께 움직이면 빗길 속에서도 어느 길을 버리고 어느 길을 지킬지 빠르게 정할 수 있습니다.'
+ },
+ {
+ id: 'zhuge-liang__wei-yan_northern-seventh',
+ unitIds: ['zhuge-liang', 'wei-yan'],
+ title: '돌파와 제동',
+ level: 48,
+ exp: 0,
+ description: '제갈량의 전선 계산과 위연의 돌파력이 서로의 한계를 인정하면, 조진의 역공을 막으면서도 반격할 순간을 잃지 않습니다.'
+ },
+ {
+ id: 'ma-liang__huang-quan_northern-seventh',
+ unitIds: ['ma-liang', 'huang-quan'],
+ title: '고시문과 군량표',
+ level: 44,
+ exp: 0,
+ description: '마량의 민심 문서와 황권의 군량표가 함께 움직이면 새 고을의 백성이 방어전 속에서도 촉한의 약속을 실제 배분으로 확인할 수 있습니다.'
+ }
+];
+
+export const sixtyFirstBattleBonds: BattleBond[] = [
+ ...sixtiethBattleBonds,
+ ...northernSeventhBattleBonds
+].map(cloneBattleBondForScenario);
+
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
Array.from({ length: 26 }, (_, x): TerrainType => {
@@ -22211,6 +22401,84 @@ function createSixtiethBattleTerrain(): TerrainType[][] {
);
}
+function createSixtyFirstBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 104 }, (_, y) =>
+ Array.from({ length: 122 }, (_, x): TerrainType => {
+ if (
+ (x >= 5 && x <= 12 && y >= 0 && y <= 103) ||
+ (x >= 63 && x <= 70 && y >= 14 && y <= 103) ||
+ (x >= 0 && x <= 121 && y >= 96)
+ ) {
+ return 'river';
+ }
+ if (
+ (x >= 8 && x <= 82 && y >= 88 && y <= 94) ||
+ (x >= 22 && x <= 101 && y >= 77 && y <= 85) ||
+ (x >= 48 && x <= 110 && y >= 65 && y <= 75) ||
+ (x >= 76 && x <= 118 && y >= 51 && y <= 63) ||
+ (x >= 92 && x <= 121 && y >= 37 && y <= 48) ||
+ (x >= 30 && x <= 39 && y >= 69 && y <= 98) ||
+ (x >= 54 && x <= 63 && y >= 56 && y <= 90) ||
+ (x >= 84 && x <= 94 && y >= 43 && y <= 80) ||
+ (x >= 98 && x <= 108 && y >= 24 && y <= 66)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 0 && x <= 36 && y >= 60 && y <= 92) ||
+ (x >= 42 && x <= 80 && y >= 74 && y <= 95) ||
+ (x >= 88 && x <= 121 && y >= 68 && y <= 93) ||
+ (x >= 14 && x <= 40 && y >= 44 && y <= 62) ||
+ (x >= 98 && x <= 121 && y >= 52 && y <= 72)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 50 && y <= 40) ||
+ (x >= 46 && x <= 92 && y <= 34) ||
+ (x >= 86 && x <= 121 && y <= 52) ||
+ (x >= 98 && x <= 121 && y >= 45 && y <= 72)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 94) ||
+ (x >= 119 && y <= 88) ||
+ (x >= 38 && x <= 45 && y >= 0 && y <= 64) ||
+ (x >= 76 && x <= 82 && y >= 0 && y <= 46) ||
+ (x >= 59 && x <= 67 && y >= 85 && y <= 103)
+ ) {
+ return 'cliff';
+ }
+ if (
+ (x >= 12 && x <= 29 && y >= 84 && y <= 94) ||
+ (x >= 43 && x <= 63 && y >= 72 && y <= 84) ||
+ (x >= 79 && x <= 98 && y >= 58 && y <= 70) ||
+ (x >= 101 && x <= 118 && y >= 43 && y <= 56)
+ ) {
+ return 'village';
+ }
+ if (
+ (x >= 19 && x <= 46 && y >= 79 && y <= 92) ||
+ (x >= 54 && x <= 80 && y >= 66 && y <= 78) ||
+ (x >= 88 && x <= 112 && y >= 52 && y <= 65) ||
+ (x >= 98 && x <= 116 && y >= 24 && y <= 38)
+ ) {
+ return 'camp';
+ }
+ if (
+ (x >= 48 && x <= 68 && y >= 47 && y <= 62) ||
+ (x >= 72 && x <= 98 && y >= 37 && y <= 52) ||
+ (x >= 94 && x <= 118 && y >= 46 && y <= 62) ||
+ (x >= 103 && x <= 121 && y >= 14 && y <= 35)
+ ) {
+ 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 9fe39d7..d0fc568 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -55,6 +55,7 @@ import {
northernFourthBattleBonds,
northernFirstBattleBonds,
northernSecondBattleBonds,
+ northernSeventhBattleBonds,
northernSixthBattleBonds,
northernThirdBattleBonds,
wulingRecruitBonds,
@@ -445,9 +446,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'fifty-seventh-battle-jieting-crisis',
'fifty-eighth-battle-qishan-retreat',
'fifty-ninth-battle-chencang-siege',
- 'sixtieth-battle-wudu-yinping'
+ 'sixtieth-battle-wudu-yinping',
+ 'sixty-first-battle-hanzhong-rain-defense'
],
- nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보']
+ nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어']
}
];
@@ -511,7 +513,8 @@ const campBattleIds = {
fiftySeventh: 'fifty-seventh-battle-jieting-crisis',
fiftyEighth: 'fifty-eighth-battle-qishan-retreat',
fiftyNinth: 'fifty-ninth-battle-chencang-siege',
- sixtieth: 'sixtieth-battle-wudu-yinping'
+ sixtieth: 'sixtieth-battle-wudu-yinping',
+ sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1261,6 +1264,23 @@ const sortieRulesByBattleId: Partial