Add Nanzhong stabilization battle

This commit is contained in:
2026-06-23 20:32:53 +09:00
parent 199a1cee68
commit eb471d9648
11 changed files with 850 additions and 60 deletions

View File

@@ -71,16 +71,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Forty-fifth battle Yiling vanguard route, turning Jing Province's loss into Liu Bei's revenge march and using a 90x72 river-forest battlefield to establish the first Yiling camp against Lu Xun's waiting line
- Forty-sixth battle Yiling fire-attack route, using Lu Xun's delayed fire plan, stretched Shu-Han camps, a 92x74 burning river-forest battlefield, and a seven-officer retreat-route sortie choice to break through toward Baidi Castle
- 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
- 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 Baidi entrustment 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 camp states, campaign timeline state, and camp save state
- Flow verification script from title through the Nanzhong stabilization 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 camp states, campaign timeline state, and camp save state
## Next Steps
1. Build the Nanman pacification preparation chapter, using the Baidi entrustment aftermath, southern instability, and Zhuge Liang's state-rebuilding priorities as the next campaign tension
2. Expand post-Baidi camp consequences with grief checks, recovery policy choices, and regency decisions that affect sortie recommendations
1. Build Meng Huo's first main-force battle and the first explicit Nanman reconciliation mechanic after the Nanzhong stabilization route
2. Expand post-Baidi and post-Nanzhong camp consequences with grief checks, recovery policy choices, village reassurance, 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
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending

View File

