diff --git a/docs/roadmap.md b/docs/roadmap.md
index b80550c..e74a75e 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -53,15 +53,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Twenty-ninth battle Luo Castle outer wall route, putting Fa Zheng into the sortie choice across a 58x40 outer-wall battlefield and recruiting Wu Yi after Zhang Ren withdraws
- Thirtieth battle Luofeng Slope ambush route, using Wu Yi's first sortie choice and Pang Tong survival pressure across a 60x42 ambush battlefield
- Thirty-first battle Luo Castle main-gate route, turning the siege into a 62x44 castle assault with Zhang Ren's final defense and Yan Yan recruitment after the gate opens
+- Thirty-second battle Mianzhu Gate pressure route, using Yan Yan's first sortie choice across a 64x46 Chengdu approach battlefield and recruiting Li Yan after the defense line yields
- 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, 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, 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 thirty-first battle victory, 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 camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the thirty-second battle victory, 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 camp states, campaign timeline state, and camp save state
## Next Steps
-1. Continue the Yi Province arc into Chengdu pressure and final Yi Province persuasion, using Yan Yan's veteran perspective plus Pang Tong/Fa Zheng/Wu Yi planning pressure
+1. Continue the Yi Province arc into the final Chengdu surrender and Shu-Han foundation setup, using Yan Yan and Li Yan's local legitimacy plus Pang Tong/Fa Zheng/Wu Yi planning pressure
2. Add more recruitable officers as the campaign moves deeper through Yi Province and toward Shu Han
3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments
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 d488222..b2b0263 100644
--- a/scripts/verify-flow.mjs
+++ b/scripts/verify-flow.mjs
@@ -1736,7 +1736,7 @@ try {
if (
progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 ||
- progressTabState.campaignProgress?.totalKnown !== 31 ||
+ progressTabState.campaignProgress?.totalKnown !== 32 ||
progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' ||
progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전'
@@ -1888,7 +1888,7 @@ try {
if (
postBowangProgressState?.activeTab !== 'progress' ||
postBowangProgressState.campaignProgress?.completedKnown !== 18 ||
- postBowangProgressState.campaignProgress?.totalKnown !== 31 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 32 ||
postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' ||
postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로'
@@ -2026,7 +2026,7 @@ try {
if (
postChangbanProgressState?.activeTab !== 'progress' ||
postChangbanProgressState.campaignProgress?.completedKnown !== 19 ||
- postChangbanProgressState.campaignProgress?.totalKnown !== 31 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 32 ||
postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' ||
postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로'
@@ -2164,7 +2164,7 @@ try {
if (
postJiangdongProgressState?.activeTab !== 'progress' ||
postJiangdongProgressState.campaignProgress?.completedKnown !== 20 ||
- postJiangdongProgressState.campaignProgress?.totalKnown !== 31 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 32 ||
postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' ||
postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전'
@@ -2304,7 +2304,7 @@ try {
if (
postRedCliffsProgressState?.activeTab !== 'progress' ||
postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 ||
- postRedCliffsProgressState.campaignProgress?.totalKnown !== 31 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 32 ||
postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' ||
postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전'
@@ -2444,7 +2444,7 @@ try {
if (
postFireAttackProgressState?.activeTab !== 'progress' ||
postFireAttackProgressState.campaignProgress?.completedKnown !== 22 ||
- postFireAttackProgressState.campaignProgress?.totalKnown !== 31 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 32 ||
postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' ||
postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전'
@@ -2587,7 +2587,7 @@ try {
if (
postJingzhouProgressState?.activeTab !== 'progress' ||
postJingzhouProgressState.campaignProgress?.completedKnown !== 23 ||
- postJingzhouProgressState.campaignProgress?.totalKnown !== 31 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 32 ||
postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' ||
postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전'
@@ -2729,7 +2729,7 @@ try {
if (
postGuiyangProgressState?.activeTab !== 'progress' ||
postGuiyangProgressState.campaignProgress?.completedKnown !== 24 ||
- postGuiyangProgressState.campaignProgress?.totalKnown !== 31 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 32 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
@@ -2872,7 +2872,7 @@ try {
if (
postWulingProgressState?.activeTab !== 'progress' ||
postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
- postWulingProgressState.campaignProgress?.totalKnown !== 31 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 32 ||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전'
@@ -3021,7 +3021,7 @@ try {
if (
postChangshaProgressState?.activeTab !== 'progress' ||
postChangshaProgressState.campaignProgress?.completedKnown !== 26 ||
- postChangshaProgressState.campaignProgress?.totalKnown !== 31 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 32 ||
postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' ||
postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로'
@@ -3166,7 +3166,7 @@ try {
if (
postYizhouProgressState?.activeTab !== 'progress' ||
postYizhouProgressState.campaignProgress?.completedKnown !== 27 ||
- postYizhouProgressState.campaignProgress?.totalKnown !== 31 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 32 ||
postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' ||
postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전'
@@ -3313,7 +3313,7 @@ try {
if (
postFuPassProgressState?.activeTab !== 'progress' ||
postFuPassProgressState.campaignProgress?.completedKnown !== 28 ||
- postFuPassProgressState.campaignProgress?.totalKnown !== 31 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 32 ||
postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' ||
postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전'
@@ -3462,7 +3462,7 @@ try {
if (
postLuoProgressState?.activeTab !== 'progress' ||
postLuoProgressState.campaignProgress?.completedKnown !== 29 ||
- postLuoProgressState.campaignProgress?.totalKnown !== 31 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 32 ||
postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' ||
postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전'
@@ -3608,7 +3608,7 @@ try {
if (
postLuofengProgressState?.activeTab !== 'progress' ||
postLuofengProgressState.campaignProgress?.completedKnown !== 30 ||
- postLuofengProgressState.campaignProgress?.totalKnown !== 31 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 32 ||
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략'
@@ -3760,15 +3760,168 @@ try {
if (
postLuoMainProgressState?.activeTab !== 'progress' ||
postLuoMainProgressState.campaignProgress?.completedKnown !== 31 ||
- postLuoMainProgressState.campaignProgress?.totalKnown !== 31 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 32 ||
postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' ||
- postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전'
) {
- throw new Error(`Expected post-Luo-main progress tab to complete Luo Castle proper and pause before Chengdu pressure: ${JSON.stringify(postLuoMainProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Luo-main progress tab to complete Luo Castle proper and open Mianzhu Gate pressure: ${JSON.stringify(postLuoMainProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-luo-main-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const mianzhuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !mianzhuSortieState?.sortieVisible ||
+ !mianzhuSortieState.sortiePlan?.objectiveLine?.includes('면죽관 압박전') ||
+ !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) ||
+ !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
+ !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
+ !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) ||
+ !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
+ mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan') ||
+ mianzhuSortieState.sortieRoster?.length < 17 ||
+ mianzhuSortieState.sortiePlan?.maxCount !== 6
+ ) {
+ throw new Error(`Expected Luo main camp sortie prep to target Mianzhu with Yan Yan selectable and Li Yan absent before victory: ${JSON.stringify(mianzhuSortieState)}`);
+ }
+ assertSortieTacticalRoster(mianzhuSortieState, [
+ 'liu-bei',
+ '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'
+ ]);
+
+ const mianzhuPriorityUnits = ['yan-yan', 'fa-zheng', 'wu-yi', 'pang-tong', 'huang-zhong'];
+ for (const unitId of mianzhuPriorityUnits) {
+ 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.id !== 'liu-bei' && !mianzhuPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const mianzhuSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !mianzhuPriorityUnits.every((unitId) =>
+ mianzhuSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ mianzhuSortieReadyState.sortiePlan?.selectedCount !== 6 ||
+ mianzhuSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
+ ) {
+ throw new Error(`Expected Mianzhu sortie to deploy Yan Yan, Fa Zheng, Wu Yi, Pang Tong, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(mianzhuSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-mianzhu-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-mianzhu-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredThirtySecondBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate';
+ });
+ if (enteredThirtySecondBattle) {
+ 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 === 'thirty-second-battle-mianzhu-gate' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-thirty-second-battle.png', fullPage: true });
+
+ const thirtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const thirtySecondEnemies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const thirtySecondAllies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'ally');
+ const thirtySecondEnemyBehaviors = new Set(thirtySecondEnemies.map((unit) => unit.ai));
+ if (
+ thirtySecondBattleState.camera?.mapWidth !== 64 ||
+ thirtySecondBattleState.camera?.mapHeight !== 46 ||
+ thirtySecondBattleState.victoryConditionLabel !== '이엄 수비선 격파' ||
+ thirtySecondEnemies.length < 25 ||
+ !thirtySecondEnemyBehaviors.has('aggressive') ||
+ !thirtySecondEnemyBehaviors.has('guard') ||
+ !thirtySecondEnemyBehaviors.has('hold') ||
+ !thirtySecondEnemies.some((unit) => unit.id === 'chengdu-leader-li-yan') ||
+ !thirtySecondEnemies.some((unit) => unit.id === 'chengdu-officer-fei-guan') ||
+ !thirtySecondAllies.some((unit) => unit.id === 'yan-yan') ||
+ !thirtySecondAllies.some((unit) => unit.id === 'fa-zheng') ||
+ !thirtySecondAllies.some((unit) => unit.id === 'wu-yi') ||
+ !thirtySecondAllies.some((unit) => unit.id === 'pang-tong') ||
+ !thirtySecondAllies.some((unit) => unit.id === 'huang-zhong') ||
+ thirtySecondAllies.some((unit) => unit.id === 'li-yan')
+ ) {
+ throw new Error(`Expected thirty-second battle to use Mianzhu map, Li Yan objective, Yan Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtySecondBattleState)}`);
+ }
+
+ 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 thirtySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ thirtySecondCampState?.campBattleId !== 'thirty-second-battle-mianzhu-gate' ||
+ thirtySecondCampState.campTitle !== '면죽관 압박전 후 군영' ||
+ thirtySecondCampState.availableDialogueIds?.length !== 3 ||
+ !thirtySecondCampState.availableDialogueIds.every((id) => id.endsWith('mianzhu')) ||
+ thirtySecondCampState.availableVisitIds?.length !== 2 ||
+ !thirtySecondCampState.campaign?.roster?.some((unit) => unit.id === 'li-yan') ||
+ !thirtySecondCampState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited) ||
+ thirtySecondCampState.rosterCollection?.total < 18
+ ) {
+ throw new Error(`Expected thirty-second camp to recruit Li Yan and expose Mianzhu dialogue/visit sets: ${JSON.stringify(thirtySecondCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-mianzhu-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postMianzhuProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postMianzhuProgressState?.activeTab !== 'progress' ||
+ postMianzhuProgressState.campaignProgress?.completedKnown !== 32 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 32 ||
+ postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
+ postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
+ postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Mianzhu progress tab to complete Mianzhu pressure and pause before Chengdu surrender: ${JSON.stringify(postMianzhuProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-mianzhu-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -3787,7 +3940,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-thirty-first-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan joins, Luofeng and Luo main gate battles, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-thirty-second-battle 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 joins, Luofeng, Luo main gate, and Mianzhu battles, result states, and debug API at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/thirty-second-battle-map.svg b/src/assets/images/battle/thirty-second-battle-map.svg
new file mode 100644
index 0000000..d7893e1
--- /dev/null
+++ b/src/assets/images/battle/thirty-second-battle-map.svg
@@ -0,0 +1,84 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 2ef6e64..13812a2 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -47,6 +47,10 @@ import {
thirtyFirstBattleMap,
thirtyFirstBattleUnits,
thirtyFirstBattleVictoryPages,
+ thirtySecondBattleBonds,
+ thirtySecondBattleMap,
+ thirtySecondBattleUnits,
+ thirtySecondBattleVictoryPages,
thirteenthBattleBonds,
thirteenthBattleMap,
thirteenthBattleUnits,
@@ -160,7 +164,8 @@ export type BattleScenarioId =
| 'twenty-eighth-battle-fu-pass-entry'
| 'twenty-ninth-battle-luo-outer-wall'
| 'thirtieth-battle-luofeng-ambush'
- | 'thirty-first-battle-luo-main-gate';
+ | 'thirty-first-battle-luo-main-gate'
+ | 'thirty-second-battle-mianzhu-gate';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1973,6 +1978,69 @@ export const thirtyFirstBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const thirtySecondBattleScenario: BattleScenarioDefinition = {
+ id: 'thirty-second-battle-mianzhu-gate',
+ title: '면죽관 압박전',
+ victoryConditionLabel: '이엄 수비선 격파',
+ defeatConditionLabel: '유비 또는 엄안 퇴각',
+ openingObjectiveLines: [
+ '면죽관은 성도 앞 마지막 완충 지대입니다. 관문을 장악하면 유장의 항복 권고가 현실적인 압박으로 바뀝니다.',
+ '엄안은 익주 장수들의 마음을 흔드는 첫 증거입니다. 엄안을 지키며 이엄에게 유비군의 군율과 예우를 보여 주십시오.',
+ '이엄을 격파하되 마을과 창고를 안정시키면 성도 항복 권고의 명분이 강해집니다.'
+ ],
+ map: thirtySecondBattleMap,
+ units: thirtySecondBattleUnits,
+ bonds: thirtySecondBattleBonds,
+ mapTextureKey: 'battle-map-thirty-second',
+ leaderUnitId: 'chengdu-leader-li-yan',
+ quickVictoryTurnLimit: 45,
+ baseVictoryGold: 5040,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '이엄 수비선 격파',
+ rewardGold: 3120,
+ unitId: 'chengdu-leader-li-yan'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 960,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'yan-yan',
+ kind: 'keep-unit-alive',
+ label: '엄안 생존',
+ rewardGold: 1260,
+ unitId: 'yan-yan'
+ },
+ {
+ id: 'mianzhu-storehouse',
+ kind: 'secure-terrain',
+ label: '면죽관 창고 확보',
+ rewardGold: 1540,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '45턴 이내 승리',
+ rewardGold: 1180,
+ maxTurn: 45
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'yan-yan' }
+ ],
+ itemRewards: ['콩 18', '상처약 13', '탁주 7', '성도 항복 권고문 +1', '이엄 합류'],
+ victoryPages: thirtySecondBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -2006,7 +2074,8 @@ export const battleScenarios: Record
'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario,
'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario,
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario,
- 'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario
+ 'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario,
+ 'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index da69ffb..158a2a8 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -18,6 +18,7 @@ import {
tenthBattleScenario,
thirteenthBattleScenario,
thirtyFirstBattleScenario,
+ thirtySecondBattleScenario,
thirtiethBattleScenario,
twentyEighthBattleScenario,
twentyNinthBattleScenario,
@@ -69,6 +70,8 @@ import {
thirteenthBattleVictoryPages,
thirtyFirstBattleIntroPages,
thirtyFirstBattleVictoryPages,
+ thirtySecondBattleIntroPages,
+ thirtySecondBattleVictoryPages,
thirtiethBattleIntroPages,
thirtiethBattleVictoryPages,
twentyEighthBattleIntroPages,
@@ -431,13 +434,24 @@ const sortieFlows: Record = {
},
[thirtyFirstBattleScenario.id]: {
afterBattleId: thirtyFirstBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '성도 압박 준비',
+ eyebrow: '다음 전장',
+ title: thirtySecondBattleScenario.title,
description:
- '낙성 본성이 열리고 엄안이 합류했습니다. 이제 유비군은 익주의 심부를 흔들며 성도까지 이어지는 길을 준비해야 합니다.',
- rewardHint: '다음 장: 성도 압박 준비 중',
- pages: thirtyFirstBattleVictoryPages,
- unavailableNotice: '성도 압박과 익주 최종 설득은 다음 작업에서 이어집니다. 새로 합류한 엄안을 포함해 성벽과 고지 전투 조합을 준비하십시오.'
+ '낙성 본성이 열리고 엄안이 합류했습니다. 이제 엄안의 첫 출전과 법정·오의의 길 안내를 엮어 성도 앞 면죽관을 압박해야 합니다.',
+ rewardHint: `예상 보상: ${thirtySecondBattleScenario.title} 개방 / 이엄 합류`,
+ nextBattleId: thirtySecondBattleScenario.id,
+ campaignStep: 'thirty-second-battle',
+ pages: [...thirtyFirstBattleVictoryPages, ...thirtySecondBattleIntroPages]
+ },
+ [thirtySecondBattleScenario.id]: {
+ afterBattleId: thirtySecondBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '성도 항복 권고 준비',
+ description:
+ '면죽관이 열리고 이엄이 합류했습니다. 이제 성도 포위와 유장의 항복 권고, 그리고 익주 통치의 첫 선택을 준비해야 합니다.',
+ rewardHint: '다음 장: 성도 항복 권고 준비 중',
+ pages: thirtySecondBattleVictoryPages,
+ unavailableNotice: '성도 항복 권고와 익주 최종 수습은 다음 작업에서 이어집니다. 새로 합류한 이엄을 포함해 보급과 내정형 무장 조합을 준비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 48c9bf7..5a8760f 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -2014,6 +2014,68 @@ export const thirtyFirstBattleVictoryPages: StoryPage[] = [
}
];
+export const thirtySecondBattleIntroPages: StoryPage[] = [
+ {
+ id: 'thirty-second-chengdu-pressure',
+ bgm: 'story-dark',
+ chapter: '성도 압박의 시작',
+ background: 'story-three-heroes',
+ speaker: '법정',
+ text: '낙성이 열리자 성도는 놀랐으나 아직 문을 닫고 있습니다. 면죽관의 수비장 이엄이 항복과 결전을 저울질하고 있으니, 힘과 명분을 함께 보여 주어야 합니다.'
+ },
+ {
+ id: 'thirty-second-yan-yan-first-counsel',
+ bgm: 'battle-prep',
+ chapter: '엄안의 첫 출전',
+ background: 'story-liu-bei',
+ speaker: '엄안',
+ text: '이엄은 무턱대고 물러설 장수가 아닙니다. 그러나 백성을 해치지 않는 군율과 익주 장수를 예우하는 마음을 보이면, 그의 칼끝도 흔들릴 것입니다.'
+ },
+ {
+ id: 'thirty-second-mianzhu-plan',
+ bgm: 'battle-prep',
+ chapter: '면죽관 설득',
+ background: 'story-militia',
+ speaker: '방통',
+ text: '정면으로만 밀면 성도는 더 굳게 닫힙니다. 엄안으로 익주 장수의 마음을 열고, 법정과 오의로 길을 짚으며, 황충과 장비가 수비선을 짧게 끊어야 합니다.'
+ },
+ {
+ id: 'thirty-second-sortie',
+ bgm: 'battle-prep',
+ chapter: '면죽관 압박전',
+ background: 'story-sortie',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '우리가 얻으려는 것은 성 하나가 아니라 익주의 마음이오. 면죽관을 넘되 백성을 다치게 하지 말고, 이엄에게도 함께 살 길을 보여 주시오.'
+ }
+];
+
+export const thirtySecondBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'thirty-second-victory-pass-yields',
+ bgm: 'militia-theme',
+ chapter: '면죽관이 열리다',
+ background: 'story-sortie',
+ text: '면죽관의 수비선은 무너졌지만 성도 길목의 마을은 불타지 않았습니다. 유비군이 군율을 지키자 익주 병사들은 패배보다 이후의 처우를 먼저 바라보았습니다.'
+ },
+ {
+ id: 'thirty-second-li-yan-joins',
+ bgm: 'battle-prep',
+ chapter: '이엄의 선택',
+ background: 'story-liu-bei',
+ speaker: '이엄',
+ text: '칼을 거두는 일이 반드시 부끄러운 것은 아니군요. 백성을 살리고 익주의 길을 보전한다면, 이엄 또한 새 주군의 군율 아래 서겠습니다.'
+ },
+ {
+ id: 'thirty-second-next-chengdu-surrender',
+ bgm: 'story-dark',
+ chapter: '성도 항복 권고',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '낙성과 면죽이 열렸으니 유장의 선택지는 더욱 좁아졌습니다. 다음 길은 성도 포위와 항복 권고, 그리고 익주를 어떻게 다스릴 것인가에 달렸습니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2345,6 +2407,12 @@ export const thirtyFirstBattleMap: BattleMap = {
terrain: createThirtyFirstBattleTerrain()
};
+export const thirtySecondBattleMap: BattleMap = {
+ width: 64,
+ height: 46,
+ terrain: createThirtySecondBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4514,6 +4582,30 @@ export const luoCastleProperRecruitUnits: UnitData[] = [
}
];
+export const chengduPressureRecruitUnits: UnitData[] = [
+ {
+ id: 'li-yan',
+ name: '이엄',
+ faction: 'ally',
+ className: '익주 수비장',
+ classKey: 'infantry',
+ level: 20,
+ exp: 0,
+ hp: 46,
+ maxHp: 46,
+ attack: 15,
+ move: 4,
+ stats: { might: 80, intelligence: 78, leadership: 88, agility: 70, luck: 72 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 3, exp: 24 },
+ armor: { itemId: 'reinforced-lamellar', level: 3, exp: 12 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 11,
+ y: 28
+ }
+];
+
const eighthBattleAllyPositions: Record = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -13037,6 +13129,70 @@ export const thirtyFirstBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('luo-main-leader-zhang-ren', '장임', '낙성 본성 주장', 'rebelLeader', 58, 286, 78, 58, 18)
];
+const thirtySecondBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 40 },
+ 'guan-yu': { x: 5, y: 38 },
+ 'zhang-fei': { x: 4, y: 42 },
+ 'jian-yong': { x: 3, y: 43 },
+ 'mi-zhu': { x: 5, y: 44 },
+ 'sun-qian': { x: 7, y: 43 },
+ 'zhao-yun': { x: 8, y: 39 },
+ 'zhuge-liang': { x: 6, y: 38 },
+ 'ma-liang': { x: 8, y: 44 },
+ 'yi-ji': { x: 9, y: 41 },
+ 'gong-zhi': { x: 10, y: 44 },
+ 'huang-zhong': { x: 7, y: 40 },
+ 'wei-yan': { x: 10, y: 38 },
+ 'pang-tong': { x: 9, y: 39 },
+ 'fa-zheng': { x: 11, y: 41 },
+ 'wu-yi': { x: 12, y: 39 },
+ 'yan-yan': { x: 11, y: 37 }
+};
+
+export const thirtySecondBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits,
+ ...jingzhouRecruitUnits,
+ ...guiyangRecruitUnits,
+ ...wulingRecruitUnits,
+ ...changshaRecruitUnits,
+ ...yizhouRecruitUnits,
+ ...fuPassRecruitUnits,
+ ...luoCastleRecruitUnits,
+ ...luoCastleProperRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, thirtySecondBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('chengdu-scout-a', '성도 정찰병', '남로 정찰병', 'bandit', 56, 164, 60, 18, 39),
+ createThirtyFirstEnemyUnit('chengdu-scout-b', '성도 정찰병', '수로 정찰병', 'bandit', 56, 164, 60, 24, 36),
+ createThirtyFirstEnemyUnit('chengdu-scout-c', '성도 정찰병', '산길 정찰병', 'bandit', 57, 172, 62, 31, 35),
+ createThirtyFirstEnemyUnit('chengdu-infantry-a', '면죽관 보병', '외곽 보병', 'yellowTurban', 57, 236, 74, 29, 33),
+ createThirtyFirstEnemyUnit('chengdu-infantry-b', '면죽관 보병', '외곽 보병', 'yellowTurban', 57, 236, 74, 34, 31),
+ createThirtyFirstEnemyUnit('chengdu-infantry-c', '면죽관 보병', '관문 보병', 'yellowTurban', 58, 246, 76, 41, 27),
+ createThirtyFirstEnemyUnit('chengdu-infantry-d', '면죽관 보병', '관문 보병', 'yellowTurban', 58, 246, 76, 45, 25),
+ createThirtyFirstEnemyUnit('chengdu-infantry-e', '성도 보병', '내곽 보병', 'yellowTurban', 59, 256, 78, 53, 22),
+ createThirtyFirstEnemyUnit('chengdu-infantry-f', '성도 보병', '창고 보병', 'yellowTurban', 59, 256, 78, 57, 25),
+ createThirtyFirstEnemyUnit('chengdu-archer-a', '면죽관 궁병', '숲길 궁병', 'archer', 57, 160, 60, 31, 29),
+ createThirtyFirstEnemyUnit('chengdu-archer-b', '면죽관 궁병', '성벽 궁병', 'archer', 58, 166, 62, 43, 21),
+ createThirtyFirstEnemyUnit('chengdu-archer-c', '성도 궁병', '고지 궁병', 'archer', 58, 166, 62, 50, 18),
+ createThirtyFirstEnemyUnit('chengdu-archer-d', '성도 궁병', '창고 궁병', 'archer', 59, 174, 64, 58, 23),
+ createThirtyFirstEnemyUnit('chengdu-cavalry-a', '성도 기병', '우익 기병', 'cavalry', 58, 246, 78, 28, 41),
+ createThirtyFirstEnemyUnit('chengdu-cavalry-b', '성도 기병', '좌익 기병', 'cavalry', 58, 246, 78, 40, 36),
+ createThirtyFirstEnemyUnit('chengdu-cavalry-c', '성도 기병', '내성 기병', 'cavalry', 59, 258, 80, 55, 30),
+ createThirtyFirstEnemyUnit('chengdu-guard-a', '이엄 친위대', '관문 친위대', 'yellowTurban', 59, 266, 80, 48, 22),
+ createThirtyFirstEnemyUnit('chengdu-guard-b', '이엄 친위대', '관문 친위대', 'yellowTurban', 59, 266, 80, 52, 21),
+ createThirtyFirstEnemyUnit('chengdu-guard-c', '이엄 친위대', '성도 친위대', 'yellowTurban', 60, 276, 82, 58, 19),
+ createThirtyFirstEnemyUnit('chengdu-strategist-a', '성도 책사', '항복 반대파', 'strategist', 58, 166, 62, 45, 18),
+ createThirtyFirstEnemyUnit('chengdu-strategist-b', '성도 책사', '창고 책사', 'strategist', 59, 174, 64, 55, 15),
+ createThirtyFirstEnemyUnit('chengdu-ambusher-a', '성도 복병', '남벽 복병', 'bandit', 57, 174, 64, 35, 39),
+ createThirtyFirstEnemyUnit('chengdu-ambusher-b', '성도 복병', '동문 복병', 'bandit', 57, 174, 64, 49, 33),
+ createThirtyFirstEnemyUnit('chengdu-ambusher-c', '성도 복병', '북벽 복병', 'bandit', 58, 182, 66, 59, 11),
+ createThirtyFirstEnemyUnit('chengdu-officer-fei-guan', '비관', '익주 부장', 'rebelLeader', 60, 286, 84, 55, 20),
+ createThirtyFirstEnemyUnit('chengdu-leader-li-yan', '이엄', '면죽관 수비장', 'rebelLeader', 61, 306, 86, 60, 18)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -13388,6 +13544,33 @@ export const luoCastleProperRecruitBonds: BattleBond[] = [
}
];
+export const chengduPressureRecruitBonds: BattleBond[] = [
+ {
+ id: 'liu-bei__li-yan',
+ unitIds: ['liu-bei', 'li-yan'],
+ title: '성도 앞의 군율',
+ level: 30,
+ exp: 0,
+ description: '유비의 군율과 이엄의 수비 책임이 맞닿아, 익주 백성을 해치지 않는 항복 명분을 더 단단하게 만듭니다.'
+ },
+ {
+ id: 'zhuge-liang__li-yan',
+ unitIds: ['zhuge-liang', 'li-yan'],
+ title: '내정과 책략',
+ level: 28,
+ exp: 0,
+ description: '제갈량의 큰 판과 이엄의 창고·관문 지식이 만나, 전투 이후 익주를 다스릴 준비를 앞당깁니다.'
+ },
+ {
+ id: 'yan-yan__li-yan',
+ unitIds: ['yan-yan', 'li-yan'],
+ title: '익주 장수의 결단',
+ level: 28,
+ exp: 0,
+ description: '엄안의 선례와 이엄의 신중함이 서로를 비추며, 익주 장수들이 유비군을 받아들이는 길을 넓힙니다.'
+ }
+];
+
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -13424,6 +13607,9 @@ export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBond
export const twentyNinthBattleBonds: BattleBond[] = [...twentyEighthBattleBonds, ...fuPassRecruitBonds].map(cloneBattleBondForScenario);
export const thirtiethBattleBonds: BattleBond[] = [...twentyNinthBattleBonds, ...luoCastleRecruitBonds].map(cloneBattleBondForScenario);
export const thirtyFirstBattleBonds: BattleBond[] = [...thirtiethBattleBonds].map(cloneBattleBondForScenario);
+export const thirtySecondBattleBonds: BattleBond[] = [...thirtyFirstBattleBonds, ...luoCastleProperRecruitBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -14685,6 +14871,72 @@ function createThirtyFirstBattleTerrain(): TerrainType[][] {
);
}
+function createThirtySecondBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 46 }, (_, y) =>
+ Array.from({ length: 64 }, (_, x): TerrainType => {
+ if (x <= 4 && y >= 39) {
+ return 'camp';
+ }
+ if (
+ (x >= 49 && x <= 62 && y >= 12 && y <= 26) ||
+ (x >= 45 && x <= 54 && y >= 8 && y <= 17) ||
+ (x >= 54 && x <= 62 && y >= 17 && y <= 30) ||
+ (x >= 52 && x <= 58 && y >= 27 && y <= 32)
+ ) {
+ return 'fort';
+ }
+ if (
+ (x >= 18 && x <= 21 && y >= 37 && y <= 39) ||
+ (x >= 36 && x <= 38 && y >= 31 && y <= 33) ||
+ (x >= 45 && x <= 47 && y >= 26 && y <= 28) ||
+ (x >= 55 && x <= 57 && y >= 29 && y <= 31)
+ ) {
+ return 'village';
+ }
+ if (
+ (y >= 41 && x >= 4 && x <= 19) ||
+ (x >= 18 && x <= 31 && y >= 37 && y <= 41) ||
+ (x >= 30 && x <= 43 && y >= 32 && y <= 37) ||
+ (x >= 42 && x <= 54 && y >= 26 && y <= 32) ||
+ (x >= 53 && x <= 61 && y >= 20 && y <= 26) ||
+ (x >= 58 && x <= 62 && y >= 18 && y <= 21)
+ ) {
+ return 'road';
+ }
+ if ((x === 14 || x === 15) && y >= 0 && y <= 35) {
+ return 'river';
+ }
+ if ((x === 35 || x === 36) && y >= 4 && y <= 30) {
+ return 'river';
+ }
+ if ((x === 46 || x === 47) && y >= 32 && y <= 45) {
+ return 'river';
+ }
+ if (
+ (x >= 4 && x <= 21 && y >= 8 && y <= 30) ||
+ (x >= 22 && x <= 34 && y >= 14 && y <= 31) ||
+ (x >= 37 && x <= 50 && y >= 33 && y <= 45) ||
+ (x >= 49 && x <= 63 && y >= 29 && y <= 44)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 12 && y <= 16) ||
+ (x >= 22 && x <= 34 && y <= 13) ||
+ (x >= 36 && x <= 52 && y >= 2 && y <= 14) ||
+ (x >= 55 && x <= 63 && y <= 12) ||
+ (x >= 26 && x <= 38 && y >= 38 && y <= 45)
+ ) {
+ return 'hill';
+ }
+ if ((x <= 2 && y <= 38) || (x >= 62 && y >= 26) || (x >= 49 && x <= 52 && y <= 8)) {
+ return 'cliff';
+ }
+ 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 3127705..58480dc 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -38,6 +38,7 @@ const unitTexture: Record = {
'fa-zheng': 'unit-liu-bei',
'wu-yi': 'unit-guan-yu',
'yan-yan': 'unit-rebel-archer',
+ 'li-yan': 'unit-guan-yu',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -427,7 +428,33 @@ const unitTexture: Record = {
'luo-main-ambusher-c': 'unit-rebel',
'luo-main-veteran-yan-yan': 'unit-rebel-archer',
'luo-main-officer-a': 'unit-rebel-leader',
- 'luo-main-leader-zhang-ren': 'unit-rebel-leader'
+ 'luo-main-leader-zhang-ren': 'unit-rebel-leader',
+ 'chengdu-scout-a': 'unit-rebel',
+ 'chengdu-scout-b': 'unit-rebel',
+ 'chengdu-scout-c': 'unit-rebel',
+ 'chengdu-infantry-a': 'unit-rebel',
+ 'chengdu-infantry-b': 'unit-rebel',
+ 'chengdu-infantry-c': 'unit-rebel',
+ 'chengdu-infantry-d': 'unit-rebel',
+ 'chengdu-infantry-e': 'unit-rebel',
+ 'chengdu-infantry-f': 'unit-rebel',
+ 'chengdu-archer-a': 'unit-rebel-archer',
+ 'chengdu-archer-b': 'unit-rebel-archer',
+ 'chengdu-archer-c': 'unit-rebel-archer',
+ 'chengdu-archer-d': 'unit-rebel-archer',
+ 'chengdu-cavalry-a': 'unit-rebel-cavalry',
+ 'chengdu-cavalry-b': 'unit-rebel-cavalry',
+ 'chengdu-cavalry-c': 'unit-rebel-cavalry',
+ 'chengdu-guard-a': 'unit-rebel',
+ 'chengdu-guard-b': 'unit-rebel',
+ 'chengdu-guard-c': 'unit-rebel',
+ 'chengdu-strategist-a': 'unit-rebel-archer',
+ 'chengdu-strategist-b': 'unit-rebel-archer',
+ 'chengdu-ambusher-a': 'unit-rebel',
+ 'chengdu-ambusher-b': 'unit-rebel',
+ 'chengdu-ambusher-c': 'unit-rebel',
+ 'chengdu-officer-fei-guan': 'unit-rebel-leader',
+ 'chengdu-leader-li-yan': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -1317,7 +1344,33 @@ const enemyAiByUnitId: Record = {
'luo-main-ambusher-c': 'aggressive',
'luo-main-veteran-yan-yan': 'hold',
'luo-main-officer-a': 'guard',
- 'luo-main-leader-zhang-ren': 'guard'
+ 'luo-main-leader-zhang-ren': 'guard',
+ 'chengdu-scout-a': 'aggressive',
+ 'chengdu-scout-b': 'aggressive',
+ 'chengdu-scout-c': 'aggressive',
+ 'chengdu-infantry-a': 'guard',
+ 'chengdu-infantry-b': 'guard',
+ 'chengdu-infantry-c': 'guard',
+ 'chengdu-infantry-d': 'guard',
+ 'chengdu-infantry-e': 'guard',
+ 'chengdu-infantry-f': 'guard',
+ 'chengdu-archer-a': 'hold',
+ 'chengdu-archer-b': 'hold',
+ 'chengdu-archer-c': 'hold',
+ 'chengdu-archer-d': 'hold',
+ 'chengdu-cavalry-a': 'aggressive',
+ 'chengdu-cavalry-b': 'aggressive',
+ 'chengdu-cavalry-c': 'aggressive',
+ 'chengdu-guard-a': 'guard',
+ 'chengdu-guard-b': 'guard',
+ 'chengdu-guard-c': 'guard',
+ 'chengdu-strategist-a': 'hold',
+ 'chengdu-strategist-b': 'hold',
+ 'chengdu-ambusher-a': 'aggressive',
+ 'chengdu-ambusher-b': 'aggressive',
+ 'chengdu-ambusher-c': 'aggressive',
+ 'chengdu-officer-fei-guan': 'guard',
+ 'chengdu-leader-li-yan': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 25165f0..d601797 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -17,6 +17,7 @@ import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-batt
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
+import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
@@ -120,6 +121,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-twenty-ninth', twentyNinthBattleMapUrl);
this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl);
this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl);
+ this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl);
this.load.image('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 52dac59..f158450 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -7,6 +7,8 @@ import { getSortieFlow } from '../data/campaignFlow';
import {
caoBreakRecruitBonds,
caoBreakRecruitUnits,
+ chengduPressureRecruitBonds,
+ chengduPressureRecruitUnits,
changshaRecruitBonds,
changshaRecruitUnits,
firstBattleBonds,
@@ -322,7 +324,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'twenty-eighth-battle-fu-pass-entry',
'twenty-ninth-battle-luo-outer-wall',
'thirtieth-battle-luofeng-ambush',
- 'thirty-first-battle-luo-main-gate'
+ 'thirty-first-battle-luo-main-gate',
+ 'thirty-second-battle-mianzhu-gate'
],
nextHints: ['형주 방위', '익주 진입', '방통·법정 합류']
},
@@ -367,7 +370,8 @@ const campBattleIds = {
twentyEighth: 'twenty-eighth-battle-fu-pass-entry',
twentyNinth: 'twenty-ninth-battle-luo-outer-wall',
thirtieth: 'thirtieth-battle-luofeng-ambush',
- thirtyFirst: 'thirty-first-battle-luo-main-gate'
+ thirtyFirst: 'thirty-first-battle-luo-main-gate',
+ thirtySecond: 'thirty-second-battle-mianzhu-gate'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -714,6 +718,18 @@ const sortieRulesByBattleId: Partial