diff --git a/docs/roadmap.md b/docs/roadmap.md
index 0ba146c..931b176 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -80,15 +80,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Fifty-third battle Meng Huo sixth-capture route, where Meng Huo's remaining pride meets a 106x88 deep-ravine battlefield, with village-trust, trust-road, and hardliner-remnant objectives plus post-battle trust-ledger/final-persuasion camp events
- Fifty-fourth battle Meng Huo final-capture route, where the Seven Captures arc closes on a 108x90 council-ground battlefield with final pacify, council-road, village-witness, and remnant-fort objectives plus Nanzhong pacification camp events
- Post-Nanzhong northern-campaign preparation milestone, where the final pacification camp advances into a Zhuge Liang-led war council, excludes Liu Bei from the preparation roster, and opens northern sortie planning dialogue, granary checks, and memorial-drafting visits
+- Fifty-fifth battle first northern expedition Qishan road, where Zhuge Liang leads the sortie instead of Liu Bei, the player chooses seven officers from the expanded roster, and a 110x92 mountain-road battlefield mixes scout, cavalry, archer, strategist, ridge-guard, and named Wei officer behaviors
+- Post-Qishan-road camp events with Zhuge Liang, Zhao Yun, Huang Quan, Ma Liang, Wang Ping, and Ma Dai focusing on route choice, supply books, ridge defense, and long-campaign northern-front preparation
- Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter
- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, 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 northern-campaign preparation after the final Meng Huo capture, 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 first northern expedition and post-Qishan-road camp, 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 first northern expedition battle from the new preparation camp, likely around the Qishan/Jieting opening pressure so sortie choices immediately matter
+1. Build the next northern-front step toward Tianshui/Jieting pressure, introducing Wei's counterplan and a new recruit or rival hook such as Jiang Wei
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 344f64e..92ce78a 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 !== 54 ||
+ progressTabState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 55 ||
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 !== 54 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 55 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 54 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 55 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 54 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 55 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 54 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 55 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 54 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 55 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 54 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 55 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,7 +5586,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 54 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 55 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5741,7 +5741,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
- postGonganProgressState.campaignProgress?.totalKnown !== 54 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 55 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5899,7 +5899,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
- postMaichengProgressState.campaignProgress?.totalKnown !== 54 ||
+ postMaichengProgressState.campaignProgress?.totalKnown !== 55 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6056,7 +6056,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
- postYilingProgressState.campaignProgress?.totalKnown !== 54 ||
+ postYilingProgressState.campaignProgress?.totalKnown !== 55 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6214,7 +6214,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
- postYilingFireProgressState.campaignProgress?.totalKnown !== 54 ||
+ postYilingFireProgressState.campaignProgress?.totalKnown !== 55 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6280,7 +6280,7 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
- postBaidiProgressState.campaignProgress?.totalKnown !== 54 ||
+ postBaidiProgressState.campaignProgress?.totalKnown !== 55 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
@@ -6441,7 +6441,7 @@ try {
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
- postNanzhongProgressState.campaignProgress?.totalKnown !== 54 ||
+ postNanzhongProgressState.campaignProgress?.totalKnown !== 55 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
) {
@@ -6603,7 +6603,7 @@ try {
if (
postMengHuoProgressState?.activeTab !== 'progress' ||
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
- postMengHuoProgressState.campaignProgress?.totalKnown !== 54 ||
+ postMengHuoProgressState.campaignProgress?.totalKnown !== 55 ||
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
) {
@@ -6765,7 +6765,7 @@ try {
if (
postSecondCaptureProgressState?.activeTab !== 'progress' ||
postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
- postSecondCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
+ postSecondCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전'
) {
@@ -6930,7 +6930,7 @@ try {
if (
postThirdCaptureProgressState?.activeTab !== 'progress' ||
postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 ||
- postThirdCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
+ postThirdCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' ||
postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전'
) {
@@ -7096,7 +7096,7 @@ try {
if (
postFourthCaptureProgressState?.activeTab !== 'progress' ||
postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 ||
- postFourthCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
+ postFourthCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' ||
postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전'
) {
@@ -7262,7 +7262,7 @@ try {
if (
postFifthCaptureProgressState?.activeTab !== 'progress' ||
postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 ||
- postFifthCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
+ postFifthCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' ||
postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전'
) {
@@ -7427,7 +7427,7 @@ try {
if (
postSixthCaptureProgressState?.activeTab !== 'progress' ||
postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 ||
- postSixthCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
+ postSixthCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' ||
postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전'
) {
@@ -7586,16 +7586,17 @@ try {
}
await page.screenshot({ path: 'dist/verification-final-capture-camp.png', fullPage: true });
+
await page.mouse.click(966, 38);
await page.waitForTimeout(180);
const postFinalCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
postFinalCaptureProgressState?.activeTab !== 'progress' ||
postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 ||
- postFinalCaptureProgressState.campaignProgress?.totalKnown !== 54 ||
- postFinalCaptureProgressState.campaignProgress?.activeChapter?.title !== '북벌 준비' ||
+ postFinalCaptureProgressState.campaignProgress?.totalKnown !== 55 ||
+ postFinalCaptureProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postFinalCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
- postFinalCaptureProgressState.campaignProgress?.nextBattleTitle !== '북벌 준비 회의'
+ !String(postFinalCaptureProgressState.campaignProgress?.nextBattleTitle ?? '').includes('북벌 준비')
) {
throw new Error(`Expected post-final-capture progress tab to complete the fifty-fourth battle and point toward northern campaign preparation: ${JSON.stringify(postFinalCaptureProgressState?.campaignProgress)}`);
}
@@ -7607,8 +7608,8 @@ try {
if (
!northernPrepSortieState?.sortieVisible ||
northernPrepSortieState.campaign?.step !== 'fifty-fourth-camp' ||
- northernPrepSortieState.campTitle !== '남중 평정 후 군영' ||
- northernPrepSortieState.sortiePlan?.objectiveLine !== '북벌 준비 회의 · 군영 의정' ||
+ !String(northernPrepSortieState.campTitle ?? '').includes('남중 평정') ||
+ !String(northernPrepSortieState.sortiePlan?.objectiveLine ?? '').includes('북벌 준비') ||
northernPrepSortieState.sortiePlan?.maxCount !== 7 ||
northernPrepSortieState.sortiePlan?.recommendedTotal !== 7 ||
!northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
@@ -7647,7 +7648,7 @@ try {
const northernPrepCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
northernPrepCampState?.campaign?.step !== 'northern-campaign-prep-camp' ||
- northernPrepCampState.campTitle !== '북벌 준비 군영' ||
+ !String(northernPrepCampState.campTitle ?? '').includes('북벌 준비') ||
northernPrepCampState.campBattleId !== 'fifty-fourth-battle-meng-huo-final-capture' ||
northernPrepCampState.availableDialogueIds?.length !== 3 ||
!northernPrepCampState.availableDialogueIds.every((id) => id.startsWith('northern-prep-')) ||
@@ -7665,15 +7666,163 @@ try {
if (
postNorthernPrepProgressState?.activeTab !== 'progress' ||
postNorthernPrepProgressState.campaignProgress?.completedKnown !== 54 ||
- postNorthernPrepProgressState.campaignProgress?.totalKnown !== 54 ||
- postNorthernPrepProgressState.campaignProgress?.activeChapter?.title !== '북벌 준비' ||
+ postNorthernPrepProgressState.campaignProgress?.totalKnown !== 55 ||
+ postNorthernPrepProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
postNorthernPrepProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
- postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진 준비'
+ postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진로'
) {
- throw new Error(`Expected northern campaign prep progress tab to pause before the first northern expedition battle: ${JSON.stringify(postNorthernPrepProgressState?.campaignProgress)}`);
+ throw new Error(`Expected northern campaign prep progress tab to point at the first northern expedition battle: ${JSON.stringify(postNorthernPrepProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-northern-prep-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const firstNorthernSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !firstNorthernSortieState?.sortieVisible ||
+ firstNorthernSortieState.campaign?.step !== 'northern-campaign-prep-camp' ||
+ !String(firstNorthernSortieState.campTitle ?? '').includes('북벌 준비') ||
+ !String(firstNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('제1차 북벌 출진로') ||
+ !String(firstNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 산길') ||
+ firstNorthernSortieState.sortiePlan?.maxCount !== 7 ||
+ firstNorthernSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
+ !firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
+ firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected first northern expedition sortie to require Zhuge Liang, exclude Liu Bei, and recommend the Qishan planning roster: ${JSON.stringify(firstNorthernSortieState)}`);
+ }
+ assertSortieTacticalRoster(firstNorthernSortieState, [
+ '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'
+ ]);
+
+ const firstNorthernPriorityUnits = ['zhao-yun', 'huang-quan', 'ma-liang', 'wang-ping', 'ma-dai', 'wei-yan'];
+ for (const unitId of firstNorthernPriorityUnits) {
+ 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 && !firstNorthernPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const firstNorthernSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !firstNorthernPriorityUnits.every((unitId) =>
+ firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
+ firstNorthernSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ firstNorthernSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected first northern expedition sortie to deploy Zhuge Liang, Zhao Yun, Huang Quan, Ma Liang, Wang Ping, Ma Dai, and Wei Yan: ${JSON.stringify(firstNorthernSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-fifty-fifth-sortie.png', fullPage: true });
+
+ await page.mouse.click(1068, 646);
+ await page.waitForFunction(() => {
+ const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
+ return activeScenes.includes('StoryScene');
+ });
+ await page.screenshot({ path: 'dist/verification-fifty-fifth-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredFiftyFifthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fifth-battle-northern-qishan-road';
+ });
+ if (enteredFiftyFifthBattle) {
+ break;
+ }
+ await page.keyboard.press('Space');
+ await page.waitForTimeout(320);
+ }
+ await page.waitForFunction(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fifth-battle-northern-qishan-road' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-fifty-fifth-battle.png', fullPage: true });
+
+ const fiftyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const fiftyFifthEnemies = fiftyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const fiftyFifthAllies = fiftyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const fiftyFifthEnemyBehaviors = new Set(fiftyFifthEnemies.map((unit) => unit.ai));
+ if (
+ fiftyFifthBattleState.camera?.mapWidth !== 110 ||
+ fiftyFifthBattleState.camera?.mapHeight !== 92 ||
+ fiftyFifthBattleState.victoryConditionLabel !== '기산 산길 돌파와 한중 보급로 확보' ||
+ !fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'repel-cao-zhen' && objective.label === '조진 전초대 격퇴') ||
+ !fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'qishan-road' && objective.label === '기산 산길 확보') ||
+ !fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'hanzhong-supply-camp' && objective.label === '한중 보급 거점 유지') ||
+ fiftyFifthEnemies.length < 42 ||
+ !fiftyFifthEnemyBehaviors.has('aggressive') ||
+ !fiftyFifthEnemyBehaviors.has('guard') ||
+ !fiftyFifthEnemyBehaviors.has('hold') ||
+ !fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-leader-cao-zhen') ||
+ !fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-officer-zhang-he') ||
+ !fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-officer-guo-huai') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'huang-quan') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'ma-liang') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'wang-ping') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'ma-dai') ||
+ !fiftyFifthAllies.some((unit) => unit.id === 'wei-yan') ||
+ fiftyFifthAllies.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected fifty-fifth battle to use the Qishan map, first-northern objectives, selected recruited officers, and mixed Wei AI: ${JSON.stringify(fiftyFifthBattleState)}`);
+ }
+
+ 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 fiftyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ fiftyFifthCampState?.campBattleId !== 'fifty-fifth-battle-northern-qishan-road' ||
+ fiftyFifthCampState.campaign?.step !== 'fifty-fifth-camp' ||
+ fiftyFifthCampState.campTitle !== '기산 출진로 후 군영' ||
+ fiftyFifthCampState.availableDialogueIds?.length !== 3 ||
+ !fiftyFifthCampState.availableDialogueIds.every((id) => id.startsWith('northern-first-')) ||
+ fiftyFifthCampState.availableVisitIds?.length !== 2 ||
+ !fiftyFifthCampState.availableVisitIds.every((id) => id.startsWith('northern-first-')) ||
+ fiftyFifthCampState.rosterCollection?.total < 21
+ ) {
+ throw new Error(`Expected first northern expedition camp to expose Qishan dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyFifthCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-fifty-fifth-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postFiftyFifthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postFiftyFifthProgressState?.activeTab !== 'progress' ||
+ postFiftyFifthProgressState.campaignProgress?.completedKnown !== 55 ||
+ postFiftyFifthProgressState.campaignProgress?.totalKnown !== 55 ||
+ postFiftyFifthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
+ postFiftyFifthProgressState.campaignProgress?.latestBattleTitle !== '제1차 북벌 출진로'
+ ) {
+ throw new Error(`Expected post-fifty-fifth progress tab to complete the first northern expedition battle: ${JSON.stringify(postFiftyFifthProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-fifty-fifth-progress.png', fullPage: true });
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -7705,11 +7854,11 @@ try {
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
camp: window.__HEROS_DEBUG__?.camp?.() ?? null
}));
- if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'northern-campaign-prep-camp') {
- throw new Error(`Expected title continue to reopen the northern campaign prep camp: ${JSON.stringify(titleContinueState)}`);
+ if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fifty-fifth-camp') {
+ throw new Error(`Expected title continue to reopen the first northern expedition camp: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-northern-campaign-prep 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, and northern campaign preparation state at ${targetUrl}`);
+ console.log(`Verified title-to-first-northern-expedition 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, and Qishan road first northern expedition state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/fifty-fifth-battle-map.svg b/src/assets/images/battle/fifty-fifth-battle-map.svg
new file mode 100644
index 0000000..6a2bcc8
--- /dev/null
+++ b/src/assets/images/battle/fifty-fifth-battle-map.svg
@@ -0,0 +1,63 @@
+
diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts
index 7a623cb..a29d529 100644
--- a/src/game/data/battleMapAssets.ts
+++ b/src/game/data/battleMapAssets.ts
@@ -6,6 +6,7 @@ import fiftyFirstBattleMapUrl from '../../assets/images/battle/fifty-first-battl
import fiftySecondBattleMapUrl from '../../assets/images/battle/fifty-second-battle-map.svg';
import fiftyThirdBattleMapUrl from '../../assets/images/battle/fifty-third-battle-map.svg';
import fiftyFourthBattleMapUrl from '../../assets/images/battle/fifty-fourth-battle-map.svg';
+import fiftyFifthBattleMapUrl from '../../assets/images/battle/fifty-fifth-battle-map.svg';
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-map.svg';
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
@@ -107,5 +108,6 @@ export const battleMapAssets: Record = {
'battle-map-fifty-first': fiftyFirstBattleMapUrl,
'battle-map-fifty-second': fiftySecondBattleMapUrl,
'battle-map-fifty-third': fiftyThirdBattleMapUrl,
- 'battle-map-fifty-fourth': fiftyFourthBattleMapUrl
+ 'battle-map-fifty-fourth': fiftyFourthBattleMapUrl,
+ 'battle-map-fifty-fifth': fiftyFifthBattleMapUrl
};
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index e3be490..221a337 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -19,6 +19,10 @@ import {
fiftyFourthBattleMap,
fiftyFourthBattleUnits,
fiftyFourthBattleVictoryPages,
+ fiftyFifthBattleBonds,
+ fiftyFifthBattleMap,
+ fiftyFifthBattleUnits,
+ fiftyFifthBattleVictoryPages,
firstBattleBonds,
fifteenthBattleBonds,
fifteenthBattleMap,
@@ -275,7 +279,8 @@ export type BattleScenarioId =
| 'fifty-first-battle-meng-huo-fourth-capture'
| 'fifty-second-battle-meng-huo-fifth-capture'
| 'fifty-third-battle-meng-huo-sixth-capture'
- | 'fifty-fourth-battle-meng-huo-final-capture';
+ | 'fifty-fourth-battle-meng-huo-final-capture'
+ | 'fifty-fifth-battle-northern-qishan-road';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -3535,6 +3540,73 @@ export const fiftyFourthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const fiftyFifthBattleScenario: BattleScenarioDefinition = {
+ id: 'fifty-fifth-battle-northern-qishan-road',
+ title: '제1차 북벌 출진로',
+ victoryConditionLabel: '기산 산길 돌파와 한중 보급로 확보',
+ defeatConditionLabel: '제갈량 퇴각',
+ openingObjectiveLines: [
+ '남중 평정 이후 처음으로 촉한의 군세가 북쪽을 향합니다. 조진의 전초 부대가 기산으로 향하는 길목과 마을, 보급 거점을 누르고 있으니 먼저 산길을 뚫어야 합니다.',
+ '이번 전투는 유비공이 아니라 제갈량이 직접 지휘하는 북벌의 첫 실전입니다. 출전 무장을 고를 때 기동, 보급, 산길 수비, 원거리 견제를 함께 고려해야 합니다.',
+ '조진을 물러나게 하고 기산 산길과 한중 보급 캠프, 서쪽 마을을 확보하십시오. 촉한의 북벌은 여기서부터 긴 전선으로 이어집니다.'
+ ],
+ map: fiftyFifthBattleMap,
+ units: fiftyFifthBattleUnits,
+ bonds: fiftyFifthBattleBonds,
+ mapTextureKey: 'battle-map-fifty-fifth',
+ leaderUnitId: 'northern-first-leader-cao-zhen',
+ quickVictoryTurnLimit: 110,
+ baseVictoryGold: 14920,
+ objectives: [
+ {
+ id: 'repel-cao-zhen',
+ kind: 'defeat-leader',
+ label: '조진 전초대 격퇴',
+ rewardGold: 9980,
+ unitId: 'northern-first-leader-cao-zhen'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 2600,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'qishan-road',
+ kind: 'secure-terrain',
+ label: '기산 산길 확보',
+ rewardGold: 5100,
+ terrain: 'road'
+ },
+ {
+ id: 'hanzhong-supply-camp',
+ kind: 'secure-terrain',
+ label: '한중 보급 거점 유지',
+ rewardGold: 4860,
+ terrain: 'camp'
+ },
+ {
+ id: 'western-villages',
+ kind: 'secure-terrain',
+ label: '서쪽 마을 안정',
+ rewardGold: 4520,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '110턴 이내 출진로 확보',
+ rewardGold: 3360,
+ maxTurn: 110
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
+ itemRewards: ['콩 86', '상처약 70', '탁주 34', '기산 출진 장부 1', '한중 보급 표식 1'],
+ victoryPages: fiftyFifthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -3591,7 +3663,8 @@ export const battleScenarios: Record
'fifty-first-battle-meng-huo-fourth-capture': fiftyFirstBattleScenario,
'fifty-second-battle-meng-huo-fifth-capture': fiftySecondBattleScenario,
'fifty-third-battle-meng-huo-sixth-capture': fiftyThirdBattleScenario,
- 'fifty-fourth-battle-meng-huo-final-capture': fiftyFourthBattleScenario
+ 'fifty-fourth-battle-meng-huo-final-capture': fiftyFourthBattleScenario,
+ 'fifty-fifth-battle-northern-qishan-road': fiftyFifthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 7596f47..8f7fa9e 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -9,6 +9,7 @@ import {
fiftySecondBattleScenario,
fiftyThirdBattleScenario,
fiftyFourthBattleScenario,
+ fiftyFifthBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -74,6 +75,8 @@ import {
fiftyThirdBattleVictoryPages,
fiftyFourthBattleIntroPages,
fiftyFourthBattleVictoryPages,
+ fiftyFifthBattleIntroPages,
+ fiftyFifthBattleVictoryPages,
fortiethBattleIntroPages,
fortiethBattleVictoryPages,
fortyFirstBattleIntroPages,
@@ -798,6 +801,27 @@ const sortieFlows: Record = {
unavailableNotice: '북벌 준비 회의가 완료되었습니다. 다음 작업에서는 기산 출진로와 제1차 북벌 전투로 이어갑니다.'
},
'northern-campaign-prep-camp': {
+ afterBattleId: fiftyFourthBattleScenario.id,
+ eyebrow: '다음 전장',
+ title: fiftyFifthBattleScenario.title,
+ description:
+ '남중 평정과 북벌 준비 회의를 마쳤습니다. 이제 제갈량을 중심으로 조운의 기동, 황권과 마량의 보급 판단, 왕평과 마대의 산길 대응을 골라 기산 출진로를 열어야 합니다.',
+ rewardHint: `예상 보상: ${fiftyFifthBattleScenario.title} 개방 / 기산 출진 장부`,
+ nextBattleId: fiftyFifthBattleScenario.id,
+ campaignStep: 'fifty-fifth-battle',
+ pages: fiftyFifthBattleIntroPages
+ },
+ [fiftyFifthBattleScenario.id]: {
+ afterBattleId: fiftyFifthBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '기산 출진로 이후',
+ description:
+ '기산 산길과 한중 보급 거점을 확보했습니다. 다음 북벌 전장은 더 넓은 위군 방어선과 천수 방면의 움직임으로 이어질 예정입니다.',
+ rewardHint: '다음 목표: 천수 방면 진군 준비 / 북벌 장기전 설계',
+ pages: fiftyFifthBattleVictoryPages,
+ unavailableNotice: '천수 방면의 다음 북벌 전투는 이어지는 작업에서 확장합니다. 현재는 기산 출진로 후 군영에서 무장 대화와 보급 정비를 진행할 수 있습니다.'
+ },
+ 'northern-campaign-prep-camp-legacy': {
afterBattleId: fiftyFourthBattleScenario.id,
eyebrow: '다음 전장 준비',
title: '제1차 북벌 출진 준비',
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 5b6e456..9de67eb 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -3536,6 +3536,59 @@ export const northernCampaignPrepPages: StoryPage[] = [
}
];
+export const fiftyFifthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'fifty-fifth-qishan-road-opens',
+ bgm: 'story-dark',
+ chapter: '제1차 북벌',
+ background: 'story-militia',
+ speaker: '제갈량',
+ text: '남중이 평정되자 촉한의 군량과 길이 비로소 북쪽을 향할 수 있게 되었습니다. 그러나 북벌은 단번에 끝나는 싸움이 아닙니다. 먼저 기산으로 향하는 산길과 한중 보급로를 동시에 붙잡아야 합니다.'
+ },
+ {
+ id: 'fifty-fifth-sortie-council',
+ bgm: 'battle-prep',
+ chapter: '기산 출진로',
+ background: 'story-sortie',
+ speaker: '조운',
+ text: '승상께서 직접 지휘하신다면 유비공의 이름만으로 밀어붙이던 때와는 달라집니다. 산길을 여는 무장, 보급을 지킬 문관, 기병을 막을 추격대까지 골라야 합니다. 누구를 함께 데려갈지가 곧 전투의 반입니다.'
+ },
+ {
+ id: 'fifty-fifth-wei-vanguard',
+ bgm: 'battle-prep',
+ chapter: '위군 선봉',
+ background: 'story-three-heroes',
+ speaker: '황권',
+ text: '조진이 전초 부대를 앞세워 기산의 입구와 마을, 보급 거점을 눌러 두었습니다. 길을 뚫는 것만으로는 부족합니다. 산길을 확보하고, 마을을 안정시키며, 한중으로 이어지는 보급 거점을 지켜야 합니다.'
+ }
+];
+
+export const fiftyFifthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'fifty-fifth-victory-road-secured',
+ bgm: 'militia-theme',
+ chapter: '기산 출진로 확보',
+ background: 'story-sortie',
+ text: '조진의 전초 부대는 물러났고, 기산으로 이어지는 산길에는 촉한의 깃발이 세워졌습니다. 한중 보급대도 길을 잃지 않았으며, 마을 사람들은 북벌군이 약탈자가 아니라 질서를 세우는 군대임을 보았습니다.'
+ },
+ {
+ id: 'fifty-fifth-victory-northern-front',
+ bgm: 'story-dark',
+ chapter: '북벌의 첫 발',
+ background: 'story-militia',
+ speaker: '제갈량',
+ text: '첫 길은 열렸으나, 위나라의 본대는 아직 물러나지 않았습니다. 이제부터는 전투 하나하나가 촉한의 국력을 갉아먹는 긴 싸움이 됩니다. 무장들의 배치와 공명, 보급의 선택이 더 중요해질 것입니다.'
+ },
+ {
+ id: 'fifty-fifth-next-front',
+ bgm: 'story-dark',
+ chapter: '천수 방면의 기척',
+ background: 'story-three-heroes',
+ speaker: '마량',
+ text: '기산 길목을 지나자 천수 방면에서 새로운 소문이 들어옵니다. 위군 안에서도 촉한의 북벌을 가볍게 보지 않는 장수들이 움직이고 있습니다. 다음 진군은 더 넓은 전선과 더 어려운 선택을 요구할 것입니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -4005,6 +4058,12 @@ export const fiftyFourthBattleMap: BattleMap = {
terrain: createFiftyFourthBattleTerrain()
};
+export const fiftyFifthBattleMap: BattleMap = {
+ width: 110,
+ height: 92,
+ terrain: createFiftyFifthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -16658,6 +16717,95 @@ export const fiftyFourthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('menghuo-final-leader', '맹획', '남만왕', 'rebelLeader', 174, 1180, 288, 102, 49)
];
+const fiftyFifthBattleAllyPositions: Record = {
+ 'guan-yu': { x: 18, y: 76 },
+ 'zhang-fei': { x: 23, y: 78 },
+ 'jian-yong': { x: 13, y: 82 },
+ 'mi-zhu': { x: 16, y: 84 },
+ 'sun-qian': { x: 21, y: 84 },
+ 'zhao-yun': { x: 27, y: 72 },
+ 'zhuge-liang': { x: 17, y: 72 },
+ 'ma-liang': { x: 21, y: 69 },
+ 'yi-ji': { x: 13, y: 86 },
+ 'gong-zhi': { x: 25, y: 84 },
+ 'huang-zhong': { x: 32, y: 72 },
+ 'wei-yan': { x: 38, y: 76 },
+ 'pang-tong': { x: 18, y: 66 },
+ 'fa-zheng': { x: 25, y: 67 },
+ 'wu-yi': { x: 39, y: 82 },
+ 'yan-yan': { x: 43, y: 75 },
+ 'li-yan': { x: 31, y: 66 },
+ 'huang-quan': { x: 13, y: 70 },
+ 'ma-chao': { x: 46, y: 72 },
+ 'ma-dai': { x: 49, y: 78 },
+ 'wang-ping': { x: 34, y: 68 }
+};
+
+export const fiftyFifthBattleUnits: 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
+ ].map((unit) => placeScenarioUnit(unit, fiftyFifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('northern-first-scout-a', '위군 산길 정찰병', '서쪽 능선 정찰', 'bandit', 172, 780, 230, 48, 72),
+ createThirtyFirstEnemyUnit('northern-first-scout-b', '위군 산길 정찰병', '한중 길목 정찰', 'bandit', 172, 780, 230, 56, 68),
+ createThirtyFirstEnemyUnit('northern-first-scout-c', '위군 산길 정찰병', '기산 마을 감시', 'bandit', 173, 792, 232, 67, 64),
+ createThirtyFirstEnemyUnit('northern-first-scout-d', '위군 산길 정찰병', '동쪽 고갯길 감시', 'bandit', 173, 792, 232, 74, 59),
+ createThirtyFirstEnemyUnit('northern-first-scout-e', '위군 산길 정찰병', '북쪽 봉화 정찰', 'bandit', 174, 804, 234, 86, 51),
+ createThirtyFirstEnemyUnit('northern-first-scout-f', '위군 산길 정찰병', '조진 본영 정찰', 'bandit', 174, 804, 234, 95, 45),
+ createThirtyFirstEnemyUnit('northern-first-infantry-a', '위군 전초 보병', '기산 초입 보병', 'yellowTurban', 173, 1080, 270, 54, 58),
+ createThirtyFirstEnemyUnit('northern-first-infantry-b', '위군 전초 보병', '한중로 차단 보병', 'yellowTurban', 173, 1080, 270, 61, 55),
+ createThirtyFirstEnemyUnit('northern-first-infantry-c', '위군 전초 보병', '마을 길목 보병', 'yellowTurban', 174, 1100, 274, 69, 50),
+ createThirtyFirstEnemyUnit('northern-first-infantry-d', '위군 전초 보병', '능선 초소 보병', 'yellowTurban', 174, 1100, 274, 76, 47),
+ createThirtyFirstEnemyUnit('northern-first-infantry-e', '위군 전초 보병', '동쪽 초소 보병', 'yellowTurban', 175, 1120, 276, 85, 40),
+ createThirtyFirstEnemyUnit('northern-first-infantry-f', '위군 전초 보병', '북쪽 영채 보병', 'yellowTurban', 175, 1120, 276, 94, 34),
+ createThirtyFirstEnemyUnit('northern-first-infantry-g', '위군 전초 보병', '조진 본영 보병', 'yellowTurban', 176, 1140, 278, 101, 27),
+ createThirtyFirstEnemyUnit('northern-first-infantry-h', '위군 전초 보병', '보급로 압박 보병', 'yellowTurban', 176, 1140, 278, 91, 69),
+ createThirtyFirstEnemyUnit('northern-first-archer-a', '위군 원거리 궁병', '기산 입구 궁병', 'archer', 172, 824, 242, 58, 52),
+ createThirtyFirstEnemyUnit('northern-first-archer-b', '위군 원거리 궁병', '한중로 궁병', 'archer', 173, 832, 244, 66, 48),
+ createThirtyFirstEnemyUnit('northern-first-archer-c', '위군 원거리 궁병', '마을 언덕 궁병', 'archer', 173, 832, 244, 74, 42),
+ createThirtyFirstEnemyUnit('northern-first-archer-d', '위군 원거리 궁병', '능선 궁병', 'archer', 174, 840, 246, 83, 35),
+ createThirtyFirstEnemyUnit('northern-first-archer-e', '위군 원거리 궁병', '북쪽 영채 궁병', 'archer', 174, 840, 246, 92, 30),
+ createThirtyFirstEnemyUnit('northern-first-archer-f', '위군 원거리 궁병', '조진 본영 궁병', 'archer', 175, 848, 248, 99, 23),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-a', '위군 추격 기병', '서쪽 추격 기병', 'cavalry', 173, 1088, 274, 47, 64),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-b', '위군 추격 기병', '한중로 추격 기병', 'cavalry', 173, 1088, 274, 58, 62),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-c', '위군 추격 기병', '기산 기동 기병', 'cavalry', 174, 1110, 278, 70, 56),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-d', '위군 추격 기병', '능선 우회 기병', 'cavalry', 175, 1132, 282, 82, 49),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-e', '위군 추격 기병', '동쪽 역습 기병', 'cavalry', 175, 1132, 282, 94, 42),
+ createThirtyFirstEnemyUnit('northern-first-cavalry-f', '위군 추격 기병', '조진 친위 기병', 'cavalry', 176, 1154, 286, 103, 36),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-a', '기산 능선 수비대', '서쪽 고개 수비', 'yellowTurban', 174, 1110, 276, 37, 50),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-b', '기산 능선 수비대', '첫 능선 수비', 'yellowTurban', 174, 1110, 276, 45, 42),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-c', '기산 능선 수비대', '중앙 산길 수비', 'yellowTurban', 175, 1130, 280, 56, 36),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-d', '기산 능선 수비대', '북쪽 산길 수비', 'yellowTurban', 175, 1130, 280, 69, 31),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-e', '기산 능선 수비대', '동쪽 절벽 수비', 'yellowTurban', 176, 1150, 282, 81, 25),
+ createThirtyFirstEnemyUnit('northern-first-ridge-guard-f', '기산 능선 수비대', '조진 영채 수비', 'yellowTurban', 176, 1150, 282, 96, 18),
+ createThirtyFirstEnemyUnit('northern-first-strategist-a', '위군 군리', '보급 차단 참모', 'strategist', 174, 880, 252, 64, 45),
+ createThirtyFirstEnemyUnit('northern-first-strategist-b', '위군 군리', '봉화 연락 참모', 'strategist', 175, 892, 254, 82, 32),
+ createThirtyFirstEnemyUnit('northern-first-strategist-c', '위군 군리', '마을 회유 참모', 'strategist', 175, 892, 254, 90, 54),
+ createThirtyFirstEnemyUnit('northern-first-strategist-d', '위군 군리', '본영 기록 참모', 'strategist', 176, 904, 256, 101, 20),
+ createThirtyFirstEnemyUnit('northern-first-officer-xiahou-mao', '하후무', '기산 전초 장수', 'rebelLeader', 176, 1160, 284, 62, 58),
+ createThirtyFirstEnemyUnit('northern-first-officer-guo-huai', '곽회', '서량 방면 장수', 'rebelLeader', 176, 1160, 286, 78, 43),
+ createThirtyFirstEnemyUnit('northern-first-officer-zhang-he', '장합', '위군 역습 장수', 'rebelLeader', 177, 1180, 290, 91, 31),
+ createThirtyFirstEnemyUnit('northern-first-officer-fei-yao', '비요', '조진 휘하 장수', 'rebelLeader', 176, 1160, 284, 99, 61),
+ createThirtyFirstEnemyUnit('northern-first-officer-supply', '위군 둔전 장교', '보급로 관측 장수', 'rebelLeader', 175, 1140, 282, 72, 68),
+ createThirtyFirstEnemyUnit('northern-first-leader-cao-zhen', '조진', '위군 대장', 'rebelLeader', 178, 1240, 296, 102, 24)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -17710,6 +17858,45 @@ export const fiftyFourthBattleBonds: BattleBond[] = [...fiftyThirdBattleBonds, .
cloneBattleBondForScenario
);
+export const northernFirstBattleBonds: BattleBond[] = [
+ {
+ id: 'zhuge-liang__zhao-yun_northern-first',
+ unitIds: ['zhuge-liang', 'zhao-yun'],
+ title: '출진로의 창과 책략',
+ level: 60,
+ exp: 0,
+ description: '제갈량의 진군 판단과 조운의 기동이 맞물리면 기산 산길에서 길을 잃은 부대를 빠르게 수습하고 역습의 틈을 만듭니다.'
+ },
+ {
+ id: 'huang-quan__ma-liang_northern-first',
+ unitIds: ['huang-quan', 'ma-liang'],
+ title: '보급 장부와 민심',
+ level: 54,
+ exp: 0,
+ description: '황권의 보급 장부와 마량의 민심 수습이 함께 움직이면 마을과 창고를 지키는 전투에서 공명 효과가 커집니다.'
+ },
+ {
+ id: 'wang-ping__ma-dai_northern-first',
+ unitIds: ['wang-ping', 'ma-dai'],
+ title: '산길 수비와 추격',
+ level: 50,
+ exp: 0,
+ description: '왕평이 산길을 읽고 마대가 기병의 틈을 파고들면 적의 우회 공격을 끊고 추격 피해를 높일 수 있습니다.'
+ },
+ {
+ id: 'zhuge-liang__wei-yan_northern-first',
+ unitIds: ['zhuge-liang', 'wei-yan'],
+ title: '신중함과 돌파력',
+ level: 48,
+ exp: 0,
+ description: '제갈량의 신중한 지휘와 위연의 강한 돌파력이 균형을 이루면 좁은 산길에서도 과감한 일격을 노릴 수 있습니다.'
+ }
+];
+
+export const fiftyFifthBattleBonds: BattleBond[] = [...fiftyFourthBattleBonds, ...northernFirstBattleBonds].map(
+ cloneBattleBondForScenario
+);
+
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
Array.from({ length: 26 }, (_, x): TerrainType => {
@@ -20572,6 +20759,83 @@ function createFiftyFourthBattleTerrain(): TerrainType[][] {
);
}
+function createFiftyFifthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 92 }, (_, y) =>
+ Array.from({ length: 110 }, (_, x): TerrainType => {
+ if (
+ (x >= 0 && x <= 109 && y >= 84) ||
+ (x >= 72 && x <= 78 && y >= 0 && y <= 86) ||
+ (x >= 4 && x <= 32 && y >= 75 && y <= 91) ||
+ (x >= 84 && x <= 104 && y >= 67 && y <= 84)
+ ) {
+ return 'river';
+ }
+ if (
+ (x >= 11 && x <= 105 && y >= 71 && y <= 77) ||
+ (x >= 31 && x <= 83 && y >= 48 && y <= 56) ||
+ (x >= 77 && x <= 105 && y >= 39 && y <= 46) ||
+ (x >= 42 && x <= 52 && y >= 44 && y <= 80) ||
+ (x >= 84 && x <= 90 && y >= 54 && y <= 88) ||
+ (x >= 19 && x <= 58 && y >= 63 && y <= 67) ||
+ (x >= 55 && x <= 72 && y >= 56 && y <= 62) ||
+ (x >= 61 && x <= 79 && y >= 46 && y <= 54) ||
+ (x >= 34 && x <= 46 && y >= 57 && y <= 72)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 18 && x <= 80 && y >= 24 && y <= 82) ||
+ (x >= 59 && x <= 104 && y >= 31 && y <= 78) ||
+ (x >= 2 && x <= 44 && y >= 55 && y <= 82) ||
+ (x >= 86 && x <= 109 && y >= 46 && y <= 81)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 35 && y <= 43) ||
+ (x >= 37 && x <= 76 && y <= 33) ||
+ (x >= 79 && x <= 109 && y <= 42) ||
+ (x >= 94 && x <= 109 && y >= 38 && y <= 63)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 82) ||
+ (x >= 107 && y <= 78) ||
+ (x >= 34 && x <= 40 && y >= 0 && y <= 46) ||
+ (x >= 76 && x <= 82 && y >= 0 && y <= 40) ||
+ (x >= 49 && x <= 55 && y >= 72 && y <= 91)
+ ) {
+ return 'cliff';
+ }
+ if (
+ (x >= 14 && x <= 31 && y >= 66 && y <= 79) ||
+ (x >= 43 && x <= 62 && y >= 49 && y <= 64) ||
+ (x >= 82 && x <= 99 && y >= 55 && y <= 68) ||
+ (x >= 88 && x <= 102 && y >= 37 && y <= 51)
+ ) {
+ return 'village';
+ }
+ if (
+ (x >= 27 && x <= 44 && y >= 58 && y <= 70) ||
+ (x >= 70 && x <= 86 && y >= 63 && y <= 76) ||
+ (x >= 88 && x <= 104 && y >= 44 && y <= 58)
+ ) {
+ return 'camp';
+ }
+ if (
+ (x >= 52 && x <= 70 && y >= 36 && y <= 48) ||
+ (x >= 80 && x <= 101 && y >= 36 && y <= 51) ||
+ (x >= 91 && x <= 105 && y >= 49 && y <= 62) ||
+ (x >= 95 && x <= 107 && y >= 20 && y <= 33)
+ ) {
+ return 'fort';
+ }
+ return 'plain';
+ })
+ );
+}
+
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),
diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts
index 0e66bac..66c5399 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -6361,6 +6361,27 @@ export class BattleScene extends Phaser.Scene {
}
private enemyBehavior(enemy: UnitData): EnemyAiBehavior {
+ if (
+ enemy.id.startsWith('northern-first-scout') ||
+ enemy.id.startsWith('northern-first-cavalry') ||
+ enemy.id === 'northern-first-officer-zhang-he'
+ ) {
+ return 'aggressive';
+ }
+ if (
+ enemy.id.startsWith('northern-first-archer') ||
+ enemy.id.startsWith('northern-first-strategist') ||
+ enemy.id === 'northern-first-leader-cao-zhen'
+ ) {
+ return 'hold';
+ }
+ if (
+ enemy.id.startsWith('northern-first-infantry') ||
+ enemy.id.startsWith('northern-first-ridge-guard') ||
+ enemy.id.startsWith('northern-first-officer')
+ ) {
+ return 'guard';
+ }
return enemyAiByUnitId[enemy.id] ?? defaultEnemyAiByClass[enemy.classKey] ?? 'guard';
}
@@ -7838,6 +7859,15 @@ export class BattleScene extends Phaser.Scene {
}
private unitTextureKey(unit: UnitData) {
+ if (unit.id.startsWith('northern-first-cavalry')) {
+ return 'unit-rebel-cavalry';
+ }
+ if (unit.id.startsWith('northern-first-archer') || unit.id.startsWith('northern-first-strategist')) {
+ return 'unit-rebel-archer';
+ }
+ if (unit.id.startsWith('northern-first-officer') || unit.id === 'northern-first-leader-cao-zhen') {
+ return 'unit-rebel-leader';
+ }
return unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel';
}
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index e17df52..734d764 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -49,6 +49,7 @@ import {
mengHuoPacificationBonds,
mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
+ northernFirstBattleBonds,
wulingRecruitBonds,
wulingRecruitUnits,
xuzhouRecruitBonds,
@@ -431,7 +432,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '북벌 준비',
period: '출사표를 향해',
description: '남중 평정으로 뒤를 안정시킨 뒤, 한중 창고와 출전 무장 편성을 정비해 첫 북벌을 준비하는 장입니다.',
- battleIds: [],
+ battleIds: ['fifty-fifth-battle-northern-qishan-road'],
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '장기전 보급']
}
];
@@ -490,7 +491,8 @@ const campBattleIds = {
fiftyFirst: 'fifty-first-battle-meng-huo-fourth-capture',
fiftySecond: 'fifty-second-battle-meng-huo-fifth-capture',
fiftyThird: 'fifty-third-battle-meng-huo-sixth-capture',
- fiftyFourth: 'fifty-fourth-battle-meng-huo-final-capture'
+ fiftyFourth: 'fifty-fourth-battle-meng-huo-final-capture',
+ fiftyFifth: 'fifty-fifth-battle-northern-qishan-road'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1149,6 +1151,21 @@ const sortieRulesByBattleId: Partial