Add Qishan renewed offensive chapter

This commit is contained in:
2026-06-24 15:00:24 +09:00
parent 305ead1777
commit 44c017ab6c
16 changed files with 892 additions and 98 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -14,13 +14,10 @@
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="3" seed="60"/>
<feColorMatrix type="saturate" values="0.18"/>
<feBlend in="SourceGraphic" mode="multiply"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
</defs>
<rect width="1920" height="1632" fill="url(#ground)"/>
<g opacity="0.2" filter="url(#grain)">
<rect width="1920" height="1632" fill="#2f2a1f"/>
</g>
<path d="M48 98 C230 210 190 360 380 500 C558 631 505 812 720 928 C928 1042 890 1218 1110 1342 C1328 1468 1494 1464 1872 1562" fill="none" stroke="url(#river)" stroke-width="88" stroke-linecap="round"/>
<path d="M974 8 C920 170 998 306 924 454 C842 620 946 776 874 934 C812 1070 850 1208 760 1450" fill="none" stroke="#356e7b" stroke-width="54" stroke-linecap="round" opacity="0.88"/>
<path d="M86 1410 C324 1358 468 1258 612 1142 C790 1000 1008 956 1188 806 C1388 640 1524 488 1840 358" fill="none" stroke="#c8a66b" stroke-width="42" stroke-linecap="round"/>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -15,16 +15,13 @@
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.014" numOctaves="3" seed="61"/>
<feColorMatrix type="saturate" values="0.16"/>
<feBlend in="SourceGraphic" mode="multiply"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
<filter id="rainBlur" x="-4%" y="-4%" width="108%" height="108%">
<feGaussianBlur stdDeviation="1.2"/>
</filter>
</defs>
<rect width="1952" height="1664" fill="url(#ground)"/>
<g opacity="0.22" filter="url(#grain)">
<rect width="1952" height="1664" fill="#1f251f"/>
</g>
<path d="M72 118 C254 246 216 406 424 558 C606 692 560 882 790 1010 C1010 1134 982 1304 1216 1444 C1410 1562 1594 1586 1888 1608" fill="none" stroke="url(#rainRiver)" stroke-width="92" stroke-linecap="round"/>
<path d="M998 0 C942 164 1032 330 944 492 C858 650 950 824 882 990 C812 1162 860 1300 746 1518" fill="none" stroke="#2c6775" stroke-width="60" stroke-linecap="round" opacity="0.9"/>
<path d="M260 1440 C448 1276 616 1228 812 1090 C1002 956 1234 948 1418 784 C1594 626 1696 496 1900 438" fill="none" stroke="#d0b173" stroke-width="44" stroke-linecap="round"/>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1984 1696" role="img" aria-labelledby="title desc">
<title id="title">Qishan renewed offensive map</title>
<desc id="desc">Original tactical battlefield map with renewed Qishan roads, Lucheng forts, Shu supply camps, Wei delay lines, rivers, forests, ridges, and cliff gates.</desc>
<defs>
<linearGradient id="ground" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#445343"/>
<stop offset="0.42" stop-color="#6f7652"/>
<stop offset="1" stop-color="#9d8358"/>
</linearGradient>
<linearGradient id="river" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#25465d"/>
<stop offset="0.5" stop-color="#3f7483"/>
<stop offset="1" stop-color="#8eb1a4"/>
</linearGradient>
<linearGradient id="road" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#c49a62"/>
<stop offset="0.5" stop-color="#dfc07c"/>
<stop offset="1" stop-color="#a9784f"/>
</linearGradient>
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="4" seed="62"/>
<feColorMatrix type="saturate" values="0.18"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
<filter id="soft" x="-4%" y="-4%" width="108%" height="108%">
<feGaussianBlur stdDeviation="1.4"/>
</filter>
</defs>
<rect width="1984" height="1696" fill="url(#ground)"/>
<path d="M88 122 C318 258 258 472 506 626 C732 768 678 982 948 1128 C1210 1266 1208 1430 1462 1542 C1650 1626 1816 1650 1918 1668" fill="none" stroke="url(#river)" stroke-width="88" stroke-linecap="round"/>
<path d="M1048 0 C1000 180 1082 352 1004 522 C922 704 1044 864 956 1050 C878 1212 908 1390 790 1618" fill="none" stroke="#2c6875" stroke-width="54" stroke-linecap="round" opacity="0.92"/>
<path d="M146 1458 C390 1308 612 1260 818 1102 C1032 938 1294 906 1498 714 C1688 534 1790 382 1940 292" fill="none" stroke="url(#road)" stroke-width="46" stroke-linecap="round"/>
<path d="M178 1308 C412 1242 592 1118 750 950 C942 746 1170 754 1390 586 C1588 434 1754 310 1948 178" fill="none" stroke="#b48758" stroke-width="30" stroke-linecap="round"/>
<path d="M490 1560 C604 1398 742 1324 950 1196 C1152 1070 1374 1082 1544 920 C1714 758 1806 596 1948 548" fill="none" stroke="#d3aa68" stroke-width="30" stroke-linecap="round"/>
<path d="M706 1148 C828 1012 894 872 1030 736 C1166 600 1334 506 1518 372" fill="none" stroke="#e3c17b" stroke-width="24" stroke-linecap="round" opacity="0.95"/>
<g fill="#324432" opacity="0.92">
<path d="M0 0 L500 0 L374 214 L520 414 L268 526 L0 424 Z"/>
<path d="M630 0 L1036 0 L932 258 L732 386 L584 204 Z"/>
<path d="M1168 0 L1984 0 L1984 334 L1786 282 L1566 396 L1378 226 Z"/>
<path d="M1514 560 L1984 462 L1984 1018 L1716 918 L1462 990 Z"/>
<path d="M0 598 L428 658 L260 930 L452 1150 L0 1232 Z"/>
</g>
<g fill="#22372e" opacity="0.78">
<ellipse cx="294" cy="748" rx="238" ry="152"/>
<ellipse cx="588" cy="1000" rx="224" ry="138"/>
<ellipse cx="1228" cy="1226" rx="310" ry="154"/>
<ellipse cx="1590" cy="1084" rx="236" ry="128"/>
<ellipse cx="1042" cy="654" rx="252" ry="132"/>
<ellipse cx="1446" cy="446" rx="196" ry="104"/>
</g>
<g fill="#755c42" stroke="#30251d" stroke-width="8">
<rect x="238" y="1340" width="250" height="142" rx="6"/>
<rect x="574" y="1160" width="270" height="154" rx="6"/>
<rect x="946" y="978" width="282" height="158" rx="6"/>
<rect x="1290" y="742" width="310" height="174" rx="6"/>
<rect x="1620" y="402" width="274" height="160" rx="6"/>
</g>
<g fill="#593d34" stroke="#241c17" stroke-width="10">
<path d="M1376 414 L1600 380 L1734 504 L1676 670 L1442 706 L1304 574 Z"/>
<path d="M926 670 L1146 628 L1274 748 L1214 922 L978 952 L850 808 Z"/>
<path d="M1540 152 L1770 126 L1912 242 L1850 394 L1618 430 L1474 302 Z"/>
<path d="M1112 1184 L1322 1154 L1452 1272 L1392 1414 L1156 1438 L1034 1300 Z"/>
</g>
<g fill="#e2ca8f" stroke="#59442f" stroke-width="7">
<rect x="164" y="1244" width="122" height="82" rx="5"/>
<rect x="484" y="1084" width="130" height="84" rx="5"/>
<rect x="738" y="930" width="136" height="86" rx="5"/>
<rect x="1184" y="826" width="144" height="90" rx="5"/>
<rect x="1418" y="556" width="134" height="86" rx="5"/>
</g>
<g opacity="0.23" stroke="#eef0dc" stroke-width="4" stroke-linecap="round" filter="url(#soft)">
<path d="M120 230 C300 188 424 190 604 238"/>
<path d="M710 420 C876 372 1058 382 1238 440"/>
<path d="M1140 150 C1300 112 1486 126 1658 176"/>
<path d="M1264 1018 C1430 986 1608 998 1814 1054"/>
</g>
<g fill="#d9b869" stroke="#4a3322" stroke-width="5">
<circle cx="318" cy="1352" r="22"/>
<circle cx="384" cy="1378" r="18"/>
<circle cx="642" cy="1188" r="20"/>
<circle cx="1038" cy="1000" r="22"/>
<circle cx="1368" cy="762" r="20"/>
<circle cx="1708" cy="418" r="22"/>
</g>
<g opacity="0.58" fill="#7b2f2d">
<path d="M314 1290 l56 16 -56 16 z"/>
<path d="M650 1130 l60 18 -60 18 z"/>
<path d="M1048 946 l64 18 -64 18 z"/>
<path d="M1400 704 l64 20 -64 20 z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -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<string, string> = {
'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
};

