diff --git a/docs/roadmap.md b/docs/roadmap.md
index 0029d11..2f8d361 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -55,17 +55,18 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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
- Thirty-third battle Chengdu surrender route, using Li Yan's first sortie choice across a 66x48 Chengdu outer-wall battlefield and recruiting Huang Quan after Yi Province yields
+- Thirty-fourth battle Jiameng Pass Ma Chao route, using Huang Quan's first sortie choice across a 68x50 Hanzhong gate battlefield and recruiting Ma Chao after his charge is redirected
- 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-third 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/Chengdu camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the thirty-fourth 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/Chengdu/Jiameng camp states, campaign timeline state, and camp save state
## Next Steps
-1. Open the Shu-Han foundation setup after Yi Province, turning the new Chengdu camp into policy, title, and northward Hanzhong preparation choices
-2. Add more recruitable officers as the campaign moves from Yi Province stabilization toward Hanzhong and Shu Han
-3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments
+1. Continue the Hanzhong front after Jiameng Pass, turning Ma Chao's cavalry and Huang Quan's counsel into the first clear Shu-Han northern campaign choices
+2. Add more recruitable officers as the campaign moves from Hanzhong preparation toward the formal Shu-Han foundation
+3. Expand sortie selection into richer pre-battle officer planning, including explicit reserve drills, class practice, and bond-focused camp assignments
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
diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs
index cf0ae47..9f1d801 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 !== 33 ||
+ progressTabState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 34 ||
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 !== 33 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 34 ||
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략'
@@ -3760,7 +3760,7 @@ try {
if (
postLuoMainProgressState?.activeTab !== 'progress' ||
postLuoMainProgressState.campaignProgress?.completedKnown !== 31 ||
- postLuoMainProgressState.campaignProgress?.totalKnown !== 33 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 34 ||
postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' ||
postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전'
@@ -3913,7 +3913,7 @@ try {
if (
postMianzhuProgressState?.activeTab !== 'progress' ||
postMianzhuProgressState.campaignProgress?.completedKnown !== 32 ||
- postMianzhuProgressState.campaignProgress?.totalKnown !== 33 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 34 ||
postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전'
@@ -4067,15 +4067,170 @@ try {
if (
postChengduProgressState?.activeTab !== 'progress' ||
postChengduProgressState.campaignProgress?.completedKnown !== 33 ||
- postChengduProgressState.campaignProgress?.totalKnown !== 33 ||
- postChengduProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 34 ||
+ postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' ||
- postChengduProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전'
) {
- throw new Error(`Expected post-Chengdu progress tab to complete Yi Province capture and pause before Shu-Han foundation: ${JSON.stringify(postChengduProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Chengdu progress tab to complete Yi Province capture and open Jiameng Pass: ${JSON.stringify(postChengduProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-chengdu-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const jiamengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !jiamengSortieState?.sortieVisible ||
+ !jiamengSortieState.sortiePlan?.objectiveLine?.includes('가맹관 마초 대면전') ||
+ !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) ||
+ !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
+ !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) ||
+ !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
+ !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
+ jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao') ||
+ jiamengSortieState.sortieRoster?.length < 19 ||
+ jiamengSortieState.sortiePlan?.maxCount !== 6
+ ) {
+ throw new Error(`Expected Chengdu camp sortie prep to target Jiameng Pass with Huang Quan selectable and Ma Chao absent before victory: ${JSON.stringify(jiamengSortieState)}`);
+ }
+ assertSortieTacticalRoster(jiamengSortieState, [
+ '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',
+ 'li-yan',
+ 'huang-quan'
+ ]);
+
+ const jiamengPriorityUnits = ['huang-quan', 'zhao-yun', 'zhang-fei', 'fa-zheng', 'huang-zhong'];
+ for (const unitId of jiamengPriorityUnits) {
+ 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' && !jiamengPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const jiamengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !jiamengPriorityUnits.every((unitId) =>
+ jiamengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ jiamengSortieReadyState.sortiePlan?.selectedCount !== 6 ||
+ jiamengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
+ ) {
+ throw new Error(`Expected Jiameng Pass sortie to deploy Huang Quan, Zhao Yun, Zhang Fei, Fa Zheng, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(jiamengSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-jiameng-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-jiameng-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredThirtyFourthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass';
+ });
+ if (enteredThirtyFourthBattle) {
+ 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-fourth-battle-jiameng-pass' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-thirty-fourth-battle.png', fullPage: true });
+
+ const thirtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const thirtyFourthEnemies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const thirtyFourthAllies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const thirtyFourthEnemyBehaviors = new Set(thirtyFourthEnemies.map((unit) => unit.ai));
+ if (
+ thirtyFourthBattleState.camera?.mapWidth !== 68 ||
+ thirtyFourthBattleState.camera?.mapHeight !== 50 ||
+ thirtyFourthBattleState.victoryConditionLabel !== '마초 돌격 저지' ||
+ thirtyFourthEnemies.length < 22 ||
+ !thirtyFourthEnemyBehaviors.has('aggressive') ||
+ !thirtyFourthEnemyBehaviors.has('guard') ||
+ !thirtyFourthEnemyBehaviors.has('hold') ||
+ !thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-leader-ma-chao') ||
+ !thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-officer-pang-de') ||
+ !thirtyFourthAllies.some((unit) => unit.id === 'huang-quan') ||
+ !thirtyFourthAllies.some((unit) => unit.id === 'zhao-yun') ||
+ !thirtyFourthAllies.some((unit) => unit.id === 'zhang-fei') ||
+ !thirtyFourthAllies.some((unit) => unit.id === 'fa-zheng') ||
+ !thirtyFourthAllies.some((unit) => unit.id === 'huang-zhong') ||
+ thirtyFourthAllies.some((unit) => unit.id === 'ma-chao')
+ ) {
+ throw new Error(`Expected thirty-fourth battle to use Jiameng map, Ma Chao objective, Huang Quan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFourthBattleState)}`);
+ }
+
+ 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 thirtyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ thirtyFourthCampState?.campBattleId !== 'thirty-fourth-battle-jiameng-pass' ||
+ thirtyFourthCampState.campTitle !== '가맹관 대면전 후 군영' ||
+ thirtyFourthCampState.availableDialogueIds?.length !== 3 ||
+ !thirtyFourthCampState.availableDialogueIds.every((id) => id.endsWith('jiameng')) ||
+ thirtyFourthCampState.availableVisitIds?.length !== 2 ||
+ !thirtyFourthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-chao') ||
+ !thirtyFourthCampState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited) ||
+ thirtyFourthCampState.rosterCollection?.total < 20
+ ) {
+ throw new Error(`Expected thirty-fourth camp to recruit Ma Chao and expose Jiameng dialogue/visit sets: ${JSON.stringify(thirtyFourthCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-jiameng-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postJiamengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postJiamengProgressState?.activeTab !== 'progress' ||
+ postJiamengProgressState.campaignProgress?.completedKnown !== 34 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 34 ||
+ postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
+ postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' ||
+ postJiamengProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Jiameng progress tab to complete Ma Chao chapter and pause on the next Hanzhong step: ${JSON.stringify(postJiamengProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-jiameng-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -4094,7 +4249,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-thirty-third-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, Huang Quan joins, Luofeng, Luo main gate, Mianzhu, and Chengdu surrender battles, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-thirty-fourth-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, Huang Quan, and Ma Chao joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, and Jiameng Pass battles, result states, and debug API at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/thirty-fourth-battle-map.svg b/src/assets/images/battle/thirty-fourth-battle-map.svg
new file mode 100644
index 0000000..ee670c2
--- /dev/null
+++ b/src/assets/images/battle/thirty-fourth-battle-map.svg
@@ -0,0 +1,85 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index c79d8dd..6d79412 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -43,6 +43,10 @@ import {
tenthBattleMap,
tenthBattleUnits,
tenthBattleVictoryPages,
+ thirtyFourthBattleBonds,
+ thirtyFourthBattleMap,
+ thirtyFourthBattleUnits,
+ thirtyFourthBattleVictoryPages,
thirtyFirstBattleBonds,
thirtyFirstBattleMap,
thirtyFirstBattleUnits,
@@ -170,7 +174,8 @@ export type BattleScenarioId =
| 'thirtieth-battle-luofeng-ambush'
| 'thirty-first-battle-luo-main-gate'
| 'thirty-second-battle-mianzhu-gate'
- | 'thirty-third-battle-chengdu-surrender';
+ | 'thirty-third-battle-chengdu-surrender'
+ | 'thirty-fourth-battle-jiameng-pass';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2109,6 +2114,69 @@ export const thirtyThirdBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const thirtyFourthBattleScenario: BattleScenarioDefinition = {
+ id: 'thirty-fourth-battle-jiameng-pass',
+ title: '가맹관 마초 대면전',
+ victoryConditionLabel: '마초 돌격 저지',
+ defeatConditionLabel: '유비 또는 황권 퇴각',
+ openingObjectiveLines: [
+ '가맹관은 익주와 한중을 잇는 목입니다. 이 길을 열면 촉한 건국 이후 북방 전선의 첫 기반이 마련됩니다.',
+ '황권은 이번 전장이 첫 출전입니다. 한중 길과 익주 내정의 균형을 잡으려면 황권을 지켜야 합니다.',
+ '마초의 돌격을 받아내고 방덕의 친위대를 제압하면, 서량 기병을 유비군으로 끌어들일 말길이 열립니다.'
+ ],
+ map: thirtyFourthBattleMap,
+ units: thirtyFourthBattleUnits,
+ bonds: thirtyFourthBattleBonds,
+ mapTextureKey: 'battle-map-thirty-fourth',
+ leaderUnitId: 'jiameng-leader-ma-chao',
+ quickVictoryTurnLimit: 50,
+ baseVictoryGold: 5660,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '마초 돌격 저지',
+ rewardGold: 3560,
+ unitId: 'jiameng-leader-ma-chao'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 1000,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'huang-quan',
+ kind: 'keep-unit-alive',
+ label: '황권 생존',
+ rewardGold: 1360,
+ unitId: 'huang-quan'
+ },
+ {
+ id: 'jiameng-storehouse',
+ kind: 'secure-terrain',
+ label: '가맹관 보급 창고 확보',
+ rewardGold: 1680,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '50턴 이내 승리',
+ rewardGold: 1300,
+ maxTurn: 50
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'huang-quan' }
+ ],
+ itemRewards: ['콩 22', '상처약 16', '탁주 8', '서량 기병 전술서 +1', '마초 합류'],
+ victoryPages: thirtyFourthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -2144,7 +2212,8 @@ export const battleScenarios: Record
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario,
'thirty-first-battle-luo-main-gate': thirtyFirstBattleScenario,
'thirty-second-battle-mianzhu-gate': thirtySecondBattleScenario,
- 'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario
+ 'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario,
+ 'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 4b20565..7b7e076 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -17,6 +17,7 @@ import {
seventeenthBattleScenario,
tenthBattleScenario,
thirteenthBattleScenario,
+ thirtyFourthBattleScenario,
thirtyFirstBattleScenario,
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
@@ -69,6 +70,8 @@ import {
tenthBattleVictoryPages,
thirteenthBattleIntroPages,
thirteenthBattleVictoryPages,
+ thirtyFourthBattleIntroPages,
+ thirtyFourthBattleVictoryPages,
thirtyFirstBattleIntroPages,
thirtyFirstBattleVictoryPages,
thirtySecondBattleIntroPages,
@@ -459,13 +462,24 @@ const sortieFlows: Record = {
},
[thirtyThirdBattleScenario.id]: {
afterBattleId: thirtyThirdBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '촉한 건국 준비',
+ eyebrow: '다음 전장',
+ title: thirtyFourthBattleScenario.title,
description:
- '성도가 항복하고 황권이 합류했습니다. 형주와 익주의 기반이 마련되었으니, 다음 장은 나라의 틀을 세우고 한중과 촉한 건국으로 향하는 준비입니다.',
- rewardHint: '다음 장: 촉한 건국 준비 중',
- pages: thirtyThirdBattleVictoryPages,
- unavailableNotice: '촉한 건국과 한중 방면 전개는 다음 작업에서 이어집니다. 새로 합류한 황권을 포함해 내정과 방어형 무장 조합을 준비하십시오.'
+ '성도가 항복하고 황권이 합류했습니다. 이제 익주의 북문인 가맹관에서 한중 방면을 살피고, 서량의 기병장 마초를 유비군의 깃발 아래로 끌어들일 차례입니다.',
+ rewardHint: `예상 보상: ${thirtyFourthBattleScenario.title} 개방 / 마초 합류`,
+ nextBattleId: thirtyFourthBattleScenario.id,
+ campaignStep: 'thirty-fourth-battle',
+ pages: [...thirtyThirdBattleVictoryPages, ...thirtyFourthBattleIntroPages]
+ },
+ [thirtyFourthBattleScenario.id]: {
+ afterBattleId: thirtyFourthBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '한중 전선 준비',
+ description:
+ '가맹관에서 마초가 합류했습니다. 촉한 건국의 군사 기반을 다지려면 이제 한중 전선을 본격적으로 준비해야 합니다.',
+ rewardHint: '다음 장: 한중 전선 준비 중',
+ pages: thirtyFourthBattleVictoryPages,
+ unavailableNotice: '한중 전선과 촉한 건국 선포는 다음 작업에서 이어집니다. 새로 합류한 마초를 포함해 기병 돌파와 내정 안정 조합을 준비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 3638a12..1822917 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -2138,6 +2138,68 @@ export const thirtyThirdBattleVictoryPages: StoryPage[] = [
}
];
+export const thirtyFourthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'thirty-fourth-hanzhong-road',
+ bgm: 'story-dark',
+ chapter: '한중으로 향하는 길',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '성도는 안정되었으나 북쪽 한중은 아직 장로와 조조의 그늘 아래 흔들립니다. 촉한의 이름을 세우려면 북문을 지킬 길부터 열어야 합니다.'
+ },
+ {
+ id: 'thirty-fourth-huang-quan-first-counsel',
+ bgm: 'battle-prep',
+ chapter: '황권의 첫 출전',
+ background: 'story-liu-bei',
+ speaker: '황권',
+ text: '가맹관은 익주와 한중을 잇는 목입니다. 그곳에 마초의 기병이 나타났다면, 싸움만이 아니라 그가 누구의 길을 택할지도 보아야 합니다.'
+ },
+ {
+ id: 'thirty-fourth-ma-chao-appears',
+ bgm: 'battle-prep',
+ chapter: '서량의 기병',
+ background: 'story-militia',
+ speaker: '법정',
+ text: '마초는 조조에게 쫓기고 장로에게도 마음을 다 주지 못했습니다. 그의 돌격을 막아 내고, 유비군이 서량의 창을 품을 그릇임을 보여 주십시오.'
+ },
+ {
+ id: 'thirty-fourth-sortie',
+ bgm: 'battle-prep',
+ chapter: '가맹관 마초 대면전',
+ background: 'story-sortie',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '마초의 용맹을 꺾으려 하지 말고, 그 창끝이 향할 뜻을 바꾸어야 하오. 황권과 법정은 길을 열고, 장수들은 기병의 돌파를 받아내시오.'
+ }
+];
+
+export const thirtyFourthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'thirty-fourth-victory-charge-stopped',
+ bgm: 'militia-theme',
+ chapter: '서량 돌격을 막다',
+ background: 'story-sortie',
+ text: '가맹관의 좁은 길에서 서량 기병의 첫 돌격은 멈췄습니다. 마초는 패배보다도 유비군의 군율과 익주 장수들의 안정된 태도를 더 오래 바라보았습니다.'
+ },
+ {
+ id: 'thirty-fourth-ma-chao-joins',
+ bgm: 'battle-prep',
+ chapter: '마초의 합류',
+ background: 'story-liu-bei',
+ speaker: '마초',
+ text: '창을 맡길 주군을 찾지 못해 오래 떠돌았습니다. 유비공이 백성을 보전하고 장수를 예우한다면, 서량의 창 또한 그 깃발 아래 서겠습니다.'
+ },
+ {
+ id: 'thirty-fourth-next-hanzhong',
+ bgm: 'story-dark',
+ chapter: '한중 전선의 개막',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '마초가 합류하자 북쪽 길이 한층 넓어졌습니다. 이제 촉한의 기반은 성도 안쪽에서 끝나지 않고, 한중을 향한 전선으로 나아가게 됩니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2481,6 +2543,12 @@ export const thirtyThirdBattleMap: BattleMap = {
terrain: createThirtyThirdBattleTerrain()
};
+export const thirtyFourthBattleMap: BattleMap = {
+ width: 68,
+ height: 50,
+ terrain: createThirtyFourthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -4698,6 +4766,30 @@ export const chengduSurrenderRecruitUnits: UnitData[] = [
}
];
+export const hanzhongOpeningRecruitUnits: UnitData[] = [
+ {
+ id: 'ma-chao',
+ name: '마초',
+ faction: 'ally',
+ className: '서량 기병',
+ classKey: 'cavalry',
+ level: 23,
+ exp: 0,
+ hp: 52,
+ maxHp: 52,
+ attack: 18,
+ move: 6,
+ stats: { might: 92, intelligence: 62, leadership: 86, agility: 84, luck: 70 },
+ equipment: {
+ weapon: { itemId: 'sky-piercer-halberd', level: 4, exp: 22 },
+ armor: { itemId: 'reinforced-lamellar', level: 3, exp: 18 },
+ accessory: { itemId: 'bravery-token', level: 2, exp: 14 }
+ },
+ x: 13,
+ y: 32
+ }
+];
+
const eighthBattleAllyPositions: Record = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -13351,6 +13443,70 @@ export const thirtyThirdBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('chengdu-final-leader-huang-quan', '황권', '성도 수비 참모', 'rebelLeader', 65, 336, 94, 62, 18)
];
+const thirtyFourthBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 44 },
+ 'guan-yu': { x: 5, y: 42 },
+ 'zhang-fei': { x: 4, y: 46 },
+ 'jian-yong': { x: 3, y: 47 },
+ 'mi-zhu': { x: 5, y: 48 },
+ 'sun-qian': { x: 7, y: 47 },
+ 'zhao-yun': { x: 8, y: 43 },
+ 'zhuge-liang': { x: 6, y: 42 },
+ 'ma-liang': { x: 8, y: 48 },
+ 'yi-ji': { x: 9, y: 45 },
+ 'gong-zhi': { x: 10, y: 48 },
+ 'huang-zhong': { x: 7, y: 44 },
+ 'wei-yan': { x: 10, y: 42 },
+ 'pang-tong': { x: 9, y: 43 },
+ 'fa-zheng': { x: 11, y: 45 },
+ 'wu-yi': { x: 12, y: 43 },
+ 'yan-yan': { x: 11, y: 41 },
+ 'li-yan': { x: 13, y: 42 },
+ 'huang-quan': { x: 14, y: 44 }
+};
+
+export const thirtyFourthBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits,
+ ...jingzhouRecruitUnits,
+ ...guiyangRecruitUnits,
+ ...wulingRecruitUnits,
+ ...changshaRecruitUnits,
+ ...yizhouRecruitUnits,
+ ...fuPassRecruitUnits,
+ ...luoCastleRecruitUnits,
+ ...luoCastleProperRecruitUnits,
+ ...chengduPressureRecruitUnits,
+ ...chengduSurrenderRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, thirtyFourthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('jiameng-scout-a', '한중 척후', '산길 척후', 'bandit', 64, 198, 72, 18, 43),
+ createThirtyFirstEnemyUnit('jiameng-scout-b', '한중 척후', '관문 척후', 'bandit', 64, 198, 72, 26, 41),
+ createThirtyFirstEnemyUnit('jiameng-scout-c', '한중 척후', '수로 척후', 'bandit', 65, 206, 74, 34, 39),
+ createThirtyFirstEnemyUnit('jiameng-infantry-a', '장로 보병', '외곽 보병', 'yellowTurban', 65, 288, 88, 33, 36),
+ createThirtyFirstEnemyUnit('jiameng-infantry-b', '장로 보병', '외곽 보병', 'yellowTurban', 65, 288, 88, 38, 35),
+ createThirtyFirstEnemyUnit('jiameng-infantry-c', '가맹관 보병', '관문 보병', 'yellowTurban', 66, 300, 90, 45, 30),
+ createThirtyFirstEnemyUnit('jiameng-infantry-d', '가맹관 보병', '관문 보병', 'yellowTurban', 66, 300, 90, 49, 29),
+ createThirtyFirstEnemyUnit('jiameng-archer-a', '한중 궁병', '산허리 궁병', 'archer', 65, 206, 74, 36, 32),
+ createThirtyFirstEnemyUnit('jiameng-archer-b', '한중 궁병', '관문 궁병', 'archer', 66, 214, 76, 47, 24),
+ createThirtyFirstEnemyUnit('jiameng-archer-c', '한중 궁병', '고지 궁병', 'archer', 66, 214, 76, 55, 19),
+ createThirtyFirstEnemyUnit('jiameng-cavalry-a', '서량 기병', '좌익 기병', 'cavalry', 66, 304, 92, 30, 45),
+ createThirtyFirstEnemyUnit('jiameng-cavalry-b', '서량 기병', '우익 기병', 'cavalry', 66, 304, 92, 44, 40),
+ createThirtyFirstEnemyUnit('jiameng-cavalry-c', '서량 기병', '돌격 기병', 'cavalry', 67, 316, 94, 56, 33),
+ createThirtyFirstEnemyUnit('jiameng-cavalry-d', '서량 기병', '친위 기병', 'cavalry', 67, 316, 94, 61, 26),
+ createThirtyFirstEnemyUnit('jiameng-guard-a', '마초 친위대', '돌격 친위대', 'yellowTurban', 67, 326, 96, 53, 24),
+ createThirtyFirstEnemyUnit('jiameng-guard-b', '마초 친위대', '관문 친위대', 'yellowTurban', 67, 326, 96, 58, 23),
+ createThirtyFirstEnemyUnit('jiameng-strategist-a', '한중 책사', '장로 사자', 'strategist', 66, 214, 76, 50, 20),
+ createThirtyFirstEnemyUnit('jiameng-strategist-b', '한중 책사', '관문 책사', 'strategist', 67, 222, 78, 60, 18),
+ createThirtyFirstEnemyUnit('jiameng-ambusher-a', '한중 복병', '남령 복병', 'bandit', 65, 206, 76, 39, 43),
+ createThirtyFirstEnemyUnit('jiameng-ambusher-b', '한중 복병', '동령 복병', 'bandit', 65, 206, 76, 52, 37),
+ createThirtyFirstEnemyUnit('jiameng-officer-pang-de', '방덕', '서량 부장', 'rebelLeader', 68, 344, 98, 59, 25),
+ createThirtyFirstEnemyUnit('jiameng-leader-ma-chao', '마초', '서량 기병장', 'rebelLeader', 69, 364, 102, 63, 20)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -13756,6 +13912,33 @@ export const chengduSurrenderRecruitBonds: BattleBond[] = [
}
];
+export const hanzhongOpeningRecruitBonds: BattleBond[] = [
+ {
+ id: 'liu-bei__ma-chao',
+ unitIds: ['liu-bei', 'ma-chao'],
+ title: '떠도는 창과 새 깃발',
+ level: 30,
+ exp: 0,
+ description: '유비의 포용과 마초의 돌격심이 맞물려, 서량 기병의 창끝이 촉한의 깃발 아래로 향합니다.'
+ },
+ {
+ id: 'zhao-yun__ma-chao',
+ unitIds: ['zhao-yun', 'ma-chao'],
+ title: '백마와 금마초',
+ level: 28,
+ exp: 0,
+ description: '조운의 절제된 기동과 마초의 거친 돌격이 서로를 자극하며, 기병 운용의 폭을 넓힙니다.'
+ },
+ {
+ id: 'zhang-fei__ma-chao',
+ unitIds: ['zhang-fei', 'ma-chao'],
+ 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);
@@ -13798,6 +13981,9 @@ export const thirtySecondBattleBonds: BattleBond[] = [...thirtyFirstBattleBonds,
export const thirtyThirdBattleBonds: BattleBond[] = [...thirtySecondBattleBonds, ...chengduPressureRecruitBonds].map(
cloneBattleBondForScenario
);
+export const thirtyFourthBattleBonds: BattleBond[] = [...thirtyThirdBattleBonds, ...chengduSurrenderRecruitBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -15191,6 +15377,77 @@ function createThirtyThirdBattleTerrain(): TerrainType[][] {
);
}
+function createThirtyFourthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 50 }, (_, y) =>
+ Array.from({ length: 68 }, (_, x): TerrainType => {
+ if (x <= 4 && y >= 43) {
+ return 'camp';
+ }
+ if (
+ (x >= 51 && x <= 66 && y >= 11 && y <= 28) ||
+ (x >= 47 && x <= 58 && y >= 7 && y <= 18) ||
+ (x >= 57 && x <= 67 && y >= 20 && y <= 34) ||
+ (x >= 54 && x <= 61 && y >= 31 && y <= 37)
+ ) {
+ return 'fort';
+ }
+ if (
+ (x >= 18 && x <= 22 && y >= 41 && y <= 43) ||
+ (x >= 37 && x <= 40 && y >= 35 && y <= 37) ||
+ (x >= 47 && x <= 49 && y >= 30 && y <= 32) ||
+ (x >= 58 && x <= 61 && y >= 33 && y <= 36)
+ ) {
+ return 'village';
+ }
+ if (
+ (y >= 45 && x >= 4 && x <= 22) ||
+ (x >= 21 && x <= 34 && y >= 41 && y <= 45) ||
+ (x >= 33 && x <= 46 && y >= 36 && y <= 41) ||
+ (x >= 45 && x <= 57 && y >= 30 && y <= 36) ||
+ (x >= 56 && x <= 65 && y >= 24 && y <= 30) ||
+ (x >= 60 && x <= 67 && y >= 20 && y <= 24)
+ ) {
+ return 'road';
+ }
+ if ((x === 15 || x === 16) && y >= 0 && y <= 39) {
+ return 'river';
+ }
+ if ((x === 37 || x === 38) && y >= 3 && y <= 34) {
+ return 'river';
+ }
+ if ((x === 49 || x === 50) && y >= 36 && y <= 49) {
+ return 'river';
+ }
+ if (
+ (x >= 4 && x <= 23 && y >= 8 && y <= 34) ||
+ (x >= 24 && x <= 36 && y >= 15 && y <= 35) ||
+ (x >= 40 && x <= 54 && y >= 37 && y <= 49) ||
+ (x >= 52 && x <= 67 && y >= 33 && y <= 48)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 14 && y <= 19) ||
+ (x >= 24 && x <= 36 && y <= 14) ||
+ (x >= 38 && x <= 55 && y >= 1 && y <= 15) ||
+ (x >= 57 && x <= 67 && y <= 13) ||
+ (x >= 28 && x <= 42 && y >= 42 && y <= 49)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 42) ||
+ (x >= 65 && y >= 29) ||
+ (x >= 51 && x <= 54 && y <= 8) ||
+ (x >= 23 && x <= 27 && y >= 22 && y <= 32)
+ ) {
+ 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 920ee72..3700afe 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -481,7 +481,30 @@ const unitTexture: Record = {
'chengdu-final-ambusher-b': 'unit-rebel',
'chengdu-final-ambusher-c': 'unit-rebel',
'chengdu-final-officer-a': 'unit-rebel-leader',
- 'chengdu-final-leader-huang-quan': 'unit-rebel-leader'
+ 'chengdu-final-leader-huang-quan': 'unit-rebel-leader',
+ 'ma-chao': 'unit-rebel-cavalry',
+ 'jiameng-scout-a': 'unit-rebel',
+ 'jiameng-scout-b': 'unit-rebel',
+ 'jiameng-scout-c': 'unit-rebel',
+ 'jiameng-infantry-a': 'unit-rebel',
+ 'jiameng-infantry-b': 'unit-rebel',
+ 'jiameng-infantry-c': 'unit-rebel',
+ 'jiameng-infantry-d': 'unit-rebel',
+ 'jiameng-archer-a': 'unit-rebel-archer',
+ 'jiameng-archer-b': 'unit-rebel-archer',
+ 'jiameng-archer-c': 'unit-rebel-archer',
+ 'jiameng-cavalry-a': 'unit-rebel-cavalry',
+ 'jiameng-cavalry-b': 'unit-rebel-cavalry',
+ 'jiameng-cavalry-c': 'unit-rebel-cavalry',
+ 'jiameng-cavalry-d': 'unit-rebel-cavalry',
+ 'jiameng-guard-a': 'unit-rebel',
+ 'jiameng-guard-b': 'unit-rebel',
+ 'jiameng-strategist-a': 'unit-rebel-archer',
+ 'jiameng-strategist-b': 'unit-rebel-archer',
+ 'jiameng-ambusher-a': 'unit-rebel',
+ 'jiameng-ambusher-b': 'unit-rebel',
+ 'jiameng-officer-pang-de': 'unit-rebel-leader',
+ 'jiameng-leader-ma-chao': 'unit-rebel-cavalry'
};
const unitTextureByClass: Partial> = {
@@ -1423,7 +1446,29 @@ const enemyAiByUnitId: Record = {
'chengdu-final-ambusher-b': 'aggressive',
'chengdu-final-ambusher-c': 'aggressive',
'chengdu-final-officer-a': 'guard',
- 'chengdu-final-leader-huang-quan': 'guard'
+ 'chengdu-final-leader-huang-quan': 'guard',
+ 'jiameng-scout-a': 'aggressive',
+ 'jiameng-scout-b': 'aggressive',
+ 'jiameng-scout-c': 'aggressive',
+ 'jiameng-infantry-a': 'guard',
+ 'jiameng-infantry-b': 'guard',
+ 'jiameng-infantry-c': 'guard',
+ 'jiameng-infantry-d': 'guard',
+ 'jiameng-archer-a': 'hold',
+ 'jiameng-archer-b': 'hold',
+ 'jiameng-archer-c': 'hold',
+ 'jiameng-cavalry-a': 'aggressive',
+ 'jiameng-cavalry-b': 'aggressive',
+ 'jiameng-cavalry-c': 'aggressive',
+ 'jiameng-cavalry-d': 'aggressive',
+ 'jiameng-guard-a': 'guard',
+ 'jiameng-guard-b': 'guard',
+ 'jiameng-strategist-a': 'hold',
+ 'jiameng-strategist-b': 'hold',
+ 'jiameng-ambusher-a': 'aggressive',
+ 'jiameng-ambusher-b': 'aggressive',
+ 'jiameng-officer-pang-de': 'guard',
+ 'jiameng-leader-ma-chao': 'aggressive'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 7090ba7..413d753 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -16,6 +16,7 @@ import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-m
import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
+import thirtyFourthBattleMapUrl from '../../assets/images/battle/thirty-fourth-battle-map.svg';
import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-battle-map.svg';
import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
@@ -124,6 +125,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirty-first', thirtyFirstBattleMapUrl);
this.load.image('battle-map-thirty-second', thirtySecondBattleMapUrl);
this.load.image('battle-map-thirty-third', thirtyThirdBattleMapUrl);
+ this.load.image('battle-map-thirty-fourth', thirtyFourthBattleMapUrl);
this.load.image('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 a2e1a64..44af3a3 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -19,6 +19,8 @@ import {
fuPassRecruitUnits,
guiyangRecruitBonds,
guiyangRecruitUnits,
+ hanzhongOpeningRecruitBonds,
+ hanzhongOpeningRecruitUnits,
jingzhouRecruitBonds,
jingzhouRecruitUnits,
liuBiaoRecruitBonds,
@@ -337,7 +339,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '촉한 건국',
period: '한실을 잇는 깃발',
description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.',
- battleIds: [],
+ battleIds: ['thirty-fourth-battle-jiameng-pass'],
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
}
];
@@ -375,7 +377,8 @@ const campBattleIds = {
thirtieth: 'thirtieth-battle-luofeng-ambush',
thirtyFirst: 'thirty-first-battle-luo-main-gate',
thirtySecond: 'thirty-second-battle-mianzhu-gate',
- thirtyThird: 'thirty-third-battle-chengdu-surrender'
+ thirtyThird: 'thirty-third-battle-chengdu-surrender',
+ thirtyFourth: 'thirty-fourth-battle-jiameng-pass'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -746,6 +749,18 @@ const sortieRulesByBattleId: Partial