diff --git a/docs/roadmap.md b/docs/roadmap.md
index d837f60..bba3112 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -63,16 +63,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Shu-Han foundation story milestone, where the King of Hanzhong council advances into Liu Bei's formal state-building scene and points the next arc toward Guan Yu's Jing Province defense
- Thirty-eighth battle Jing Province defense vanguard route, using a twenty-two-officer roster but limiting sortie to seven officers so Guan Yu's front, Ma Liang's local judgment, Mi Zhu's supply role, and Wu/Wei pressure become a deliberate pre-battle choice
- Thirty-ninth battle Fan Castle outer-front route, locking Liu Bei and Guan Yu as required sortie officers while the player chooses five support officers from the twenty-two-officer roster to pressure Pang De, Cao Ren, and Yu Jin
+- Fortieth battle Han River flood route, using Guan Yu's water-control plan across an 80x62 flooded battlefield to break Yu Jin's seven armies and open the Fan Castle siege setup
- 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-ninth battle victory, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the fortieth battle victory, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River camp states, campaign timeline state, and camp save state
## Next Steps
-1. Build the Han River flood and Seven Armies setup, using Guan Yu's northward pressure, Yu Jin's main force, Pang De's resolve, and the water-control plan as the next campaign tension
-2. Expand Fan Castle inter-battle events, including water-level scouting, siege logistics, bond choices, and supply decisions that affect sortie recommendations
+1. Build the Fan Castle siege follow-up, using the aftermath of Yu Jin's defeat, Pang De's resolve, Cao Ren's defense, and Wu's growing shadow as the next campaign tension
+2. Expand Han River aftermath events, including prisoner handling, dike repair, siege logistics, bond choices, and supply decisions that affect sortie recommendations
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 b69f78e..0141028 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 !== 39 ||
+ progressTabState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 40 ||
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 !== 39 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 40 ||
postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' ||
postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전'
@@ -4535,7 +4535,7 @@ try {
if (
postDingjunProgressState?.activeTab !== 'progress' ||
postDingjunProgressState.campaignProgress?.completedKnown !== 36 ||
- postDingjunProgressState.campaignProgress?.totalKnown !== 39 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 40 ||
postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' ||
postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전'
@@ -4688,7 +4688,7 @@ try {
if (
postHanzhongProgressState?.activeTab !== 'progress' ||
postHanzhongProgressState.campaignProgress?.completedKnown !== 37 ||
- postHanzhongProgressState.campaignProgress?.totalKnown !== 39 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 40 ||
postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비'
@@ -4749,7 +4749,7 @@ try {
if (
postKingCouncilProgressState?.activeTab !== 'progress' ||
postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 ||
- postKingCouncilProgressState.campaignProgress?.totalKnown !== 39 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 40 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4809,7 +4809,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 39 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 40 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4960,7 +4960,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 39 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 40 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5113,14 +5113,168 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 39 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 40 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
- postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
- throw new Error(`Expected post-Fan Castle progress tab to complete the thirty-ninth battle and leave Han River flood as the next prepared slice: ${JSON.stringify(postFanCastleProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Fan Castle progress tab to complete the thirty-ninth battle and point to Han River flood: ${JSON.stringify(postFanCastleProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-fan-castle-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const hanRiverSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanRiverSortieState?.sortieVisible ||
+ hanRiverSortieState.campaign?.step !== 'thirty-ninth-camp' ||
+ hanRiverSortieState.sortiePlan?.objectiveLine !== '한수 수공 · 우금 본대 격파' ||
+ hanRiverSortieState.sortiePlan?.maxCount !== 7 ||
+ hanRiverSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !hanRiverSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
+ ) {
+ throw new Error(`Expected Han River sortie prep to lock Guan Yu and expose a seven-officer water-control choice: ${JSON.stringify(hanRiverSortieState)}`);
+ }
+ assertSortieTacticalRoster(hanRiverSortieState, [
+ '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 hanRiverPriorityUnits = ['fa-zheng', 'ma-liang', 'huang-quan', 'zhao-yun', 'ma-chao'];
+ for (const unitId of hanRiverPriorityUnits) {
+ const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
+ const removable = currentSortieState.sortieRoster?.find(
+ (unit) => unit.selected && !unit.required && !hanRiverPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const hanRiverSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !hanRiverPriorityUnits.every((unitId) =>
+ hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
+ !hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
+ hanRiverSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ hanRiverSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected Han River sortie to deploy Liu Bei, Guan Yu, Fa Zheng, Ma Liang, Huang Quan, Zhao Yun, and Ma Chao: ${JSON.stringify(hanRiverSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-han-river-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-han-river-story.png', fullPage: true });
+
+ for (let i = 0; i < 45; i += 1) {
+ const enteredFortiethBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'fortieth-battle-han-river-flood';
+ });
+ if (enteredFortiethBattle) {
+ 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 === 'fortieth-battle-han-river-flood' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-fortieth-battle.png', fullPage: true });
+
+ const fortiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const fortiethEnemies = fortiethBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const fortiethAllies = fortiethBattleState.units.filter((unit) => unit.faction === 'ally');
+ const fortiethEnemyBehaviors = new Set(fortiethEnemies.map((unit) => unit.ai));
+ if (
+ fortiethBattleState.camera?.mapWidth !== 80 ||
+ fortiethBattleState.camera?.mapHeight !== 62 ||
+ fortiethBattleState.victoryConditionLabel !== '우금 본대 격파' ||
+ fortiethEnemies.length < 30 ||
+ !fortiethEnemyBehaviors.has('aggressive') ||
+ !fortiethEnemyBehaviors.has('guard') ||
+ !fortiethEnemyBehaviors.has('hold') ||
+ !fortiethEnemies.some((unit) => unit.id === 'han-river-leader-yu-jin') ||
+ !fortiethEnemies.some((unit) => unit.id === 'han-river-officer-pang-de') ||
+ !fortiethEnemies.some((unit) => unit.id === 'han-river-officer-cao-ren') ||
+ !fortiethAllies.some((unit) => unit.id === 'guan-yu') ||
+ !fortiethAllies.some((unit) => unit.id === 'fa-zheng') ||
+ !fortiethAllies.some((unit) => unit.id === 'ma-liang') ||
+ !fortiethAllies.some((unit) => unit.id === 'huang-quan') ||
+ !fortiethAllies.some((unit) => unit.id === 'zhao-yun')
+ ) {
+ throw new Error(`Expected fortieth battle to use Han River flood map, Yu Jin objective, selected allied officers, and mixed flood AI: ${JSON.stringify(fortiethBattleState)}`);
+ }
+
+ 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 fortiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ fortiethCampState?.campBattleId !== 'fortieth-battle-han-river-flood' ||
+ fortiethCampState.campTitle !== '한수 수공 후 군영' ||
+ fortiethCampState.availableDialogueIds?.length !== 3 ||
+ !fortiethCampState.availableDialogueIds.every((id) => id.endsWith('han-river')) ||
+ fortiethCampState.availableVisitIds?.length !== 2 ||
+ !fortiethCampState.availableVisitIds.every((id) => id.includes('han-river')) ||
+ fortiethCampState.rosterCollection?.total < 22
+ ) {
+ throw new Error(`Expected fortieth camp to expose Han River dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortiethCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-han-river-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postHanRiverProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postHanRiverProgressState?.activeTab !== 'progress' ||
+ postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 40 ||
+ postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
+ postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Han River progress tab to complete the fortieth battle and leave Fan Castle siege as the next prepared slice: ${JSON.stringify(postHanRiverProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-han-river-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -5153,10 +5307,10 @@ try {
camp: window.__HEROS_DEBUG__?.camp?.() ?? null
}));
if (!titleContinueState.activeScenes.includes('CampScene')) {
- throw new Error(`Expected title continue to reopen the latest camp after Fan Castle: ${JSON.stringify(titleContinueState)}`);
+ throw new Error(`Expected title continue to reopen the latest camp after Han River flood: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-thirty-ninth-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, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, and Fan Castle vanguard state at ${targetUrl}`);
+ console.log(`Verified title-to-fortieth-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, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, and Han River flood state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/fortieth-battle-map.svg b/src/assets/images/battle/fortieth-battle-map.svg
new file mode 100644
index 0000000..5b6ba7c
--- /dev/null
+++ b/src/assets/images/battle/fortieth-battle-map.svg
@@ -0,0 +1,102 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 285ecfc..c1e9d30 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -20,6 +20,10 @@ import {
eleventhBattleMap,
eleventhBattleUnits,
eleventhBattleVictoryPages,
+ fortiethBattleBonds,
+ fortiethBattleMap,
+ fortiethBattleUnits,
+ fortiethBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -200,7 +204,8 @@ export type BattleScenarioId =
| 'thirty-sixth-battle-dingjun-vanguard'
| 'thirty-seventh-battle-hanzhong-decisive'
| 'thirty-eighth-battle-jing-defense'
- | 'thirty-ninth-battle-fan-castle-vanguard';
+ | 'thirty-ninth-battle-fan-castle-vanguard'
+ | 'fortieth-battle-han-river-flood';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2517,6 +2522,69 @@ export const thirtyNinthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const fortiethBattleScenario: BattleScenarioDefinition = {
+ id: 'fortieth-battle-han-river-flood',
+ title: '한수 수공',
+ victoryConditionLabel: '우금 본대 격파',
+ defeatConditionLabel: '유비 또는 관우 퇴각',
+ openingObjectiveLines: [
+ '한수의 수위가 오르며 번성 외곽이 진흙과 물길로 갈라졌습니다. 우금의 본대를 강과 둑 사이에 묶어 칠군을 흔드십시오.',
+ '관우는 이번 전투의 중심입니다. 관우가 전선을 잃으면 수공의 명분과 병사의 사기가 함께 무너집니다.',
+ '우금은 본진을 지키고, 방덕은 기병으로 돌파를 시도합니다. 법정, 마량, 황권 같은 참모와 기동대를 함께 운용하면 물길을 장악하기 쉽습니다.'
+ ],
+ map: fortiethBattleMap,
+ units: fortiethBattleUnits,
+ bonds: fortiethBattleBonds,
+ mapTextureKey: 'battle-map-fortieth',
+ leaderUnitId: 'han-river-leader-yu-jin',
+ quickVictoryTurnLimit: 64,
+ baseVictoryGold: 7920,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '우금 본대 격파',
+ rewardGold: 5060,
+ unitId: 'han-river-leader-yu-jin'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 1240,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'guan-yu',
+ kind: 'keep-unit-alive',
+ label: '관우 생존',
+ rewardGold: 1960,
+ unitId: 'guan-yu'
+ },
+ {
+ id: 'river-bank',
+ kind: 'secure-terrain',
+ label: '한수 둑길 확보',
+ rewardGold: 2440,
+ terrain: 'road'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '64턴 이내 승리',
+ rewardGold: 1800,
+ maxTurn: 64
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'guan-yu' }
+ ],
+ itemRewards: ['콩 36', '상처약 26', '탁주 11', '한수 수위표 1', '칠군 항복문 1'],
+ victoryPages: fortiethBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -2558,7 +2626,8 @@ export const battleScenarios: Record
'thirty-sixth-battle-dingjun-vanguard': thirtySixthBattleScenario,
'thirty-seventh-battle-hanzhong-decisive': thirtySeventhBattleScenario,
'thirty-eighth-battle-jing-defense': thirtyEighthBattleScenario,
- 'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario
+ 'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario,
+ 'fortieth-battle-han-river-flood': fortiethBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 5d388f1..b36bd50 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -4,6 +4,7 @@ import {
eighthBattleScenario,
eighteenthBattleScenario,
eleventhBattleScenario,
+ fortiethBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -48,6 +49,8 @@ import {
eighteenthBattleVictoryPages,
eleventhBattleIntroPages,
eleventhBattleVictoryPages,
+ fortiethBattleIntroPages,
+ fortiethBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fifteenthBattleIntroPages,
@@ -566,13 +569,24 @@ const sortieFlows: Record = {
},
[thirtyNinthBattleScenario.id]: {
afterBattleId: thirtyNinthBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '한수 수공 준비',
+ eyebrow: '다음 전장',
+ title: fortiethBattleScenario.title,
description:
- '번성 외곽의 보루를 눌렀지만 조인과 우금의 본대는 아직 성 안과 수로를 지키고 있습니다. 다음 장에서는 한수의 물길, 둑, 성벽을 함께 다루는 더 큰 전장이 이어집니다.',
- rewardHint: '다음 장: 한수 수공과 칠군 침수 준비 중',
- pages: thirtyNinthBattleVictoryPages,
- unavailableNotice: '한수 수공과 칠군 침수 전장은 다음 작업에서 이어집니다. 지금은 군영에서 무장 성장, 공명도, 보급을 정비할 수 있습니다.'
+ '번성 외곽의 보루를 눌렀지만 조인과 우금의 본대는 아직 성 안과 수로를 지키고 있습니다. 다음 출진에서는 한수의 물길을 장악해 칠군을 흔들고 번성 포위의 결정적 발판을 마련합니다.',
+ rewardHint: `예상 보상: ${fortiethBattleScenario.title} 개방 / 칠군 항복문`,
+ nextBattleId: fortiethBattleScenario.id,
+ campaignStep: 'fortieth-battle',
+ pages: [...thirtyNinthBattleVictoryPages, ...fortiethBattleIntroPages]
+ },
+ [fortiethBattleScenario.id]: {
+ afterBattleId: fortiethBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '번성 포위 준비',
+ description:
+ '한수의 물길이 조조군을 무너뜨렸지만 번성의 성문은 아직 열리지 않았습니다. 다음 장에서는 수공 이후의 포위전, 방덕의 결말, 오와 위의 시선이 더 날카롭게 이어집니다.',
+ rewardHint: '다음 장: 번성 포위와 형주 위기 준비 중',
+ pages: fortiethBattleVictoryPages,
+ unavailableNotice: '번성 포위전과 형주 위기는 다음 작업에서 이어집니다. 지금은 군영에서 물길 전투의 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 0c9f588..e21304c 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -2589,6 +2589,70 @@ export const thirtyNinthBattleVictoryPages: StoryPage[] = [
}
];
+export const fortiethBattleIntroPages: StoryPage[] = [
+ {
+ id: 'fortieth-han-river-rain',
+ bgm: 'story-dark',
+ chapter: '불어난 한수',
+ background: 'story-militia',
+ speaker: '제갈량',
+ text: '비가 이어지며 한수의 물길이 불어났습니다. 번성 밖의 평지는 진흙으로 변했고, 우금의 본대와 방덕의 선봉은 강과 둑 사이에 길게 늘어섰습니다.'
+ },
+ {
+ id: 'fortieth-water-plan',
+ bgm: 'battle-prep',
+ chapter: '수공의 군령',
+ background: 'story-liu-bei',
+ speaker: '법정',
+ text: '둑을 무너뜨리는 것만이 수공은 아닙니다. 물이 차오를 때 적이 믿는 퇴로를 먼저 끊고, 궁병과 기병을 높은 둑 위로 몰아야 합니다.'
+ },
+ {
+ id: 'fortieth-guan-yu-command',
+ bgm: 'battle-prep',
+ chapter: '관우의 결단',
+ background: 'story-three-heroes',
+ speaker: '관우',
+ portrait: 'guanYu',
+ text: '물길이 우리 편에 선다면 적의 칼도 무뎌질 것이오. 우금의 본대를 눌러 번성의 숨통을 끊고, 방덕의 돌격은 둑 위에서 맞받아치겠소.'
+ },
+ {
+ id: 'fortieth-sortie',
+ bgm: 'battle-prep',
+ chapter: '한수 수공',
+ background: 'story-sortie',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '관우가 앞을 열고, 법정과 마량이 물길을 살피며, 기동대는 둑과 나루를 끊어라. 이번 싸움은 성벽보다 먼저 강을 얻는 싸움이다.'
+ }
+];
+
+export const fortiethBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'fortieth-victory-seven-armies',
+ bgm: 'militia-theme',
+ chapter: '칠군이 물에 잠기다',
+ background: 'story-sortie',
+ text: '한수의 물길이 전장을 덮치자 조조군의 진형은 무너졌습니다. 우금의 본대는 퇴로를 잃었고, 번성 외곽에는 관우군의 깃발이 더욱 가까워졌습니다.'
+ },
+ {
+ id: 'fortieth-yu-jin-surrenders',
+ bgm: 'battle-prep',
+ chapter: '우금의 항복',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '우금은 병사들을 살리기 위해 무기를 내려놓았습니다. 그러나 방덕은 끝까지 물러서지 않았고, 번성 안의 조인은 더 깊은 수성전을 준비하기 시작했습니다.'
+ },
+ {
+ id: 'fortieth-next-siege',
+ bgm: 'story-dark',
+ chapter: '번성 포위',
+ background: 'story-militia',
+ speaker: '관우',
+ portrait: 'guanYu',
+ text: '강은 길을 열었으나 성문은 아직 남아 있소. 병사들에게 쉬게 하되 경계를 늦추지 마시오. 다음은 번성의 성벽을 직접 마주할 차례요.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -2968,6 +3032,12 @@ export const thirtyNinthBattleMap: BattleMap = {
terrain: createThirtyNinthBattleTerrain()
};
+export const fortiethBattleMap: BattleMap = {
+ width: 80,
+ height: 62,
+ terrain: createFortiethBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -14347,6 +14417,84 @@ export const thirtyNinthBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('fan-castle-leader-pang-de', '방덕', '번성 선봉장', 'cavalry', 92, 566, 150, 71, 31)
];
+const fortiethBattleAllyPositions: Record = {
+ 'liu-bei': { x: 5, y: 56 },
+ 'guan-yu': { x: 11, y: 51 },
+ 'zhang-fei': { x: 6, y: 58 },
+ 'jian-yong': { x: 4, y: 59 },
+ 'mi-zhu': { x: 8, y: 57 },
+ 'sun-qian': { x: 9, y: 60 },
+ 'zhao-yun': { x: 14, y: 54 },
+ 'zhuge-liang': { x: 9, y: 53 },
+ 'ma-liang': { x: 13, y: 56 },
+ 'yi-ji': { x: 15, y: 59 },
+ 'gong-zhi': { x: 16, y: 57 },
+ 'huang-zhong': { x: 17, y: 52 },
+ 'wei-yan': { x: 15, y: 50 },
+ 'pang-tong': { x: 12, y: 54 },
+ 'fa-zheng': { x: 18, y: 56 },
+ 'wu-yi': { x: 19, y: 53 },
+ 'yan-yan': { x: 20, y: 55 },
+ 'li-yan': { x: 18, y: 50 },
+ 'huang-quan': { x: 21, y: 57 },
+ 'ma-chao': { x: 22, y: 51 },
+ 'ma-dai': { x: 23, y: 53 },
+ 'wang-ping': { x: 21, y: 49 }
+};
+
+export const fortiethBattleUnits: 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, fortiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('han-river-scout-a', '우금군 척후', '둑길 척후', 'bandit', 88, 326, 108, 30, 53),
+ createThirtyFirstEnemyUnit('han-river-scout-b', '우금군 척후', '수문 척후', 'bandit', 88, 326, 108, 38, 50),
+ createThirtyFirstEnemyUnit('han-river-scout-c', '우금군 척후', '상류 척후', 'bandit', 89, 334, 110, 55, 44),
+ createThirtyFirstEnemyUnit('han-river-infantry-a', '우금군 보병', '하류 보병', 'yellowTurban', 89, 492, 138, 42, 46),
+ createThirtyFirstEnemyUnit('han-river-infantry-b', '우금군 보병', '둑길 보병', 'yellowTurban', 89, 492, 138, 50, 43),
+ createThirtyFirstEnemyUnit('han-river-infantry-c', '우금군 보병', '중앙 보병', 'yellowTurban', 90, 504, 140, 59, 37),
+ createThirtyFirstEnemyUnit('han-river-infantry-d', '우금군 보병', '수로 보병', 'yellowTurban', 90, 504, 140, 66, 33),
+ createThirtyFirstEnemyUnit('han-river-infantry-e', '우금군 보병', '상류 보병', 'yellowTurban', 91, 516, 142, 72, 27),
+ createThirtyFirstEnemyUnit('han-river-archer-a', '우금군 궁병', '제방 궁병', 'archer', 89, 346, 116, 45, 48),
+ createThirtyFirstEnemyUnit('han-river-archer-b', '우금군 궁병', '나루 궁병', 'archer', 90, 354, 118, 55, 40),
+ createThirtyFirstEnemyUnit('han-river-archer-c', '우금군 궁병', '중앙 궁병', 'archer', 90, 354, 118, 64, 35),
+ createThirtyFirstEnemyUnit('han-river-archer-d', '우금군 궁병', '성벽 궁병', 'archer', 91, 362, 120, 73, 29),
+ createThirtyFirstEnemyUnit('han-river-archer-e', '우금군 궁병', '상류 궁병', 'archer', 91, 362, 120, 75, 21),
+ createThirtyFirstEnemyUnit('han-river-cavalry-a', '방덕 기병', '좌익 기병', 'cavalry', 90, 504, 142, 34, 54),
+ createThirtyFirstEnemyUnit('han-river-cavalry-b', '방덕 기병', '둑길 기병', 'cavalry', 90, 504, 142, 49, 49),
+ createThirtyFirstEnemyUnit('han-river-cavalry-c', '방덕 기병', '중앙 기병', 'cavalry', 91, 516, 144, 60, 43),
+ createThirtyFirstEnemyUnit('han-river-cavalry-d', '방덕 기병', '상류 기병', 'cavalry', 91, 516, 144, 70, 37),
+ createThirtyFirstEnemyUnit('han-river-guard-a', '우금군 친위대', '수문 친위대', 'yellowTurban', 92, 548, 148, 61, 34),
+ createThirtyFirstEnemyUnit('han-river-guard-b', '우금군 친위대', '본진 친위대', 'yellowTurban', 92, 548, 148, 68, 30),
+ createThirtyFirstEnemyUnit('han-river-guard-c', '우금군 친위대', '우금 친위대', 'yellowTurban', 93, 560, 150, 72, 24),
+ createThirtyFirstEnemyUnit('han-river-guard-d', '우금군 친위대', '성문 친위대', 'yellowTurban', 93, 560, 150, 76, 18),
+ createThirtyFirstEnemyUnit('han-river-strategist-a', '조조군 책사', '수위 책사', 'strategist', 90, 362, 122, 57, 41),
+ createThirtyFirstEnemyUnit('han-river-strategist-b', '조조군 책사', '본진 책사', 'strategist', 91, 370, 124, 68, 32),
+ createThirtyFirstEnemyUnit('han-river-strategist-c', '조조군 책사', '상류 책사', 'strategist', 92, 378, 126, 74, 23),
+ createThirtyFirstEnemyUnit('han-river-ambusher-a', '둑길 복병', '진흙 복병', 'bandit', 89, 336, 112, 40, 55),
+ createThirtyFirstEnemyUnit('han-river-ambusher-b', '둑길 복병', '제방 복병', 'bandit', 90, 344, 114, 54, 47),
+ createThirtyFirstEnemyUnit('han-river-ambusher-c', '둑길 복병', '나루 복병', 'bandit', 90, 344, 114, 66, 42),
+ createThirtyFirstEnemyUnit('han-river-officer-pang-de', '방덕', '조조군 선봉장', 'cavalry', 93, 584, 154, 72, 34),
+ createThirtyFirstEnemyUnit('han-river-officer-cao-ren', '조인', '번성 수비장', 'rebelLeader', 94, 588, 154, 76, 19),
+ createThirtyFirstEnemyUnit('han-river-leader-yu-jin', '우금', '칠군 총대장', 'rebelLeader', 95, 612, 158, 73, 26)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -14906,6 +15054,25 @@ export const fanCastleBonds: BattleBond[] = [
}
];
+export const hanRiverFloodBonds: BattleBond[] = [
+ {
+ id: 'guan-yu__huang-quan',
+ unitIds: ['guan-yu', 'huang-quan'],
+ title: '한수 물길 판단',
+ level: 25,
+ exp: 0,
+ description: '관우의 결단과 황권의 신중한 물길 판단이 맞물리면, 강과 둑을 활용한 포위 공격의 안정성이 높아집니다.'
+ },
+ {
+ id: 'guan-yu__li-yan',
+ unitIds: ['guan-yu', 'li-yan'],
+ title: '제방 보급선',
+ level: 24,
+ exp: 0,
+ description: '관우의 전선 압박과 이엄의 보급 관리가 이어지면, 진흙과 물길 사이에서도 병력 회복과 장기전 준비가 단단해집니다.'
+ }
+];
+
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -14968,6 +15135,9 @@ export const thirtyEighthBattleBonds: BattleBond[] = [...thirtySeventhBattleBond
export const thirtyNinthBattleBonds: BattleBond[] = [...thirtyEighthBattleBonds, ...fanCastleBonds].map(
cloneBattleBondForScenario
);
+export const fortiethBattleBonds: BattleBond[] = [...thirtyNinthBattleBonds, ...hanRiverFloodBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -16789,6 +16959,79 @@ function createThirtyNinthBattleTerrain(): TerrainType[][] {
);
}
+function createFortiethBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 62 }, (_, y) =>
+ Array.from({ length: 80 }, (_, x): TerrainType => {
+ if (x <= 6 && y >= 55) {
+ return 'camp';
+ }
+ if (
+ (x >= 62 && x <= 79 && y >= 6 && y <= 22) ||
+ (x >= 55 && x <= 68 && y >= 10 && y <= 24) ||
+ (x >= 68 && x <= 79 && y >= 22 && y <= 39) ||
+ (x >= 70 && x <= 76 && y >= 36 && y <= 45)
+ ) {
+ return 'fort';
+ }
+ if (
+ (x >= 22 && x <= 26 && y >= 52 && y <= 56) ||
+ (x >= 44 && x <= 48 && y >= 45 && y <= 48) ||
+ (x >= 58 && x <= 62 && y >= 38 && y <= 42) ||
+ (x >= 72 && x <= 75 && y >= 42 && y <= 46)
+ ) {
+ return 'village';
+ }
+ if (
+ (x >= 19 && x <= 23 && y >= 0 && y <= 53) ||
+ (x >= 34 && x <= 37 && y >= 13 && y <= 58) ||
+ (x >= 50 && x <= 53 && y >= 4 && y <= 47) ||
+ (x >= 60 && x <= 62 && y >= 41 && y <= 61) ||
+ (y >= 30 && y <= 33 && x >= 22 && x <= 62)
+ ) {
+ return 'river';
+ }
+ if (
+ (y >= 57 && x >= 6 && x <= 29) ||
+ (x >= 28 && x <= 44 && y >= 52 && y <= 57) ||
+ (x >= 43 && x <= 58 && y >= 46 && y <= 52) ||
+ (x >= 57 && x <= 70 && y >= 40 && y <= 46) ||
+ (x >= 69 && x <= 78 && y >= 32 && y <= 40) ||
+ (x >= 72 && x <= 79 && y >= 18 && y <= 32) ||
+ (x >= 58 && x <= 77 && y >= 16 && y <= 20) ||
+ (y >= 26 && y <= 29 && x >= 22 && x <= 72)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 4 && x <= 29 && y >= 11 && y <= 43) ||
+ (x >= 30 && x <= 46 && y >= 19 && y <= 48) ||
+ (x >= 52 && x <= 66 && y >= 46 && y <= 61) ||
+ (x >= 63 && x <= 79 && y >= 42 && y <= 60)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 18 && y <= 28) ||
+ (x >= 28 && x <= 48 && y <= 18) ||
+ (x >= 49 && x <= 67 && y >= 1 && y <= 20) ||
+ (x >= 68 && x <= 79 && y <= 15) ||
+ (x >= 33 && x <= 52 && y >= 54 && y <= 61)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 54) ||
+ (x >= 77 && y >= 37) ||
+ (x >= 60 && x <= 64 && y <= 8) ||
+ (x >= 27 && x <= 33 && y >= 28 && y <= 43)
+ ) {
+ 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 f10fc63..66a4c36 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -645,7 +645,37 @@ const unitTexture: Record = {
'fan-castle-ambusher-c': 'unit-rebel',
'fan-castle-officer-yu-jin': 'unit-rebel-leader',
'fan-castle-officer-cao-ren': 'unit-rebel-leader',
- 'fan-castle-leader-pang-de': 'unit-rebel-cavalry'
+ 'fan-castle-leader-pang-de': 'unit-rebel-cavalry',
+ 'han-river-scout-a': 'unit-rebel',
+ 'han-river-scout-b': 'unit-rebel',
+ 'han-river-scout-c': 'unit-rebel',
+ 'han-river-infantry-a': 'unit-rebel',
+ 'han-river-infantry-b': 'unit-rebel',
+ 'han-river-infantry-c': 'unit-rebel',
+ 'han-river-infantry-d': 'unit-rebel',
+ 'han-river-infantry-e': 'unit-rebel',
+ 'han-river-archer-a': 'unit-rebel-archer',
+ 'han-river-archer-b': 'unit-rebel-archer',
+ 'han-river-archer-c': 'unit-rebel-archer',
+ 'han-river-archer-d': 'unit-rebel-archer',
+ 'han-river-archer-e': 'unit-rebel-archer',
+ 'han-river-cavalry-a': 'unit-rebel-cavalry',
+ 'han-river-cavalry-b': 'unit-rebel-cavalry',
+ 'han-river-cavalry-c': 'unit-rebel-cavalry',
+ 'han-river-cavalry-d': 'unit-rebel-cavalry',
+ 'han-river-guard-a': 'unit-rebel',
+ 'han-river-guard-b': 'unit-rebel',
+ 'han-river-guard-c': 'unit-rebel',
+ 'han-river-guard-d': 'unit-rebel',
+ 'han-river-strategist-a': 'unit-rebel-archer',
+ 'han-river-strategist-b': 'unit-rebel-archer',
+ 'han-river-strategist-c': 'unit-rebel-archer',
+ 'han-river-ambusher-a': 'unit-rebel',
+ 'han-river-ambusher-b': 'unit-rebel',
+ 'han-river-ambusher-c': 'unit-rebel',
+ 'han-river-officer-pang-de': 'unit-rebel-cavalry',
+ 'han-river-officer-cao-ren': 'unit-rebel-leader',
+ 'han-river-leader-yu-jin': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -1748,7 +1778,37 @@ const enemyAiByUnitId: Record = {
'fan-castle-ambusher-c': 'aggressive',
'fan-castle-officer-yu-jin': 'guard',
'fan-castle-officer-cao-ren': 'guard',
- 'fan-castle-leader-pang-de': 'aggressive'
+ 'fan-castle-leader-pang-de': 'aggressive',
+ 'han-river-scout-a': 'aggressive',
+ 'han-river-scout-b': 'aggressive',
+ 'han-river-scout-c': 'aggressive',
+ 'han-river-infantry-a': 'guard',
+ 'han-river-infantry-b': 'guard',
+ 'han-river-infantry-c': 'guard',
+ 'han-river-infantry-d': 'guard',
+ 'han-river-infantry-e': 'guard',
+ 'han-river-archer-a': 'hold',
+ 'han-river-archer-b': 'hold',
+ 'han-river-archer-c': 'hold',
+ 'han-river-archer-d': 'hold',
+ 'han-river-archer-e': 'hold',
+ 'han-river-cavalry-a': 'aggressive',
+ 'han-river-cavalry-b': 'aggressive',
+ 'han-river-cavalry-c': 'aggressive',
+ 'han-river-cavalry-d': 'aggressive',
+ 'han-river-guard-a': 'guard',
+ 'han-river-guard-b': 'guard',
+ 'han-river-guard-c': 'guard',
+ 'han-river-guard-d': 'guard',
+ 'han-river-strategist-a': 'hold',
+ 'han-river-strategist-b': 'hold',
+ 'han-river-strategist-c': 'hold',
+ 'han-river-ambusher-a': 'aggressive',
+ 'han-river-ambusher-b': 'aggressive',
+ 'han-river-ambusher-c': 'aggressive',
+ 'han-river-officer-pang-de': 'aggressive',
+ 'han-river-officer-cao-ren': 'guard',
+ 'han-river-leader-yu-jin': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 0673502..f0aa74e 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -5,6 +5,7 @@ import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
import fifteenthBattleMapUrl from '../../assets/images/battle/fifteenth-battle-map.svg';
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
+import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
@@ -136,6 +137,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-thirty-seventh', thirtySeventhBattleMapUrl);
this.load.image('battle-map-thirty-eighth', thirtyEighthBattleMapUrl);
this.load.image('battle-map-thirty-ninth', thirtyNinthBattleMapUrl);
+ this.load.image('battle-map-fortieth', fortiethBattleMapUrl);
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 6447210..22e4ae1 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -20,6 +20,7 @@ import {
guiyangRecruitBonds,
guiyangRecruitUnits,
hanzhongDecisiveBonds,
+ hanRiverFloodBonds,
hanzhongOpeningRecruitBonds,
hanzhongOpeningRecruitUnits,
hanzhongMainRecruitBonds,
@@ -353,7 +354,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'thirty-sixth-battle-dingjun-vanguard',
'thirty-seventh-battle-hanzhong-decisive',
'thirty-eighth-battle-jing-defense',
- 'thirty-ninth-battle-fan-castle-vanguard'
+ 'thirty-ninth-battle-fan-castle-vanguard',
+ 'fortieth-battle-han-river-flood'
],
nextHints: ['왕업 선언', '오호대장군', '북벌 준비']
}
@@ -398,7 +400,8 @@ const campBattleIds = {
thirtySixth: 'thirty-sixth-battle-dingjun-vanguard',
thirtySeventh: 'thirty-seventh-battle-hanzhong-decisive',
thirtyEighth: 'thirty-eighth-battle-jing-defense',
- thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard'
+ thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard',
+ fortieth: 'fortieth-battle-han-river-flood'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -847,6 +850,20 @@ const sortieRulesByBattleId: Partial