diff --git a/docs/roadmap.md b/docs/roadmap.md
index 64bc4e5..0c1c786 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -94,15 +94,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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
+- Sixty-second battle Qishan renewed offensive route, where the preserved Hanzhong and Wudu/Yinping supply roads turn back into an attack decision, forcing nine recommended officers into seven slots across a 124x106 battlefield while Zhuge Liang pressures Sima Yi's delay line, Zhang He's pursuit, Guo Huai's flank block, and Lucheng-front villages
+- Post-Qishan-renewed camp events with Jiang Wei, Wei Yan, Zhao Yun, Wang Ping, Huang Quan, Ma Liang, Li Yan, and Ma Dai focusing on breakthrough limits, return markers, village proclamations, and the next deeper northern-front calculation
- 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, 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
+- Flow verification script from title through the Wudu/Yinping commanderies, Hanzhong rainy-pass defense, Qishan renewed offensive, and post-renewed-offensive 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 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
+1. Build the next northern-front step after Qishan renewed offensive, turning the pressure on Sima Yi's delay line into the next deeper northern campaign choice without treating the successful push 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 7c725c7..f2acfc9 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/';
@@ -1829,7 +1829,7 @@ try {
if (
progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 ||
- progressTabState.campaignProgress?.totalKnown !== 61 ||
+ progressTabState.campaignProgress?.totalKnown !== 62 ||
progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' ||
progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전'
@@ -1980,7 +1980,7 @@ try {
if (
postBowangProgressState?.activeTab !== 'progress' ||
postBowangProgressState.campaignProgress?.completedKnown !== 18 ||
- postBowangProgressState.campaignProgress?.totalKnown !== 61 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 62 ||
postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' ||
postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로'
@@ -2118,7 +2118,7 @@ try {
if (
postChangbanProgressState?.activeTab !== 'progress' ||
postChangbanProgressState.campaignProgress?.completedKnown !== 19 ||
- postChangbanProgressState.campaignProgress?.totalKnown !== 61 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 62 ||
postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' ||
postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로'
@@ -2256,7 +2256,7 @@ try {
if (
postJiangdongProgressState?.activeTab !== 'progress' ||
postJiangdongProgressState.campaignProgress?.completedKnown !== 20 ||
- postJiangdongProgressState.campaignProgress?.totalKnown !== 61 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 62 ||
postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' ||
postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전'
@@ -2396,7 +2396,7 @@ try {
if (
postRedCliffsProgressState?.activeTab !== 'progress' ||
postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 ||
- postRedCliffsProgressState.campaignProgress?.totalKnown !== 61 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 62 ||
postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' ||
postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전'
@@ -2536,7 +2536,7 @@ try {
if (
postFireAttackProgressState?.activeTab !== 'progress' ||
postFireAttackProgressState.campaignProgress?.completedKnown !== 22 ||
- postFireAttackProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 62 ||
postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' ||
postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전'
@@ -2679,7 +2679,7 @@ try {
if (
postJingzhouProgressState?.activeTab !== 'progress' ||
postJingzhouProgressState.campaignProgress?.completedKnown !== 23 ||
- postJingzhouProgressState.campaignProgress?.totalKnown !== 61 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 62 ||
postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' ||
postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전'
@@ -2821,7 +2821,7 @@ try {
if (
postGuiyangProgressState?.activeTab !== 'progress' ||
postGuiyangProgressState.campaignProgress?.completedKnown !== 24 ||
- postGuiyangProgressState.campaignProgress?.totalKnown !== 61 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 62 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
@@ -2964,7 +2964,7 @@ try {
if (
postWulingProgressState?.activeTab !== 'progress' ||
postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
- postWulingProgressState.campaignProgress?.totalKnown !== 61 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 62 ||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전'
@@ -3113,7 +3113,7 @@ try {
if (
postChangshaProgressState?.activeTab !== 'progress' ||
postChangshaProgressState.campaignProgress?.completedKnown !== 26 ||
- postChangshaProgressState.campaignProgress?.totalKnown !== 61 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 62 ||
postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' ||
postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로'
@@ -3258,7 +3258,7 @@ try {
if (
postYizhouProgressState?.activeTab !== 'progress' ||
postYizhouProgressState.campaignProgress?.completedKnown !== 27 ||
- postYizhouProgressState.campaignProgress?.totalKnown !== 61 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 62 ||
postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' ||
postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전'
@@ -3405,7 +3405,7 @@ try {
if (
postFuPassProgressState?.activeTab !== 'progress' ||
postFuPassProgressState.campaignProgress?.completedKnown !== 28 ||
- postFuPassProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 62 ||
postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' ||
postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전'
@@ -3554,7 +3554,7 @@ try {
if (
postLuoProgressState?.activeTab !== 'progress' ||
postLuoProgressState.campaignProgress?.completedKnown !== 29 ||
- postLuoProgressState.campaignProgress?.totalKnown !== 61 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 62 ||
postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' ||
postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전'
@@ -3700,7 +3700,7 @@ try {
if (
postLuofengProgressState?.activeTab !== 'progress' ||
postLuofengProgressState.campaignProgress?.completedKnown !== 30 ||
- postLuofengProgressState.campaignProgress?.totalKnown !== 61 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 62 ||
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략'
@@ -3852,7 +3852,7 @@ try {
if (
postLuoMainProgressState?.activeTab !== 'progress' ||
postLuoMainProgressState.campaignProgress?.completedKnown !== 31 ||
- postLuoMainProgressState.campaignProgress?.totalKnown !== 61 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 62 ||
postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' ||
postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전'
@@ -4005,7 +4005,7 @@ try {
if (
postMianzhuProgressState?.activeTab !== 'progress' ||
postMianzhuProgressState.campaignProgress?.completedKnown !== 32 ||
- postMianzhuProgressState.campaignProgress?.totalKnown !== 61 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 62 ||
postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전'
@@ -4159,7 +4159,7 @@ try {
if (
postChengduProgressState?.activeTab !== 'progress' ||
postChengduProgressState.campaignProgress?.completedKnown !== 33 ||
- postChengduProgressState.campaignProgress?.totalKnown !== 61 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 62 ||
postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' ||
postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전'
@@ -4314,7 +4314,7 @@ try {
if (
postJiamengProgressState?.activeTab !== 'progress' ||
postJiamengProgressState.campaignProgress?.completedKnown !== 34 ||
- postJiamengProgressState.campaignProgress?.totalKnown !== 61 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 62 ||
postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' ||
postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전'
@@ -4470,7 +4470,7 @@ try {
if (
postYangpingProgressState?.activeTab !== 'progress' ||
postYangpingProgressState.campaignProgress?.completedKnown !== 35 ||
- postYangpingProgressState.campaignProgress?.totalKnown !== 61 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 62 ||
postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' ||
postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전'
@@ -4627,7 +4627,7 @@ try {
if (
postDingjunProgressState?.activeTab !== 'progress' ||
postDingjunProgressState.campaignProgress?.completedKnown !== 36 ||
- postDingjunProgressState.campaignProgress?.totalKnown !== 61 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 62 ||
postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' ||
postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전'
@@ -4780,7 +4780,7 @@ try {
if (
postHanzhongProgressState?.activeTab !== 'progress' ||
postHanzhongProgressState.campaignProgress?.completedKnown !== 37 ||
- postHanzhongProgressState.campaignProgress?.totalKnown !== 61 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 62 ||
postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비'
@@ -4841,7 +4841,7 @@ try {
if (
postKingCouncilProgressState?.activeTab !== 'progress' ||
postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 ||
- postKingCouncilProgressState.campaignProgress?.totalKnown !== 61 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 62 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4901,7 +4901,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 62 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -5052,7 +5052,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 61 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 62 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5205,7 +5205,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 62 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5359,7 +5359,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 61 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 62 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5513,7 +5513,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 62 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5675,7 +5675,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 61 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 62 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5830,7 +5830,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
- postGonganProgressState.campaignProgress?.totalKnown !== 61 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 62 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5988,7 +5988,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
- postMaichengProgressState.campaignProgress?.totalKnown !== 61 ||
+ postMaichengProgressState.campaignProgress?.totalKnown !== 62 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6145,7 +6145,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
- postYilingProgressState.campaignProgress?.totalKnown !== 61 ||
+ postYilingProgressState.campaignProgress?.totalKnown !== 62 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6303,7 +6303,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
- postYilingFireProgressState.campaignProgress?.totalKnown !== 61 ||
+ postYilingFireProgressState.campaignProgress?.totalKnown !== 62 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6369,7 +6369,7 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
- postBaidiProgressState.campaignProgress?.totalKnown !== 61 ||
+ postBaidiProgressState.campaignProgress?.totalKnown !== 62 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
@@ -6530,7 +6530,7 @@ try {
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
- postNanzhongProgressState.campaignProgress?.totalKnown !== 61 ||
+ postNanzhongProgressState.campaignProgress?.totalKnown !== 62 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
) {
@@ -6692,7 +6692,7 @@ try {
if (
postMengHuoProgressState?.activeTab !== 'progress' ||
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
- postMengHuoProgressState.campaignProgress?.totalKnown !== 61 ||
+ postMengHuoProgressState.campaignProgress?.totalKnown !== 62 ||
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
) {
@@ -6854,7 +6854,7 @@ try {
if (
postSecondCaptureProgressState?.activeTab !== 'progress' ||
postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
- postSecondCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postSecondCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전'
) {
@@ -7019,7 +7019,7 @@ try {
if (
postThirdCaptureProgressState?.activeTab !== 'progress' ||
postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 ||
- postThirdCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postThirdCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' ||
postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전'
) {
@@ -7185,7 +7185,7 @@ try {
if (
postFourthCaptureProgressState?.activeTab !== 'progress' ||
postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 ||
- postFourthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFourthCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' ||
postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전'
) {
@@ -7351,7 +7351,7 @@ try {
if (
postFifthCaptureProgressState?.activeTab !== 'progress' ||
postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 ||
- postFifthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFifthCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' ||
postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전'
) {
@@ -7516,7 +7516,7 @@ try {
if (
postSixthCaptureProgressState?.activeTab !== 'progress' ||
postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 ||
- postSixthCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postSixthCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' ||
postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전'
) {
@@ -7682,7 +7682,7 @@ try {
if (
postFinalCaptureProgressState?.activeTab !== 'progress' ||
postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 ||
- postFinalCaptureProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFinalCaptureProgressState.campaignProgress?.totalKnown !== 62 ||
postFinalCaptureProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFinalCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
!String(postFinalCaptureProgressState.campaignProgress?.nextBattleTitle ?? '').includes('북벌 준비')
@@ -7755,7 +7755,7 @@ try {
if (
postNorthernPrepProgressState?.activeTab !== 'progress' ||
postNorthernPrepProgressState.campaignProgress?.completedKnown !== 54 ||
- postNorthernPrepProgressState.campaignProgress?.totalKnown !== 61 ||
+ postNorthernPrepProgressState.campaignProgress?.totalKnown !== 62 ||
postNorthernPrepProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postNorthernPrepProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진로'
@@ -7905,7 +7905,7 @@ try {
if (
postFiftyFifthProgressState?.activeTab !== 'progress' ||
postFiftyFifthProgressState.campaignProgress?.completedKnown !== 55 ||
- postFiftyFifthProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFiftyFifthProgressState.campaignProgress?.totalKnown !== 62 ||
postFiftyFifthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftyFifthProgressState.campaignProgress?.latestBattleTitle !== '제1차 북벌 출진로' ||
postFiftyFifthProgressState.campaignProgress?.nextBattleTitle !== '천수 진군로'
@@ -8055,7 +8055,7 @@ try {
if (
postFiftySixthProgressState?.activeTab !== 'progress' ||
postFiftySixthProgressState.campaignProgress?.completedKnown !== 56 ||
- postFiftySixthProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFiftySixthProgressState.campaignProgress?.totalKnown !== 62 ||
postFiftySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftySixthProgressState.campaignProgress?.latestBattleTitle !== '천수 진군로' ||
postFiftySixthProgressState.campaignProgress?.nextBattleTitle !== '가정 배치 위기'
@@ -8210,7 +8210,7 @@ try {
if (
postFiftySeventhProgressState?.activeTab !== 'progress' ||
postFiftySeventhProgressState.campaignProgress?.completedKnown !== 57 ||
- postFiftySeventhProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFiftySeventhProgressState.campaignProgress?.totalKnown !== 62 ||
postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' ||
postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로'
@@ -8361,7 +8361,7 @@ try {
if (
postFiftyEighthProgressState?.activeTab !== 'progress' ||
postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 ||
- postFiftyEighthProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFiftyEighthProgressState.campaignProgress?.totalKnown !== 62 ||
postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' ||
postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '진창 공성전'
@@ -8512,7 +8512,7 @@ try {
if (
postFiftyNinthProgressState?.activeTab !== 'progress' ||
postFiftyNinthProgressState.campaignProgress?.completedKnown !== 59 ||
- postFiftyNinthProgressState.campaignProgress?.totalKnown !== 61 ||
+ postFiftyNinthProgressState.campaignProgress?.totalKnown !== 62 ||
postFiftyNinthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFiftyNinthProgressState.campaignProgress?.latestBattleTitle !== '진창 공성전' ||
postFiftyNinthProgressState.campaignProgress?.nextBattleTitle !== '무도·음평 확보전'
@@ -8669,7 +8669,7 @@ try {
if (
postSixtiethProgressState?.activeTab !== 'progress' ||
postSixtiethProgressState.campaignProgress?.completedKnown !== 60 ||
- postSixtiethProgressState.campaignProgress?.totalKnown !== 61 ||
+ postSixtiethProgressState.campaignProgress?.totalKnown !== 62 ||
postSixtiethProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postSixtiethProgressState.campaignProgress?.latestBattleTitle !== '무도·음평 확보전' ||
postSixtiethProgressState.campaignProgress?.nextBattleTitle !== '한중 우로 방어전'
@@ -8829,15 +8829,178 @@ try {
if (
postSixtyFirstProgressState?.activeTab !== 'progress' ||
postSixtyFirstProgressState.campaignProgress?.completedKnown !== 61 ||
- postSixtyFirstProgressState.campaignProgress?.totalKnown !== 61 ||
+ postSixtyFirstProgressState.campaignProgress?.totalKnown !== 62 ||
postSixtyFirstProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postSixtyFirstProgressState.campaignProgress?.latestBattleTitle !== '한중 우로 방어전' ||
- postSixtyFirstProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ 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)}`);
+ throw new Error(`Expected post-sixty-first progress tab to complete Hanzhong rainy-pass defense and point toward Qishan renewed offensive: ${JSON.stringify(postSixtyFirstProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-sixty-first-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const qishanRenewedSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !qishanRenewedSortieState?.sortieVisible ||
+ qishanRenewedSortieState.campaign?.step !== 'sixty-first-camp' ||
+ qishanRenewedSortieState.campTitle !== '한중 우로 방어 후 군영' ||
+ !String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 재출정전') ||
+ !String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('노성 지연선') ||
+ qishanRenewedSortieState.sortiePlan?.maxCount !== 7 ||
+ qishanRenewedSortieState.sortiePlan?.recommendedTotal !== 9 ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
+ !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
+ qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected Qishan renewed sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing offensive/return-line roles: ${JSON.stringify(qishanRenewedSortieState)}`);
+ }
+ assertSortieTacticalRoster(qishanRenewedSortieState, [
+ '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 qishanRenewedPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang'];
+ for (const unitId of qishanRenewedPriorityUnits) {
+ 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 && !qishanRenewedPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const qishanRenewedSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !qishanRenewedPriorityUnits.every((unitId) =>
+ qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
+ qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.selected) ||
+ qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
+ qishanRenewedSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ qishanRenewedSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
+ qishanRenewedSortieReadyState.sortiePlan?.recommendedTotal !== 9
+ ) {
+ throw new Error(`Expected Qishan renewed sortie to deploy Zhuge Liang plus six forward/return-line officers while benching Li Yan and Ma Dai: ${JSON.stringify(qishanRenewedSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-sixty-second-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-second-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredSixtySecondBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'sixty-second-battle-qishan-renewed-offensive';
+ });
+ if (enteredSixtySecondBattle) {
+ 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-second-battle-qishan-renewed-offensive' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-sixty-second-battle.png', fullPage: true });
+
+ const sixtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const sixtySecondEnemies = sixtySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const sixtySecondAllies = sixtySecondBattleState.units.filter((unit) => unit.faction === 'ally');
+ const sixtySecondEnemyBehaviors = new Set(sixtySecondEnemies.map((unit) => unit.ai));
+ if (
+ sixtySecondBattleState.camera?.mapWidth !== 124 ||
+ sixtySecondBattleState.camera?.mapHeight !== 106 ||
+ sixtySecondBattleState.mapTextureKey !== 'battle-map-sixty-second' ||
+ sixtySecondBattleState.mapTextureReady !== true ||
+ sixtySecondBattleState.mapBackgroundReady !== true ||
+ sixtySecondBattleState.victoryConditionLabel !== '노성 지연선 돌파와 기산 보급로 전진' ||
+ !sixtySecondBattleState.objectives?.some((objective) => objective.id === 'press-sima-yi-delay-line' && objective.label === '사마의 지연선 돌파') ||
+ !sixtySecondBattleState.objectives?.some((objective) => objective.id === 'check-zhang-he-pursuit' && objective.label === '장합 추격대 저지') ||
+ !sixtySecondBattleState.objectives?.some((objective) => objective.id === 'break-guo-huai-flank' && objective.label === '곽회 측면 차단 해제') ||
+ !sixtySecondBattleState.objectives?.some((objective) => objective.id === 'qishan-supply-camps' && objective.label === '기산 보급 진영 유지') ||
+ !sixtySecondBattleState.objectives?.some((objective) => objective.id === 'lucheng-front-villages' && objective.label === '노성 앞 마을 안심') ||
+ sixtySecondEnemies.length < 42 ||
+ !sixtySecondEnemyBehaviors.has('aggressive') ||
+ !sixtySecondEnemyBehaviors.has('guard') ||
+ !sixtySecondEnemyBehaviors.has('hold') ||
+ !sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-leader-sima-yi') ||
+ !sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-officer-zhang-he') ||
+ !sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-officer-guo-huai') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'jiang-wei') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'wei-yan') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'wang-ping') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'huang-quan') ||
+ !sixtySecondAllies.some((unit) => unit.id === 'ma-liang') ||
+ sixtySecondAllies.some((unit) => unit.id === 'li-yan') ||
+ sixtySecondAllies.some((unit) => unit.id === 'ma-dai') ||
+ sixtySecondAllies.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected sixty-second battle to use the Qishan renewed-offensive map, Sima Yi/Zhang He/Guo Huai objectives, selected forward/return-line officers, and mixed Wei AI: ${JSON.stringify(sixtySecondBattleState)}`);
+ }
+
+ 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 sixtySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ sixtySecondCampState?.campBattleId !== 'sixty-second-battle-qishan-renewed-offensive' ||
+ sixtySecondCampState.campaign?.step !== 'sixty-second-camp' ||
+ sixtySecondCampState.campTitle !== '기산 재출정 후 군영' ||
+ sixtySecondCampState.availableDialogueIds?.length !== 3 ||
+ !sixtySecondCampState.availableDialogueIds.every((id) => id.startsWith('northern-eighth-')) ||
+ sixtySecondCampState.availableVisitIds?.length !== 1 ||
+ !sixtySecondCampState.availableVisitIds.every((id) => id.startsWith('northern-eighth-')) ||
+ sixtySecondCampState.rosterCollection?.total < 22 ||
+ !sixtySecondCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__wei-yan_northern-eighth')
+ ) {
+ throw new Error(`Expected Qishan renewed-offensive aftermath camp to expose eighth northern campaign events and bonds: ${JSON.stringify(sixtySecondCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-sixty-second-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postSixtySecondProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postSixtySecondProgressState?.activeTab !== 'progress' ||
+ postSixtySecondProgressState.campaignProgress?.completedKnown !== 62 ||
+ postSixtySecondProgressState.campaignProgress?.totalKnown !== 62 ||
+ postSixtySecondProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
+ postSixtySecondProgressState.campaignProgress?.latestBattleTitle !== '기산 재출정전' ||
+ postSixtySecondProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-sixty-second progress tab to complete Qishan renewed offensive and leave the next northern step pending: ${JSON.stringify(postSixtySecondProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-sixty-second-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -8866,12 +9029,12 @@ try {
}
}
for (let attempt = 0; attempt < 24; attempt += 1) {
- const continuedToRainDefenseCamp = await page.evaluate(() => {
+ const continuedToQishanRenewedCamp = await page.evaluate(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const camp = window.__HEROS_DEBUG__?.camp?.() ?? null;
- return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixty-first-camp';
+ return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixty-second-camp';
});
- if (continuedToRainDefenseCamp) {
+ if (continuedToQishanRenewedCamp) {
break;
}
await page.keyboard.press('Space');
@@ -8880,17 +9043,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 === 'sixty-first-camp';
+ return activeScenes.includes('CampScene') && camp?.campaign?.step === 'sixty-second-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 !== 'sixty-first-camp') {
- throw new Error(`Expected title continue to reopen the Hanzhong rainy-pass aftermath camp: ${JSON.stringify(titleContinueState)}`);
+ if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'sixty-second-camp') {
+ throw new Error(`Expected title continue to reopen the Qishan renewed-offensive aftermath camp: ${JSON.stringify(titleContinueState)}`);
}
- 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}`);
+ console.log(`Verified title-to-Qishan-renewed-offensive 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, Qishan renewed offensive, and pending next northern step state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/sixtieth-battle-map.svg b/src/assets/images/battle/sixtieth-battle-map.svg
index 1607d05..754cbd3 100644
--- a/src/assets/images/battle/sixtieth-battle-map.svg
+++ b/src/assets/images/battle/sixtieth-battle-map.svg
@@ -14,13 +14,10 @@
-
+
-
-
-
diff --git a/src/assets/images/battle/sixty-first-battle-map.svg b/src/assets/images/battle/sixty-first-battle-map.svg
index 58d9fc4..86bf095 100644
--- a/src/assets/images/battle/sixty-first-battle-map.svg
+++ b/src/assets/images/battle/sixty-first-battle-map.svg
@@ -15,16 +15,13 @@
-
+
-
-
-
diff --git a/src/assets/images/battle/sixty-second-battle-map.svg b/src/assets/images/battle/sixty-second-battle-map.svg
new file mode 100644
index 0000000..b81c0ef
--- /dev/null
+++ b/src/assets/images/battle/sixty-second-battle-map.svg
@@ -0,0 +1,91 @@
+
diff --git a/src/assets/images/story/16-qishan-renewed-offensive.png b/src/assets/images/story/16-qishan-renewed-offensive.png
new file mode 100644
index 0000000..ef448a6
Binary files /dev/null and b/src/assets/images/story/16-qishan-renewed-offensive.png differ
diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts
index b1281e3..345e82d 100644
--- a/src/game/data/battleMapAssets.ts
+++ b/src/game/data/battleMapAssets.ts
@@ -11,6 +11,7 @@ import fiftyEighthBattleMapUrl from '../../assets/images/battle/fifty-eighth-bat
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 sixtySecondBattleMapUrl from '../../assets/images/battle/sixty-second-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';
@@ -120,6 +121,7 @@ export const battleMapAssets: Record = {
'battle-map-fifty-ninth': fiftyNinthBattleMapUrl,
'battle-map-sixtieth': sixtiethBattleMapUrl,
'battle-map-sixty-first': sixtyFirstBattleMapUrl,
+ 'battle-map-sixty-second': sixtySecondBattleMapUrl,
'battle-map-fifty-seventh': fiftySeventhBattleMapUrl,
'battle-map-fifty-sixth': fiftySixthBattleMapUrl
};
diff --git a/src/game/data/battleRules.ts b/src/game/data/battleRules.ts
index f6074f5..bc0ca68 100644
--- a/src/game/data/battleRules.ts
+++ b/src/game/data/battleRules.ts
@@ -34,15 +34,15 @@ export type UnitClassDefinition = {
};
export const terrainRules: Record = {
- plain: { label: '평지', color: 0x6d8056, alpha: 0.01, moveCost: 1, defenseBonus: 0 },
- road: { label: '길', color: 0xb08b55, alpha: 0.015, moveCost: 1, defenseBonus: 0 },
- forest: { label: '숲', color: 0x355f42, alpha: 0.025, moveCost: 2, defenseBonus: 10 },
- hill: { label: '언덕', color: 0x82724e, alpha: 0.025, moveCost: 2, defenseBonus: 8 },
- village: { label: '마을', color: 0x8d6f4b, alpha: 0.03, moveCost: 1, defenseBonus: 12, recoveryHp: 8, moraleBonus: 3 },
- fort: { label: '요새', color: 0x735c46, alpha: 0.035, moveCost: 1, defenseBonus: 18, recoveryHp: 10, moraleBonus: 4 },
- camp: { label: '진영', color: 0x4f6a73, alpha: 0.03, moveCost: 1, defenseBonus: 15, recoveryHp: 6, moraleBonus: 4 },
- river: { label: '강', color: 0x2c6687, alpha: 0.035, moveCost: 99, defenseBonus: -4, passable: false },
- cliff: { label: '절벽', color: 0x39342e, alpha: 0.045, moveCost: 99, defenseBonus: 20, passable: false }
+ plain: { label: '평지', color: 0x6d8056, alpha: 0.18, moveCost: 1, defenseBonus: 0 },
+ road: { label: '길', color: 0xb08b55, alpha: 0.22, moveCost: 1, defenseBonus: 0 },
+ forest: { label: '숲', color: 0x355f42, alpha: 0.24, moveCost: 2, defenseBonus: 10 },
+ hill: { label: '언덕', color: 0x82724e, alpha: 0.24, moveCost: 2, defenseBonus: 8 },
+ village: { label: '마을', color: 0x8d6f4b, alpha: 0.28, moveCost: 1, defenseBonus: 12, recoveryHp: 8, moraleBonus: 3 },
+ fort: { label: '요새', color: 0x735c46, alpha: 0.3, moveCost: 1, defenseBonus: 18, recoveryHp: 10, moraleBonus: 4 },
+ camp: { label: '진영', color: 0x4f6a73, alpha: 0.3, moveCost: 1, defenseBonus: 15, recoveryHp: 6, moraleBonus: 4 },
+ river: { label: '강', color: 0x2c6687, alpha: 0.32, moveCost: 99, defenseBonus: -4, passable: false },
+ cliff: { label: '절벽', color: 0x39342e, alpha: 0.34, moveCost: 99, defenseBonus: 20, passable: false }
};
export const unitClasses: Record = {
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 354da4f..2cc5570 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -43,6 +43,10 @@ import {
sixtyFirstBattleMap,
sixtyFirstBattleUnits,
sixtyFirstBattleVictoryPages,
+ sixtySecondBattleBonds,
+ sixtySecondBattleMap,
+ sixtySecondBattleUnits,
+ sixtySecondBattleVictoryPages,
fiftySeventhBattleBonds,
fiftySeventhBattleMap,
fiftySeventhBattleUnits,
@@ -310,7 +314,8 @@ export type BattleScenarioId =
| 'fifty-eighth-battle-qishan-retreat'
| 'fifty-ninth-battle-chencang-siege'
| 'sixtieth-battle-wudu-yinping'
- | 'sixty-first-battle-hanzhong-rain-defense';
+ | 'sixty-first-battle-hanzhong-rain-defense'
+ | 'sixty-second-battle-qishan-renewed-offensive';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -4119,6 +4124,73 @@ export const sixtyFirstBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const sixtySecondBattleScenario: BattleScenarioDefinition = {
+ id: 'sixty-second-battle-qishan-renewed-offensive',
+ title: '기산 재출정전',
+ victoryConditionLabel: '노성 지연선 돌파와 기산 보급로 전진',
+ defeatConditionLabel: '제갈량 퇴각 또는 회수선 붕괴',
+ openingObjectiveLines: [
+ '한중 우로 방어 후 촉한군은 지켜 낸 무도·음평 보급선을 다시 북쪽 공격로로 바꾸려 합니다. 노성 앞의 위군 지연선은 사마의가 시간을 벌기 위해 세운 첫 벽입니다.',
+ '강유와 위연은 앞길을 열고, 조운과 왕평은 깊이 들어간 부대가 돌아올 표식을 남겨야 합니다. 황권과 마량은 군량과 민심을 함께 보아 공세가 약탈처럼 보이지 않게 해야 합니다.',
+ '사마의의 본영, 장합의 추격대, 곽회의 측면 차단을 동시에 끊으십시오. 기산 수레 길과 전방 마을, 회수 진영을 지켜야 다음 북벌이 더 깊이 이어집니다.'
+ ],
+ map: sixtySecondBattleMap,
+ units: sixtySecondBattleUnits,
+ bonds: sixtySecondBattleBonds,
+ mapTextureKey: 'battle-map-sixty-second',
+ leaderUnitId: 'northern-eighth-leader-sima-yi',
+ quickVictoryTurnLimit: 124,
+ baseVictoryGold: 19240,
+ objectives: [
+ {
+ id: 'press-sima-yi-delay-line',
+ kind: 'defeat-leader',
+ label: '사마의 지연선 돌파',
+ rewardGold: 13600,
+ unitId: 'northern-eighth-leader-sima-yi'
+ },
+ {
+ id: 'check-zhang-he-pursuit',
+ kind: 'defeat-leader',
+ label: '장합 추격대 저지',
+ rewardGold: 8200,
+ unitId: 'northern-eighth-officer-zhang-he'
+ },
+ {
+ id: 'break-guo-huai-flank',
+ kind: 'defeat-leader',
+ label: '곽회 측면 차단 해제',
+ rewardGold: 7600,
+ unitId: 'northern-eighth-officer-guo-huai'
+ },
+ {
+ id: 'qishan-supply-camps',
+ kind: 'secure-terrain',
+ label: '기산 보급 진영 유지',
+ rewardGold: 6400,
+ terrain: 'camp'
+ },
+ {
+ id: 'lucheng-front-villages',
+ kind: 'secure-terrain',
+ label: '노성 앞 마을 안심',
+ rewardGold: 6200,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '124턴 이내 지연선 돌파',
+ rewardGold: 4180,
+ maxTurn: 124
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
+ itemRewards: ['금 126', '상처약 98', '군주 48', '기산 재출정 장부 1', '노성 지연선 기록 1'],
+ victoryPages: sixtySecondBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -4182,7 +4254,8 @@ export const battleScenarios: Record
'fifty-eighth-battle-qishan-retreat': fiftyEighthBattleScenario,
'fifty-ninth-battle-chencang-siege': fiftyNinthBattleScenario,
'sixtieth-battle-wudu-yinping': sixtiethBattleScenario,
- 'sixty-first-battle-hanzhong-rain-defense': sixtyFirstBattleScenario
+ 'sixty-first-battle-hanzhong-rain-defense': sixtyFirstBattleScenario,
+ 'sixty-second-battle-qishan-renewed-offensive': sixtySecondBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 6ed8d7a..eb723eb 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -16,6 +16,7 @@ import {
fiftySeventhBattleScenario,
sixtiethBattleScenario,
sixtyFirstBattleScenario,
+ sixtySecondBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -81,6 +82,8 @@ import {
sixtiethBattleVictoryPages,
sixtyFirstBattleIntroPages,
sixtyFirstBattleVictoryPages,
+ sixtySecondBattleIntroPages,
+ sixtySecondBattleVictoryPages,
fiftyFirstBattleIntroPages,
fiftyFirstBattleVictoryPages,
fiftySecondBattleIntroPages,
@@ -897,13 +900,24 @@ const sortieFlows: Record = {
},
[sixtyFirstBattleScenario.id]: {
afterBattleId: sixtyFirstBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '한중 우로 방어 이후',
+ eyebrow: '다음 전장',
+ title: sixtySecondBattleScenario.title,
description:
- '조진의 역공을 막아 냈지만 한중 북문, 무도, 음평을 함께 지키는 부담은 그대로 남았습니다. 다음 북방 계산에서는 지켜 낸 길을 다시 공격로로 바꾸는 일과 후방에 남길 장수를 함께 정해야 합니다.',
- rewardHint: '다음 목표: 한중 방어선 재정비 / 다음 북벌 공격로 검토',
- pages: sixtyFirstBattleVictoryPages,
- unavailableNotice: '한중 우로 방어 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 조진 역공 방어, 한중 창고 유지, 무도·음평 보급 책임을 확인할 수 있습니다.'
+ '조진의 역공을 막아 낸 뒤, 지켜 낸 한중 북문과 무도·음평 수레 길을 다시 기산 방면 공격로로 바꿉니다. 사마의의 지연전, 장합의 추격, 곽회의 차단을 상대하려면 7명 안에서 전진과 회수, 보급과 민심을 다시 나누어야 합니다.',
+ rewardHint: `예상 보상: ${sixtySecondBattleScenario.title} 개방 / 기산 재출정 장부`,
+ nextBattleId: sixtySecondBattleScenario.id,
+ campaignStep: 'sixty-second-battle',
+ pages: [...sixtyFirstBattleVictoryPages, ...sixtySecondBattleIntroPages]
+ },
+ [sixtySecondBattleScenario.id]: {
+ afterBattleId: sixtySecondBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '기산 재출정 이후',
+ description:
+ '사마의의 지연선을 밀어 냈지만 북방 전선은 더 깊어졌습니다. 다음 계산에서는 노성 앞에서 확보한 길을 어디까지 밀고, 누가 돌아올 표식과 군량 장부를 맡을지 다시 정해야 합니다.',
+ rewardHint: '다음 목표: 노성 전선 재정비 / 더 깊은 북벌 공격로 검토',
+ pages: sixtySecondBattleVictoryPages,
+ unavailableNotice: '기산 재출정 이후의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 사마의 지연선 돌파, 회수선 표식, 기산 보급로 전진을 확인할 수 있습니다.'
},
'northern-campaign-prep-camp-legacy': {
afterBattleId: fiftyFourthBattleScenario.id,
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index aef978a..f6224ec 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -3907,6 +3907,59 @@ export const sixtyFirstBattleVictoryPages: StoryPage[] = [
}
];
+export const sixtySecondBattleIntroPages: StoryPage[] = [
+ {
+ id: 'sixty-second-renewed-qishan-council',
+ bgm: 'story-dark',
+ chapter: '기산 재출정',
+ background: 'story-qishan-renewed',
+ speaker: '제갈량',
+ text: '한중 북문의 비가 그치자 지켜 낸 길은 다시 북쪽을 향한 길이 되었습니다. 이번에는 노성 앞의 위군 지연선을 흔들어, 무도와 음평에서 이어진 보급로를 공격의 숨으로 바꾸겠습니다.'
+ },
+ {
+ id: 'sixty-second-sima-yi-delay-line',
+ bgm: 'battle-prep',
+ chapter: '사마의의 지연전',
+ background: 'story-qishan-renewed',
+ speaker: '강유',
+ text: '사마의는 성을 내주지 않고 길을 늦추려 합니다. 장합은 기병으로 앞선을 찌르고, 곽회는 돌아갈 길을 끊으려 할 것입니다. 빠른 돌파와 돌아올 표식이 함께 움직여야 합니다.'
+ },
+ {
+ id: 'sixty-second-supply-into-offense',
+ bgm: 'story-dark',
+ chapter: '보급을 공세로',
+ background: 'story-qishan-renewed',
+ speaker: '황권',
+ text: '지난 전투에서 지킨 장부를 이번에는 밀어 올릴 차례입니다. 이엄을 후방에 남길지, 마대의 측면 기동을 넣을지, 혹은 마량의 민심 문서를 앞세울지 출전 명단에서 먼저 정해야 합니다.'
+ }
+];
+
+export const sixtySecondBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'sixty-second-victory-lucheng-pressure',
+ bgm: 'militia-theme',
+ chapter: '노성 앞을 누르다',
+ background: 'story-qishan-renewed',
+ text: '촉한군은 노성 앞의 지연선을 밀어 냈고, 사마의의 봉화와 곽회의 차단로가 동시에 이어지는 것을 막았습니다. 기산의 길은 다시 북벌군의 군문 앞에 열렸습니다.'
+ },
+ {
+ id: 'sixty-second-victory-return-markers',
+ bgm: 'story-dark',
+ chapter: '돌아올 표식',
+ background: 'story-qishan-renewed',
+ speaker: '왕평',
+ text: '앞으로 나아간 길마다 돌아올 표식이 함께 세워졌습니다. 강유와 위연이 열어 둔 길을 조운이 회수선으로 묶고, 황권과 마량은 그 길이 백성의 부담이 되지 않게 장부를 고쳤습니다.'
+ },
+ {
+ id: 'sixty-second-next-northern-plan',
+ bgm: 'story-dark',
+ chapter: '더 깊은 북방 계산',
+ background: 'story-qishan-renewed',
+ speaker: '제갈량',
+ text: '사마의는 한 번 물러났으나 전쟁은 아직 짧아지지 않았습니다. 다음 북벌에서는 이긴 길을 지키는 사람과 더 깊이 들어갈 사람을 다시 나누어야 합니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -4418,6 +4471,12 @@ export const sixtyFirstBattleMap: BattleMap = {
terrain: createSixtyFirstBattleTerrain()
};
+export const sixtySecondBattleMap: BattleMap = {
+ width: 124,
+ height: 106,
+ terrain: createSixtySecondBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -17726,6 +17785,97 @@ export const sixtyFirstBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('northern-seventh-leader-cao-zhen', '조진', '위군 역공 총대장', 'rebelLeader', 213, 1660, 392, 114, 38)
];
+const sixtySecondBattleAllyPositions: Record = {
+ 'guan-yu': { x: 15, y: 94 },
+ 'zhang-fei': { x: 22, y: 96 },
+ 'jian-yong': { x: 8, y: 101 },
+ 'mi-zhu': { x: 13, y: 102 },
+ 'sun-qian': { x: 18, y: 102 },
+ 'zhao-yun': { x: 30, y: 93 },
+ 'zhuge-liang': { x: 18, y: 89 },
+ 'ma-liang': { x: 24, y: 86 },
+ 'yi-ji': { x: 8, y: 103 },
+ 'gong-zhi': { x: 28, y: 101 },
+ 'huang-zhong': { x: 38, y: 90 },
+ 'wei-yan': { x: 45, y: 91 },
+ 'pang-tong': { x: 15, y: 85 },
+ 'fa-zheng': { x: 28, y: 84 },
+ 'wu-yi': { x: 47, y: 99 },
+ 'yan-yan': { x: 51, y: 92 },
+ 'li-yan': { x: 34, y: 83 },
+ 'huang-quan': { x: 12, y: 88 },
+ 'ma-chao': { x: 54, y: 88 },
+ 'ma-dai': { x: 59, y: 95 },
+ 'wang-ping': { x: 41, y: 83 },
+ 'jiang-wei': { x: 35, y: 86 }
+};
+
+export const sixtySecondBattleUnits: 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, sixtySecondBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-a', '노성 서로 척후병', '서쪽 고개 정찰', 'bandit', 214, 1060, 292, 52, 84),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-b', '기산 보급로 척후병', '보급로 정찰', 'bandit', 214, 1060, 292, 64, 81),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-c', '노성 앞 척후병', '중앙 고개 정찰', 'bandit', 215, 1072, 294, 78, 74),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-d', '동쪽 물목 척후병', '동쪽 물목 정찰', 'bandit', 215, 1072, 294, 92, 66),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-e', '사마의 전초 척후병', '본영 정찰', 'bandit', 216, 1084, 296, 108, 50),
+ createThirtyFirstEnemyUnit('northern-eighth-scout-f', '북쪽 봉화 척후병', '봉화대 정찰', 'bandit', 216, 1084, 296, 121, 30),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-a', '위군 기산 보병', '서쪽 고개 보병', 'yellowTurban', 215, 1540, 368, 55, 78),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-b', '위군 보급로 보병', '보급로 보병', 'yellowTurban', 215, 1540, 368, 68, 74),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-c', '위군 노성 보병', '중앙 보병', 'yellowTurban', 216, 1560, 372, 81, 68),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-d', '위군 물목 보병', '동쪽 물목 보병', 'yellowTurban', 216, 1560, 372, 94, 61),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-e', '위군 지연 보병', '지연선 보병', 'yellowTurban', 217, 1580, 376, 106, 53),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-f', '사마의 본영 보병', '본영 보병', 'yellowTurban', 217, 1580, 376, 116, 41),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-g', '위군 보급 차단병', '수레길 차단 보병', 'yellowTurban', 218, 1600, 380, 100, 82),
+ createThirtyFirstEnemyUnit('northern-eighth-infantry-h', '장합 별동 보병', '별동대 보병', 'yellowTurban', 218, 1600, 380, 121, 23),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-a', '기산 서로 궁병', '서쪽 능선 궁병', 'archer', 215, 1130, 310, 62, 69),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-b', '보급로 궁병', '보급로 궁병', 'archer', 215, 1130, 310, 74, 64),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-c', '노성 능선 궁병', '중앙 능선 궁병', 'archer', 216, 1142, 312, 89, 57),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-d', '동쪽 물목 궁병', '동쪽 물목 궁병', 'archer', 217, 1154, 314, 102, 49),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-e', '사마의 본영 궁병', '본영 궁병', 'archer', 217, 1154, 314, 114, 37),
+ createThirtyFirstEnemyUnit('northern-eighth-archer-f', '북쪽 봉화 궁병', '봉화대 궁병', 'archer', 218, 1166, 316, 122, 18),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-a', '장합 추격 기병', '서쪽 추격 기병', 'cavalry', 216, 1600, 388, 53, 72),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-b', '장합 추격 기병', '보급로 추격 기병', 'cavalry', 216, 1600, 388, 66, 69),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-c', '장합 추격 기병', '중앙 추격 기병', 'cavalry', 217, 1622, 392, 82, 62),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-d', '곽회 차단 기병', '동쪽 차단 기병', 'cavalry', 218, 1644, 396, 96, 55),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-e', '사마의 예비 기병', '본영 예비 기병', 'cavalry', 218, 1644, 396, 110, 47),
+ createThirtyFirstEnemyUnit('northern-eighth-cavalry-f', '북쪽 봉화 기병', '봉화 차단 기병', 'cavalry', 219, 1666, 400, 121, 28),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-a', '기산 서로 수비대', '서쪽 고개 수비', 'yellowTurban', 216, 1560, 374, 48, 58),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-b', '보급로 수비대', '보급로 수비', 'yellowTurban', 216, 1560, 374, 64, 53),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-c', '노성 앞 수비대', '중앙 고개 수비', 'yellowTurban', 217, 1580, 378, 80, 45),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-d', '동쪽 물목 수비대', '동쪽 물목 수비', 'yellowTurban', 217, 1580, 378, 96, 38),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-e', '사마의 본영 수비대', '본영 수비', 'yellowTurban', 218, 1600, 382, 110, 30),
+ createThirtyFirstEnemyUnit('northern-eighth-ridge-guard-f', '북쪽 봉화 수비대', '북쪽 봉화 수비', 'yellowTurban', 218, 1600, 382, 122, 14),
+ createThirtyFirstEnemyUnit('northern-eighth-strategist-a', '위군 지연 군리', '지연 장부 군리', 'strategist', 216, 1200, 328, 78, 60),
+ createThirtyFirstEnemyUnit('northern-eighth-strategist-b', '위군 보급 군리', '보급 차단 군리', 'strategist', 217, 1212, 330, 92, 51),
+ createThirtyFirstEnemyUnit('northern-eighth-strategist-c', '사마의 본영 군리', '본영 장부 군리', 'strategist', 217, 1212, 330, 106, 42),
+ createThirtyFirstEnemyUnit('northern-eighth-strategist-d', '곽회 차단 군리', '차단로 장부 군리', 'strategist', 218, 1224, 332, 112, 70),
+ createThirtyFirstEnemyUnit('northern-eighth-officer-lucheng', '노성 전초장', '노성 압박 장수', 'rebelLeader', 220, 1660, 398, 90, 76),
+ createThirtyFirstEnemyUnit('northern-eighth-officer-supply', '보급 차단장', '기산 보급 차단 장수', 'rebelLeader', 220, 1660, 398, 103, 64),
+ createThirtyFirstEnemyUnit('northern-eighth-officer-guo-huai', '곽회', '측면 차단 장수', 'rebelLeader', 219, 1640, 394, 112, 74),
+ createThirtyFirstEnemyUnit('northern-eighth-officer-relay', '봉화 연락장', '노성 봉화 장수', 'rebelLeader', 219, 1640, 394, 108, 58),
+ createThirtyFirstEnemyUnit('northern-eighth-officer-zhang-he', '장합', '추격 별동대장', 'rebelLeader', 221, 1700, 404, 118, 27),
+ createThirtyFirstEnemyUnit('northern-eighth-leader-sima-yi', '사마의', '위군 지연 총지휘', 'rebelLeader', 222, 1740, 410, 116, 40)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -19083,6 +19233,46 @@ export const sixtyFirstBattleBonds: BattleBond[] = [
...northernSeventhBattleBonds
].map(cloneBattleBondForScenario);
+export const northernEighthBattleBonds: BattleBond[] = [
+ {
+ id: 'jiang-wei__wei-yan_northern-eighth',
+ unitIds: ['jiang-wei', 'wei-yan'],
+ title: '앞길과 돌파',
+ level: 52,
+ exp: 0,
+ description: '강유의 길눈과 위연의 돌파 판단이 맞물리면, 노성 앞 지연선을 빠르게 흔들되 돌아올 길을 잃지 않을 수 있습니다.'
+ },
+ {
+ id: 'zhao-yun__wang-ping_northern-eighth',
+ unitIds: ['zhao-yun', 'wang-ping'],
+ title: '회수선의 표식',
+ level: 54,
+ exp: 0,
+ description: '조운의 예비대 회수와 왕평의 산길 표식이 이어지면, 깊이 들어간 부대도 사마의의 지연전 속에서 제때 돌아올 수 있습니다.'
+ },
+ {
+ id: 'huang-quan__ma-liang_northern-eighth',
+ unitIds: ['huang-quan', 'ma-liang'],
+ title: '장부와 고시문',
+ level: 48,
+ exp: 0,
+ description: '황권의 군량 장부와 마량의 민심 문서가 함께 움직이면, 공격로가 백성의 부담으로만 남지 않게 조정할 수 있습니다.'
+ },
+ {
+ id: 'li-yan__ma-dai_northern-eighth',
+ unitIds: ['li-yan', 'ma-dai'],
+ title: '수레와 측면',
+ level: 44,
+ exp: 0,
+ description: '이엄의 운송 기준과 마대의 측면 기동이 맞으면, 보급 수레가 늦어질 때도 기병이 빈틈을 메울 수 있습니다.'
+ }
+];
+
+export const sixtySecondBattleBonds: BattleBond[] = [
+ ...sixtyFirstBattleBonds,
+ ...northernEighthBattleBonds
+].map(cloneBattleBondForScenario);
+
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
Array.from({ length: 26 }, (_, x): TerrainType => {
@@ -22479,6 +22669,86 @@ function createSixtyFirstBattleTerrain(): TerrainType[][] {
);
}
+function createSixtySecondBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 106 }, (_, y) =>
+ Array.from({ length: 124 }, (_, x): TerrainType => {
+ if (
+ (x >= 5 && x <= 12 && y >= 0 && y <= 105) ||
+ (x >= 64 && x <= 71 && y >= 12 && y <= 105) ||
+ (x >= 0 && x <= 123 && y >= 98)
+ ) {
+ return 'river';
+ }
+ if (
+ (x >= 7 && x <= 84 && y >= 90 && y <= 96) ||
+ (x >= 22 && x <= 103 && y >= 78 && y <= 86) ||
+ (x >= 48 && x <= 112 && y >= 66 && y <= 76) ||
+ (x >= 76 && x <= 120 && y >= 52 && y <= 64) ||
+ (x >= 93 && x <= 123 && y >= 37 && y <= 49) ||
+ (x >= 30 && x <= 40 && y >= 70 && y <= 100) ||
+ (x >= 55 && x <= 64 && y >= 56 && y <= 92) ||
+ (x >= 85 && x <= 95 && y >= 42 && y <= 82) ||
+ (x >= 99 && x <= 109 && y >= 24 && y <= 68) ||
+ (x >= 44 && x <= 82 && y >= 82 && y <= 88)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 0 && x <= 38 && y >= 60 && y <= 94) ||
+ (x >= 42 && x <= 82 && y >= 75 && y <= 97) ||
+ (x >= 88 && x <= 123 && y >= 69 && y <= 95) ||
+ (x >= 14 && x <= 42 && y >= 44 && y <= 64) ||
+ (x >= 98 && x <= 123 && y >= 52 && y <= 74) ||
+ (x >= 116 && x <= 123 && y >= 26 && y <= 48)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 52 && y <= 41) ||
+ (x >= 46 && x <= 94 && y <= 35) ||
+ (x >= 86 && x <= 123 && y <= 53) ||
+ (x >= 98 && x <= 123 && y >= 45 && y <= 74)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 96) ||
+ (x >= 121 && y <= 90) ||
+ (x >= 39 && x <= 46 && y >= 0 && y <= 66) ||
+ (x >= 77 && x <= 83 && y >= 0 && y <= 48) ||
+ (x >= 60 && x <= 68 && y >= 86 && y <= 105)
+ ) {
+ return 'cliff';
+ }
+ if (
+ (x >= 12 && x <= 30 && y >= 86 && y <= 96) ||
+ (x >= 43 && x <= 64 && y >= 73 && y <= 85) ||
+ (x >= 80 && x <= 100 && y >= 59 && y <= 71) ||
+ (x >= 102 && x <= 120 && y >= 43 && y <= 57)
+ ) {
+ return 'village';
+ }
+ if (
+ (x >= 20 && x <= 48 && y >= 80 && y <= 94) ||
+ (x >= 55 && x <= 82 && y >= 67 && y <= 80) ||
+ (x >= 89 && x <= 114 && y >= 53 && y <= 66) ||
+ (x >= 99 && x <= 118 && y >= 24 && y <= 39)
+ ) {
+ return 'camp';
+ }
+ if (
+ (x >= 49 && x <= 70 && y >= 48 && y <= 64) ||
+ (x >= 73 && x <= 100 && y >= 38 && y <= 53) ||
+ (x >= 95 && x <= 120 && y >= 47 && y <= 63) ||
+ (x >= 104 && x <= 123 && y >= 14 && y <= 36)
+ ) {
+ return 'fort';
+ }
+ return 'plain';
+ })
+ );
+}
+
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),
diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts
index 0f1cbdd..ef8624b 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -6458,6 +6458,26 @@ export class BattleScene extends Phaser.Scene {
}
private enemyBehavior(enemy: UnitData): EnemyAiBehavior {
+ const lateNorthernPrefixes = [
+ 'northern-fourth',
+ 'northern-fifth',
+ 'northern-sixth',
+ 'northern-seventh',
+ 'northern-eighth'
+ ];
+ if (lateNorthernPrefixes.some((prefix) => enemy.id.startsWith(prefix))) {
+ if (
+ enemy.id.includes('-scout') ||
+ enemy.id.includes('-cavalry') ||
+ enemy.id === 'northern-eighth-officer-zhang-he'
+ ) {
+ return 'aggressive';
+ }
+ if (enemy.id.includes('-archer') || enemy.id.includes('-strategist') || enemy.id.includes('-leader-')) {
+ return 'hold';
+ }
+ return 'guard';
+ }
if (
enemy.id.startsWith('northern-first-scout') ||
enemy.id.startsWith('northern-second-scout') ||
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index ab643fc..da52494 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -18,6 +18,7 @@ import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-moun
import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png';
import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png';
import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png';
+import storyQishanRenewedOffensiveUrl from '../../assets/images/story/16-qishan-renewed-offensive.png';
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png';
import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png';
@@ -79,6 +80,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('story-northern', storyNorthernExpeditionUrl);
this.load.image('story-nanzhong', storyNanzhongPacificationUrl);
this.load.image('story-yiling-baidi', storyYilingBaidiAftermathUrl);
+ this.load.image('story-qishan-renewed', storyQishanRenewedOffensiveUrl);
this.load.image('story-resolve', storyLiuBeiUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index d0fc568..5819c0d 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -51,6 +51,7 @@ import {
mengHuoPacificationBonds,
mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
+ northernEighthBattleBonds,
northernFifthBattleBonds,
northernFourthBattleBonds,
northernFirstBattleBonds,
@@ -447,9 +448,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'fifty-eighth-battle-qishan-retreat',
'fifty-ninth-battle-chencang-siege',
'sixtieth-battle-wudu-yinping',
- 'sixty-first-battle-hanzhong-rain-defense'
+ 'sixty-first-battle-hanzhong-rain-defense',
+ 'sixty-second-battle-qishan-renewed-offensive'
],
- nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어']
+ nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어', '기산 재출정']
}
];
@@ -514,7 +516,8 @@ const campBattleIds = {
fiftyEighth: 'fifty-eighth-battle-qishan-retreat',
fiftyNinth: 'fifty-ninth-battle-chencang-siege',
sixtieth: 'sixtieth-battle-wudu-yinping',
- sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense'
+ sixtyFirst: 'sixty-first-battle-hanzhong-rain-defense',
+ sixtySecond: 'sixty-second-battle-qishan-renewed-offensive'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1281,6 +1284,23 @@ const sortieRulesByBattleId: Partial