diff --git a/docs/roadmap.md b/docs/roadmap.md
index 95c2ef0..d807991 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -73,15 +73,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Baidi Castle entrustment story milestone, where Liu Bei's retreat from Yiling becomes a playable camp transition into Zhuge Liang's regency burden and post-defeat recovery planning
- Forty-seventh battle Nanzhong stabilization route, where the Baidi entrustment aftermath opens a Zhuge Liang-led sortie, removes Liu Bei from the deployable roster, and uses a 94x76 southern mountain battlefield with village, warehouse, and rebel command objectives
- 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
- 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 Meng Huo main-force 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 camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the second 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 next capture-and-release Nanman battle so Meng Huo's return becomes a repeated persuasion arc rather than a one-off fight
+1. Build the third Seven Captures Nanman battle so Meng Huo's repeated release begins to sway named southern clans and unlock stronger persuasion choices
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 fd2a884..7d049ec 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 !== 48 ||
+ progressTabState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 49 ||
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 !== 48 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 49 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 48 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 49 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 48 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 49 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 48 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 49 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 48 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 49 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 48 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 49 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,7 +5586,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 48 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 49 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5741,7 +5741,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
- postGonganProgressState.campaignProgress?.totalKnown !== 48 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 49 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5899,7 +5899,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
- postMaichengProgressState.campaignProgress?.totalKnown !== 48 ||
+ postMaichengProgressState.campaignProgress?.totalKnown !== 49 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6056,7 +6056,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
- postYilingProgressState.campaignProgress?.totalKnown !== 48 ||
+ postYilingProgressState.campaignProgress?.totalKnown !== 49 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6214,7 +6214,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
- postYilingFireProgressState.campaignProgress?.totalKnown !== 48 ||
+ postYilingFireProgressState.campaignProgress?.totalKnown !== 49 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6280,7 +6280,7 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
- postBaidiProgressState.campaignProgress?.totalKnown !== 48 ||
+ postBaidiProgressState.campaignProgress?.totalKnown !== 49 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
@@ -6441,7 +6441,7 @@ try {
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
- postNanzhongProgressState.campaignProgress?.totalKnown !== 48 ||
+ postNanzhongProgressState.campaignProgress?.totalKnown !== 49 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
) {
@@ -6603,14 +6603,176 @@ try {
if (
postMengHuoProgressState?.activeTab !== 'progress' ||
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
- postMengHuoProgressState.campaignProgress?.totalKnown !== 48 ||
+ postMengHuoProgressState.campaignProgress?.totalKnown !== 49 ||
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
- postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
) {
- throw new Error(`Expected post-Meng-Huo progress tab to complete the forty-eighth battle and pause before the next capture-and-release arc: ${JSON.stringify(postMengHuoProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Meng-Huo progress tab to complete the forty-eighth battle and point to the second capture arc: ${JSON.stringify(postMengHuoProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-meng-huo-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const secondCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !secondCaptureSortieState?.sortieVisible ||
+ secondCaptureSortieState.campaign?.step !== 'forty-eighth-camp' ||
+ secondCaptureSortieState.campTitle !== '맹획 본대전 후 군영' ||
+ secondCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 2차전 · 맹획 재생포와 마을 회유' ||
+ secondCaptureSortieState.sortiePlan?.maxCount !== 7 ||
+ secondCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ !secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
+ !secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
+ secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected second Meng Huo sortie prep to lock Zhuge Liang, recommend capture-and-release officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(secondCaptureSortieState)}`);
+ }
+ assertSortieTacticalRoster(secondCaptureSortieState, [
+ '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 secondCapturePriorityUnits = ['huang-quan', 'wang-ping', 'ma-liang', 'zhao-yun', 'huang-zhong', 'ma-chao'];
+ for (const unitId of secondCapturePriorityUnits) {
+ 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 && !secondCapturePriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const secondCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !secondCapturePriorityUnits.every((unitId) =>
+ secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
+ secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
+ secondCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ secondCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected second Meng Huo sortie to deploy Zhuge Liang, Huang Quan, Wang Ping, Ma Liang, Zhao Yun, Huang Zhong, and Ma Chao: ${JSON.stringify(secondCaptureSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-second-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-second-capture-story.png', fullPage: true });
+
+ for (let i = 0; i < 60; i += 1) {
+ const enteredFortyNinthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'forty-ninth-battle-meng-huo-second-capture';
+ });
+ if (enteredFortyNinthBattle) {
+ 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 === 'forty-ninth-battle-meng-huo-second-capture' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-forty-ninth-battle.png', fullPage: true });
+
+ const fortyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const fortyNinthEnemies = fortyNinthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const fortyNinthAllies = fortyNinthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const fortyNinthEnemyBehaviors = new Set(fortyNinthEnemies.map((unit) => unit.ai));
+ if (
+ fortyNinthBattleState.camera?.mapWidth !== 98 ||
+ fortyNinthBattleState.camera?.mapHeight !== 80 ||
+ fortyNinthBattleState.victoryConditionLabel !== '맹획 재생포와 마을 회유' ||
+ !fortyNinthBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-second' && objective.label === '맹획 두 번째 생포') ||
+ fortyNinthEnemies.length < 33 ||
+ !fortyNinthEnemyBehaviors.has('aggressive') ||
+ !fortyNinthEnemyBehaviors.has('guard') ||
+ !fortyNinthEnemyBehaviors.has('hold') ||
+ !fortyNinthEnemies.some((unit) => unit.id === 'menghuo-second-leader') ||
+ !fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-meng-you') ||
+ !fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-duosi') ||
+ !fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-mulu') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'zhuge-liang') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'huang-quan') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'wang-ping') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'ma-liang') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'huang-zhong') ||
+ !fortyNinthAllies.some((unit) => unit.id === 'ma-chao') ||
+ fortyNinthAllies.some((unit) => unit.id === 'liu-bei')
+ ) {
+ throw new Error(`Expected forty-ninth battle to use second Meng Huo map, pacification objective, selected officers, and mixed Nanman AI: ${JSON.stringify(fortyNinthBattleState)}`);
+ }
+
+ 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 fortyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ fortyNinthCampState?.campBattleId !== 'forty-ninth-battle-meng-huo-second-capture' ||
+ fortyNinthCampState.campTitle !== '칠종칠금 2차전 후 군영' ||
+ fortyNinthCampState.availableDialogueIds?.length !== 3 ||
+ !fortyNinthCampState.availableDialogueIds.every((id) => id.includes('second-capture')) ||
+ fortyNinthCampState.availableVisitIds?.length !== 2 ||
+ !fortyNinthCampState.availableVisitIds.every((id) => id.includes('second-capture')) ||
+ fortyNinthCampState.rosterCollection?.total < 21
+ ) {
+ throw new Error(`Expected forty-ninth camp to expose second capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortyNinthCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-second-capture-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postSecondCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postSecondCaptureProgressState?.activeTab !== 'progress' ||
+ postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
+ postSecondCaptureProgressState.campaignProgress?.totalKnown !== 49 ||
+ postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
+ postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-second-capture progress tab to complete the forty-ninth battle and pause before the third persuasion arc: ${JSON.stringify(postSecondCaptureProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-second-capture-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -6642,11 +6804,11 @@ try {
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
camp: window.__HEROS_DEBUG__?.camp?.() ?? null
}));
- if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-eighth-camp') {
- throw new Error(`Expected title continue to reopen the latest Meng Huo camp: ${JSON.stringify(titleContinueState)}`);
+ if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-ninth-camp') {
+ throw new Error(`Expected title continue to reopen the latest second Meng Huo capture camp: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-Meng-Huo-main-force 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, and Meng Huo pacification state at ${targetUrl}`);
+ console.log(`Verified title-to-second-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, and second capture state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/forty-ninth-battle-map.svg b/src/assets/images/battle/forty-ninth-battle-map.svg
new file mode 100644
index 0000000..e2999ff
--- /dev/null
+++ b/src/assets/images/battle/forty-ninth-battle-map.svg
@@ -0,0 +1,77 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index aefa2fe..3e84a41 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -48,6 +48,10 @@ import {
fortyEighthBattleMap,
fortyEighthBattleUnits,
fortyEighthBattleVictoryPages,
+ fortyNinthBattleBonds,
+ fortyNinthBattleMap,
+ fortyNinthBattleUnits,
+ fortyNinthBattleVictoryPages,
fortySixthBattleBonds,
fortySixthBattleMap,
fortySixthBattleUnits,
@@ -245,7 +249,8 @@ export type BattleScenarioId =
| 'forty-fifth-battle-yiling-vanguard'
| 'forty-sixth-battle-yiling-fire'
| 'forty-seventh-battle-nanzhong-stabilization'
- | 'forty-eighth-battle-meng-huo-main-force';
+ | 'forty-eighth-battle-meng-huo-main-force'
+ | 'forty-ninth-battle-meng-huo-second-capture';
export type BattleObjectiveKind = 'defeat-leader' | 'pacify-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -3117,6 +3122,66 @@ export const fortyEighthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const fortyNinthBattleScenario: BattleScenarioDefinition = {
+ id: 'forty-ninth-battle-meng-huo-second-capture',
+ title: '칠종칠금 2차전',
+ victoryConditionLabel: '맹획 재생포와 마을 회유',
+ defeatConditionLabel: '제갈량 퇴각',
+ openingObjectiveLines: [
+ '맹획은 한 번 풀려났지만 곧바로 다시 산채를 모았습니다. 이번 싸움의 목적도 참수가 아니라 두 번째 생포와 더 넓은 회유 명분입니다.',
+ '강가 늪지와 산채 길목에 매복이 많습니다. 포로장과 마을을 지키면 맹획을 돌려보낸 뒤에도 남중 사람들이 촉한군의 군율을 기억할 것입니다.',
+ '제갈량은 반드시 생존해야 합니다. 왕평으로 산길을 읽고, 황권으로 포로 장부를 지키며, 조운과 황충으로 맹수 돌격과 고지 궁병을 동시에 억제하십시오.'
+ ],
+ map: fortyNinthBattleMap,
+ units: fortyNinthBattleUnits,
+ bonds: fortyNinthBattleBonds,
+ mapTextureKey: 'battle-map-forty-ninth',
+ leaderUnitId: 'menghuo-second-leader',
+ quickVictoryTurnLimit: 86,
+ baseVictoryGold: 11140,
+ objectives: [
+ {
+ id: 'pacify-meng-huo-second',
+ kind: 'pacify-leader',
+ label: '맹획 두 번째 생포',
+ rewardGold: 7600,
+ unitId: 'menghuo-second-leader'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 1980,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'amnesty-villages',
+ kind: 'secure-terrain',
+ label: '회유 마을 유지',
+ rewardGold: 3740,
+ terrain: 'village'
+ },
+ {
+ id: 'captive-camps',
+ kind: 'secure-terrain',
+ label: '포로장 군율 유지',
+ rewardGold: 3020,
+ terrain: 'camp'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '86턴 이내 재생포',
+ rewardGold: 2520,
+ maxTurn: 86
+ }
+ ],
+ defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
+ itemRewards: ['콩 58', '상처약 44', '탁주 19', '칠종칠금 회유문 1', '포로 안심 장부 1'],
+ victoryPages: fortyNinthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -3167,7 +3232,8 @@ export const battleScenarios: Record
'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario,
'forty-sixth-battle-yiling-fire': fortySixthBattleScenario,
'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario,
- 'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario
+ 'forty-eighth-battle-meng-huo-main-force': fortyEighthBattleScenario,
+ 'forty-ninth-battle-meng-huo-second-capture': fortyNinthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index c5eea59..34e9037 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -11,6 +11,7 @@ import {
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortyEighthBattleScenario,
+ fortyNinthBattleScenario,
fortySixthBattleScenario,
fortySeventhBattleScenario,
fifthBattleScenario,
@@ -72,6 +73,8 @@ import {
fortyFifthBattleVictoryPages,
fortyEighthBattleIntroPages,
fortyEighthBattleVictoryPages,
+ fortyNinthBattleIntroPages,
+ fortyNinthBattleVictoryPages,
fortySixthBattleIntroPages,
fortySeventhBattleIntroPages,
fortySeventhBattleVictoryPages,
@@ -703,13 +706,24 @@ const sortieFlows: Record = {
},
[fortyEighthBattleScenario.id]: {
afterBattleId: fortyEighthBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '칠종칠금의 첫 장',
+ eyebrow: '다음 전장',
+ title: fortyNinthBattleScenario.title,
description:
- '맹획을 처음 생포하고 돌려보낸 뒤, 남중의 호족들은 촉한군의 방식이 단순 토벌이 아님을 보기 시작했습니다. 다음 장에서는 풀려난 맹획이 다시 세운 진영과, 그를 완전히 설득하기 위한 반복 회유 흐름을 다룹니다.',
- rewardHint: '다음 장: 칠종칠금 회유전 준비 중',
- pages: fortyEighthBattleVictoryPages,
- unavailableNotice: '칠종칠금 회유전은 다음 작업에서 이어집니다. 지금은 맹획 본대전 뒤의 공명도, 포로 장부, 회유 선택지를 정비할 수 있습니다.'
+ '맹획을 처음 생포하고 돌려보낸 뒤, 남중의 호족들은 촉한군의 방식이 단순 토벌이 아님을 보기 시작했습니다. 풀려난 맹획은 다시 산채를 모으지만, 이번에는 마을과 포로장이 촉한군의 군율을 증언할 수 있습니다.',
+ rewardHint: `예상 보상: ${fortyNinthBattleScenario.title} 개방 / 칠종칠금 회유문`,
+ nextBattleId: fortyNinthBattleScenario.id,
+ campaignStep: 'forty-ninth-battle',
+ pages: [...fortyEighthBattleVictoryPages, ...fortyNinthBattleIntroPages]
+ },
+ [fortyNinthBattleScenario.id]: {
+ afterBattleId: fortyNinthBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '남중 회유의 확장',
+ description:
+ '맹획은 두 번째로 붙잡혔지만 아직 남중 전체가 굴복한 것은 아닙니다. 이제는 맹획 개인의 고집을 꺾는 것과 동시에, 호족과 마을이 어떤 조건에서 촉한의 질서를 받아들일지 다루어야 합니다.',
+ rewardHint: '다음 장: 칠종칠금 3차전과 남중 호족 선택지 준비 중',
+ pages: fortyNinthBattleVictoryPages,
+ unavailableNotice: '칠종칠금 3차전은 다음 작업에서 이어집니다. 지금은 두 번째 생포 뒤의 회유 공명, 포로 안심 장부, 남중 마을 선택지를 정비할 수 있습니다.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index a198682..0f84fac 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -3191,6 +3191,59 @@ export const fortyEighthBattleVictoryPages: StoryPage[] = [
}
];
+export const fortyNinthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'forty-ninth-meng-huo-returns',
+ bgm: 'story-dark',
+ chapter: '돌아온 맹획',
+ background: 'story-militia',
+ speaker: '왕평',
+ text: '맹획은 풀려나자마자 다시 산채를 모았습니다. 그러나 지난 싸움과 달리 마을 사람들은 촉한군이 약탈하지 않았다는 사실을 알고 있습니다. 이번 싸움은 그 마음의 흔들림을 전장 위에서 붙잡는 일입니다.'
+ },
+ {
+ id: 'forty-ninth-second-capture-plan',
+ bgm: 'battle-prep',
+ chapter: '두 번째 생포',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '이번에도 베어서는 안 됩니다. 강가의 늪지와 산채 길목을 끊어 맹획을 다시 생포하고, 마을과 포로장을 지켜 그가 돌아갈 때 들을 말까지 남겨 두십시오.'
+ },
+ {
+ id: 'forty-ninth-sortie',
+ bgm: 'battle-prep',
+ chapter: '칠종칠금 2차전',
+ background: 'story-sortie',
+ speaker: '마량',
+ text: '맹획은 이번에 매복과 주술사를 앞세웠습니다. 왕평이 길을 읽고, 황권이 포로 장부를 지키며, 조운과 황충이 돌격과 고지 궁병을 끊어야 합니다. 회유의 말은 전열이 버텨야 닿습니다.'
+ }
+];
+
+export const fortyNinthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'forty-ninth-victory-second-capture',
+ bgm: 'militia-theme',
+ chapter: '두 번째 생포',
+ background: 'story-sortie',
+ text: '맹획은 다시 포위되었고, 이번에도 촉한군은 진영을 불태우지 않았습니다. 포로장은 닫혔지만 약탈은 없었고, 마을 사람들은 어느 군대가 오래 남을 수 있는지를 보기 시작했습니다.'
+ },
+ {
+ id: 'forty-ninth-victory-release-again',
+ bgm: 'story-dark',
+ chapter: '다시 풀어 보내다',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '두 번 잡아도 마음이 굽지 않는다면 다시 놓아 보낼 뿐입니다. 다만 이번에는 그가 돌아가는 길마다 촉한군이 지킨 마을과 포로들이 그의 말을 시험할 것입니다.'
+ },
+ {
+ id: 'forty-ninth-next-southern-minds',
+ bgm: 'story-dark',
+ chapter: '남중의 마음',
+ background: 'story-militia',
+ speaker: '황권',
+ text: '포로 명부와 군량 장부가 어긋나지 않았습니다. 다음 싸움에서는 맹획만이 아니라 그를 따르는 호족들의 마음도 흔들릴 것입니다. 회유는 이제 전투 뒤의 일이 아니라 전투 중의 목표가 되었습니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3624,6 +3677,12 @@ export const fortyEighthBattleMap: BattleMap = {
terrain: createFortyEighthBattleTerrain()
};
+export const fortyNinthBattleMap: BattleMap = {
+ width: 98,
+ height: 80,
+ terrain: createFortyNinthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -15762,6 +15821,86 @@ export const fortyEighthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('menghuo-leader', '맹획', '남만왕', 'rebelLeader', 138, 940, 232, 90, 38)
];
+const fortyNinthBattleAllyPositions: Record = {
+ 'guan-yu': { x: 12, y: 59 },
+ 'zhang-fei': { x: 17, y: 61 },
+ 'jian-yong': { x: 8, y: 65 },
+ 'mi-zhu': { x: 10, y: 63 },
+ 'sun-qian': { x: 14, y: 67 },
+ 'zhao-yun': { x: 22, y: 56 },
+ 'zhuge-liang': { x: 11, y: 56 },
+ 'ma-liang': { x: 15, y: 53 },
+ 'yi-ji': { x: 9, y: 70 },
+ 'gong-zhi': { x: 18, y: 68 },
+ 'huang-zhong': { x: 25, y: 57 },
+ 'wei-yan': { x: 30, y: 61 },
+ 'pang-tong': { x: 13, y: 50 },
+ 'fa-zheng': { x: 19, y: 51 },
+ 'wu-yi': { x: 32, y: 66 },
+ 'yan-yan': { x: 35, y: 59 },
+ 'li-yan': { x: 24, y: 49 },
+ 'huang-quan': { x: 8, y: 54 },
+ 'ma-chao': { x: 37, y: 56 },
+ 'ma-dai': { x: 39, y: 61 },
+ 'wang-ping': { x: 27, y: 52 }
+};
+
+export const fortyNinthBattleUnits: 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, fortyNinthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('menghuo-return-scout-a', '남만 재집결 척후', '강가 척후', 'bandit', 136, 586, 178, 43, 33),
+ createThirtyFirstEnemyUnit('menghuo-return-scout-b', '남만 재집결 척후', '늪지 척후', 'bandit', 136, 586, 178, 58, 31),
+ createThirtyFirstEnemyUnit('menghuo-return-scout-c', '남만 재집결 척후', '마을 척후', 'bandit', 137, 594, 180, 73, 48),
+ createThirtyFirstEnemyUnit('menghuo-return-scout-d', '남만 재집결 척후', '동쪽 척후', 'bandit', 137, 594, 180, 88, 62),
+ createThirtyFirstEnemyUnit('menghuo-return-vine-a', '등갑 재집결병', '습지 등갑병', 'yellowTurban', 137, 846, 214, 48, 41),
+ createThirtyFirstEnemyUnit('menghuo-return-vine-b', '등갑 재집결병', '산채 등갑병', 'yellowTurban', 138, 858, 216, 60, 44),
+ createThirtyFirstEnemyUnit('menghuo-return-vine-c', '등갑 재집결병', '마을 등갑병', 'yellowTurban', 138, 858, 216, 72, 54),
+ createThirtyFirstEnemyUnit('menghuo-return-vine-d', '등갑 재집결병', '본대 등갑병', 'yellowTurban', 139, 870, 218, 85, 47),
+ createThirtyFirstEnemyUnit('menghuo-return-vine-e', '등갑 재집결병', '후위 등갑병', 'yellowTurban', 139, 870, 218, 92, 38),
+ createThirtyFirstEnemyUnit('menghuo-return-archer-a', '남만 고지 궁병', '강변 궁병', 'archer', 136, 636, 188, 45, 25),
+ createThirtyFirstEnemyUnit('menghuo-return-archer-b', '남만 고지 궁병', '절벽 궁병', 'archer', 137, 644, 190, 59, 23),
+ createThirtyFirstEnemyUnit('menghuo-return-archer-c', '남만 고지 궁병', '마을 궁병', 'archer', 137, 644, 190, 70, 41),
+ createThirtyFirstEnemyUnit('menghuo-return-archer-d', '남만 고지 궁병', '늪지 궁병', 'archer', 138, 652, 192, 82, 58),
+ createThirtyFirstEnemyUnit('menghuo-return-archer-e', '남만 고지 궁병', '본영 궁병', 'archer', 138, 652, 192, 91, 51),
+ createThirtyFirstEnemyUnit('menghuo-return-beast-a', '맹수 재돌격대', '강가 맹수대', 'cavalry', 138, 862, 218, 53, 34),
+ createThirtyFirstEnemyUnit('menghuo-return-beast-b', '맹수 재돌격대', '늪지 맹수대', 'cavalry', 138, 862, 218, 66, 36),
+ createThirtyFirstEnemyUnit('menghuo-return-beast-c', '맹수 재돌격대', '산채 맹수대', 'cavalry', 139, 874, 220, 77, 49),
+ createThirtyFirstEnemyUnit('menghuo-return-beast-d', '맹수 재돌격대', '추격 맹수대', 'cavalry', 139, 874, 220, 89, 55),
+ createThirtyFirstEnemyUnit('menghuo-return-ambusher-a', '남만 재매복병', '덩굴 매복병', 'bandit', 137, 602, 182, 36, 47),
+ createThirtyFirstEnemyUnit('menghuo-return-ambusher-b', '남만 재매복병', '숲속 매복병', 'bandit', 138, 610, 184, 51, 50),
+ createThirtyFirstEnemyUnit('menghuo-return-ambusher-c', '남만 재매복병', '강가 매복병', 'bandit', 138, 610, 184, 64, 63),
+ createThirtyFirstEnemyUnit('menghuo-return-ambusher-d', '남만 재매복병', '후방 차단대', 'bandit', 139, 618, 186, 82, 70),
+ createThirtyFirstEnemyUnit('menghuo-return-ambusher-e', '남만 재매복병', '서쪽 차단대', 'bandit', 139, 618, 186, 31, 60),
+ createThirtyFirstEnemyUnit('menghuo-return-guard-a', '맹획 재집결 친위', '포로장 친위대', 'yellowTurban', 140, 894, 224, 69, 38),
+ createThirtyFirstEnemyUnit('menghuo-return-guard-b', '맹획 재집결 친위', '산채 친위대', 'yellowTurban', 140, 894, 224, 80, 44),
+ createThirtyFirstEnemyUnit('menghuo-return-guard-c', '맹획 재집결 친위', '본영 친위대', 'yellowTurban', 141, 906, 226, 90, 41),
+ createThirtyFirstEnemyUnit('menghuo-return-shaman-a', '남만 독안개 주술사', '늪지 주술사', 'strategist', 139, 676, 198, 61, 32),
+ createThirtyFirstEnemyUnit('menghuo-return-shaman-b', '남만 독안개 주술사', '강가 주술사', 'strategist', 140, 684, 200, 74, 45),
+ createThirtyFirstEnemyUnit('menghuo-return-shaman-c', '남만 독안개 주술사', '산채 주술사', 'strategist', 140, 684, 200, 84, 34),
+ createThirtyFirstEnemyUnit('menghuo-return-officer-meng-you', '맹우', '맹획의 아우', 'rebelLeader', 141, 922, 230, 73, 33),
+ createThirtyFirstEnemyUnit('menghuo-return-officer-duosi', '타사대왕', '독천 산채장', 'rebelLeader', 141, 922, 230, 79, 61),
+ createThirtyFirstEnemyUnit('menghuo-return-officer-mulu', '목록대왕', '맹수대장', 'rebelLeader', 142, 934, 232, 87, 50),
+ createThirtyFirstEnemyUnit('menghuo-second-leader', '맹획', '남만왕', 'rebelLeader', 144, 974, 240, 92, 39)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -16639,6 +16778,35 @@ export const mengHuoPacificationBonds: BattleBond[] = [
export const fortyEighthBattleBonds: BattleBond[] = [...fortySeventhBattleBonds, ...mengHuoPacificationBonds].map(
cloneBattleBondForScenario
);
+export const mengHuoSecondCaptureBonds: BattleBond[] = [
+ {
+ id: 'zhuge-liang__huang-quan_second-capture',
+ unitIds: ['zhuge-liang', 'huang-quan'],
+ title: '회유 장부',
+ level: 45,
+ exp: 0,
+ description: '제갈량의 회유 방침과 황권의 포로 장부는 맹획을 다시 놓아 보내도 군율이 흔들리지 않게 만든다.'
+ },
+ {
+ id: 'ma-liang__wang-ping_second-capture',
+ unitIds: ['ma-liang', 'wang-ping'],
+ title: '마을 길잡이',
+ level: 41,
+ exp: 0,
+ description: '마량이 민심을 살피고 왕평이 산길을 끊어, 맹획이 돌아가는 길마다 촉한군의 질서를 보이게 한다.'
+ },
+ {
+ id: 'zhao-yun__huang-zhong_second-capture',
+ unitIds: ['zhao-yun', 'huang-zhong'],
+ title: '돌격과 고지',
+ level: 39,
+ exp: 0,
+ description: '조운은 맹수 돌격을 받쳐 세우고 황충은 고지 궁병을 억눌러, 생포망이 무너지지 않게 한다.'
+ }
+];
+export const fortyNinthBattleBonds: BattleBond[] = [...fortyEighthBattleBonds, ...mengHuoSecondCaptureBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -19088,6 +19256,68 @@ function createFortyEighthBattleTerrain(): TerrainType[][] {
);
}
+function createFortyNinthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 80 }, (_, y) =>
+ Array.from({ length: 98 }, (_, x): TerrainType => {
+ if (
+ (x >= 0 && x <= 97 && y >= 72) ||
+ (x >= 56 && x <= 62 && y >= 24 && y <= 72) ||
+ (x >= 4 && x <= 25 && y >= 64 && y <= 79) ||
+ (x >= 73 && x <= 88 && y >= 54 && y <= 72)
+ ) {
+ return 'river';
+ }
+ if (
+ (x >= 8 && x <= 91 && y >= 59 && y <= 65) ||
+ (x >= 25 && x <= 70 && y >= 39 && y <= 45) ||
+ (x >= 67 && x <= 92 && y >= 31 && y <= 36) ||
+ (x >= 34 && x <= 42 && y >= 36 && y <= 68)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 20 && x <= 68 && y >= 20 && y <= 70) ||
+ (x >= 52 && x <= 91 && y >= 25 && y <= 66) ||
+ (x >= 2 && x <= 34 && y >= 46 && y <= 70) ||
+ (x >= 74 && x <= 97 && y >= 38 && y <= 69)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 27 && y <= 35) ||
+ (x >= 31 && x <= 65 && y <= 25) ||
+ (x >= 68 && x <= 97 && y <= 33) ||
+ (x >= 84 && x <= 97 && y >= 31 && y <= 51)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 70) ||
+ (x >= 95 && y <= 66) ||
+ (x >= 28 && x <= 34 && y >= 0 && y <= 36) ||
+ (x >= 65 && x <= 71 && y >= 0 && y <= 30) ||
+ (x >= 40 && x <= 46 && y >= 60 && y <= 79)
+ ) {
+ return 'cliff';
+ }
+ if (
+ (x >= 9 && x <= 23 && y >= 55 && y <= 67) ||
+ (x >= 36 && x <= 50 && y >= 40 && y <= 52) ||
+ (x >= 72 && x <= 87 && y >= 45 && y <= 57)
+ ) {
+ return 'village';
+ }
+ if ((x >= 22 && x <= 35 && y >= 47 && y <= 58) || (x >= 60 && x <= 74 && y >= 52 && y <= 64)) {
+ return 'camp';
+ }
+ if ((x >= 46 && x <= 60 && y >= 28 && y <= 38) || (x >= 70 && x <= 88 && y >= 28 && y <= 41)) {
+ 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 872cc8f..38133a4 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -858,7 +858,14 @@ const unitTexture: Record = {
'menghuo-officer-ahuinan': 'unit-rebel-leader',
'menghuo-officer-dongtuna': 'unit-rebel-leader',
'menghuo-officer-mangyachang': 'unit-rebel-leader',
- 'menghuo-leader': 'unit-rebel-leader'
+ 'menghuo-leader': 'unit-rebel-leader',
+ 'menghuo-return-shaman-a': 'unit-rebel-archer',
+ 'menghuo-return-shaman-b': 'unit-rebel-archer',
+ 'menghuo-return-shaman-c': 'unit-rebel-archer',
+ 'menghuo-return-officer-meng-you': 'unit-rebel-leader',
+ 'menghuo-return-officer-duosi': 'unit-rebel-leader',
+ 'menghuo-return-officer-mulu': 'unit-rebel-leader',
+ 'menghuo-second-leader': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -2290,7 +2297,40 @@ const enemyAiByUnitId: Record = {
'menghuo-officer-ahuinan': 'guard',
'menghuo-officer-dongtuna': 'guard',
'menghuo-officer-mangyachang': 'aggressive',
- 'menghuo-leader': 'hold'
+ 'menghuo-leader': 'hold',
+ 'menghuo-return-scout-a': 'aggressive',
+ 'menghuo-return-scout-b': 'aggressive',
+ 'menghuo-return-scout-c': 'aggressive',
+ 'menghuo-return-scout-d': 'aggressive',
+ 'menghuo-return-vine-a': 'guard',
+ 'menghuo-return-vine-b': 'guard',
+ 'menghuo-return-vine-c': 'guard',
+ 'menghuo-return-vine-d': 'guard',
+ 'menghuo-return-vine-e': 'guard',
+ 'menghuo-return-archer-a': 'hold',
+ 'menghuo-return-archer-b': 'hold',
+ 'menghuo-return-archer-c': 'hold',
+ 'menghuo-return-archer-d': 'hold',
+ 'menghuo-return-archer-e': 'hold',
+ 'menghuo-return-beast-a': 'aggressive',
+ 'menghuo-return-beast-b': 'aggressive',
+ 'menghuo-return-beast-c': 'aggressive',
+ 'menghuo-return-beast-d': 'aggressive',
+ 'menghuo-return-ambusher-a': 'aggressive',
+ 'menghuo-return-ambusher-b': 'aggressive',
+ 'menghuo-return-ambusher-c': 'aggressive',
+ 'menghuo-return-ambusher-d': 'aggressive',
+ 'menghuo-return-ambusher-e': 'aggressive',
+ 'menghuo-return-guard-a': 'guard',
+ 'menghuo-return-guard-b': 'guard',
+ 'menghuo-return-guard-c': 'guard',
+ 'menghuo-return-shaman-a': 'hold',
+ 'menghuo-return-shaman-b': 'hold',
+ 'menghuo-return-shaman-c': 'hold',
+ 'menghuo-return-officer-meng-you': 'aggressive',
+ 'menghuo-return-officer-duosi': 'guard',
+ 'menghuo-return-officer-mulu': 'aggressive',
+ 'menghuo-second-leader': 'hold'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index e733afc..4d21fa8 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -12,6 +12,7 @@ import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battl
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';
@@ -154,6 +155,7 @@ export class BootScene extends Phaser.Scene {
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 334f5d3..0fff0da 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -42,6 +42,7 @@ import {
luoCastleRecruitUnits,
maichengIsolationBonds,
mengHuoPacificationBonds,
+ mengHuoSecondCaptureBonds,
nanzhongRecoveryBonds,
wulingRecruitBonds,
wulingRecruitUnits,
@@ -409,9 +410,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'forty-fifth-battle-yiling-vanguard',
'forty-sixth-battle-yiling-fire',
'forty-seventh-battle-nanzhong-stabilization',
- 'forty-eighth-battle-meng-huo-main-force'
+ 'forty-eighth-battle-meng-huo-main-force',
+ 'forty-ninth-battle-meng-huo-second-capture'
],
- nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유']
+ nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금']
}
];
@@ -463,7 +465,8 @@ const campBattleIds = {
fortyFifth: 'forty-fifth-battle-yiling-vanguard',
fortySixth: 'forty-sixth-battle-yiling-fire',
fortySeventh: 'forty-seventh-battle-nanzhong-stabilization',
- fortyEighth: 'forty-eighth-battle-meng-huo-main-force'
+ fortyEighth: 'forty-eighth-battle-meng-huo-main-force',
+ fortyNinth: 'forty-ninth-battle-meng-huo-second-capture'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1038,6 +1041,20 @@ const sortieRulesByBattleId: Partial