View File

@@ -34,15 +34,15 @@ export type UnitClassDefinition = {
};
export const terrainRules: Record<TerrainType, TerrainRule> = {
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<UnitClassKey, UnitClassDefinition> = {

View File

@@ -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<BattleScenarioId, BattleScenarioDefinition> = {
@@ -4182,7 +4254,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'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];

View File

@@ -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<string, SortieFlow> = {
},
[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,

View File

@@ -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<string, { x: number; y: number }> = {
'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),

View File

@@ -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') ||

View File

@@ -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);

View File

@@ -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<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '서량 기병의 우회 감각으로 음평 물목과 조진 기병의 측면 압박을 견제합니다.' }
],
note: '한중 우로 방어전은 추천 무장이 9명으로 출전 제한보다 둘 많습니다. 새 고을 보급을 지킬 장부 담당, 빗길 표식, 반격 돌파, 기병 회수 중 무엇을 뒤에 남길지 정해야 합니다.'
},
'sixty-second-battle-qishan-renewed-offensive': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '노성 앞 지연선을 상대하며 전진과 회수, 보급과 민심을 동시에 나누어야 하는 총지휘관입니다.' },
{ unitId: 'jiang-wei', reason: '지켜 낸 무도·음평 길을 다시 공격로로 바꾸는 길잡이입니다. 사마의의 지연선을 먼저 읽어야 합니다.' },
{ unitId: 'wei-yan', reason: '노성 앞 지연선을 짧게 흔들 돌파 장수입니다. 다만 장합의 추격과 돌아올 길을 함께 계산해야 합니다.' },
{ unitId: 'zhao-yun', reason: '깊이 들어간 부대를 회수하고 예비대를 묶어 장합 기병의 추격을 늦춥니다.' },
{ unitId: 'wang-ping', reason: '기산 산길의 표식과 회수선을 세워 공격로가 퇴로를 잃지 않게 합니다.' },
{ unitId: 'huang-quan', reason: '한중 우로 장부를 공세 가능량으로 환산해 보급 붕괴를 막습니다.' },
{ unitId: 'ma-liang', reason: '노성 앞 마을과 새 보급로의 고시문을 정리해 북벌 명분을 실제 약속으로 보이게 합니다.' },
{ unitId: 'li-yan', reason: '수레와 운송 지연을 후방에서 관리합니다. 전장에 데려가면 보급 위기는 줄지만 기동 선택이 줄어듭니다.' },
{ unitId: 'ma-dai', reason: '서량 기병의 측면 감각으로 곽회의 차단로와 동쪽 물목을 견제합니다.' }
],
note: '기산 재출정전은 추천 무장이 9명입니다. 공격로를 열 장수, 돌아올 표식을 세울 장수, 후방 장부를 맡길 장수 중 둘을 반드시 군영에 남겨야 합니다.'
}
};
@@ -1362,6 +1382,23 @@ const sortieRulesByCampaignStep: Partial<Record<CampaignStep, SortieRuleDefiniti
{ unitId: 'ma-dai', reason: '음평 물목의 기병 우회로를 다음 측면 대응 기준으로 정리합니다.' }
],
note: '한중 우로 방어 이후도 엔딩이 아니라 방어 책임이 넓어진 다음 북벌 준비입니다. 지킨 길을 공격로로 바꾸려면 누가 전방에 나가고 누가 보급을 붙들지 다시 골라야 합니다.'
},
'sixty-second-camp': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
excludedUnitIds: ['liu-bei'],
recommended: [
{ unitId: 'zhuge-liang', reason: '기산 재출정 이후 더 깊은 북방 계산을 다시 세워야 하는 중심입니다.' },
{ unitId: 'jiang-wei', reason: '노성 앞에서 확인한 위군 지연선을 다음 공격로 지도로 정리합니다.' },
{ unitId: 'wei-yan', reason: '돌파가 성과를 냈을 때 어디까지 허용할지 다음 군령으로 남깁니다.' },
{ unitId: 'zhao-yun', reason: '장합 추격을 견딘 회수 기동을 예비대 훈련 기준으로 정리합니다.' },
{ unitId: 'wang-ping', reason: '기산 회수 표식과 진영 정지선을 다음 전선의 안전장치로 남깁니다.' },
{ unitId: 'huang-quan', reason: '노성 앞 공세가 실제로 소모한 군량과 남은 장부를 계산합니다.' },
{ unitId: 'ma-liang', reason: '전방 마을 안심 약속과 고시문을 다음 북벌 명분으로 이어 줍니다.' },
{ unitId: 'li-yan', reason: '운송 지연과 예비 수레 배정을 후방 기준으로 다시 정리합니다.' },
{ unitId: 'ma-dai', reason: '곽회 차단로 대응과 서량 기병 우회 기준을 다음 측면 운용에 반영합니다.' }
],
note: '기산 재출정 이후도 엔딩이 아니라 더 깊어진 북방 전선 준비입니다. 이긴 길을 지킬 사람과 더 깊이 들어갈 사람을 나누어 보십시오.'
}
};
@@ -6555,6 +6592,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 30
}
]
},
{
id: 'northern-eighth-jiangwei-weiyan-forward',
title: '앞길을 여는 칼끝',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['jiang-wei', 'wei-yan'],
bondId: 'jiang-wei__wei-yan_northern-eighth',
rewardExp: 100,
lines: [
'강유: 노성 앞에서는 빠른 길이 곧 안전한 길은 아니었습니다. 먼저 열어도 돌아올 표식이 없으면 길이 아닙니다.',
'위연: 그렇다면 다음에는 그대가 길을 고르고 내가 길을 넓히겠소. 대신 멈출 곳도 처음부터 말해 주시오.',
'강유: 전진선과 회수선을 함께 그리겠습니다. 돌파가 장수의 고집이 아니라 군의 길이 되도록 하겠습니다.'
],
choices: [
{
id: 'mark-breakthrough-return-line',
label: '돌파선과 회수선을 함께 긋는다',
response: '강유는 낮은 길을 지도에 표시했고, 위연은 그 선을 넘어설 때 필요한 깃발 신호를 따로 정했습니다.',
rewardExp: 35
},
{
id: 'set-lucheng-pressure-rhythm',
label: '노성 압박 호흡을 맞춘다',
response: '위연은 성급한 돌파 대신 강유의 정찰 보고가 닿는 박자에 맞춰 전열을 흔들기로 했습니다.',
rewardExp: 32
}
]
},
{
id: 'northern-eighth-zhaoyun-wangping-return',
title: '회수선의 표식',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['zhao-yun', 'wang-ping'],
bondId: 'zhao-yun__wang-ping_northern-eighth',
rewardExp: 98,
lines: [
'조운: 장합의 기병은 길이 열린 순간보다 돌아가는 순간을 노렸습니다.',
'왕평: 그래서 표식은 앞에만 세우면 안 됩니다. 돌아오는 병사가 어둠 속에서도 볼 수 있어야 합니다.',
'조운: 다음에는 예비대가 표식을 따라 먼저 움직이겠습니다. 병사들이 표식을 보면 제 깃발도 함께 보게 하겠습니다.'
],
choices: [
{
id: 'pair-reserve-with-road-markers',
label: '예비대와 표식을 묶는다',
response: '왕평은 산길 표식을 다시 세웠고, 조운은 예비대가 어느 표식을 보면 즉시 움직일지 훈련 명령에 적었습니다.',
rewardExp: 34
},
{
id: 'test-zhanghe-pursuit-drill',
label: '장합 추격 대비를 훈련한다',
response: '조운의 기동대가 일부러 늦게 물러났고, 왕평은 그 길목마다 병사들이 헷갈린 지점을 따로 고쳤습니다.',
rewardExp: 31
}
]
},
{
id: 'northern-eighth-huangquan-maliang-ledger',
title: '장부와 고시문',
availableAfterBattleIds: [campBattleIds.sixtySecond],
unitIds: ['huang-quan', 'ma-liang'],
bondId: 'huang-quan__ma-liang_northern-eighth',
rewardExp: 96,
lines: [
'황권: 공세가 성공해도 장부가 비면 마을은 승리보다 징발을 먼저 기억합니다.',
'마량: 그래서 고시문은 승전 뒤가 아니라 출정 전부터 준비되어야 합니다. 약속이 늦으면 민심도 늦습니다.',
'황권: 다음 장부에는 군량 수치 옆에 고시문 날짜를 함께 적겠습니다.'
],
choices: [
{
id: 'bind-ledger-to-proclamation',
label: '장부와 고시문 날짜를 묶는다',
response: '황권은 군량 배분표 옆에 마량의 고시문 날짜를 적었고, 마량은 약속한 배분이 실제 수레에 실리는지 확인했습니다.',
rewardExp: 33
},
{
id: 'prepare-front-village-reassurance',
label: '전방 마을 안심 문서를 준비한다',
response: '마량은 노성 앞 마을에 붙일 문서를 고쳤고, 황권은 그 문서가 지킬 수 있는 군량 수치를 함께 봉했습니다.',
rewardExp: 30
}
]
}
];
@@ -9341,6 +9459,36 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['쌀 34', '상처약 24']
}
]
},
{
id: 'northern-eighth-lucheng-supply-table',
title: '노성 앞 보급 탁자',
location: '기산 재출정 군막',
availableAfterBattleIds: [campBattleIds.sixtySecond],
bondId: 'huang-quan__ma-liang_northern-eighth',
description: '황권과 마량이 노성 앞 전진로, 마을 안심 약속, 다음 북벌의 실제 군량 여유를 하나의 탁자 위에서 맞춥니다.',
lines: [
'마량: 전투가 이긴 뒤에 붙이는 고시문은 늦습니다. 마을은 군량이 지나간 자리를 먼저 봅니다.',
'황권: 그렇다면 장부의 첫 줄을 병사 몫이 아니라 지나갈 마을 몫으로 두겠습니다.',
'군막의 탁자에는 노성 앞 길과 회수 진영, 전방 마을의 약속이 한 장의 지도로 묶였습니다.'
],
choices: [
{
id: 'seal-lucheng-supply-ledger',
label: '기산 재출정 장부를 봉한다',
response: '황권은 노성 앞에서 쓴 군량과 남은 수레를 봉했고, 마량은 그 장부를 다음 고시문 기준으로 삼았습니다.',
bondExp: 82,
itemRewards: ['기산 재출정 장부 1']
},
{
id: 'reserve-front-village-grain',
label: '전방 마을 예비 군량을 남긴다',
response: '마량은 마을 이름을 하나씩 불렀고, 황권은 그 이름 옆에 실제로 남길 군량 숫자를 적었습니다.',
bondExp: 76,
gold: 2860,
itemRewards: ['쌀 36', '상처약 26']
}
]
}
];
@@ -9450,7 +9598,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fiftyEighth ||
battleId === campBattleIds.fiftyNinth ||
battleId === campBattleIds.sixtieth ||
battleId === campBattleIds.sixtyFirst
battleId === campBattleIds.sixtyFirst ||
battleId === campBattleIds.sixtySecond
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -9471,7 +9620,7 @@ export class CampScene extends Phaser.Scene {
...hanzhongOpeningRecruitUnits,
...hanzhongScoutRecruitUnits,
...hanzhongMainRecruitUnits,
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? jiangWeiRecruitUnits : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? jiangWeiRecruitUnits : [])
],
[
...xuzhouRecruitBonds,
@@ -9511,12 +9660,13 @@ export class CampScene extends Phaser.Scene {
...mengHuoFinalCaptureBonds,
...northernFirstBattleBonds,
...northernSecondBattleBonds,
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernFourthBattleBonds : []),
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernFifthBattleBonds : []),
...(battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst ? northernSixthBattleBonds : []),
...(battleId === campBattleIds.sixtyFirst ? northernSeventhBattleBonds : [])
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernThirdBattleBonds : []),
...(battleId === campBattleIds.fiftySeventh || battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? jiangWeiRecruitBonds : []),
...(battleId === campBattleIds.fiftyEighth || battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernFourthBattleBonds : []),
...(battleId === campBattleIds.fiftyNinth || battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernFifthBattleBonds : []),
...(battleId === campBattleIds.sixtieth || battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernSixthBattleBonds : []),
...(battleId === campBattleIds.sixtyFirst || battleId === campBattleIds.sixtySecond ? northernSeventhBattleBonds : []),
...(battleId === campBattleIds.sixtySecond ? northernEighthBattleBonds : [])
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -10157,6 +10307,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'northern-campaign-prep-camp') {
return '북벌 준비 군영';
}
if (battleId === campBattleIds.sixtySecond) {
return '기산 재출정 후 군영';
}
if (battleId === campBattleIds.sixtyFirst) {
return '한중 우로 방어 후 군영';
}

