diff --git a/docs/roadmap.md b/docs/roadmap.md
index 79a0ef4..3e2f8a1 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -75,15 +75,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Forty-eighth battle Meng Huo main-force route, where Zhuge Liang leads a seven-officer pacification sortie across a 96x78 southern mountain battlefield, captures Meng Huo through a dedicated pacify-leader objective, and opens post-battle release, captive-ledger, and market-reassurance camp events
- Forty-ninth battle Meng Huo second-capture route, turning the first release into a repeated Seven Captures arc with a 98x80 marsh-jungle battlefield, renewed pacify-leader objective, second-release camp dialogues, and village/captive reassurance visits
- Fiftieth battle Meng Huo third-capture route, making the growing recruited roster matter through a Zhuge Liang-led seven-officer sortie choice, a 100x82 clan-and-marsh battlefield, named southern clan officers, and post-battle clan-persuasion camp events
+- Fifty-first battle Meng Huo fourth-capture route, where prior clan-persuasion camp choices become battlefield goals through an amnesty-route objective, fire-fort suppression, a 102x84 poison-river battlefield, and Ma Chao/Ma Dai pursuit-control sortie recommendations
- 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 the third Meng Huo capture 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
+- Flow verification script from title through the fourth Meng Huo capture 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 fourth Seven Captures Nanman battle so the previous clan persuasion choices begin changing objectives, enemy morale, and recommended sortie officers
+1. Build the fifth Seven Captures Nanman battle so hardline clan separation and repeated amnesty routes begin changing enemy morale, optional objectives, and post-battle persuasion outcomes
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 a268d53..34c3851 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 !== 50 ||
+ progressTabState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 51 ||
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 !== 50 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 51 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 50 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 51 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 50 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 51 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 50 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 51 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 50 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 51 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 50 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 51 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,7 +5586,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 50 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 51 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5741,7 +5741,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
- postGonganProgressState.campaignProgress?.totalKnown !== 50 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 51 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5899,7 +5899,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
- postMaichengProgressState.campaignProgress?.totalKnown !== 50 ||
+ postMaichengProgressState.campaignProgress?.totalKnown !== 51 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6056,7 +6056,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
- postYilingProgressState.campaignProgress?.totalKnown !== 50 ||
+ postYilingProgressState.campaignProgress?.totalKnown !== 51 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6214,7 +6214,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
- postYilingFireProgressState.campaignProgress?.totalKnown !== 50 ||
+ postYilingFireProgressState.campaignProgress?.totalKnown !== 51 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6280,7 +6280,7 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
- postBaidiProgressState.campaignProgress?.totalKnown !== 50 ||
+ postBaidiProgressState.campaignProgress?.totalKnown !== 51 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
@@ -6441,7 +6441,7 @@ try {
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
- postNanzhongProgressState.campaignProgress?.totalKnown !== 50 ||
+ postNanzhongProgressState.campaignProgress?.totalKnown !== 51 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
) {
@@ -6603,7 +6603,7 @@ try {
if (
postMengHuoProgressState?.activeTab !== 'progress' ||
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
- postMengHuoProgressState.campaignProgress?.totalKnown !== 50 ||
+ postMengHuoProgressState.campaignProgress?.totalKnown !== 51 ||
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
) {
@@ -6765,7 +6765,7 @@ try {
if (
postSecondCaptureProgressState?.activeTab !== 'progress' ||
postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
- postSecondCaptureProgressState.campaignProgress?.totalKnown !== 50 ||
+ postSecondCaptureProgressState.campaignProgress?.totalKnown !== 51 ||
postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전'
) {
@@ -6930,14 +6930,180 @@ try {
if (
postThirdCaptureProgressState?.activeTab !== 'progress' ||
postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 ||
- postThirdCaptureProgressState.campaignProgress?.totalKnown !== 50 ||
+ postThirdCaptureProgressState.campaignProgress?.totalKnown !== 51 ||
postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' ||
- postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전'
) {
- throw new Error(`Expected post-third-capture progress tab to complete the fiftieth battle and pause before the fourth capture arc: ${JSON.stringify(postThirdCaptureProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-third-capture progress tab to complete the fiftieth battle and point to the fourth capture arc: ${JSON.stringify(postThirdCaptureProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-third-capture-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const fourthCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !fourthCaptureSortieState?.sortieVisible ||
+ fourthCaptureSortieState.campaign?.step !== 'fiftieth-camp' ||
+ fourthCaptureSortieState.campTitle !== '칠종칠금 3차전 후 군영' ||
+ fourthCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 4차전 · 맹획 네 번째 생포와 회유 퇴로 유지' ||
+ fourthCaptureSortieState.sortiePlan?.maxCount !== 7 ||
+ fourthCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ !fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
+ !fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
+ !fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
+ !fourthCaptureSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') ||
+ fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected fourth Meng Huo sortie prep to expose clan-persuasion recommendations, Zhuge Liang lock, and the expanded recruited roster: ${JSON.stringify(fourthCaptureSortieState)}`);
+ }
+ assertSortieTacticalRoster(fourthCaptureSortieState, [
+ '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 fourthCapturePriorityUnits = ['ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'ma-chao', 'ma-dai'];
+ for (const unitId of fourthCapturePriorityUnits) {
+ 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 && !fourthCapturePriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const fourthCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !fourthCapturePriorityUnits.every((unitId) =>
+ fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
+ fourthCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ fourthCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected fourth Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Huang Quan, Wang Ping, Zhao Yun, Ma Chao, and Ma Dai: ${JSON.stringify(fourthCaptureSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-fourth-capture-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-fourth-capture-story.png', fullPage: true });
+
+ for (let i = 0; i < 70; i += 1) {
+ const enteredFiftyFirstBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'fifty-first-battle-meng-huo-fourth-capture';
+ });
+ if (enteredFiftyFirstBattle) {
+ 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-first-battle-meng-huo-fourth-capture' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-fifty-first-battle.png', fullPage: true });
+
+ const fiftyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const fiftyFirstEnemies = fiftyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const fiftyFirstAllies = fiftyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
+ const fiftyFirstEnemyBehaviors = new Set(fiftyFirstEnemies.map((unit) => unit.ai));
+ if (
+ fiftyFirstBattleState.camera?.mapWidth !== 102 ||
+ fiftyFirstBattleState.camera?.mapHeight !== 84 ||
+ fiftyFirstBattleState.victoryConditionLabel !== '맹획 네 번째 생포와 회유 퇴로 유지' ||
+ !fiftyFirstBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-fourth' && objective.label === '맹획 네 번째 생포') ||
+ !fiftyFirstBattleState.objectives?.some((objective) => objective.id === 'amnesty-route' && objective.label === '회유 퇴로 유지') ||
+ fiftyFirstEnemies.length < 39 ||
+ !fiftyFirstEnemyBehaviors.has('aggressive') ||
+ !fiftyFirstEnemyBehaviors.has('guard') ||
+ !fiftyFirstEnemyBehaviors.has('hold') ||
+ !fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-leader') ||
+ !fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-wutugu') ||
+ !fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-mangyachang') ||
+ !fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-dongtuna') ||
+ !fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-ahuinan') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'ma-liang') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'huang-quan') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'wang-ping') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'ma-chao') ||
+ !fiftyFirstAllies.some((unit) => unit.id === 'ma-dai') ||
+ fiftyFirstAllies.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected fifty-first battle to use the fourth Meng Huo map, amnesty-route objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftyFirstBattleState)}`);
+ }
+
+ 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 fiftyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ fiftyFirstCampState?.campBattleId !== 'fifty-first-battle-meng-huo-fourth-capture' ||
+ fiftyFirstCampState.campTitle !== '칠종칠금 4차전 후 군영' ||
+ fiftyFirstCampState.availableDialogueIds?.length !== 3 ||
+ !fiftyFirstCampState.availableDialogueIds.every((id) => id.includes('fourth-capture')) ||
+ fiftyFirstCampState.availableVisitIds?.length !== 2 ||
+ !fiftyFirstCampState.availableVisitIds.every((id) => id.includes('fourth-capture')) ||
+ fiftyFirstCampState.rosterCollection?.total < 21
+ ) {
+ throw new Error(`Expected fifty-first camp to expose fourth capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyFirstCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-fourth-capture-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postFourthCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postFourthCaptureProgressState?.activeTab !== 'progress' ||
+ postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 ||
+ postFourthCaptureProgressState.campaignProgress?.totalKnown !== 51 ||
+ postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' ||
+ postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-fourth-capture progress tab to complete the fifty-first battle and pause before the fifth capture arc: ${JSON.stringify(postFourthCaptureProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-fourth-capture-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -6969,11 +7135,11 @@ try {
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
camp: window.__HEROS_DEBUG__?.camp?.() ?? null
}));
- if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fiftieth-camp') {
- throw new Error(`Expected title continue to reopen the latest third Meng Huo capture camp: ${JSON.stringify(titleContinueState)}`);
+ if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fifty-first-camp') {
+ throw new Error(`Expected title continue to reopen the latest fourth Meng Huo capture camp: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-third-Meng-Huo-capture 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 Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, and third capture state at ${targetUrl}`);
+ console.log(`Verified title-to-fourth-Meng-Huo-capture 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 Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, and fourth capture state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/fifty-first-battle-map.svg b/src/assets/images/battle/fifty-first-battle-map.svg
new file mode 100644
index 0000000..2b02add
--- /dev/null
+++ b/src/assets/images/battle/fifty-first-battle-map.svg
@@ -0,0 +1,77 @@
+
diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts
new file mode 100644
index 0000000..a1e0e8f
--- /dev/null
+++ b/src/game/data/battleMapAssets.ts
@@ -0,0 +1,105 @@
+import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg';
+import eighteenthBattleMapUrl from '../../assets/images/battle/eighteenth-battle-map.svg';
+import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
+import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-battle-map.svg';
+import fiftyFirstBattleMapUrl from '../../assets/images/battle/fifty-first-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';
+import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg';
+import fortyEighthBattleMapUrl from '../../assets/images/battle/forty-eighth-battle-map.svg';
+import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg';
+import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg';
+import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg';
+import fortyNinthBattleMapUrl from '../../assets/images/battle/forty-ninth-battle-map.svg';
+import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-battle-map.svg';
+import fortySeventhBattleMapUrl from '../../assets/images/battle/forty-seventh-battle-map.svg';
+import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg';
+import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battle-map.svg';
+import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
+import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
+import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg';
+import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
+import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
+import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
+import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
+import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg';
+import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
+import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
+import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
+import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
+import thirtyEighthBattleMapUrl from '../../assets/images/battle/thirty-eighth-battle-map.svg';
+import thirtyFifthBattleMapUrl from '../../assets/images/battle/thirty-fifth-battle-map.svg';
+import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
+import thirtyFourthBattleMapUrl from '../../assets/images/battle/thirty-fourth-battle-map.svg';
+import thirtyNinthBattleMapUrl from '../../assets/images/battle/thirty-ninth-battle-map.svg';
+import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
+import thirtySeventhBattleMapUrl from '../../assets/images/battle/thirty-seventh-battle-map.svg';
+import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-battle-map.svg';
+import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
+import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
+import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
+import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
+import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg';
+import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
+import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg';
+import twentyNinthBattleMapUrl from '../../assets/images/battle/twenty-ninth-battle-map.svg';
+import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg';
+import twentySeventhBattleMapUrl from '../../assets/images/battle/twenty-seventh-battle-map.svg';
+import twentySixthBattleMapUrl from '../../assets/images/battle/twenty-sixth-battle-map.svg';
+import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg';
+import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
+
+export const battleMapAssets: Record = {
+ 'battle-map-first': firstBattleMapUrl,
+ 'battle-map-second': secondBattleMapUrl,
+ 'battle-map-third': thirdBattleMapUrl,
+ 'battle-map-fourth': fourthBattleMapUrl,
+ 'battle-map-fifth': fifthBattleMapUrl,
+ 'battle-map-sixth': sixthBattleMapUrl,
+ 'battle-map-seventh': seventhBattleMapUrl,
+ 'battle-map-eighth': eighthBattleMapUrl,
+ 'battle-map-ninth': ninthBattleMapUrl,
+ 'battle-map-tenth': tenthBattleMapUrl,
+ 'battle-map-eleventh': eleventhBattleMapUrl,
+ 'battle-map-twelfth': twelfthBattleMapUrl,
+ 'battle-map-thirteenth': thirteenthBattleMapUrl,
+ 'battle-map-fourteenth': fourteenthBattleMapUrl,
+ 'battle-map-fifteenth': fifteenthBattleMapUrl,
+ 'battle-map-sixteenth': sixteenthBattleMapUrl,
+ 'battle-map-seventeenth': seventeenthBattleMapUrl,
+ 'battle-map-eighteenth': eighteenthBattleMapUrl,
+ 'battle-map-nineteenth': nineteenthBattleMapUrl,
+ 'battle-map-twentieth': twentiethBattleMapUrl,
+ 'battle-map-twenty-first': twentyFirstBattleMapUrl,
+ 'battle-map-twenty-second': twentySecondBattleMapUrl,
+ 'battle-map-twenty-third': twentyThirdBattleMapUrl,
+ 'battle-map-twenty-fourth': twentyFourthBattleMapUrl,
+ 'battle-map-twenty-fifth': twentyFifthBattleMapUrl,
+ 'battle-map-twenty-sixth': twentySixthBattleMapUrl,
+ 'battle-map-twenty-seventh': twentySeventhBattleMapUrl,
+ 'battle-map-twenty-eighth': twentyEighthBattleMapUrl,
+ 'battle-map-twenty-ninth': twentyNinthBattleMapUrl,
+ 'battle-map-thirtieth': thirtiethBattleMapUrl,
+ 'battle-map-thirty-first': thirtyFirstBattleMapUrl,
+ 'battle-map-thirty-second': thirtySecondBattleMapUrl,
+ 'battle-map-thirty-third': thirtyThirdBattleMapUrl,
+ 'battle-map-thirty-fourth': thirtyFourthBattleMapUrl,
+ 'battle-map-thirty-fifth': thirtyFifthBattleMapUrl,
+ 'battle-map-thirty-sixth': thirtySixthBattleMapUrl,
+ 'battle-map-thirty-seventh': thirtySeventhBattleMapUrl,
+ 'battle-map-thirty-eighth': thirtyEighthBattleMapUrl,
+ 'battle-map-thirty-ninth': thirtyNinthBattleMapUrl,
+ 'battle-map-fortieth': fortiethBattleMapUrl,
+ 'battle-map-forty-first': fortyFirstBattleMapUrl,
+ 'battle-map-forty-second': fortySecondBattleMapUrl,
+ 'battle-map-forty-third': fortyThirdBattleMapUrl,
+ 'battle-map-forty-fourth': fortyFourthBattleMapUrl,
+ 'battle-map-forty-fifth': fortyFifthBattleMapUrl,
+ 'battle-map-forty-sixth': fortySixthBattleMapUrl,
+ 'battle-map-forty-seventh': fortySeventhBattleMapUrl,
+ 'battle-map-forty-eighth': fortyEighthBattleMapUrl,
+ 'battle-map-forty-ninth': fortyNinthBattleMapUrl,
+ 'battle-map-fiftieth': fiftiethBattleMapUrl,
+ 'battle-map-fifty-first': fiftyFirstBattleMapUrl
+};
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 478e5cc..2450572 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -3,6 +3,10 @@ import {
fiftiethBattleMap,
fiftiethBattleUnits,
fiftiethBattleVictoryPages,
+ fiftyFirstBattleBonds,
+ fiftyFirstBattleMap,
+ fiftyFirstBattleUnits,
+ fiftyFirstBattleVictoryPages,
firstBattleBonds,
fifteenthBattleBonds,
fifteenthBattleMap,
@@ -255,7 +259,8 @@ export type BattleScenarioId =
| 'forty-seventh-battle-nanzhong-stabilization'
| 'forty-eighth-battle-meng-huo-main-force'
| 'forty-ninth-battle-meng-huo-second-capture'
- | 'fiftieth-battle-meng-huo-third-capture';
+ | 'fiftieth-battle-meng-huo-third-capture'
+ | 'fifty-first-battle-meng-huo-fourth-capture';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -3247,6 +3252,73 @@ export const fiftiethBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const fiftyFirstBattleScenario: BattleScenarioDefinition = {
+ id: 'fifty-first-battle-meng-huo-fourth-capture',
+ title: '칠종칠금 4차전',
+ victoryConditionLabel: '맹획 네 번째 생포와 회유 퇴로 유지',
+ defeatConditionLabel: '제갈량 퇴각',
+ openingObjectiveLines: [
+ '세 번째 생포 뒤 호족 회의에서 열어 둔 퇴로가 이번 전장의 핵심입니다. 물러설 뜻이 있는 호족을 몰살하면 남중은 다시 모두 적이 됩니다.',
+ '오돌골의 등갑병과 독천 제사장들이 화덕 주변을 지키고 있습니다. 화덕 요새를 제압하되 마을과 포로장을 지켜, 촉한군의 약속이 전투 중에도 지켜진다는 것을 보여야 합니다.',
+ '제갈량은 반드시 생존해야 합니다. 마량으로 호족 문서를 전하고, 황권과 왕평으로 장부와 퇴로를 지키며, 마초와 마대로 추격선을 조절하십시오.'
+ ],
+ map: fiftyFirstBattleMap,
+ units: fiftyFirstBattleUnits,
+ bonds: fiftyFirstBattleBonds,
+ mapTextureKey: 'battle-map-fifty-first',
+ leaderUnitId: 'menghuo-fourth-leader',
+ quickVictoryTurnLimit: 94,
+ baseVictoryGold: 12280,
+ objectives: [
+ {
+ id: 'pacify-meng-huo-fourth',
+ kind: 'pacify-leader',
+ label: '맹획 네 번째 생포',
+ rewardGold: 8420,
+ unitId: 'menghuo-fourth-leader'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 2140,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'amnesty-route',
+ kind: 'secure-terrain',
+ label: '회유 퇴로 유지',
+ rewardGold: 4140,
+ terrain: 'road'
+ },
+ {
+ id: 'captive-camps',
+ kind: 'secure-terrain',
+ label: '포로장 보증 유지',
+ rewardGold: 3360,
+ terrain: 'camp'
+ },
+ {
+ id: 'fire-forts',
+ kind: 'secure-terrain',
+ label: '화덕 요새 제압',
+ rewardGold: 3640,
+ terrain: 'fort'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '94턴 이내 네 번째 생포',
+ rewardGold: 2780,
+ maxTurn: 94
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
+ itemRewards: ['콩 66', '상처약 50', '탁주 22', '호족 보증 장부 1', '회유 퇴로 표식 1'],
+ victoryPages: fiftyFirstBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -3299,7 +3371,8 @@ export const battleScenarios: Record
'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario,
'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario,
'forty-ninth-battle-meng-huo-second-capture': fortyNinthBattleScenario,
- 'fiftieth-battle-meng-huo-third-capture': fiftiethBattleScenario
+ 'fiftieth-battle-meng-huo-third-capture': fiftiethBattleScenario,
+ 'fifty-first-battle-meng-huo-fourth-capture': fiftyFirstBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 17a7d80..b11098f 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -5,6 +5,7 @@ import {
eighteenthBattleScenario,
eleventhBattleScenario,
fiftiethBattleScenario,
+ fiftyFirstBattleScenario,
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
@@ -62,6 +63,8 @@ import {
eleventhBattleVictoryPages,
fiftiethBattleIntroPages,
fiftiethBattleVictoryPages,
+ fiftyFirstBattleIntroPages,
+ fiftyFirstBattleVictoryPages,
fortiethBattleIntroPages,
fortiethBattleVictoryPages,
fortyFirstBattleIntroPages,
@@ -731,13 +734,24 @@ const sortieFlows: Record = {
},
[fiftiethBattleScenario.id]: {
afterBattleId: fiftiethBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '호족 회유 회의',
+ eyebrow: '다음 전장',
+ title: fiftyFirstBattleScenario.title,
description:
- '맹획은 세 번째로 붙잡혔고, 그를 따르던 호족들의 말도 흔들리기 시작했습니다. 다음 장에서는 전투 전 회의에서 어떤 호족을 먼저 달랠지 고르고, 그 선택이 다음 남중 전장의 목표와 추천 출전에 영향을 주는 흐름을 준비합니다.',
- rewardHint: '다음 장: 남중 호족 회유 회의와 칠종칠금 4차전 준비 중',
- pages: fiftiethBattleVictoryPages,
- unavailableNotice: '칠종칠금 4차전은 다음 작업에서 이어집니다. 지금은 세 번째 생포 뒤의 호족 회유 선택지와 출전 추천 변화를 정비할 수 있습니다.'
+ '세 번째 생포 뒤 열린 호족 회의는 전장의 조건을 바꾸었습니다. 이제 회유 퇴로를 남기고, 독천과 화덕 요새를 억제하며, 흔들리는 호족들이 직접 촉한군의 약속을 보게 해야 합니다.',
+ rewardHint: `예상 보상: ${fiftyFirstBattleScenario.title} 개방 / 호족 보증 장부`,
+ nextBattleId: fiftyFirstBattleScenario.id,
+ campaignStep: 'fifty-first-battle',
+ pages: [...fiftiethBattleVictoryPages, ...fiftyFirstBattleIntroPages]
+ },
+ [fiftyFirstBattleScenario.id]: {
+ afterBattleId: fiftyFirstBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '더 깊은 남중 회유',
+ description:
+ '맹획은 네 번째로 붙잡혔고, 회유 퇴로와 보증 장부는 남중 호족들의 말 속에 남았습니다. 다음 장에서는 더 깊은 산채와 강경파 호족을 상대로 같은 약속을 지킬 수 있는지 다룹니다.',
+ rewardHint: '다음 장: 칠종칠금 5차전과 남중 강경파 회유 준비 중',
+ pages: fiftyFirstBattleVictoryPages,
+ unavailableNotice: '칠종칠금 5차전은 다음 작업에서 이어집니다. 지금은 네 번째 생포 뒤의 보증 장부, 퇴로 표식, 강경파 분리 흐름을 정비할 수 있습니다.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index fe8de6b..48260f6 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -3297,6 +3297,59 @@ export const fiftiethBattleVictoryPages: StoryPage[] = [
}
];
+export const fiftyFirstBattleIntroPages: StoryPage[] = [
+ {
+ id: 'fifty-first-clan-council-result',
+ bgm: 'story-dark',
+ chapter: '회유가 전장으로',
+ background: 'story-militia',
+ speaker: '마량',
+ text: '호족 회의 뒤 일부 촌장은 맹획을 따르되 끝까지 싸우지는 않겠다고 속내를 보였습니다. 그들이 물러날 길을 남겨 두면 다음 전투는 섬멸보다 설득에 가까워질 것입니다.'
+ },
+ {
+ id: 'fifty-first-fourth-capture-plan',
+ bgm: 'battle-prep',
+ chapter: '네 번째 생포',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '이번에는 맹획의 본영뿐 아니라 오돌골의 등갑병과 독천 제사장들이 전장을 뒤덮을 것입니다. 화덕과 독천을 함부로 건드리지 말고, 회유 퇴로와 마을 보증을 함께 지키십시오.'
+ },
+ {
+ id: 'fifty-first-sortie',
+ bgm: 'battle-prep',
+ chapter: '칠종칠금 4차전',
+ background: 'story-sortie',
+ speaker: '왕평',
+ text: '숲길은 더 좁고 강물은 더 깊습니다. 마량이 호족의 말을 붙잡고, 황권이 보증 장부를 지키며, 왕평과 마대가 열어 둔 산길을 따라 맹획을 네 번째 생포해야 합니다.'
+ }
+];
+
+export const fiftyFirstBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'fifty-first-victory-fourth-capture',
+ bgm: 'militia-theme',
+ chapter: '네 번째 생포',
+ background: 'story-sortie',
+ text: '맹획은 네 번째로 포위되었고, 등갑병의 화덕과 독천 제사장들은 마을까지 밀려오지 못했습니다. 회유 퇴로로 물러난 호족들은 촉한군이 약속한 길을 지켰다는 사실을 직접 보았습니다.'
+ },
+ {
+ id: 'fifty-first-victory-amnesty-route',
+ bgm: 'story-dark',
+ chapter: '열어 둔 길',
+ background: 'story-militia',
+ speaker: '황권',
+ text: '돌아간 자와 끝까지 싸운 자의 이름이 장부에 나뉘었습니다. 이제 남중의 호족들은 맹획의 고집만이 아니라, 촉한군의 약속이 지켜지는지도 함께 따지게 될 것입니다.'
+ },
+ {
+ id: 'fifty-first-next-deeper-nanzhong',
+ bgm: 'story-dark',
+ chapter: '더 깊은 남중',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '네 번 잡아도 맹획은 아직 남중 전체의 마음을 대표한다고 말합니다. 그렇다면 더 깊은 산채와 더 어려운 조건 앞에서도 같은 군율을 지켜 보여야 합니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3742,6 +3795,12 @@ export const fiftiethBattleMap: BattleMap = {
terrain: createFiftiethBattleTerrain()
};
+export const fiftyFirstBattleMap: BattleMap = {
+ width: 102,
+ height: 84,
+ terrain: createFiftyFirstBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -16041,6 +16100,93 @@ export const fiftiethBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('menghuo-third-leader', '맹획', '남만왕', 'rebelLeader', 150, 1010, 248, 94, 41)
];
+const fiftyFirstBattleAllyPositions: Record = {
+ 'guan-yu': { x: 14, y: 63 },
+ 'zhang-fei': { x: 19, y: 65 },
+ 'jian-yong': { x: 10, y: 70 },
+ 'mi-zhu': { x: 12, y: 68 },
+ 'sun-qian': { x: 16, y: 72 },
+ 'zhao-yun': { x: 24, y: 60 },
+ 'zhuge-liang': { x: 13, y: 60 },
+ 'ma-liang': { x: 17, y: 57 },
+ 'yi-ji': { x: 10, y: 75 },
+ 'gong-zhi': { x: 20, y: 73 },
+ 'huang-zhong': { x: 27, y: 61 },
+ 'wei-yan': { x: 32, y: 65 },
+ 'pang-tong': { x: 15, y: 54 },
+ 'fa-zheng': { x: 21, y: 55 },
+ 'wu-yi': { x: 34, y: 70 },
+ 'yan-yan': { x: 37, y: 63 },
+ 'li-yan': { x: 26, y: 53 },
+ 'huang-quan': { x: 10, y: 58 },
+ 'ma-chao': { x: 39, y: 60 },
+ 'ma-dai': { x: 42, y: 65 },
+ 'wang-ping': { x: 29, y: 56 }
+};
+
+export const fiftyFirstBattleUnits: 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, fiftyFirstBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('menghuo-fourth-scout-a', '남만 회유 감시병', '서쪽 퇴로 감시', 'bandit', 148, 650, 196, 46, 36),
+ createThirtyFirstEnemyUnit('menghuo-fourth-scout-b', '남만 회유 감시병', '늪지 감시', 'bandit', 148, 650, 196, 61, 34),
+ createThirtyFirstEnemyUnit('menghuo-fourth-scout-c', '남만 회유 감시병', '장터 감시', 'bandit', 149, 658, 198, 76, 51),
+ createThirtyFirstEnemyUnit('menghuo-fourth-scout-d', '남만 회유 감시병', '동쪽 산길 감시', 'bandit', 149, 658, 198, 92, 66),
+ createThirtyFirstEnemyUnit('menghuo-fourth-scout-e', '남만 회유 감시병', '북쪽 협곡 감시', 'bandit', 150, 666, 200, 54, 24),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-a', '등갑 화덕병', '늪지 등갑병', 'yellowTurban', 149, 914, 232, 51, 45),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-b', '등갑 화덕병', '화덕 등갑병', 'yellowTurban', 150, 926, 234, 63, 48),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-c', '등갑 화덕병', '마을 등갑병', 'yellowTurban', 150, 926, 234, 76, 58),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-d', '등갑 화덕병', '본대 등갑병', 'yellowTurban', 151, 938, 236, 88, 50),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-e', '등갑 화덕병', '후위 등갑병', 'yellowTurban', 151, 938, 236, 96, 42),
+ createThirtyFirstEnemyUnit('menghuo-fourth-vine-f', '등갑 화덕병', '협곡 등갑병', 'yellowTurban', 152, 950, 238, 84, 31),
+ createThirtyFirstEnemyUnit('menghuo-fourth-archer-a', '남만 산림 궁병', '독천 궁병', 'archer', 148, 702, 208, 47, 28),
+ createThirtyFirstEnemyUnit('menghuo-fourth-archer-b', '남만 산림 궁병', '절벽 궁병', 'archer', 149, 710, 210, 62, 26),
+ createThirtyFirstEnemyUnit('menghuo-fourth-archer-c', '남만 산림 궁병', '마을 궁병', 'archer', 149, 710, 210, 74, 44),
+ createThirtyFirstEnemyUnit('menghuo-fourth-archer-d', '남만 산림 궁병', '늪지 궁병', 'archer', 150, 718, 212, 86, 62),
+ createThirtyFirstEnemyUnit('menghuo-fourth-archer-e', '남만 산림 궁병', '본영 궁병', 'archer', 150, 718, 212, 95, 55),
+ createThirtyFirstEnemyUnit('menghuo-fourth-beast-a', '남만 맹수 유인대', '강가 맹수대', 'cavalry', 150, 930, 236, 56, 38),
+ createThirtyFirstEnemyUnit('menghuo-fourth-beast-b', '남만 맹수 유인대', '늪지 맹수대', 'cavalry', 150, 930, 236, 69, 40),
+ createThirtyFirstEnemyUnit('menghuo-fourth-beast-c', '남만 맹수 유인대', '산채 맹수대', 'cavalry', 151, 942, 238, 81, 53),
+ createThirtyFirstEnemyUnit('menghuo-fourth-beast-d', '남만 맹수 유인대', '추격 맹수대', 'cavalry', 151, 942, 238, 93, 59),
+ createThirtyFirstEnemyUnit('menghuo-fourth-beast-e', '남만 맹수 유인대', '회유로 차단대', 'cavalry', 152, 954, 240, 67, 68),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-a', '호족 반전 매복병', '덩굴 매복병', 'bandit', 149, 670, 202, 38, 51),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-b', '호족 반전 매복병', '숲속 매복병', 'bandit', 150, 678, 204, 54, 54),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-c', '호족 반전 매복병', '강가 매복병', 'bandit', 150, 678, 204, 67, 67),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-d', '호족 반전 매복병', '후방 차단대', 'bandit', 151, 686, 206, 86, 74),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-e', '호족 반전 매복병', '서쪽 차단대', 'bandit', 151, 686, 206, 33, 64),
+ createThirtyFirstEnemyUnit('menghuo-fourth-ambusher-f', '호족 반전 매복병', '회유로 감시대', 'bandit', 152, 694, 208, 73, 72),
+ createThirtyFirstEnemyUnit('menghuo-fourth-guard-a', '맹획 사차 친위', '포로장 친위대', 'yellowTurban', 152, 966, 242, 71, 42),
+ createThirtyFirstEnemyUnit('menghuo-fourth-guard-b', '맹획 사차 친위', '산채 친위대', 'yellowTurban', 152, 966, 242, 83, 48),
+ createThirtyFirstEnemyUnit('menghuo-fourth-guard-c', '맹획 사차 친위', '본영 친위대', 'yellowTurban', 153, 978, 244, 94, 45),
+ createThirtyFirstEnemyUnit('menghuo-fourth-guard-d', '맹획 사차 친위', '화덕 친위대', 'yellowTurban', 153, 978, 244, 89, 35),
+ createThirtyFirstEnemyUnit('menghuo-fourth-shaman-a', '독천 제사장', '늪지 제사장', 'strategist', 151, 744, 218, 63, 36),
+ createThirtyFirstEnemyUnit('menghuo-fourth-shaman-b', '독천 제사장', '강가 제사장', 'strategist', 152, 752, 220, 77, 49),
+ createThirtyFirstEnemyUnit('menghuo-fourth-shaman-c', '독천 제사장', '산채 제사장', 'strategist', 152, 752, 220, 88, 38),
+ createThirtyFirstEnemyUnit('menghuo-fourth-shaman-d', '독천 제사장', '화덕 제사장', 'strategist', 153, 760, 222, 82, 57),
+ createThirtyFirstEnemyUnit('menghuo-fourth-officer-wutugu', '오돌골', '등갑병 두령', 'rebelLeader', 154, 1010, 252, 86, 37),
+ createThirtyFirstEnemyUnit('menghuo-fourth-officer-mangyachang', '망아장', '맹수 선봉장', 'rebelLeader', 154, 1010, 252, 81, 65),
+ createThirtyFirstEnemyUnit('menghuo-fourth-officer-dongtuna', '동도나', '흔들리는 호족장', 'rebelLeader', 153, 990, 246, 70, 60),
+ createThirtyFirstEnemyUnit('menghuo-fourth-officer-ahuinan', '아회남', '회유로 감시 호족', 'rebelLeader', 153, 990, 246, 66, 52),
+ createThirtyFirstEnemyUnit('menghuo-fourth-leader', '맹획', '남만왕', 'rebelLeader', 156, 1048, 258, 96, 43)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -16976,6 +17122,35 @@ export const mengHuoThirdCaptureBonds: BattleBond[] = [
export const fiftiethBattleBonds: BattleBond[] = [...fortyNinthBattleBonds, ...mengHuoThirdCaptureBonds].map(
cloneBattleBondForScenario
);
+export const mengHuoFourthCaptureBonds: BattleBond[] = [
+ {
+ id: 'zhuge-liang__ma-liang_fourth-capture',
+ unitIds: ['zhuge-liang', 'ma-liang'],
+ title: '회유 회의의 결실',
+ level: 51,
+ exp: 0,
+ description: '제갈량의 큰 회유책과 마량의 호족 설득문은 전장 안에서 물러날 길과 항복의 명분을 함께 만든다.'
+ },
+ {
+ id: 'huang-quan__wang-ping_fourth-capture',
+ unitIds: ['huang-quan', 'wang-ping'],
+ title: '보증 장부와 퇴로',
+ level: 47,
+ exp: 0,
+ description: '황권의 보증 장부와 왕평의 산길 판단은 흔들리는 호족들이 다시 창을 들지 않고 물러날 길을 보장한다.'
+ },
+ {
+ id: 'ma-chao__ma-dai_fourth-capture',
+ unitIds: ['ma-chao', 'ma-dai'],
+ title: '남중 추격 차단',
+ level: 43,
+ exp: 0,
+ description: '마초와 마대는 거친 남만 기병을 몰아붙이되, 회유 퇴로를 짓밟지 않도록 돌격의 선을 맞춘다.'
+ }
+];
+export const fiftyFirstBattleBonds: BattleBond[] = [...fiftiethBattleBonds, ...mengHuoFourthCaptureBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -19549,6 +19724,74 @@ function createFiftiethBattleTerrain(): TerrainType[][] {
);
}
+function createFiftyFirstBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 84 }, (_, y) =>
+ Array.from({ length: 102 }, (_, x): TerrainType => {
+ if (
+ (x >= 0 && x <= 101 && y >= 76) ||
+ (x >= 62 && x <= 68 && y >= 0 && y <= 78) ||
+ (x >= 5 && x <= 30 && y >= 67 && y <= 83) ||
+ (x >= 76 && x <= 92 && y >= 58 && y <= 76)
+ ) {
+ return 'river';
+ }
+ if (
+ (x >= 9 && x <= 96 && y >= 63 && y <= 69) ||
+ (x >= 27 && x <= 74 && y >= 42 && y <= 49) ||
+ (x >= 70 && x <= 96 && y >= 34 && y <= 40) ||
+ (x >= 37 && x <= 45 && y >= 39 && y <= 72) ||
+ (x >= 76 && x <= 82 && y >= 48 && y <= 80)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 18 && x <= 72 && y >= 20 && y <= 74) ||
+ (x >= 55 && x <= 96 && y >= 28 && y <= 70) ||
+ (x >= 2 && x <= 38 && y >= 49 && y <= 74) ||
+ (x >= 78 && x <= 101 && y >= 41 && y <= 73)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 31 && y <= 38) ||
+ (x >= 33 && x <= 68 && y <= 28) ||
+ (x >= 71 && x <= 101 && y <= 37) ||
+ (x >= 86 && x <= 101 && y >= 33 && y <= 55)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 74) ||
+ (x >= 99 && y <= 70) ||
+ (x >= 30 && x <= 36 && y >= 0 && y <= 40) ||
+ (x >= 68 && x <= 74 && y >= 0 && y <= 34) ||
+ (x >= 42 && x <= 48 && y >= 64 && y <= 83)
+ ) {
+ return 'cliff';
+ }
+ if (
+ (x >= 10 && x <= 25 && y >= 59 && y <= 71) ||
+ (x >= 39 && x <= 54 && y >= 43 && y <= 56) ||
+ (x >= 75 && x <= 91 && y >= 49 && y <= 61) ||
+ (x >= 80 && x <= 94 && y >= 32 && y <= 44)
+ ) {
+ return 'village';
+ }
+ if ((x >= 23 && x <= 37 && y >= 51 && y <= 62) || (x >= 63 && x <= 78 && y >= 56 && y <= 68)) {
+ return 'camp';
+ }
+ if (
+ (x >= 47 && x <= 63 && y >= 31 && y <= 42) ||
+ (x >= 72 && x <= 92 && y >= 31 && y <= 45) ||
+ (x >= 84 && x <= 96 && y >= 43 && y <= 54)
+ ) {
+ 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 6462fe3..f649b55 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -1,5 +1,6 @@
import Phaser from 'phaser';
import { soundDirector } from '../audio/SoundDirector';
+import { battleMapAssets } from '../data/battleMapAssets';
import { type BattleBond, type UnitData, type UnitStats } from '../data/scenario';
import { defaultBattleScenario, getBattleScenario, type BattleObjectiveDefinition, type BattleScenarioDefinition } from '../data/battles';
import { getTerrainRule, getUnitClass, type TerrainType, type UnitClassKey } from '../data/battleRules';
@@ -873,7 +874,16 @@ const unitTexture: Record = {
'menghuo-third-officer-zhurong-vanguard': 'unit-rebel-leader',
'menghuo-third-officer-jinhuan': 'unit-rebel-leader',
'menghuo-third-officer-ahuinan': 'unit-rebel-leader',
- 'menghuo-third-leader': 'unit-rebel-leader'
+ 'menghuo-third-leader': 'unit-rebel-leader',
+ 'menghuo-fourth-shaman-a': 'unit-rebel-archer',
+ 'menghuo-fourth-shaman-b': 'unit-rebel-archer',
+ 'menghuo-fourth-shaman-c': 'unit-rebel-archer',
+ 'menghuo-fourth-shaman-d': 'unit-rebel-archer',
+ 'menghuo-fourth-officer-wutugu': 'unit-rebel-leader',
+ 'menghuo-fourth-officer-mangyachang': 'unit-rebel-leader',
+ 'menghuo-fourth-officer-dongtuna': 'unit-rebel-leader',
+ 'menghuo-fourth-officer-ahuinan': 'unit-rebel-leader',
+ 'menghuo-fourth-leader': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -2372,7 +2382,47 @@ const enemyAiByUnitId: Record = {
'menghuo-third-officer-zhurong-vanguard': 'aggressive',
'menghuo-third-officer-jinhuan': 'aggressive',
'menghuo-third-officer-ahuinan': 'guard',
- 'menghuo-third-leader': 'hold'
+ 'menghuo-third-leader': 'hold',
+ 'menghuo-fourth-scout-a': 'aggressive',
+ 'menghuo-fourth-scout-b': 'aggressive',
+ 'menghuo-fourth-scout-c': 'aggressive',
+ 'menghuo-fourth-scout-d': 'aggressive',
+ 'menghuo-fourth-scout-e': 'aggressive',
+ 'menghuo-fourth-vine-a': 'guard',
+ 'menghuo-fourth-vine-b': 'guard',
+ 'menghuo-fourth-vine-c': 'guard',
+ 'menghuo-fourth-vine-d': 'guard',
+ 'menghuo-fourth-vine-e': 'guard',
+ 'menghuo-fourth-vine-f': 'guard',
+ 'menghuo-fourth-archer-a': 'hold',
+ 'menghuo-fourth-archer-b': 'hold',
+ 'menghuo-fourth-archer-c': 'hold',
+ 'menghuo-fourth-archer-d': 'hold',
+ 'menghuo-fourth-archer-e': 'hold',
+ 'menghuo-fourth-beast-a': 'aggressive',
+ 'menghuo-fourth-beast-b': 'aggressive',
+ 'menghuo-fourth-beast-c': 'aggressive',
+ 'menghuo-fourth-beast-d': 'aggressive',
+ 'menghuo-fourth-beast-e': 'aggressive',
+ 'menghuo-fourth-ambusher-a': 'aggressive',
+ 'menghuo-fourth-ambusher-b': 'aggressive',
+ 'menghuo-fourth-ambusher-c': 'aggressive',
+ 'menghuo-fourth-ambusher-d': 'aggressive',
+ 'menghuo-fourth-ambusher-e': 'aggressive',
+ 'menghuo-fourth-ambusher-f': 'aggressive',
+ 'menghuo-fourth-guard-a': 'guard',
+ 'menghuo-fourth-guard-b': 'guard',
+ 'menghuo-fourth-guard-c': 'guard',
+ 'menghuo-fourth-guard-d': 'guard',
+ 'menghuo-fourth-shaman-a': 'hold',
+ 'menghuo-fourth-shaman-b': 'hold',
+ 'menghuo-fourth-shaman-c': 'hold',
+ 'menghuo-fourth-shaman-d': 'hold',
+ 'menghuo-fourth-officer-wutugu': 'guard',
+ 'menghuo-fourth-officer-mangyachang': 'aggressive',
+ 'menghuo-fourth-officer-dongtuna': 'guard',
+ 'menghuo-fourth-officer-ahuinan': 'guard',
+ 'menghuo-fourth-leader': 'hold'
};
const defaultEnemyAiByClass: Partial> = {
@@ -2567,6 +2617,10 @@ export class BattleScene extends Phaser.Scene {
this.installDebugHotkeys();
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
+ this.ensureScenarioMapTexture(() => this.drawBattleScene());
+ }
+
+ private drawBattleScene() {
this.drawMap();
this.drawUnits();
this.drawSidePanel();
@@ -2575,6 +2629,39 @@ export class BattleScene extends Phaser.Scene {
this.renderRosterPanel('ally', '행동할 장수를 선택하세요.');
}
+ private ensureScenarioMapTexture(onReady: () => void) {
+ const textureKey = battleScenario.mapTextureKey;
+ if (this.textures.exists(textureKey)) {
+ onReady();
+ return;
+ }
+
+ const textureUrl = battleMapAssets[textureKey];
+ if (!textureUrl) {
+ throw new Error(`Missing battle map asset for ${textureKey}`);
+ }
+
+ const loadingText = this.add.text(this.scale.width / 2, this.scale.height / 2, '전장 지도를 펼치는 중...', {
+ fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
+ fontSize: '22px',
+ color: '#f5e6b8',
+ stroke: '#05070a',
+ strokeThickness: 4
+ });
+ loadingText.setOrigin(0.5);
+ loadingText.setDepth(90);
+
+ this.load.once('complete', () => {
+ loadingText.destroy();
+ onReady();
+ });
+ this.load.once('loaderror', () => {
+ loadingText.setText('전장 지도를 불러오지 못했습니다.');
+ });
+ this.load.image(textureKey, textureUrl);
+ this.load.start();
+ }
+
private resetBattleData(campaign?: CampaignState) {
const selected = new Set(campaign?.selectedSortieUnitIds ?? []);
const forcedAllyIds = new Set(
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 3770cb1..f71dbf0 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -1,54 +1,4 @@
import Phaser from 'phaser';
-import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg';
-import eighteenthBattleMapUrl from '../../assets/images/battle/eighteenth-battle-map.svg';
-import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
-import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-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';
-import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg';
-import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg';
-import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-battle-map.svg';
-import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battle-map.svg';
-import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg';
-import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg';
-import fortyEighthBattleMapUrl from '../../assets/images/battle/forty-eighth-battle-map.svg';
-import fortyNinthBattleMapUrl from '../../assets/images/battle/forty-ninth-battle-map.svg';
-import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg';
-import fortySeventhBattleMapUrl from '../../assets/images/battle/forty-seventh-battle-map.svg';
-import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
-import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
-import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
-import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg';
-import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
-import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
-import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
-import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg';
-import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
-import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
-import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
-import thirtyFourthBattleMapUrl from '../../assets/images/battle/thirty-fourth-battle-map.svg';
-import thirtyEighthBattleMapUrl from '../../assets/images/battle/thirty-eighth-battle-map.svg';
-import thirtyFifthBattleMapUrl from '../../assets/images/battle/thirty-fifth-battle-map.svg';
-import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
-import thirtyNinthBattleMapUrl from '../../assets/images/battle/thirty-ninth-battle-map.svg';
-import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
-import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
-import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-battle-map.svg';
-import thirtySeventhBattleMapUrl from '../../assets/images/battle/thirty-seventh-battle-map.svg';
-import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
-import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
-import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
-import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
-import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg';
-import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg';
-import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg';
-import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg';
-import twentyNinthBattleMapUrl from '../../assets/images/battle/twenty-ninth-battle-map.svg';
-import twentySixthBattleMapUrl from '../../assets/images/battle/twenty-sixth-battle-map.svg';
-import twentySeventhBattleMapUrl from '../../assets/images/battle/twenty-seventh-battle-map.svg';
-import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
-import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
@@ -108,56 +58,6 @@ export class BootScene extends Phaser.Scene {
this.load.image('story-three-heroes', storyThreeHeroesUrl);
this.load.image('story-militia', storyMilitiaUrl);
this.load.image('story-sortie', storySortieUrl);
- this.load.image('battle-map-first', firstBattleMapUrl);
- this.load.image('battle-map-second', secondBattleMapUrl);
- this.load.image('battle-map-third', thirdBattleMapUrl);
- this.load.image('battle-map-fourth', fourthBattleMapUrl);
- this.load.image('battle-map-fifth', fifthBattleMapUrl);
- this.load.image('battle-map-sixth', sixthBattleMapUrl);
- this.load.image('battle-map-seventh', seventhBattleMapUrl);
- this.load.image('battle-map-eighth', eighthBattleMapUrl);
- this.load.image('battle-map-ninth', ninthBattleMapUrl);
- this.load.image('battle-map-tenth', tenthBattleMapUrl);
- this.load.image('battle-map-eleventh', eleventhBattleMapUrl);
- this.load.image('battle-map-fiftieth', fiftiethBattleMapUrl);
- this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
- this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
- this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl);
- this.load.image('battle-map-fifteenth', fifteenthBattleMapUrl);
- this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl);
- this.load.image('battle-map-seventeenth', seventeenthBattleMapUrl);
- this.load.image('battle-map-eighteenth', eighteenthBattleMapUrl);
- this.load.image('battle-map-nineteenth', nineteenthBattleMapUrl);
- this.load.image('battle-map-twentieth', twentiethBattleMapUrl);
- this.load.image('battle-map-twenty-first', twentyFirstBattleMapUrl);
- this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl);
- this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl);
- this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl);
- this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl);
- this.load.image('battle-map-twenty-sixth', twentySixthBattleMapUrl);
- this.load.image('battle-map-twenty-seventh', twentySeventhBattleMapUrl);
- this.load.image('battle-map-twenty-eighth', twentyEighthBattleMapUrl);
- this.load.image('battle-map-twenty-ninth', twentyNinthBattleMapUrl);
- this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl);
- this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl);
- this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl);
- this.load.image('battle-map-thirty-third', thirtyThirdBattleMapUrl);
- this.load.image('battle-map-thirty-fourth', thirtyFourthBattleMapUrl);
- this.load.image('battle-map-thirty-fifth', thirtyFifthBattleMapUrl);
- this.load.image('battle-map-thirty-sixth', thirtySixthBattleMapUrl);
- this.load.image('battle-map-thirty-seventh', thirtySeventhBattleMapUrl);
- this.load.image('battle-map-thirty-eighth', thirtyEighthBattleMapUrl);
- this.load.image('battle-map-thirty-ninth', thirtyNinthBattleMapUrl);
- this.load.image('battle-map-fortieth', fortiethBattleMapUrl);
- this.load.image('battle-map-forty-first', fortyFirstBattleMapUrl);
- this.load.image('battle-map-forty-second', fortySecondBattleMapUrl);
- this.load.image('battle-map-forty-third', fortyThirdBattleMapUrl);
- this.load.image('battle-map-forty-fourth', fortyFourthBattleMapUrl);
- this.load.image('battle-map-forty-fifth', fortyFifthBattleMapUrl);
- this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl);
- this.load.image('battle-map-forty-seventh', fortySeventhBattleMapUrl);
- this.load.image('battle-map-forty-eighth', fortyEighthBattleMapUrl);
- this.load.image('battle-map-forty-ninth', fortyNinthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index 5a75bcc..4f85ddb 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -28,6 +28,7 @@ import {
hanzhongScoutRecruitBonds,
hanzhongScoutRecruitUnits,
mengHuoThirdCaptureBonds,
+ mengHuoFourthCaptureBonds,
fanCastleBonds,
fanCastleSiegeBonds,
jingCollapseBonds,
@@ -413,7 +414,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'forty-seventh-battle-nanzhong-stabilization',
'forty-eighth-battle-meng-huo-main-force',
'forty-ninth-battle-meng-huo-second-capture',
- 'fiftieth-battle-meng-huo-third-capture'
+ 'fiftieth-battle-meng-huo-third-capture',
+ 'fifty-first-battle-meng-huo-fourth-capture'
],
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금']
}
@@ -469,7 +471,8 @@ const campBattleIds = {
fortySeventh: 'forty-seventh-battle-nanzhong-stabilization',
fortyEighth: 'forty-eighth-battle-meng-huo-main-force',
fortyNinth: 'forty-ninth-battle-meng-huo-second-capture',
- fiftieth: 'fiftieth-battle-meng-huo-third-capture'
+ fiftieth: 'fiftieth-battle-meng-huo-third-capture',
+ fiftyFirst: 'fifty-first-battle-meng-huo-fourth-capture'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1072,6 +1075,20 @@ const sortieRulesByBattleId: Partial