diff --git a/docs/roadmap.md b/docs/roadmap.md
index b4786db..091b885 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -58,16 +58,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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
- Thirty-fifth battle Yangping scout route, using Ma Chao's first allied sortie across a 70x52 Hanzhong gate battlefield and recruiting Ma Dai after the pass is surveyed
- Thirty-sixth battle Dingjun Mountain vanguard route, using Huang Zhong and Fa Zheng's high-ground plan across a 72x54 Hanzhong battlefield and recruiting Wang Ping after the ridge is secured
+- Thirty-seventh battle Hanzhong decisive route, using Wang Ping's terrain knowledge and a seven-officer sortie choice across a 74x56 Hanzhong battlefield to push Cao Cao out and prepare Liu Bei's King of Hanzhong step
- 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-sixth 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/Yangping/Dingjun camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the thirty-seventh 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/Yangping/Dingjun/Hanzhong camp states, campaign timeline state, and camp save state
## Next Steps
-1. Continue from Dingjun Mountain into the Hanzhong decisive battle, using Huang Zhong/Fa Zheng and Wang Ping's terrain knowledge as the tactical axis
-2. Add the formal Shu-Han foundation setup once Hanzhong's military basis is strong enough
+1. Continue from Hanzhong victory into the King of Hanzhong and Shu-Han foundation setup, making the political transition playable in camp rather than a single text page
+2. Add the next tension around Guan Yu's Jing Province defense so the Shu-Han foundation has an immediate strategic cost
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 f2dbb34..d162be8 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 !== 36 ||
+ progressTabState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 37 ||
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 !== 36 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 37 ||
postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전'
@@ -4067,7 +4067,7 @@ try {
if (
postChengduProgressState?.activeTab !== 'progress' ||
postChengduProgressState.campaignProgress?.completedKnown !== 33 ||
- postChengduProgressState.campaignProgress?.totalKnown !== 36 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 37 ||
postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' ||
postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전'
@@ -4222,7 +4222,7 @@ try {
if (
postJiamengProgressState?.activeTab !== 'progress' ||
postJiamengProgressState.campaignProgress?.completedKnown !== 34 ||
- postJiamengProgressState.campaignProgress?.totalKnown !== 36 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 37 ||
postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' ||
postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전'
@@ -4378,7 +4378,7 @@ try {
if (
postYangpingProgressState?.activeTab !== 'progress' ||
postYangpingProgressState.campaignProgress?.completedKnown !== 35 ||
- postYangpingProgressState.campaignProgress?.totalKnown !== 36 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 37 ||
postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' ||
postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전'
@@ -4535,15 +4535,168 @@ try {
if (
postDingjunProgressState?.activeTab !== 'progress' ||
postDingjunProgressState.campaignProgress?.completedKnown !== 36 ||
- postDingjunProgressState.campaignProgress?.totalKnown !== 36 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 37 ||
postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' ||
- postDingjunProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전'
) {
- throw new Error(`Expected post-Dingjun progress tab to complete Wang Ping chapter and pause on the next Hanzhong step: ${JSON.stringify(postDingjunProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Dingjun progress tab to complete Wang Ping chapter and open the Hanzhong decisive battle: ${JSON.stringify(postDingjunProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-dingjun-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const hanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanzhongSortieState?.sortieVisible ||
+ !hanzhongSortieState.sortiePlan?.objectiveLine?.includes('한중 결전') ||
+ hanzhongSortieState.sortiePlan?.maxCount !== 7 ||
+ hanzhongSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !hanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited && unit.recommended)
+ ) {
+ throw new Error(`Expected Hanzhong decisive sortie prep to use a seven-officer roster and recommend Wang Ping: ${JSON.stringify(hanzhongSortieState)}`);
+ }
+ assertSortieTacticalRoster(hanzhongSortieState, [
+ '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',
+ 'ma-chao',
+ 'ma-dai',
+ 'wang-ping'
+ ]);
+
+ const hanzhongPriorityUnits = ['huang-zhong', 'fa-zheng', 'wang-ping', 'ma-chao', 'ma-dai', 'zhuge-liang'];
+ for (const unitId of hanzhongPriorityUnits) {
+ 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' && !hanzhongPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const hanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanzhongPriorityUnits.every((unitId) =>
+ hanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ hanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ hanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected Hanzhong decisive sortie to deploy Huang Zhong, Fa Zheng, Wang Ping, Ma Chao, Ma Dai, and Zhuge Liang with seven-officer pressure: ${JSON.stringify(hanzhongSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-hanzhong-decisive-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-hanzhong-decisive-story.png', fullPage: true });
+
+ for (let i = 0; i < 55; i += 1) {
+ const enteredThirtySeventhBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive';
+ });
+ if (enteredThirtySeventhBattle) {
+ 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-seventh-battle-hanzhong-decisive' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-thirty-seventh-battle.png', fullPage: true });
+
+ const thirtySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const thirtySeventhEnemies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const thirtySeventhAllies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'ally');
+ const thirtySeventhEnemyBehaviors = new Set(thirtySeventhEnemies.map((unit) => unit.ai));
+ if (
+ thirtySeventhBattleState.camera?.mapWidth !== 74 ||
+ thirtySeventhBattleState.camera?.mapHeight !== 56 ||
+ thirtySeventhBattleState.victoryConditionLabel !== '조조 본진 격파' ||
+ thirtySeventhEnemies.length < 30 ||
+ !thirtySeventhEnemyBehaviors.has('aggressive') ||
+ !thirtySeventhEnemyBehaviors.has('guard') ||
+ !thirtySeventhEnemyBehaviors.has('hold') ||
+ !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-leader-cao-cao') ||
+ !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-zhang-he') ||
+ !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-xu-huang') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'huang-zhong') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'fa-zheng') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'wang-ping') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'ma-chao') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'ma-dai') ||
+ !thirtySeventhAllies.some((unit) => unit.id === 'zhuge-liang')
+ ) {
+ throw new Error(`Expected thirty-seventh battle to use Hanzhong map, Cao Cao objective, Wang Ping terrain pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySeventhBattleState)}`);
+ }
+
+ 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 thirtySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ thirtySeventhCampState?.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' ||
+ thirtySeventhCampState.campTitle !== '한중 결전 후 군영' ||
+ thirtySeventhCampState.availableDialogueIds?.length !== 3 ||
+ !thirtySeventhCampState.availableDialogueIds.every((id) => id.endsWith('hanzhong-decisive')) ||
+ thirtySeventhCampState.availableVisitIds?.length !== 2 ||
+ !thirtySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') ||
+ thirtySeventhCampState.rosterCollection?.total < 22
+ ) {
+ throw new Error(`Expected thirty-seventh camp to expose Hanzhong decisive dialogue/visit sets and preserve Wang Ping roster: ${JSON.stringify(thirtySeventhCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-hanzhong-decisive-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postHanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postHanzhongProgressState?.activeTab !== 'progress' ||
+ postHanzhongProgressState.campaignProgress?.completedKnown !== 37 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 37 ||
+ postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
+ postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
+ postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Hanzhong progress tab to complete the decisive battle and pause on Shu-Han foundation prep: ${JSON.stringify(postHanzhongProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-hanzhong-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -4562,7 +4715,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-thirty-sixth-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, Ma Chao, Ma Dai, and Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng Pass, Yangping scout, and Dingjun vanguard battles, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-thirty-seventh-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, Ma Chao, Ma Dai, and Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, and Hanzhong decisive battles, result states, and debug API at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/thirty-seventh-battle-map.svg b/src/assets/images/battle/thirty-seventh-battle-map.svg
new file mode 100644
index 0000000..57630b8
--- /dev/null
+++ b/src/assets/images/battle/thirty-seventh-battle-map.svg
@@ -0,0 +1,87 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index cb879d3..b24c5d8 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -67,6 +67,10 @@ import {
thirtySixthBattleMap,
thirtySixthBattleUnits,
thirtySixthBattleVictoryPages,
+ thirtySeventhBattleBonds,
+ thirtySeventhBattleMap,
+ thirtySeventhBattleUnits,
+ thirtySeventhBattleVictoryPages,
thirteenthBattleBonds,
thirteenthBattleMap,
thirteenthBattleUnits,
@@ -185,7 +189,8 @@ export type BattleScenarioId =
| 'thirty-third-battle-chengdu-surrender'
| 'thirty-fourth-battle-jiameng-pass'
| 'thirty-fifth-battle-yangping-scout'
- | 'thirty-sixth-battle-dingjun-vanguard';
+ | 'thirty-sixth-battle-dingjun-vanguard'
+ | 'thirty-seventh-battle-hanzhong-decisive';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2313,6 +2318,69 @@ export const thirtySixthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const thirtySeventhBattleScenario: BattleScenarioDefinition = {
+ id: 'thirty-seventh-battle-hanzhong-decisive',
+ title: '한중 결전',
+ victoryConditionLabel: '조조 본진 격파',
+ defeatConditionLabel: '유비 또는 왕평 퇴각',
+ openingObjectiveLines: [
+ '한중 결전은 정군산 이후 북문 전체를 안정시키는 싸움입니다. 조조군 본진을 흔들어 한중의 주도권을 완전히 가져오십시오.',
+ '왕평은 이번 전장의 길잡이입니다. 왕평이 무너지면 산길과 보급로를 잇는 계획도 무너집니다.',
+ '황충과 법정은 정군산의 여세를 이어 고지를 누르고, 마초와 마대는 조조군 기병의 퇴로를 끊어 한중을 유비군의 깃발 아래 묶으십시오.'
+ ],
+ map: thirtySeventhBattleMap,
+ units: thirtySeventhBattleUnits,
+ bonds: thirtySeventhBattleBonds,
+ mapTextureKey: 'battle-map-thirty-seventh',
+ leaderUnitId: 'hanzhong-leader-cao-cao',
+ quickVictoryTurnLimit: 58,
+ baseVictoryGold: 6840,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '조조 본진 격파',
+ rewardGold: 4320,
+ unitId: 'hanzhong-leader-cao-cao'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 1120,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'wang-ping',
+ kind: 'keep-unit-alive',
+ label: '왕평 생존',
+ rewardGold: 1680,
+ unitId: 'wang-ping'
+ },
+ {
+ id: 'hanzhong-storehouse',
+ kind: 'secure-terrain',
+ label: '한중 보급 창고 확보',
+ rewardGold: 2040,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '58턴 이내 승리',
+ rewardGold: 1560,
+ maxTurn: 58
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'wang-ping' }
+ ],
+ itemRewards: ['콩 30', '상처약 22', '탁주 10', '한중 병법서 1', '한중왕 즉위 준비'],
+ victoryPages: thirtySeventhBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -2351,7 +2419,8 @@ export const battleScenarios: Record
'thirty-third-battle-chengdu-surrender': thirtyThirdBattleScenario,
'thirty-fourth-battle-jiameng-pass': thirtyFourthBattleScenario,
'thirty-fifth-battle-yangping-scout': thirtyFifthBattleScenario,
- 'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario
+ 'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario,
+ 'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 28896ec..7eb190b 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -23,6 +23,7 @@ import {
thirtySecondBattleScenario,
thirtyThirdBattleScenario,
thirtySixthBattleScenario,
+ thirtySeventhBattleScenario,
thirtiethBattleScenario,
twentyEighthBattleScenario,
twentyNinthBattleScenario,
@@ -84,6 +85,8 @@ import {
thirtyThirdBattleVictoryPages,
thirtySixthBattleIntroPages,
thirtySixthBattleVictoryPages,
+ thirtySeventhBattleIntroPages,
+ thirtySeventhBattleVictoryPages,
thirtiethBattleIntroPages,
thirtiethBattleVictoryPages,
twentyEighthBattleIntroPages,
@@ -501,13 +504,24 @@ const sortieFlows: Record = {
},
[thirtySixthBattleScenario.id]: {
afterBattleId: thirtySixthBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '한중 결전 준비',
+ eyebrow: '다음 전장',
+ title: thirtySeventhBattleScenario.title,
description:
- '정군산 전초전으로 한중의 고지를 확보했고 왕평이 합류했습니다. 다음은 한중 결전과 촉한 건국 선포로 이어질 큰 흐름을 준비해야 합니다.',
- rewardHint: '다음 장: 한중 결전 준비 중',
- pages: thirtySixthBattleVictoryPages,
- unavailableNotice: '한중 결전과 촉한 건국 선포는 다음 작업에서 이어집니다. 황충, 법정, 왕평의 산악 전선과 마초·마대의 기병 축을 준비하십시오.'
+ '정군산 전초전으로 한중의 고지를 확보했고 왕평이 합류했습니다. 이제 한중 결전에서 조조군 본진을 밀어내고 촉의 북문을 완전히 안정시킬 차례입니다.',
+ rewardHint: `예상 보상: ${thirtySeventhBattleScenario.title} 개방 / 한중왕 즉위 준비`,
+ nextBattleId: thirtySeventhBattleScenario.id,
+ campaignStep: 'thirty-seventh-battle',
+ pages: [...thirtySixthBattleVictoryPages, ...thirtySeventhBattleIntroPages]
+ },
+ [thirtySeventhBattleScenario.id]: {
+ afterBattleId: thirtySeventhBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '촉한 건국 준비',
+ description:
+ '한중 결전에서 조조군을 물리치고 북문을 얻었습니다. 다음은 한중왕 즉위와 촉한의 이름을 세우는 정치적 전환을 군영 이벤트와 다음 전장으로 준비해야 합니다.',
+ rewardHint: '다음 장: 한중왕 즉위와 촉한 건국 준비 중',
+ pages: thirtySeventhBattleVictoryPages,
+ unavailableNotice: '한중왕 즉위와 촉한 건국 선포는 다음 작업에서 이어집니다. 유비의 명분, 제갈량의 국정 구상, 관우가 지키는 형주의 긴장을 함께 준비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 3ed81aa..abb1b0d 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -2324,6 +2324,69 @@ export const thirtySixthBattleVictoryPages: StoryPage[] = [
}
];
+export const thirtySeventhBattleIntroPages: StoryPage[] = [
+ {
+ id: 'thirty-seventh-hanzhong-front',
+ bgm: 'story-dark',
+ chapter: '한중 결전으로',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '정군산의 능선이 열리자 한중의 길은 마침내 하나의 전장으로 이어졌습니다. 조조군은 물러나기 전에 마지막으로 북문을 움켜쥐려 합니다.'
+ },
+ {
+ id: 'thirty-seventh-wang-ping-map',
+ bgm: 'battle-prep',
+ chapter: '산길의 눈',
+ background: 'story-liu-bei',
+ speaker: '왕평',
+ text: '본진으로 곧장 오르는 길은 적이 기다리는 길입니다. 강변 창고를 잡고 서쪽 숲길을 열면 조조군의 퇴로와 보급이 동시에 흔들립니다.'
+ },
+ {
+ id: 'thirty-seventh-huang-fa-plan',
+ bgm: 'battle-prep',
+ chapter: '노장과 군사의 합',
+ background: 'story-sortie',
+ speaker: '법정',
+ text: '황충 장군이 고지를 누르고 왕평이 길을 잇는 사이, 마초와 마대가 북쪽 기병대를 묶어야 합니다. 이 싸움은 힘보다 순서가 중요합니다.'
+ },
+ {
+ id: 'thirty-seventh-sortie',
+ bgm: 'battle-prep',
+ chapter: '한중 결전',
+ background: 'story-militia',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '한중을 얻는 것은 북문을 얻는 일이고, 북문을 얻는 것은 촉의 백성을 지킬 이름을 얻는 일이오. 모두 각자의 길에서 하나의 깃발을 세우시오.'
+ }
+];
+
+export const thirtySeventhBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'thirty-seventh-victory-hanzhong-held',
+ bgm: 'militia-theme',
+ chapter: '한중을 얻다',
+ background: 'story-sortie',
+ text: '조조군의 본진이 흔들리고 한중의 창고와 산길이 유비군의 손에 들어왔습니다. 북문을 지키던 군세는 물러났고, 촉으로 향하는 길은 비로소 안정되었습니다.'
+ },
+ {
+ id: 'thirty-seventh-king-of-hanzhong',
+ bgm: 'battle-prep',
+ chapter: '한중왕의 이름',
+ background: 'story-liu-bei',
+ speaker: '제갈량',
+ text: '주공께서 한중을 얻었으니 이제 사람들은 촉의 주인이 누구인지 묻게 될 것입니다. 이름은 칼보다 늦게 움직이나, 때로는 칼보다 멀리 갑니다.'
+ },
+ {
+ id: 'thirty-seventh-next-shu-han',
+ bgm: 'story-dark',
+ chapter: '촉한의 문턱',
+ background: 'story-three-heroes',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '우리가 세우려는 것은 땅의 이름만이 아니오. 한실을 잇고 백성을 품을 뜻이라면, 이제 그 뜻을 나라의 이름으로 밝힐 준비를 해야 하오.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2685,6 +2748,12 @@ export const thirtySixthBattleMap: BattleMap = {
terrain: createThirtySixthBattleTerrain()
};
+export const thirtySeventhBattleMap: BattleMap = {
+ width: 74,
+ height: 56,
+ terrain: createThirtySeventhBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -13830,6 +13899,84 @@ export const thirtySixthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('dingjun-leader-xiahou-yuan', '하후연', '정군산 주장', 'rebelLeader', 77, 418, 116, 68, 25)
];
+const thirtySeventhBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 50 },
+ 'guan-yu': { x: 5, y: 48 },
+ 'zhang-fei': { x: 4, y: 52 },
+ 'jian-yong': { x: 3, y: 53 },
+ 'mi-zhu': { x: 5, y: 54 },
+ 'sun-qian': { x: 7, y: 53 },
+ 'zhao-yun': { x: 8, y: 49 },
+ 'zhuge-liang': { x: 6, y: 48 },
+ 'ma-liang': { x: 8, y: 54 },
+ 'yi-ji': { x: 9, y: 51 },
+ 'gong-zhi': { x: 10, y: 54 },
+ 'huang-zhong': { x: 7, y: 50 },
+ 'wei-yan': { x: 10, y: 48 },
+ 'pang-tong': { x: 9, y: 49 },
+ 'fa-zheng': { x: 11, y: 51 },
+ 'wu-yi': { x: 12, y: 49 },
+ 'yan-yan': { x: 11, y: 47 },
+ 'li-yan': { x: 13, y: 48 },
+ 'huang-quan': { x: 14, y: 50 },
+ 'ma-chao': { x: 13, y: 46 },
+ 'ma-dai': { x: 15, y: 47 },
+ 'wang-ping': { x: 16, y: 49 }
+};
+
+export const thirtySeventhBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits,
+ ...jingzhouRecruitUnits,
+ ...guiyangRecruitUnits,
+ ...wulingRecruitUnits,
+ ...changshaRecruitUnits,
+ ...yizhouRecruitUnits,
+ ...fuPassRecruitUnits,
+ ...luoCastleRecruitUnits,
+ ...luoCastleProperRecruitUnits,
+ ...chengduPressureRecruitUnits,
+ ...chengduSurrenderRecruitUnits,
+ ...hanzhongOpeningRecruitUnits,
+ ...hanzhongScoutRecruitUnits,
+ ...hanzhongMainRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, thirtySeventhBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('hanzhong-scout-a', '조조 척후', '서곡 척후', 'bandit', 76, 254, 88, 21, 49),
+ createThirtyFirstEnemyUnit('hanzhong-scout-b', '조조 척후', '강변 척후', 'bandit', 76, 254, 88, 30, 47),
+ createThirtyFirstEnemyUnit('hanzhong-scout-c', '조조 척후', '북로 척후', 'bandit', 77, 262, 90, 40, 45),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-a', '조조 보병', '서곡 보병', 'yellowTurban', 77, 386, 110, 36, 42),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-b', '조조 보병', '중로 보병', 'yellowTurban', 77, 386, 110, 42, 40),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-c', '한중 보병', '능선 보병', 'yellowTurban', 78, 398, 114, 49, 37),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-d', '한중 보병', '창고 보병', 'yellowTurban', 78, 398, 114, 56, 34),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-e', '조조 보병', '본진 보병', 'yellowTurban', 79, 410, 116, 64, 29),
+ createThirtyFirstEnemyUnit('hanzhong-infantry-f', '조조 보병', '퇴로 보병', 'yellowTurban', 79, 410, 116, 69, 24),
+ createThirtyFirstEnemyUnit('hanzhong-archer-a', '조조 궁병', '서벽 궁병', 'archer', 77, 270, 92, 39, 39),
+ createThirtyFirstEnemyUnit('hanzhong-archer-b', '조조 궁병', '능선 궁병', 'archer', 78, 278, 94, 51, 31),
+ createThirtyFirstEnemyUnit('hanzhong-archer-c', '조조 궁병', '고지 궁병', 'archer', 78, 278, 94, 60, 26),
+ createThirtyFirstEnemyUnit('hanzhong-archer-d', '조조 궁병', '본진 궁병', 'archer', 79, 286, 96, 68, 22),
+ createThirtyFirstEnemyUnit('hanzhong-archer-e', '조조 궁병', '퇴로 궁병', 'archer', 79, 286, 96, 70, 31),
+ createThirtyFirstEnemyUnit('hanzhong-cavalry-a', '조조 기병', '좌익 기병', 'cavalry', 78, 410, 116, 31, 50),
+ createThirtyFirstEnemyUnit('hanzhong-cavalry-b', '조조 기병', '우익 기병', 'cavalry', 78, 410, 116, 47, 46),
+ createThirtyFirstEnemyUnit('hanzhong-cavalry-c', '조조 기병', '돌격 기병', 'cavalry', 79, 422, 118, 59, 40),
+ createThirtyFirstEnemyUnit('hanzhong-cavalry-d', '조조 기병', '친위 기병', 'cavalry', 79, 422, 118, 66, 36),
+ createThirtyFirstEnemyUnit('hanzhong-cavalry-e', '조조 기병', '북로 기병', 'cavalry', 80, 434, 120, 71, 28),
+ createThirtyFirstEnemyUnit('hanzhong-guard-a', '조조 친위대', '능선 친위대', 'yellowTurban', 80, 444, 122, 57, 31),
+ createThirtyFirstEnemyUnit('hanzhong-guard-b', '조조 친위대', '본진 친위대', 'yellowTurban', 80, 444, 122, 65, 27),
+ createThirtyFirstEnemyUnit('hanzhong-guard-c', '조조 친위대', '퇴로 친위대', 'yellowTurban', 81, 456, 124, 70, 20),
+ createThirtyFirstEnemyUnit('hanzhong-strategist-a', '조조 책사', '능선 책사', 'strategist', 78, 278, 96, 54, 27),
+ createThirtyFirstEnemyUnit('hanzhong-strategist-b', '조조 책사', '본진 책사', 'strategist', 79, 286, 98, 66, 24),
+ createThirtyFirstEnemyUnit('hanzhong-strategist-c', '조조 책사', '퇴로 책사', 'strategist', 80, 294, 100, 71, 18),
+ createThirtyFirstEnemyUnit('hanzhong-ambusher-a', '조조 복병', '남령 복병', 'bandit', 77, 264, 92, 43, 49),
+ createThirtyFirstEnemyUnit('hanzhong-ambusher-b', '조조 복병', '동령 복병', 'bandit', 77, 264, 92, 56, 43),
+ createThirtyFirstEnemyUnit('hanzhong-officer-zhang-he', '장합', '조조 선봉장', 'cavalry', 81, 462, 126, 62, 31),
+ createThirtyFirstEnemyUnit('hanzhong-officer-xu-huang', '서황', '조조 우장군', 'rebelLeader', 82, 474, 128, 67, 26),
+ createThirtyFirstEnemyUnit('hanzhong-leader-cao-cao', '조조', '위왕', 'rebelLeader', 84, 520, 138, 70, 18)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -14316,6 +14463,33 @@ export const hanzhongMainRecruitBonds: BattleBond[] = [
}
];
+export const hanzhongDecisiveBonds: BattleBond[] = [
+ {
+ id: 'liu-bei__wang-ping',
+ unitIds: ['liu-bei', 'wang-ping'],
+ title: '한중의 새 주인',
+ level: 25,
+ exp: 0,
+ description: '유비의 명분과 왕평의 산길 판단이 맞물려 한중 병사들이 새 깃발을 받아들이는 속도를 높입니다.'
+ },
+ {
+ id: 'fa-zheng__wang-ping',
+ unitIds: ['fa-zheng', 'wang-ping'],
+ title: '산길 계책',
+ level: 27,
+ exp: 0,
+ description: '법정의 유인책과 왕평의 실제 길 지식이 합쳐져 적의 보급로와 퇴로를 동시에 흔듭니다.'
+ },
+ {
+ id: 'huang-zhong__fa-zheng',
+ unitIds: ['huang-zhong', 'fa-zheng'],
+ title: '정군산의 여세',
+ level: 32,
+ exp: 0,
+ description: '황충의 고지 돌파와 법정의 결단이 다시 맞물리며 한중 결전의 첫 압박을 강하게 만듭니다.'
+ }
+];
+
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -14367,6 +14541,11 @@ export const thirtyFifthBattleBonds: BattleBond[] = [...thirtyFourthBattleBonds,
export const thirtySixthBattleBonds: BattleBond[] = [...thirtyFifthBattleBonds, ...hanzhongScoutRecruitBonds].map(
cloneBattleBondForScenario
);
+export const thirtySeventhBattleBonds: BattleBond[] = [
+ ...thirtySixthBattleBonds,
+ ...hanzhongMainRecruitBonds,
+ ...hanzhongDecisiveBonds
+].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -15973,6 +16152,78 @@ function createThirtySixthBattleTerrain(): TerrainType[][] {
);
}
+function createThirtySeventhBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 56 }, (_, y) =>
+ Array.from({ length: 74 }, (_, x): TerrainType => {
+ if (x <= 4 && y >= 49) {
+ return 'camp';
+ }
+ if (
+ (x >= 56 && x <= 72 && y >= 9 && y <= 30) ||
+ (x >= 50 && x <= 64 && y >= 6 && y <= 19) ||
+ (x >= 61 && x <= 73 && y >= 24 && y <= 39) ||
+ (x >= 64 && x <= 72 && y >= 16 && y <= 23)
+ ) {
+ return 'fort';
+ }
+ if (
+ (x >= 18 && x <= 22 && y >= 47 && y <= 50) ||
+ (x >= 40 && x <= 44 && y >= 40 && y <= 43) ||
+ (x >= 53 && x <= 56 && y >= 34 && y <= 37) ||
+ (x >= 66 && x <= 69 && y >= 36 && y <= 39)
+ ) {
+ return 'village';
+ }
+ if (
+ (y >= 51 && x >= 4 && x <= 24) ||
+ (x >= 23 && x <= 38 && y >= 46 && y <= 51) ||
+ (x >= 37 && x <= 52 && y >= 41 && y <= 46) ||
+ (x >= 51 && x <= 64 && y >= 35 && y <= 41) ||
+ (x >= 63 && x <= 72 && y >= 29 && y <= 35) ||
+ (x >= 67 && x <= 73 && y >= 22 && y <= 29) ||
+ (x >= 58 && x <= 72 && y >= 16 && y <= 20)
+ ) {
+ return 'road';
+ }
+ if ((x === 17 || x === 18) && y >= 0 && y <= 45) {
+ return 'river';
+ }
+ if ((x === 42 || x === 43) && y >= 4 && y <= 39) {
+ return 'river';
+ }
+ if ((x === 54 || x === 55) && y >= 39 && y <= 55) {
+ return 'river';
+ }
+ if (
+ (x >= 4 && x <= 25 && y >= 8 && y <= 39) ||
+ (x >= 27 && x <= 40 && y >= 16 && y <= 41) ||
+ (x >= 45 && x <= 59 && y >= 42 && y <= 55) ||
+ (x >= 57 && x <= 73 && y >= 39 && y <= 54)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 16 && y <= 24) ||
+ (x >= 26 && x <= 41 && y <= 15) ||
+ (x >= 42 && x <= 60 && y >= 1 && y <= 18) ||
+ (x >= 62 && x <= 73 && y <= 15) ||
+ (x >= 30 && x <= 46 && y >= 48 && y <= 55)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 48) ||
+ (x >= 71 && y >= 35) ||
+ (x >= 55 && x <= 58 && y <= 8) ||
+ (x >= 25 && x <= 30 && y >= 25 && y <= 38)
+ ) {
+ 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 3e23f60..22a0029 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -555,7 +555,37 @@ const unitTexture: Record = {
'dingjun-ambusher-b': 'unit-rebel',
'dingjun-officer-zhang-he': 'unit-rebel-cavalry',
'dingjun-officer-wang-ping': 'unit-rebel',
- 'dingjun-leader-xiahou-yuan': 'unit-rebel-leader'
+ 'dingjun-leader-xiahou-yuan': 'unit-rebel-leader',
+ 'hanzhong-scout-a': 'unit-rebel',
+ 'hanzhong-scout-b': 'unit-rebel',
+ 'hanzhong-scout-c': 'unit-rebel',
+ 'hanzhong-infantry-a': 'unit-rebel',
+ 'hanzhong-infantry-b': 'unit-rebel',
+ 'hanzhong-infantry-c': 'unit-rebel',
+ 'hanzhong-infantry-d': 'unit-rebel',
+ 'hanzhong-infantry-e': 'unit-rebel',
+ 'hanzhong-infantry-f': 'unit-rebel',
+ 'hanzhong-archer-a': 'unit-rebel-archer',
+ 'hanzhong-archer-b': 'unit-rebel-archer',
+ 'hanzhong-archer-c': 'unit-rebel-archer',
+ 'hanzhong-archer-d': 'unit-rebel-archer',
+ 'hanzhong-archer-e': 'unit-rebel-archer',
+ 'hanzhong-cavalry-a': 'unit-rebel-cavalry',
+ 'hanzhong-cavalry-b': 'unit-rebel-cavalry',
+ 'hanzhong-cavalry-c': 'unit-rebel-cavalry',
+ 'hanzhong-cavalry-d': 'unit-rebel-cavalry',
+ 'hanzhong-cavalry-e': 'unit-rebel-cavalry',
+ 'hanzhong-guard-a': 'unit-rebel',
+ 'hanzhong-guard-b': 'unit-rebel',
+ 'hanzhong-guard-c': 'unit-rebel',
+ 'hanzhong-strategist-a': 'unit-rebel-archer',
+ 'hanzhong-strategist-b': 'unit-rebel-archer',
+ 'hanzhong-strategist-c': 'unit-rebel-archer',
+ 'hanzhong-ambusher-a': 'unit-rebel',
+ 'hanzhong-ambusher-b': 'unit-rebel',
+ 'hanzhong-officer-zhang-he': 'unit-rebel-cavalry',
+ 'hanzhong-officer-xu-huang': 'unit-rebel-leader',
+ 'hanzhong-leader-cao-cao': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -1568,7 +1598,37 @@ const enemyAiByUnitId: Record = {
'dingjun-ambusher-b': 'aggressive',
'dingjun-officer-zhang-he': 'aggressive',
'dingjun-officer-wang-ping': 'guard',
- 'dingjun-leader-xiahou-yuan': 'guard'
+ 'dingjun-leader-xiahou-yuan': 'guard',
+ 'hanzhong-scout-a': 'aggressive',
+ 'hanzhong-scout-b': 'aggressive',
+ 'hanzhong-scout-c': 'aggressive',
+ 'hanzhong-infantry-a': 'guard',
+ 'hanzhong-infantry-b': 'guard',
+ 'hanzhong-infantry-c': 'guard',
+ 'hanzhong-infantry-d': 'guard',
+ 'hanzhong-infantry-e': 'guard',
+ 'hanzhong-infantry-f': 'guard',
+ 'hanzhong-archer-a': 'hold',
+ 'hanzhong-archer-b': 'hold',
+ 'hanzhong-archer-c': 'hold',
+ 'hanzhong-archer-d': 'hold',
+ 'hanzhong-archer-e': 'hold',
+ 'hanzhong-cavalry-a': 'aggressive',
+ 'hanzhong-cavalry-b': 'aggressive',
+ 'hanzhong-cavalry-c': 'aggressive',
+ 'hanzhong-cavalry-d': 'aggressive',
+ 'hanzhong-cavalry-e': 'aggressive',
+ 'hanzhong-guard-a': 'guard',
+ 'hanzhong-guard-b': 'guard',
+ 'hanzhong-guard-c': 'guard',
+ 'hanzhong-strategist-a': 'hold',
+ 'hanzhong-strategist-b': 'hold',
+ 'hanzhong-strategist-c': 'hold',
+ 'hanzhong-ambusher-a': 'aggressive',
+ 'hanzhong-ambusher-b': 'aggressive',
+ 'hanzhong-officer-zhang-he': 'aggressive',
+ 'hanzhong-officer-xu-huang': 'guard',
+ 'hanzhong-leader-cao-cao': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 08ac2e4..a772031 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -22,6 +22,7 @@ import thirtyFirstBattleMapUrl from '../../assets/images/battle/thirty-first-bat
import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-battle-map.svg';
import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-battle-map.svg';
+import thirtySeventhBattleMapUrl from '../../assets/images/battle/thirty-seventh-battle-map.svg';
import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
@@ -130,6 +131,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirty-fourth', thirtyFourthBattleMapUrl);
this.load.image('battle-map-thirty-fifth', thirtyFifthBattleMapUrl);
this.load.image('battle-map-thirty-sixth', thirtySixthBattleMapUrl);
+ this.load.image('battle-map-thirty-seventh', thirtySeventhBattleMapUrl);
this.load.image('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 7a2fab8..43ef3c4 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -19,6 +19,7 @@ import {
fuPassRecruitUnits,
guiyangRecruitBonds,
guiyangRecruitUnits,
+ hanzhongDecisiveBonds,
hanzhongOpeningRecruitBonds,
hanzhongOpeningRecruitUnits,
hanzhongMainRecruitBonds,
@@ -346,7 +347,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
battleIds: [
'thirty-fourth-battle-jiameng-pass',
'thirty-fifth-battle-yangping-scout',
- 'thirty-sixth-battle-dingjun-vanguard'
+ 'thirty-sixth-battle-dingjun-vanguard',
+ 'thirty-seventh-battle-hanzhong-decisive'
],
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
}
@@ -388,7 +390,8 @@ const campBattleIds = {
thirtyThird: 'thirty-third-battle-chengdu-surrender',
thirtyFourth: 'thirty-fourth-battle-jiameng-pass',
thirtyFifth: 'thirty-fifth-battle-yangping-scout',
- thirtySixth: 'thirty-sixth-battle-dingjun-vanguard'
+ thirtySixth: 'thirty-sixth-battle-dingjun-vanguard',
+ thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -795,6 +798,19 @@ const sortieRulesByBattleId: Partial