View File

@@ -17,6 +17,7 @@ import {
fiftyNinthBattleScenario,
sixtiethBattleScenario,
sixtyFirstBattleScenario,
sixtySecondBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -422,6 +423,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'fifty-ninth-camp' ||
campaign.step === 'sixtieth-camp' ||
campaign.step === 'sixty-first-camp' ||
campaign.step === 'sixty-second-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp' ||
campaign.step === 'baidi-entrustment-camp' ||
@@ -736,6 +738,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'sixty-second-battle') {
this.scene.start('BattleScene', { battleId: sixtySecondBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -164,6 +164,8 @@ export type CampaignStep =
| 'sixtieth-camp'
| 'sixty-first-battle'
| 'sixty-first-camp'
| 'sixty-second-battle'
| 'sixty-second-camp'
| 'hanzhong-king-camp'
| 'shu-han-foundation-camp'
| 'baidi-entrustment-camp'
@@ -334,7 +336,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'fifty-eighth-battle-qishan-retreat': { victory: 'fifty-eighth-camp', retry: 'fifty-eighth-battle' },
'fifty-ninth-battle-chencang-siege': { victory: 'fifty-ninth-camp', retry: 'fifty-ninth-battle' },
'sixtieth-battle-wudu-yinping': { victory: 'sixtieth-camp', retry: 'sixtieth-battle' },
'sixty-first-battle-hanzhong-rain-defense': { victory: 'sixty-first-camp', retry: 'sixty-first-battle' }
'sixty-first-battle-hanzhong-rain-defense': { victory: 'sixty-first-camp', retry: 'sixty-first-battle' },
'sixty-second-battle-qishan-renewed-offensive': { victory: 'sixty-second-camp', retry: 'sixty-second-battle' }
};
export type CampaignSaveSlotSummary = {