@@ -1739,7 +1739,7 @@ try {
if (
progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 ||
progressTabState.campaignProgress?.totalKnown !== 46 ||
progressTabState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postBowangProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postChangbanProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postJiangdongProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postRedCliffsProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postFireAttackProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postJingzhouProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postGuiyangProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postWulingProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postChangshaProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postYizhouProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postFuPassProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postLuoProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postLuofengProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postLuoMainProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postMianzhuProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postChengduProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postJiamengProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postYangpingProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postDingjunProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postHanzhongProgressState.campaignProgress?.totalKnown !== 47 ||
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 !== 46 ||
postKingCouncilProgressState.campaignProgress?.totalKnown !== 47 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
postFoundationProgressState.campaignProgress?.totalKnown !== 46 ||
postFoundationProgressState.campaignProgress?.totalKnown !== 47 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
postJingDefenseProgressState.campaignProgress?.totalKnown !== 46 ||
postJingDefenseProgressState.campaignProgress?.totalKnown !== 47 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
postFanCastleProgressState.campaignProgress?.totalKnown !== 46 ||
postFanCastleProgressState.campaignProgress?.totalKnown !== 47 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
postHanRiverProgressState.campaignProgress?.totalKnown !== 46 ||
postHanRiverProgressState.campaignProgress?.totalKnown !== 47 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
postFanSiegeProgressState.campaignProgress?.totalKnown !== 46 ||
postFanSiegeProgressState.campaignProgress?.totalKnown !== 47 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,7 +5586,7 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
postJingRearProgressState.campaignProgress?.totalKnown !== 46 ||
postJingRearProgressState.campaignProgress?.totalKnown !== 47 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
@@ -5741,7 +5741,7 @@ try {
if (
postGonganProgressState?.activeTab !== 'progress' ||
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
postGonganProgressState.campaignProgress?.totalKnown !== 46 ||
postGonganProgressState.campaignProgress?.totalKnown !== 47 ||
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
) {
@@ -5899,7 +5899,7 @@ try {
if (
postMaichengProgressState?.activeTab !== 'progress' ||
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
postMaichengProgressState.campaignProgress?.totalKnown !== 46 ||
postMaichengProgressState.campaignProgress?.totalKnown !== 47 ||
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
) {
@@ -6056,7 +6056,7 @@ try {
if (
postYilingProgressState?.activeTab !== 'progress' ||
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
postYilingProgressState.campaignProgress?.totalKnown !== 46 ||
postYilingProgressState.campaignProgress?.totalKnown !== 47 ||
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
) {
@@ -6214,7 +6214,7 @@ try {
if (
postYilingFireProgressState?.activeTab !== 'progress' ||
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
postYilingFireProgressState.campaignProgress?.totalKnown !== 46 ||
postYilingFireProgressState.campaignProgress?.totalKnown !== 47 ||
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
) {
@@ -6280,14 +6280,175 @@ try {
if (
postBaidiProgressState?.activeTab !== 'progress' ||
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
postBaidiProgressState.campaignProgress?.totalKnown !== 46 ||
postBaidiProgressState.campaignProgress?.totalKnown !== 47 ||
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
) {
throw new Error(`Expected Baidi entrustment progress tab to keep the forty-sixth battle complete and pause before Nanman prep: ${JSON.stringify(postBaidiProgressState?.campaignProgress)}`);
throw new Error(`Expected Baidi entrustment progress tab to keep the forty-sixth battle complete and point to Nanzhong stabilization: ${JSON.stringify(postBaidiProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-baidi-progress.png', fullPage: true });
await page.mouse.click(1120, 38);
await page.waitForTimeout(180);
const nanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
!nanzhongSortieState?.sortieVisible ||
nanzhongSortieState.campaign?.step !== 'baidi-entrustment-camp' ||
nanzhongSortieState.campTitle !== '백제성 유탁 후 군영' ||
nanzhongSortieState.sortiePlan?.objectiveLine !== '남중 안정로 · 남중 반란 지휘선 진정' ||
nanzhongSortieState.sortiePlan?.maxCount !== 7 ||
nanzhongSortieState.sortiePlan?.recommendedTotal !== 7 ||
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
) {
throw new Error(`Expected Nanzhong sortie prep to lock Zhuge Liang, recommend southern-route officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(nanzhongSortieState)}`);
}
assertSortieTacticalRoster(nanzhongSortieState, [
'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 nanzhongPriorityUnits = ['zhao-yun', 'wang-ping', 'ma-liang', 'huang-quan', 'huang-zhong', 'ma-chao'];
for (const unitId of nanzhongPriorityUnits) {
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 && !nanzhongPriorityUnits.includes(unit.id)
);
if (removable) {
await clickSortieRosterUnit(page, removable.id);
}
await clickSortieRosterUnit(page, unitId);
}
}
const nanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
!nanzhongPriorityUnits.every((unitId) =>
nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
) ||
!nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
nanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 ||
nanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
) {
throw new Error(`Expected Nanzhong sortie to deploy Zhuge Liang, Zhao Yun, Wang Ping, Ma Liang, Huang Quan, Huang Zhong, and Ma Chao: ${JSON.stringify(nanzhongSortieReadyState)}`);
}
await page.screenshot({ path: 'dist/verification-nanzhong-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-nanzhong-story.png', fullPage: true });
for (let i = 0; i < 55; i += 1) {
const enteredFortySeventhBattle = await page.evaluate(() => {
const state = window.__HEROS_DEBUG__?.battle();
return state?.scene === 'BattleScene' && state?.battleId === 'forty-seventh-battle-nanzhong-stabilization';
});
if (enteredFortySeventhBattle) {
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-seventh-battle-nanzhong-stabilization' && state?.battleOutcome === null && state?.phase === 'idle';
});
await page.screenshot({ path: 'dist/verification-forty-seventh-battle.png', fullPage: true });
const fortySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
const fortySeventhEnemies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'enemy');
const fortySeventhAllies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'ally');
const fortySeventhEnemyBehaviors = new Set(fortySeventhEnemies.map((unit) => unit.ai));
if (
fortySeventhBattleState.camera?.mapWidth !== 94 ||
fortySeventhBattleState.camera?.mapHeight !== 76 ||
fortySeventhBattleState.victoryConditionLabel !== '남중 반란 지휘선 진정' ||
fortySeventhEnemies.length < 34 ||
!fortySeventhEnemyBehaviors.has('aggressive') ||
!fortySeventhEnemyBehaviors.has('guard') ||
!fortySeventhEnemyBehaviors.has('hold') ||
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-leader-yong-kai') ||
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-gao-ding') ||
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-zhu-bao') ||
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-meng-huo-vanguard') ||
!fortySeventhAllies.some((unit) => unit.id === 'zhuge-liang') ||
!fortySeventhAllies.some((unit) => unit.id === 'zhao-yun') ||
!fortySeventhAllies.some((unit) => unit.id === 'wang-ping') ||
!fortySeventhAllies.some((unit) => unit.id === 'ma-liang') ||
!fortySeventhAllies.some((unit) => unit.id === 'huang-quan') ||
!fortySeventhAllies.some((unit) => unit.id === 'huang-zhong') ||
!fortySeventhAllies.some((unit) => unit.id === 'ma-chao') ||
fortySeventhAllies.some((unit) => unit.id === 'liu-bei')
) {
throw new Error(`Expected forty-seventh battle to use Nanzhong map, selected regency-route officers, and mixed southern rebel AI: ${JSON.stringify(fortySeventhBattleState)}`);
}
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 fortySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
fortySeventhCampState?.campBattleId !== 'forty-seventh-battle-nanzhong-stabilization' ||
fortySeventhCampState.campTitle !== '남중 안정로 후 군영' ||
fortySeventhCampState.availableDialogueIds?.length !== 3 ||
!fortySeventhCampState.availableDialogueIds.every((id) => id.includes('nanzhong')) ||
fortySeventhCampState.availableVisitIds?.length !== 2 ||
!fortySeventhCampState.availableVisitIds.every((id) => id.includes('nanzhong')) ||
fortySeventhCampState.rosterCollection?.total < 21
) {
throw new Error(`Expected forty-seventh camp to expose Nanzhong dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortySeventhCampState)}`);
}
await page.screenshot({ path: 'dist/verification-nanzhong-camp.png', fullPage: true });
await page.mouse.click(966, 38);
await page.waitForTimeout(180);
const postNanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
postNanzhongProgressState?.activeTab !== 'progress' ||
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
postNanzhongProgressState.campaignProgress?.totalKnown !== 47 ||
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
) {
throw new Error(`Expected post-Nanzhong progress tab to complete the forty-seventh battle and pause before Meng Huo's main force: ${JSON.stringify(postNanzhongProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-nanzhong-progress.png', fullPage: true });
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -6319,11 +6480,11 @@ try {
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
camp: window.__HEROS_DEBUG__?.camp?.() ?? null
}));
if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'baidi-entrustment-camp') {
throw new Error(`Expected title continue to reopen the latest Baidi entrustment camp: ${JSON.stringify(titleContinueState)}`);
if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-seventh-camp') {
throw new Error(`Expected title continue to reopen the latest Nanzhong camp: ${JSON.stringify(titleContinueState)}`);
}
console.log(`Verified title-to-Baidi-entrustment 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, and Baidi entrustment state at ${targetUrl}`);
console.log(`Verified title-to-Nanzhong-stabilization 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, and Nanzhong stabilization state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
@@ -6403,7 +6564,7 @@ function assertSortieTacticalRoster(state, requiredUnitIds) {
}
const liuBei = state.sortieRoster.find((unit) => unit.id === 'liu-bei');
if (!liuBei?.equipmentLine?.includes('자웅일대검')) {
if (liuBei && !liuBei.equipmentLine?.includes('자웅일대검')) {
throw new Error(`Expected Liu Bei sortie row to expose named treasure weapon: ${JSON.stringify(liuBei)}`);
}

View File

@@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9400" height="7600" viewBox="0 0 9400 7600">
<defs>
<linearGradient id="ground" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#8d8a52"/>
<stop offset=".42" stop-color="#5f773f"/>
<stop offset=".74" stop-color="#3f6035"/>
<stop offset="1" stop-color="#2f4a31"/>
</linearGradient>
<linearGradient id="river" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#2c6a73"/>
<stop offset=".5" stop-color="#23506b"/>
<stop offset="1" stop-color="#2c6f76"/>
</linearGradient>
<pattern id="grain" width="94" height="94" patternUnits="userSpaceOnUse">
<path d="M0 46h94M46 0v94" stroke="#152112" stroke-width="5" opacity=".06"/>
<circle cx="20" cy="20" r="8" fill="#cbb369" opacity=".13"/>
<circle cx="62" cy="60" r="7" fill="#1c321c" opacity=".18"/>
<path d="M7 80c22-15 46-14 73 2" stroke="#385929" stroke-width="5" opacity=".15" fill="none"/>
</pattern>
<filter id="softShadow" x="-18%" y="-18%" width="136%" height="136%">
<feDropShadow dx="0" dy="25" stdDeviation="22" flood-color="#0f160c" flood-opacity=".38"/>
</filter>
</defs>
<rect width="9400" height="7600" fill="url(#ground)"/>
<rect width="9400" height="7600" fill="url(#grain)"/>
<g opacity=".92">
<path d="M0 6680c980-250 1710-80 2680-95 850-12 1530-290 2350-180 870 115 1730 420 2530 250 650-140 1150-240 1840-60v1005H0z" fill="url(#river)"/>
<path d="M960 6040c300 640 510 1050 760 1560H520c-210-430-330-930-410-1510z" fill="#2d6470"/>
<path d="M4600 5480c310 820 400 1400 220 2120h-520c130-730 10-1360-220-2000z" fill="#23566b" opacity=".88"/>
<path d="M360 6650c620 180 1160 210 1800 95M4580 6230c710 160 1230 310 1990 430M6840 6710c760 100 1390 45 2240-90" stroke="#91c4ae" stroke-width="26" opacity=".23" fill="none"/>
</g>
<g filter="url(#softShadow)">
<path d="M0 0h2900c-160 700-540 1340-1080 1800-640 545-1150 1050-1820 1480z" fill="#4b562f"/>
<path d="M3450 0h2900c-150 570-490 990-970 1280-560 340-1210 500-2130 650z" fill="#505b31"/>
<path d="M6600 0h2800v2950c-560-120-1040-410-1410-850-420-500-720-1150-1390-2100z" fill="#46542f"/>
<path d="M9100 2680h300v3610c-340-230-520-620-570-1110-80-740 230-1480 270-2500z" fill="#3e4c2d"/>
<path d="M3720 5700h520c55 690-65 1420-360 1900h-570c260-610 390-1240 410-1900z" fill="#44502e"/>
</g>
<g opacity=".92">
<path d="M140 4780c1280-260 2240-110 3360-130 1080-20 1850-285 2850-190 990 92 1840 280 2410 115" stroke="#b5965d" stroke-width="520" stroke-linecap="round" fill="none"/>
<path d="M2030 5880c890-150 1890-70 2940 75 1240 170 2420 115 4020-230" stroke="#b08a55" stroke-width="420" stroke-linecap="round" fill="none"/>
<path d="M3390 3360c160 840 105 1540-120 2180" stroke="#aa8351" stroke-width="300" stroke-linecap="round" fill="none"/>
<path d="M6580 2460c-90 920 130 1670 430 2480" stroke="#aa8351" stroke-width="270" stroke-linecap="round" fill="none"/>
</g>
<g opacity=".94">
<path d="M2300 1800c730-310 1630-270 2440-60 650 170 1230 395 1920 390 560-5 1080-130 1680-390v880c-1100 380-2000 300-3040 20-820-220-1600-410-2540-80-560 195-1060 480-1520 865v-910c330-280 710-500 1060-715z" fill="#315f32"/>
<path d="M3950 3260c960-260 1780-150 2440 170 850 410 1720 520 2800 135v1300c-760 260-1620 325-2470 70-820-250-1520-575-2500-315-610 160-1250 480-1950 890v-1400c500-400 1040-660 1680-850z" fill="#2f5b31"/>
<path d="M290 4550c1030-280 1880-110 2660 220 650 275 1290 365 1940 220v980c-850 240-1670 75-2490-270-730-305-1460-375-2540-40z" fill="#386735"/>
<circle cx="3300" cy="2480" r="185" fill="#1e351c" opacity=".55"/>
<circle cx="5250" cy="4050" r="205" fill="#1e351c" opacity=".55"/>
<circle cx="7760" cy="4550" r="170" fill="#1e351c" opacity=".55"/>
<path d="M1100 4040c420-180 870-170 1290 10 420 175 860 220 1280 80v560c-500 175-990 125-1480-85-390-165-760-190-1090-60z" fill="#456d35" opacity=".86"/>
</g>
<g filter="url(#softShadow)">
<path d="M1180 4280h1220v1200H1180z" fill="#5d412b"/>
<path d="M1300 4400h980v980h-980z" fill="#8b633c"/>
<path d="M1390 4540h800M1390 4880h800M1390 5200h800" stroke="#dfbf75" stroke-width="34" opacity=".72"/>
<path d="M4270 3880h1240v1160H4270z" fill="#58412b"/>
<path d="M4400 4010h980v900h-980z" fill="#86653d"/>
<path d="M4480 4160h820M4480 4510h820" stroke="#dfbf75" stroke-width="34" opacity=".72"/>
<path d="M7160 3420h1380v1300H7160z" fill="#543c29"/>
<path d="M7300 3560h1100v1020H7300z" fill="#83613c"/>
<path d="M7420 3720h860M7420 4100h860" stroke="#dfbf75" stroke-width="36" opacity=".72"/>
</g>
<g opacity=".8">
<path d="M1580 4480l215-270 215 270-215 215zM1990 5100l190-230 190 230-190 190zM4470 4140l220-270 220 270-220 215zM5040 4600l190-230 190 230-190 190zM7440 3720l205-250 205 250-205 205zM8040 4230l180-220 180 220-180 180z" fill="#c99143"/>
<path d="M620 5880c180-90 410-80 640 30M2920 5620c260-90 520-80 780 30M6420 5680c260-100 590-80 930 55" stroke="#27451f" stroke-width="52" stroke-linecap="round" opacity=".4"/>
</g>
<g opacity=".55">
<path d="M0 0h9400v7600H0z" fill="none" stroke="#17200f" stroke-width="36"/>
<path d="M0 600h9400M0 1200h9400M0 1800h9400M0 2400h9400M0 3000h9400M0 3600h9400M0 4200h9400M0 4800h9400M0 5400h9400M0 6000h9400M0 6600h9400M0 7200h9400" stroke="#1f2c18" stroke-width="7" opacity=".25"/>
<path d="M600 0v7600M1200 0v7600M1800 0v7600M2400 0v7600M3000 0v7600M3600 0v7600M4200 0v7600M4800 0v7600M5400 0v7600M6000 0v7600M6600 0v7600M7200 0v7600M7800 0v7600M8400 0v7600M9000 0v7600" stroke="#1f2c18" stroke-width="7" opacity=".25"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -48,6 +48,10 @@ import {
fortySixthBattleMap,
fortySixthBattleUnits,
fortySixthBattleVictoryPages,
fortySeventhBattleBonds,
fortySeventhBattleMap,
fortySeventhBattleUnits,
fortySeventhBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -235,7 +239,8 @@ export type BattleScenarioId =
| 'forty-third-battle-gongan-collapse'
| 'forty-fourth-battle-maicheng-isolation'
| 'forty-fifth-battle-yiling-vanguard'
| 'forty-sixth-battle-yiling-fire';
| 'forty-sixth-battle-yiling-fire'
| 'forty-seventh-battle-nanzhong-stabilization';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2987,6 +2992,66 @@ export const fortySixthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const fortySeventhBattleScenario: BattleScenarioDefinition = {
id: 'forty-seventh-battle-nanzhong-stabilization',
title: '남중 안정로',
victoryConditionLabel: '남중 반란 지휘선 진정',
defeatConditionLabel: '제갈량 퇴각',
openingObjectiveLines: [
'백제성의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고르려 합니다. 그러나 남중의 반란군은 그 틈을 타 성도 남쪽 길과 창고를 흔듭니다.',
'옹개의 지휘선을 흩뜨리되 마을과 창고를 함부로 불태우지 마십시오. 남중 평정은 적을 모두 쓰러뜨리는 싸움이 아니라, 돌아설 마음을 남겨 두는 싸움입니다.',
'제갈량은 반드시 생존해야 합니다. 조운과 왕평으로 산길을 열고, 마량과 황권으로 마을과 보급을 안정시키면 다음 남만 본전의 발판이 생깁니다.'
],
map: fortySeventhBattleMap,
units: fortySeventhBattleUnits,
bonds: fortySeventhBattleBonds,
mapTextureKey: 'battle-map-forty-seventh',
leaderUnitId: 'nanzhong-leader-yong-kai',
quickVictoryTurnLimit: 78,
baseVictoryGold: 10120,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '옹개 지휘선 진정',
rewardGold: 6840,
unitId: 'nanzhong-leader-yong-kai'
},
{
id: 'zhuge-liang',
kind: 'keep-unit-alive',
label: '제갈량 생존',
rewardGold: 1860,
unitId: 'zhuge-liang'
},
{
id: 'villages',
kind: 'secure-terrain',
label: '남중 마을 안정',
rewardGold: 3420,
terrain: 'village'
},
{
id: 'supply-camps',
kind: 'secure-terrain',
label: '남쪽 창고 확보',
rewardGold: 2720,
terrain: 'camp'
},
{
id: 'quick',
kind: 'quick-victory',
label: '78턴 이내 안정',
rewardGold: 2360,
maxTurn: 78
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }],
itemRewards: ['콩 50', '상처약 40', '탁주 17', '남중 산길 지도 1', '마을 회유 문서 1'],
victoryPages: fortySeventhBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -3035,7 +3100,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'forty-third-battle-gongan-collapse': fortyThirdBattleScenario,
'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario,
'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario,
'forty-sixth-battle-yiling-fire': fortySixthBattleScenario
'forty-sixth-battle-yiling-fire': fortySixthBattleScenario,
'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -11,6 +11,7 @@ import {
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortySixthBattleScenario,
fortySeventhBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -69,6 +70,8 @@ import {
fortyFifthBattleIntroPages,
fortyFifthBattleVictoryPages,
fortySixthBattleIntroPages,
fortySeventhBattleIntroPages,
fortySeventhBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fifteenthBattleIntroPages,
@@ -675,13 +678,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
'baidi-entrustment-camp': {
afterBattleId: fortySixthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '남만 평정 준비',
eyebrow: '다음 장',
title: fortySeventhBattleScenario.title,
description:
'유비의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고릅니다. 다음 장에서는 남중의 불안을 정리하고, 북벌을 준비할 내정을 세우는 흐름으로 확장합니다.',
rewardHint: '다음 장: 남만 평정과 촉한 재정비 준비 중',
pages: [],
unavailableNotice: '남만 평정과 촉한 재정비는 다음 작업에서 이어집니다. 지금은 백제성 유탁 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.'
'유비의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고릅니다. 이제 남중의 반란군이 흔드는 산길과 창고를 안정시켜, 북벌보다 먼저 나라 안의 불안을 수습해야 합니다.',
rewardHint: `예상 보상: ${fortySeventhBattleScenario.title} 개방 / 남중 산길 지도`,
nextBattleId: fortySeventhBattleScenario.id,
campaignStep: 'forty-seventh-battle',
pages: fortySeventhBattleIntroPages
},
[fortySeventhBattleScenario.id]: {
afterBattleId: fortySeventhBattleScenario.id,
eyebrow: '다음 장 준비',
title: '맹획과 남중 본대',
description:
'남중 안정로의 첫 길은 열렸지만 반란의 중심은 더 깊은 산과 마을 사이에 남아 있습니다. 다음 장에서는 맹획의 본대를 상대하며 힘과 회유를 함께 시험해야 합니다.',
rewardHint: '다음 장: 맹획 본대와 남중 회유 준비 중',
pages: fortySeventhBattleVictoryPages,
unavailableNotice: '맹획 본대와 남중 회유는 다음 작업에서 이어집니다. 지금은 남중 안정로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};

View File

@@ -3077,6 +3077,67 @@ export const baidiEntrustmentPages: StoryPage[] = [
}
];
export const fortySeventhBattleIntroPages: StoryPage[] = [
{
id: 'forty-seventh-southern-unrest',
bgm: 'story-dark',
chapter: '남중의 흔들림',
background: 'story-militia',
speaker: '황권',
text: '백제성의 유탁 뒤 촉한은 아직 상처를 다 씻지 못했습니다. 그러나 남중의 여러 호족과 반란군은 바로 그 틈을 보며, 성도 남쪽의 길과 창고를 흔들기 시작했습니다.'
},
{
id: 'forty-seventh-zhuge-policy',
bgm: 'battle-prep',
chapter: '먼저 안을 다스리다',
background: 'story-three-heroes',
speaker: '제갈량',
text: '북방을 논하기 전에 나라 안의 불안을 먼저 다스려야 합니다. 남중을 힘으로만 누르면 길은 열려도 마음은 닫힙니다. 반란의 길목을 끊고, 마을과 창고는 살려 두십시오.'
},
{
id: 'forty-seventh-zhao-yun-vanguard',
bgm: 'battle-prep',
chapter: '남쪽 길의 선봉',
background: 'story-sortie',
speaker: '조운',
text: '산길과 숲길이 겹친 전장이라 기병만 앞세울 수 없습니다. 왕평이 길을 보고, 황권이 보급을 재며, 마량이 마을의 말을 들어야 합니다. 저는 뒤처진 부대를 회수하겠습니다.'
},
{
id: 'forty-seventh-sortie',
bgm: 'battle-prep',
chapter: '남중 안정로',
background: 'story-sortie',
speaker: '제갈량',
text: '옹개와 남중 반란군이 밀림 길목에 진을 쳤습니다. 반란군 지휘선을 흩뜨리고, 마을과 창고를 확보해 남중 평정의 첫 발판을 세우십시오.'
}
];
export const fortySeventhBattleVictoryPages: StoryPage[] = [
{
id: 'forty-seventh-victory-road-opened',
bgm: 'militia-theme',
chapter: '남쪽 길이 열리다',
background: 'story-sortie',
text: '밀림과 산길 사이의 반란군 지휘선이 흔들렸습니다. 촉한군은 마을을 약탈하지 않고 창고를 정리하며, 남중 평정이 단순한 추격전이 아님을 병사들에게 보였습니다.'
},
{
id: 'forty-seventh-meng-huo-shadow',
bgm: 'story-dark',
chapter: '더 깊은 산',
background: 'story-militia',
speaker: '왕평',
text: '옹개의 선봉은 물러났지만 남중의 산은 아직 깊습니다. 맹획의 이름이 여러 마을 사이에서 오가고 있고, 힘으로 꺾은 길 뒤에는 설득해야 할 마음이 남아 있습니다.'
},
{
id: 'forty-seventh-next-nanman',
bgm: 'story-dark',
chapter: '남만 평정의 시작',
background: 'story-three-heroes',
speaker: '제갈량',
text: '첫 길은 열렸습니다. 이제 남중의 여러 세력을 모두 적으로 만들지 않으면서, 반란의 중심을 하나씩 마주해야 합니다. 다음 장에서는 맹획의 본대와 처음으로 맞섭니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3498,6 +3559,12 @@ export const fortySixthBattleMap: BattleMap = {
terrain: createFortySixthBattleTerrain()
};
export const fortySeventhBattleMap: BattleMap = {
width: 94,
height: 76,
terrain: createFortySeventhBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -15474,6 +15541,88 @@ export const fortySixthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('yiling-fire-leader-lu-xun', '육손', '화공 대도독', 'rebelLeader', 122, 812, 198, 86, 31)
];
const fortySeventhBattleAllyPositions: Record<string, { x: number; y: number }> = {
'guan-yu': { x: 14, y: 49 },
'zhang-fei': { x: 18, y: 50 },
'jian-yong': { x: 10, y: 55 },
'mi-zhu': { x: 12, y: 52 },
'sun-qian': { x: 14, y: 57 },
'zhao-yun': { x: 20, y: 46 },
'zhuge-liang': { x: 12, y: 47 },
'ma-liang': { x: 16, y: 44 },
'yi-ji': { x: 11, y: 60 },
'gong-zhi': { x: 18, y: 58 },
'huang-zhong': { x: 23, y: 48 },
'wei-yan': { x: 25, y: 52 },
'pang-tong': { x: 15, y: 41 },
'fa-zheng': { x: 19, y: 42 },
'wu-yi': { x: 27, y: 55 },
'yan-yan': { x: 29, y: 50 },
'li-yan': { x: 22, y: 39 },
'huang-quan': { x: 10, y: 45 },
'ma-chao': { x: 31, y: 47 },
'ma-dai': { x: 33, y: 51 },
'wang-ping': { x: 24, y: 43 }
};
export const fortySeventhBattleUnits: 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, fortySeventhBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
createThirtyFirstEnemyUnit('nanzhong-scout-a', '남중 척후', '밀림 척후', 'bandit', 123, 520, 162, 38, 28),
createThirtyFirstEnemyUnit('nanzhong-scout-b', '남중 척후', '계곡 척후', 'bandit', 123, 520, 162, 47, 41),
createThirtyFirstEnemyUnit('nanzhong-scout-c', '남중 척후', '마을 척후', 'bandit', 124, 528, 164, 58, 52),
createThirtyFirstEnemyUnit('nanzhong-scout-d', '남중 척후', '남쪽 척후', 'bandit', 124, 528, 164, 80, 58),
createThirtyFirstEnemyUnit('nanzhong-infantry-a', '남중 보병', '산길 보병', 'yellowTurban', 123, 730, 188, 42, 33),
createThirtyFirstEnemyUnit('nanzhong-infantry-b', '남중 보병', '창고 보병', 'yellowTurban', 124, 742, 190, 52, 39),
createThirtyFirstEnemyUnit('nanzhong-infantry-c', '남중 보병', '마을 보병', 'yellowTurban', 124, 742, 190, 60, 49),
createThirtyFirstEnemyUnit('nanzhong-infantry-d', '남중 보병', '남로 보병', 'yellowTurban', 125, 754, 192, 70, 57),
createThirtyFirstEnemyUnit('nanzhong-infantry-e', '남중 보병', '고개 보병', 'yellowTurban', 125, 754, 192, 82, 43),
createThirtyFirstEnemyUnit('nanzhong-infantry-f', '남중 보병', '후위 보병', 'yellowTurban', 126, 766, 194, 88, 62),
createThirtyFirstEnemyUnit('nanzhong-archer-a', '남중 궁병', '밀림 궁병', 'archer', 123, 570, 170, 40, 24),
createThirtyFirstEnemyUnit('nanzhong-archer-b', '남중 궁병', '절벽 궁병', 'archer', 124, 578, 172, 55, 30),
createThirtyFirstEnemyUnit('nanzhong-archer-c', '남중 궁병', '마을 궁병', 'archer', 124, 578, 172, 65, 47),
createThirtyFirstEnemyUnit('nanzhong-archer-d', '남중 궁병', '남로 궁병', 'archer', 125, 586, 174, 78, 55),
createThirtyFirstEnemyUnit('nanzhong-archer-e', '남중 궁병', '고지 궁병', 'archer', 125, 586, 174, 84, 31),
createThirtyFirstEnemyUnit('nanzhong-archer-f', '남중 궁병', '퇴로 궁병', 'archer', 126, 594, 176, 90, 50),
createThirtyFirstEnemyUnit('nanzhong-beast-rider-a', '남중 기병', '산짐승 기병', 'cavalry', 125, 748, 192, 45, 20),
createThirtyFirstEnemyUnit('nanzhong-beast-rider-b', '남중 기병', '밀림 기병', 'cavalry', 125, 748, 192, 62, 26),
createThirtyFirstEnemyUnit('nanzhong-beast-rider-c', '남중 기병', '남쪽 기병', 'cavalry', 126, 760, 194, 75, 45),
createThirtyFirstEnemyUnit('nanzhong-beast-rider-d', '남중 기병', '추격 기병', 'cavalry', 126, 760, 194, 88, 36),
createThirtyFirstEnemyUnit('nanzhong-ambusher-a', '남중 복병', '덩굴 복병', 'bandit', 124, 540, 166, 35, 38),
createThirtyFirstEnemyUnit('nanzhong-ambusher-b', '남중 복병', '계곡 복병', 'bandit', 125, 548, 168, 49, 46),
createThirtyFirstEnemyUnit('nanzhong-ambusher-c', '남중 복병', '강가 복병', 'bandit', 125, 548, 168, 61, 60),
createThirtyFirstEnemyUnit('nanzhong-ambusher-d', '남중 복병', '후방 복병', 'bandit', 126, 556, 170, 78, 66),
createThirtyFirstEnemyUnit('nanzhong-ambusher-e', '남중 복병', '서쪽 차단대', 'bandit', 126, 556, 170, 30, 53),
createThirtyFirstEnemyUnit('nanzhong-guard-a', '남중 친위대', '마을 친위대', 'yellowTurban', 126, 782, 198, 66, 35),
createThirtyFirstEnemyUnit('nanzhong-guard-b', '남중 친위대', '창고 친위대', 'yellowTurban', 127, 794, 200, 74, 50),
createThirtyFirstEnemyUnit('nanzhong-guard-c', '남중 친위대', '수장 친위대', 'yellowTurban', 127, 794, 200, 86, 56),
createThirtyFirstEnemyUnit('nanzhong-strategist-a', '남중 주술사', '밀림 주술사', 'strategist', 126, 610, 178, 63, 29),
createThirtyFirstEnemyUnit('nanzhong-strategist-b', '남중 주술사', '독안개 주술사', 'strategist', 127, 618, 180, 72, 47),
createThirtyFirstEnemyUnit('nanzhong-strategist-c', '남중 주술사', '강가 주술사', 'strategist', 127, 618, 180, 57, 64),
createThirtyFirstEnemyUnit('nanzhong-officer-gao-ding', '고정', '남중 산채장', 'rebelLeader', 128, 812, 204, 78, 39),
createThirtyFirstEnemyUnit('nanzhong-officer-zhu-bao', '주포', '남중 창고장', 'rebelLeader', 127, 798, 202, 66, 58),
createThirtyFirstEnemyUnit('nanzhong-officer-meng-huo-vanguard', '맹획 선봉', '밀림 호족장', 'rebelLeader', 128, 820, 206, 82, 28),
createThirtyFirstEnemyUnit('nanzhong-leader-yong-kai', '옹개', '남중 반란 수장', 'rebelLeader', 130, 856, 212, 88, 42)
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -16293,6 +16442,35 @@ export const yilingFireBonds: BattleBond[] = [
export const fortySixthBattleBonds: BattleBond[] = [...fortyFifthBattleBonds, ...yilingFireBonds].map(
cloneBattleBondForScenario
);
export const nanzhongRecoveryBonds: BattleBond[] = [
{
id: 'zhuge-liang__zhao-yun_nanzhong',
unitIds: ['zhuge-liang', 'zhao-yun'],
title: '섭정의 선봉',
level: 42,
exp: 0,
description: '제갈량의 신중한 남중 방침과 조운의 안정된 선봉은 백제성 이후 흔들린 군심을 붙잡는다.'
},
{
id: 'zhuge-liang__huang-quan_nanzhong',
unitIds: ['zhuge-liang', 'huang-quan'],
title: '나라 안의 장부',
level: 40,
exp: 0,
description: '제갈량과 황권은 남중의 반란을 단순한 토벌이 아니라 촉한 재정비의 장부로 바라본다.'
},
{
id: 'ma-liang__wang-ping_nanzhong',
unitIds: ['ma-liang', 'wang-ping'],
title: '마을과 산길',
level: 36,
exp: 0,
description: '마량의 민심 판단과 왕평의 지형 판단은 남중의 마을을 적으로 돌리지 않고 길을 여는 힘이 된다.'
}
];
export const fortySeventhBattleBonds: BattleBond[] = [...fortySixthBattleBonds, ...nanzhongRecoveryBonds].map(
cloneBattleBondForScenario
);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -18618,6 +18796,67 @@ function createFortySixthBattleTerrain(): TerrainType[][] {
);
}
function createFortySeventhBattleTerrain(): TerrainType[][] {
return Array.from({ length: 76 }, (_, y) =>
Array.from({ length: 94 }, (_, x): TerrainType => {
if (
(x >= 0 && x <= 93 && y >= 66) ||
(x >= 46 && x <= 58 && y >= 55 && y <= 75) ||
(x >= 10 && x <= 20 && y >= 59 && y <= 75)
) {
return 'river';
}
if (
(x >= 1 && x <= 86 && y >= 46 && y <= 51) ||
(x >= 20 && x <= 91 && y >= 57 && y <= 61) ||
(x >= 28 && x <= 39 && y >= 33 && y <= 57) ||
(x >= 61 && x <= 70 && y >= 24 && y <= 58)
) {
return 'road';
}
if (
(x >= 24 && x <= 64 && y >= 18 && y <= 62) ||
(x >= 48 && x <= 82 && y >= 27 && y <= 66) ||
(x >= 3 && x <= 31 && y >= 45 && y <= 65) ||
(x >= 72 && x <= 93 && y >= 38 && y <= 65)
) {
return 'forest';
}
if (
(x >= 0 && x <= 28 && y <= 32) ||
(x >= 36 && x <= 62 && y <= 22) ||
(x >= 66 && x <= 93 && y <= 30) ||
(x >= 82 && x <= 93 && y >= 29 && y <= 46)
) {
return 'hill';
}
if (
(x <= 2 && y <= 65) ||
(x >= 91 && y <= 60) ||
(x >= 31 && x <= 36 && y >= 0 && y <= 32) ||
(x >= 63 && x <= 68 && y >= 0 && y <= 26) ||
(x >= 37 && x <= 42 && y >= 56 && y <= 75)
) {
return 'cliff';
}
if (
(x >= 12 && x <= 23 && y >= 43 && y <= 55) ||
(x >= 42 && x <= 54 && y >= 38 && y <= 52) ||
(x >= 70 && x <= 84 && y >= 34 && y <= 49)
) {
return 'village';
}
if ((x >= 16 && x <= 25 && y >= 34 && y <= 42) || (x >= 76 && x <= 88 && y >= 50 && y <= 62)) {
return 'fort';
}
if ((x >= 54 && x <= 64 && y >= 52 && y <= 61) || (x >= 30 && x <= 40 && y >= 42 && y <= 50)) {
return 'camp';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -844,7 +844,14 @@ const unitTexture: Record<string, string> = {
'yiling-fire-officer-han-dang': 'unit-rebel-leader',
'yiling-fire-officer-pan-zhang': 'unit-rebel-leader',
'yiling-fire-officer-ma-zhong': 'unit-rebel',
'yiling-fire-leader-lu-xun': 'unit-rebel-leader'
'yiling-fire-leader-lu-xun': 'unit-rebel-leader',
'nanzhong-strategist-a': 'unit-rebel-archer',
'nanzhong-strategist-b': 'unit-rebel-archer',
'nanzhong-strategist-c': 'unit-rebel-archer',
'nanzhong-officer-gao-ding': 'unit-rebel-leader',
'nanzhong-officer-zhu-bao': 'unit-rebel-leader',
'nanzhong-officer-meng-huo-vanguard': 'unit-rebel-leader',
'nanzhong-leader-yong-kai': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -2208,7 +2215,42 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'yiling-fire-officer-han-dang': 'guard',
'yiling-fire-officer-pan-zhang': 'aggressive',
'yiling-fire-officer-ma-zhong': 'aggressive',
'yiling-fire-leader-lu-xun': 'hold'
'yiling-fire-leader-lu-xun': 'hold',
'nanzhong-scout-a': 'aggressive',
'nanzhong-scout-b': 'aggressive',
'nanzhong-scout-c': 'aggressive',
'nanzhong-scout-d': 'aggressive',
'nanzhong-infantry-a': 'guard',
'nanzhong-infantry-b': 'guard',
'nanzhong-infantry-c': 'guard',
'nanzhong-infantry-d': 'guard',
'nanzhong-infantry-e': 'guard',
'nanzhong-infantry-f': 'guard',
'nanzhong-archer-a': 'hold',
'nanzhong-archer-b': 'hold',
'nanzhong-archer-c': 'hold',
'nanzhong-archer-d': 'hold',
'nanzhong-archer-e': 'hold',
'nanzhong-archer-f': 'hold',
'nanzhong-beast-rider-a': 'aggressive',
'nanzhong-beast-rider-b': 'aggressive',
'nanzhong-beast-rider-c': 'aggressive',
'nanzhong-beast-rider-d': 'aggressive',
'nanzhong-ambusher-a': 'aggressive',
'nanzhong-ambusher-b': 'aggressive',
'nanzhong-ambusher-c': 'aggressive',
'nanzhong-ambusher-d': 'aggressive',
'nanzhong-ambusher-e': 'aggressive',
'nanzhong-guard-a': 'guard',
'nanzhong-guard-b': 'guard',
'nanzhong-guard-c': 'guard',
'nanzhong-strategist-a': 'hold',
'nanzhong-strategist-b': 'hold',
'nanzhong-strategist-c': 'hold',
'nanzhong-officer-gao-ding': 'guard',
'nanzhong-officer-zhu-bao': 'guard',
'nanzhong-officer-meng-huo-vanguard': 'aggressive',
'nanzhong-leader-yong-kai': 'hold'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
@@ -2413,9 +2455,14 @@ export class BattleScene extends Phaser.Scene {
private resetBattleData(campaign?: CampaignState) {
const selected = new Set(campaign?.selectedSortieUnitIds ?? []);
const forcedAllyIds = new Set(
battleScenario.defeatConditions
.filter((condition) => condition.kind === 'unit-defeated')
.map((condition) => condition.unitId)
);
const hasSelection = selected.size > 0;
const nextUnits = initialBattleUnits
.filter((unit) => unit.faction === 'enemy' || !hasSelection || selected.has(unit.id) || unit.id === 'liu-bei')
.filter((unit) => unit.faction === 'enemy' || !hasSelection || selected.has(unit.id) || forcedAllyIds.has(unit.id))
.map((unit) => this.applyCampaignUnitProgress(unit, campaign));
battleUnits.splice(0, battleUnits.length, ...nextUnits);
battleBonds.splice(0, battleBonds.length, ...initialBattleBonds.map((bond) => this.applyCampaignBondProgress(bond, campaign)));

View File

@@ -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 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';
@@ -150,6 +151,7 @@ export class BootScene extends Phaser.Scene {
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('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -41,6 +41,7 @@ import {
luoCastleRecruitBonds,
luoCastleRecruitUnits,
maichengIsolationBonds,
nanzhongRecoveryBonds,
wulingRecruitBonds,
wulingRecruitUnits,
xuzhouRecruitBonds,
@@ -405,9 +406,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'forty-third-battle-gongan-collapse',
'forty-fourth-battle-maicheng-isolation',
'forty-fifth-battle-yiling-vanguard',
'forty-sixth-battle-yiling-fire'
'forty-sixth-battle-yiling-fire',
'forty-seventh-battle-nanzhong-stabilization'
],
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공']
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정']
}
];
@@ -457,7 +459,8 @@ const campBattleIds = {
fortyThird: 'forty-third-battle-gongan-collapse',
fortyFourth: 'forty-fourth-battle-maicheng-isolation',
fortyFifth: 'forty-fifth-battle-yiling-vanguard',
fortySixth: 'forty-sixth-battle-yiling-fire'
fortySixth: 'forty-sixth-battle-yiling-fire',
fortySeventh: 'forty-seventh-battle-nanzhong-stabilization'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -1004,6 +1007,20 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'ma-chao', reason: '오군 추격 기병과 불길 바깥 차단선을 강하게 밀어낼 수 있습니다.' }
],
note: '이릉 화공전은 스물두 명 중 일곱 명만 출전합니다. 유비는 필수이며, 불길 수습, 퇴로 확보, 추격 차단 중 무엇을 두껍게 할지 선택하는 전투입니다.'
},
'forty-seventh-battle-nanzhong-stabilization': {
maxUnits: 7,
requiredUnitIds: ['zhuge-liang'],
recommended: [
{ unitId: 'zhuge-liang', reason: '백제성 이후 촉한을 이끄는 섭정이며 패배 조건의 중심입니다.' },
{ unitId: 'zhao-yun', reason: '산길 선봉과 후위 회수에 안정적인 기동 축입니다.' },
{ unitId: 'wang-ping', reason: '남중 산길과 강변 지형을 읽는 길잡이입니다.' },
{ unitId: 'ma-liang', reason: '마을 회유와 남중 민심 판단에 필요합니다.' },
{ unitId: 'huang-quan', reason: '창고와 보급 장부를 안정시키는 내정 참모입니다.' },
{ unitId: 'huang-zhong', reason: '밀림 궁병과 고지 방어선을 견제하는 원거리 축입니다.' },
{ unitId: 'ma-chao', reason: '남중 기병과 빠른 차단선에 대응하는 강한 측면 카드입니다.' }
],
note: '남중 안정로는 백제성 이후 첫 전투입니다. 유비가 아니라 제갈량이 필수이며, 산길, 마을, 창고를 동시에 보아야 합니다.'
}
};
@@ -4898,6 +4915,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 17
}
]
},
{
id: 'nanzhong-zhuge-zhaoyun-vanguard',
title: '섭정의 첫 선봉',
availableAfterBattleIds: [campBattleIds.fortySeventh],
unitIds: ['zhuge-liang', 'zhao-yun'],
bondId: 'zhuge-liang__zhao-yun_nanzhong',
rewardExp: 56,
lines: [
'제갈량: 남중의 길은 이기기만 해서는 다시 닫힙니다. 먼저 병사들이 두려워하지 않을 길을 열어야 합니다.',
'조운: 선봉은 제가 맡겠습니다. 칼끝보다 깃발이 먼저 보이도록, 지나친 추격은 삼가겠습니다.',
'제갈량: 그래서 자룡이 필요합니다. 흔들린 나라의 첫 발은 빠르되 가볍지 않아야 합니다.'
],
choices: [
{
id: 'steady-vanguard-route',
label: '안정된 선봉로를 맡긴다',
response: '조운은 산길 표식을 나누어 세우고, 제갈량은 선봉과 본대의 간격을 다시 계산했습니다.',
rewardExp: 20
},
{
id: 'protect-rear-wounded',
label: '후위 회수를 부탁한다',
response: '조운은 부상병과 보급대가 뒤처지지 않도록 후위 기동선을 먼저 확인했습니다.',
rewardExp: 19
}
]
},
{
id: 'nanzhong-zhuge-huangquan-ledger',
title: '나라 안의 장부',
availableAfterBattleIds: [campBattleIds.fortySeventh],
unitIds: ['zhuge-liang', 'huang-quan'],
bondId: 'zhuge-liang__huang-quan_nanzhong',
rewardExp: 54,
lines: [
'황권: 남중의 창고를 빼앗기만 하면 반란은 잠시 잦아들 뿐입니다. 장부가 다시 돌아와야 길이 남습니다.',
'제갈량: 토벌과 회유의 순서를 틀리면 백성은 다시 산으로 숨을 것입니다.',
'황권: 군량을 셀 때도 사람의 마음을 함께 세겠습니다.'
],
choices: [
{
id: 'audit-southern-storehouse',
label: '창고 장부를 먼저 본다',
response: '황권은 남중 창고의 빼앗긴 양과 돌려줄 양을 나누어, 전후 처리의 기준을 세웠습니다.',
rewardExp: 18
},
{
id: 'prepare-amnesty-list',
label: '회유 명단을 준비한다',
response: '제갈량과 황권은 끝까지 싸울 수장과 먼저 달랠 촌장을 분리해 적었습니다.',
rewardExp: 19
}
]
},
{
id: 'nanzhong-maliang-wangping-villages',
title: '마을과 산길',
availableAfterBattleIds: [campBattleIds.fortySeventh],
unitIds: ['ma-liang', 'wang-ping'],
bondId: 'ma-liang__wang-ping_nanzhong',
rewardExp: 52,
lines: [
'왕평: 이 길은 지도대로만 가면 늪으로 빠집니다. 마을 사람들이 쓰는 낮은 둑길이 따로 있습니다.',
'마량: 그렇다면 마을을 적으로 돌리지 않는 것이 곧 지형을 얻는 일이군요.',
'왕평: 예. 산길은 칼로 여는 길이 있고, 말로 열리는 길이 있습니다.'
],
choices: [
{
id: 'ask-village-guides',
label: '마을 길잡이를 구한다',
response: '마량은 촌장들에게 군령보다 먼저 보호 약속을 전했고, 왕평은 낮은 둑길을 지도에 표시했습니다.',
rewardExp: 18
},
{
id: 'mark-marsh-detours',
label: '늪지 우회로를 표시한다',
response: '왕평은 진흙길의 깊이를 짚었고, 마량은 병사들이 마을 밭을 밟지 않는 우회 명령을 정리했습니다.',
rewardExp: 17
}
]
}
];
@@ -6849,6 +6947,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['탁주 5']
}
]
},
{
id: 'nanzhong-village-reassurance',
title: '남중 마을 회유',
location: '남중 산마을',
availableAfterBattleIds: [campBattleIds.fortySeventh],
bondId: 'ma-liang__wang-ping_nanzhong',
description: '마량과 왕평이 산마을을 돌며 반란군과 마을 사람을 구분하고, 다음 진군로의 길잡이를 부탁합니다.',
lines: [
'마량: 군이 지나간 뒤에도 밭이 남아야 마을이 다시 우리 편이 됩니다.',
'왕평: 길잡이를 억지로 데려가면 다음 길은 모두 거짓이 됩니다. 먼저 지켜 준다는 표시가 필요합니다.',
'마을 앞 작은 사당에 촉한군의 보호 표식과 길 안내 깃발이 함께 세워졌습니다.'
],
choices: [
{
id: 'promise-village-protection',
label: '보호 표식을 세운다',
response: '마을 사람들은 군량 징발 대신 보호 표식이 먼저 세워지는 것을 보고 조금씩 길을 알려 주었습니다.',
bondExp: 42,
itemRewards: ['남중 길잡이 표식 1']
},
{
id: 'return-stored-grain',
label: '빼앗긴 곡식을 돌려준다',
response: '왕평은 창고에서 찾은 곡식을 되돌렸고, 마량은 반란군과 마을 사람의 이름을 구분해 적었습니다.',
bondExp: 40,
gold: 1180,
itemRewards: ['콩 9']
}
]
},
{
id: 'nanzhong-supply-road-ledger',
title: '남중 보급로 장부',
location: '남중 산길 창고',
availableAfterBattleIds: [campBattleIds.fortySeventh],
bondId: 'zhuge-liang__huang-quan_nanzhong',
description: '제갈량과 황권이 산길 창고에서 군량, 포로, 회유 대상의 장부를 나누어 남중 안정의 다음 순서를 정합니다.',
lines: [
'황권: 이 창고에는 반란군의 군량과 마을의 생계가 함께 섞여 있습니다.',
'제갈량: 가져갈 것과 돌려줄 것을 나누는 일이 다음 싸움의 절반입니다.',
'창고 안의 죽간에는 토벌할 수장, 달랠 촌장, 보호할 마을의 이름이 따로 적혔습니다.'
],
choices: [
{
id: 'separate-grain-ledgers',
label: '군량 장부를 나눈다',
response: '황권은 촉한군이 쓸 군량과 마을에 돌려줄 곡식을 나누어, 병사들의 약탈을 막을 기준을 세웠습니다.',
bondExp: 42,
gold: 1360,
itemRewards: ['남중 산길 지도 1']
},
{
id: 'mark-amnesty-targets',
label: '회유 대상을 표시한다',
response: '제갈량은 끝까지 반란을 부추긴 수장과 달랠 수 있는 촌장을 분리해, 다음 전투의 목표를 더 선명하게 만들었습니다.',
bondExp: 40,
itemRewards: ['마을 회유 문서 2']
}
]
}
];
@@ -6943,7 +7101,8 @@ export class CampScene extends Phaser.Scene {
battleId === campBattleIds.fortyThird ||
battleId === campBattleIds.fortyFourth ||
battleId === campBattleIds.fortyFifth ||
battleId === campBattleIds.fortySixth
battleId === campBattleIds.fortySixth ||
battleId === campBattleIds.fortySeventh
) {
this.campaign = ensureCampaignRosterUnits(
[
@@ -6992,7 +7151,8 @@ export class CampScene extends Phaser.Scene {
...jingCollapseBonds,
...maichengIsolationBonds,
...yilingVanguardBonds,
...yilingFireBonds
...yilingFireBonds,
...nanzhongRecoveryBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
@@ -7630,6 +7790,9 @@ export class CampScene extends Phaser.Scene {
if (this.campaign?.step === 'baidi-entrustment-camp') {
return '백제성 유탁 후 군영';
}
if (battleId === campBattleIds.fortySeventh) {
return '남중 안정로 후 군영';
}
if (battleId === campBattleIds.fortySixth) {
return '이릉 화공전 후 군영';
}
@@ -9735,7 +9898,13 @@ export class CampScene extends Phaser.Scene {
}
private sortieAllies() {
return this.currentUnits().filter((unit) => unit.faction === 'ally' && unit.hp > 0);
const scenario = this.nextSortieScenario();
const scenarioAllyIds = scenario
? new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id))
: undefined;
return this.currentUnits().filter(
(unit) => unit.faction === 'ally' && unit.hp > 0 && (!scenarioAllyIds || scenarioAllyIds.has(unit.id))
);
}
private selectedSortieUnits() {

View File

@@ -12,6 +12,7 @@ import {
fortyFourthBattleScenario,
fortyFifthBattleScenario,
fortySixthBattleScenario,
fortySeventhBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -392,6 +393,7 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'forty-fourth-camp' ||
campaign.step === 'forty-fifth-camp' ||
campaign.step === 'forty-sixth-camp' ||
campaign.step === 'forty-seventh-camp' ||
campaign.step === 'hanzhong-king-camp' ||
campaign.step === 'shu-han-foundation-camp' ||
campaign.step === 'baidi-entrustment-camp'
@@ -630,6 +632,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'forty-seventh-battle') {
this.scene.start('BattleScene', { battleId: fortySeventhBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -134,6 +134,8 @@ export type CampaignStep =
| 'forty-fifth-camp'
| 'forty-sixth-battle'
| 'forty-sixth-camp'
| 'forty-seventh-battle'
| 'forty-seventh-camp'
| 'hanzhong-king-camp'
| 'shu-han-foundation-camp'
| 'baidi-entrustment-camp';
@@ -288,7 +290,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'forty-third-battle-gongan-collapse': { victory: 'forty-third-camp', retry: 'forty-third-battle' },
'forty-fourth-battle-maicheng-isolation': { victory: 'forty-fourth-camp', retry: 'forty-fourth-battle' },
'forty-fifth-battle-yiling-vanguard': { victory: 'forty-fifth-camp', retry: 'forty-fifth-battle' },
'forty-sixth-battle-yiling-fire': { victory: 'forty-sixth-camp', retry: 'forty-sixth-battle' }
'forty-sixth-battle-yiling-fire': { victory: 'forty-sixth-camp', retry: 'forty-sixth-battle' },
'forty-seventh-battle-nanzhong-stabilization': { victory: 'forty-seventh-camp', retry: 'forty-seventh-battle' }
};
export type CampaignSaveSlotSummary = {