diff --git a/docs/roadmap.md b/docs/roadmap.md
index a8ff8dd..049b0f4 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -66,16 +66,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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
- Forty-first battle Fan Castle siege route, using a twenty-two-officer roster across an 82x64 castle battlefield to pressure Cao Ren's gate line, Pang De's desperate cavalry, and Wu's rear-shadow scouts
- Forty-second battle Jing rear-crisis route, using Ma Liang and Mi Zhu's rear-guard roles across an 84x66 Jiangling ferry battlefield to repel Lu Meng's disguised scouts and expose Mi Fang's wavering rear line
+- Forty-third battle Gongan gate-collapse route, using a twenty-two-officer roster across an 86x68 Gongan gate battlefield to delay Lu Meng's main infiltration, Fu Shiren's wavering guard, and Mi Fang's silent ledger crisis
- 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, selectable reserve drill focus, and post-battle reserve training growth for benched officers
- Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses
-- Flow verification script from title through the forty-second 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/Fan siege/Jing rear camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the forty-third 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/Fan siege/Jing rear/Gongan collapse camp states, campaign timeline state, and camp save state
## Next Steps
-1. Build the Jing Province rear-collapse follow-up, using Lu Meng's infiltration, Mi Fang/Fu Shiren's wavering, and Guan Yu's overextended Fan Castle front as the next campaign tension
-2. Expand Jing rear-crisis aftermath events, including warehouse trust, river rumor choices, officer loyalty checks, and supply decisions that affect sortie recommendations
+1. Build the Maicheng isolation follow-up, using Guan Yu's overextended Fan Castle front, Wu's sealed rear roads, and Wei's counterpressure as the next campaign tension
+2. Expand Gongan collapse aftermath events, including officer loyalty checks, sealed gate choices, and supply decisions that affect sortie recommendations
3. Expand reserve planning into individual officer assignments, including targeted class practice, paired bond training, and camp task slots
4. Add a dedicated treasure/equipment management view so players can compare special effects and growth
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending
diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs
index 548f866..7b6aaa2 100644
--- a/scripts/verify-flow.mjs
+++ b/scripts/verify-flow.mjs
@@ -1739,7 +1739,7 @@ try {
if (
progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 ||
- progressTabState.campaignProgress?.totalKnown !== 42 ||
+ progressTabState.campaignProgress?.totalKnown !== 43 ||
progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' ||
progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전'
@@ -1891,7 +1891,7 @@ try {
if (
postBowangProgressState?.activeTab !== 'progress' ||
postBowangProgressState.campaignProgress?.completedKnown !== 18 ||
- postBowangProgressState.campaignProgress?.totalKnown !== 42 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 43 ||
postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' ||
postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로'
@@ -2029,7 +2029,7 @@ try {
if (
postChangbanProgressState?.activeTab !== 'progress' ||
postChangbanProgressState.campaignProgress?.completedKnown !== 19 ||
- postChangbanProgressState.campaignProgress?.totalKnown !== 42 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 43 ||
postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' ||
postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로'
@@ -2167,7 +2167,7 @@ try {
if (
postJiangdongProgressState?.activeTab !== 'progress' ||
postJiangdongProgressState.campaignProgress?.completedKnown !== 20 ||
- postJiangdongProgressState.campaignProgress?.totalKnown !== 42 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 43 ||
postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' ||
postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전'
@@ -2307,7 +2307,7 @@ try {
if (
postRedCliffsProgressState?.activeTab !== 'progress' ||
postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 ||
- postRedCliffsProgressState.campaignProgress?.totalKnown !== 42 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 43 ||
postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' ||
postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전'
@@ -2447,7 +2447,7 @@ try {
if (
postFireAttackProgressState?.activeTab !== 'progress' ||
postFireAttackProgressState.campaignProgress?.completedKnown !== 22 ||
- postFireAttackProgressState.campaignProgress?.totalKnown !== 42 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 43 ||
postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' ||
postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전'
@@ -2590,7 +2590,7 @@ try {
if (
postJingzhouProgressState?.activeTab !== 'progress' ||
postJingzhouProgressState.campaignProgress?.completedKnown !== 23 ||
- postJingzhouProgressState.campaignProgress?.totalKnown !== 42 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 43 ||
postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' ||
postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전'
@@ -2732,7 +2732,7 @@ try {
if (
postGuiyangProgressState?.activeTab !== 'progress' ||
postGuiyangProgressState.campaignProgress?.completedKnown !== 24 ||
- postGuiyangProgressState.campaignProgress?.totalKnown !== 42 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 43 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
@@ -2875,7 +2875,7 @@ try {
if (
postWulingProgressState?.activeTab !== 'progress' ||
postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
- postWulingProgressState.campaignProgress?.totalKnown !== 42 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 43 ||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전'
@@ -3024,7 +3024,7 @@ try {
if (
postChangshaProgressState?.activeTab !== 'progress' ||
postChangshaProgressState.campaignProgress?.completedKnown !== 26 ||
- postChangshaProgressState.campaignProgress?.totalKnown !== 42 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 43 ||
postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' ||
postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로'
@@ -3169,7 +3169,7 @@ try {
if (
postYizhouProgressState?.activeTab !== 'progress' ||
postYizhouProgressState.campaignProgress?.completedKnown !== 27 ||
- postYizhouProgressState.campaignProgress?.totalKnown !== 42 ||
+ postYizhouProgressState.campaignProgress?.totalKnown !== 43 ||
postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' ||
postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전'
@@ -3316,7 +3316,7 @@ try {
if (
postFuPassProgressState?.activeTab !== 'progress' ||
postFuPassProgressState.campaignProgress?.completedKnown !== 28 ||
- postFuPassProgressState.campaignProgress?.totalKnown !== 42 ||
+ postFuPassProgressState.campaignProgress?.totalKnown !== 43 ||
postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' ||
postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전'
@@ -3465,7 +3465,7 @@ try {
if (
postLuoProgressState?.activeTab !== 'progress' ||
postLuoProgressState.campaignProgress?.completedKnown !== 29 ||
- postLuoProgressState.campaignProgress?.totalKnown !== 42 ||
+ postLuoProgressState.campaignProgress?.totalKnown !== 43 ||
postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' ||
postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전'
@@ -3611,7 +3611,7 @@ try {
if (
postLuofengProgressState?.activeTab !== 'progress' ||
postLuofengProgressState.campaignProgress?.completedKnown !== 30 ||
- postLuofengProgressState.campaignProgress?.totalKnown !== 42 ||
+ postLuofengProgressState.campaignProgress?.totalKnown !== 43 ||
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략'
@@ -3763,7 +3763,7 @@ try {
if (
postLuoMainProgressState?.activeTab !== 'progress' ||
postLuoMainProgressState.campaignProgress?.completedKnown !== 31 ||
- postLuoMainProgressState.campaignProgress?.totalKnown !== 42 ||
+ postLuoMainProgressState.campaignProgress?.totalKnown !== 43 ||
postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' ||
postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전'
@@ -3916,7 +3916,7 @@ try {
if (
postMianzhuProgressState?.activeTab !== 'progress' ||
postMianzhuProgressState.campaignProgress?.completedKnown !== 32 ||
- postMianzhuProgressState.campaignProgress?.totalKnown !== 42 ||
+ postMianzhuProgressState.campaignProgress?.totalKnown !== 43 ||
postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전'
@@ -4070,7 +4070,7 @@ try {
if (
postChengduProgressState?.activeTab !== 'progress' ||
postChengduProgressState.campaignProgress?.completedKnown !== 33 ||
- postChengduProgressState.campaignProgress?.totalKnown !== 42 ||
+ postChengduProgressState.campaignProgress?.totalKnown !== 43 ||
postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' ||
postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전'
@@ -4225,7 +4225,7 @@ try {
if (
postJiamengProgressState?.activeTab !== 'progress' ||
postJiamengProgressState.campaignProgress?.completedKnown !== 34 ||
- postJiamengProgressState.campaignProgress?.totalKnown !== 42 ||
+ postJiamengProgressState.campaignProgress?.totalKnown !== 43 ||
postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' ||
postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전'
@@ -4381,7 +4381,7 @@ try {
if (
postYangpingProgressState?.activeTab !== 'progress' ||
postYangpingProgressState.campaignProgress?.completedKnown !== 35 ||
- postYangpingProgressState.campaignProgress?.totalKnown !== 42 ||
+ postYangpingProgressState.campaignProgress?.totalKnown !== 43 ||
postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' ||
postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전'
@@ -4538,7 +4538,7 @@ try {
if (
postDingjunProgressState?.activeTab !== 'progress' ||
postDingjunProgressState.campaignProgress?.completedKnown !== 36 ||
- postDingjunProgressState.campaignProgress?.totalKnown !== 42 ||
+ postDingjunProgressState.campaignProgress?.totalKnown !== 43 ||
postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' ||
postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전'
@@ -4691,7 +4691,7 @@ try {
if (
postHanzhongProgressState?.activeTab !== 'progress' ||
postHanzhongProgressState.campaignProgress?.completedKnown !== 37 ||
- postHanzhongProgressState.campaignProgress?.totalKnown !== 42 ||
+ postHanzhongProgressState.campaignProgress?.totalKnown !== 43 ||
postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비'
@@ -4752,7 +4752,7 @@ try {
if (
postKingCouncilProgressState?.activeTab !== 'progress' ||
postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 ||
- postKingCouncilProgressState.campaignProgress?.totalKnown !== 42 ||
+ postKingCouncilProgressState.campaignProgress?.totalKnown !== 43 ||
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
) {
@@ -4812,7 +4812,7 @@ try {
if (
postFoundationProgressState?.activeTab !== 'progress' ||
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
- postFoundationProgressState.campaignProgress?.totalKnown !== 42 ||
+ postFoundationProgressState.campaignProgress?.totalKnown !== 43 ||
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
) {
@@ -4963,7 +4963,7 @@ try {
if (
postJingDefenseProgressState?.activeTab !== 'progress' ||
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
- postJingDefenseProgressState.campaignProgress?.totalKnown !== 42 ||
+ postJingDefenseProgressState.campaignProgress?.totalKnown !== 43 ||
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
) {
@@ -5116,7 +5116,7 @@ try {
if (
postFanCastleProgressState?.activeTab !== 'progress' ||
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
- postFanCastleProgressState.campaignProgress?.totalKnown !== 42 ||
+ postFanCastleProgressState.campaignProgress?.totalKnown !== 43 ||
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
) {
@@ -5270,7 +5270,7 @@ try {
if (
postHanRiverProgressState?.activeTab !== 'progress' ||
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
- postHanRiverProgressState.campaignProgress?.totalKnown !== 42 ||
+ postHanRiverProgressState.campaignProgress?.totalKnown !== 43 ||
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
) {
@@ -5424,7 +5424,7 @@ try {
if (
postFanSiegeProgressState?.activeTab !== 'progress' ||
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
- postFanSiegeProgressState.campaignProgress?.totalKnown !== 42 ||
+ postFanSiegeProgressState.campaignProgress?.totalKnown !== 43 ||
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
) {
@@ -5586,14 +5586,169 @@ try {
if (
postJingRearProgressState?.activeTab !== 'progress' ||
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
- postJingRearProgressState.campaignProgress?.totalKnown !== 42 ||
+ postJingRearProgressState.campaignProgress?.totalKnown !== 43 ||
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
- postJingRearProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
) {
- throw new Error(`Expected post-Jing rear progress tab to complete the forty-second battle and leave rear collapse as the next prepared slice: ${JSON.stringify(postJingRearProgressState?.campaignProgress)}`);
+ throw new Error(`Expected post-Jing rear progress tab to complete the forty-second battle and point to Gongan collapse: ${JSON.stringify(postJingRearProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-jing-rear-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const gonganSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !gonganSortieState?.sortieVisible ||
+ gonganSortieState.campaign?.step !== 'forty-second-camp' ||
+ gonganSortieState.sortiePlan?.objectiveLine !== '공안 성문 변고 · 여몽 본대 저지' ||
+ gonganSortieState.sortiePlan?.maxCount !== 7 ||
+ gonganSortieState.sortiePlan?.recommendedTotal !== 7 ||
+ !gonganSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
+ ) {
+ throw new Error(`Expected Gongan collapse sortie prep to lock Guan Yu and expose a seven-officer rear-collapse choice: ${JSON.stringify(gonganSortieState)}`);
+ }
+ assertSortieTacticalRoster(gonganSortieState, [
+ '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 gonganPriorityUnits = ['ma-liang', 'mi-zhu', 'huang-quan', 'wang-ping', 'zhao-yun'];
+ for (const unitId of gonganPriorityUnits) {
+ 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 && !gonganPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const gonganSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !gonganPriorityUnits.every((unitId) =>
+ gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ !gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
+ !gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
+ gonganSortieReadyState.sortiePlan?.selectedCount !== 7 ||
+ gonganSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
+ ) {
+ throw new Error(`Expected Gongan collapse sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Huang Quan, Wang Ping, and Zhao Yun: ${JSON.stringify(gonganSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-gongan-collapse-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-gongan-collapse-story.png', fullPage: true });
+
+ for (let i = 0; i < 45; i += 1) {
+ const enteredFortyThirdBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'forty-third-battle-gongan-collapse';
+ });
+ if (enteredFortyThirdBattle) {
+ break;
+ }
+ await page.keyboard.press('Space');
+ await page.waitForTimeout(320);
+ }
+ await page.waitForFunction(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'forty-third-battle-gongan-collapse' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-forty-third-battle.png', fullPage: true });
+
+ const fortyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const fortyThirdEnemies = fortyThirdBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const fortyThirdAllies = fortyThirdBattleState.units.filter((unit) => unit.faction === 'ally');
+ const fortyThirdEnemyBehaviors = new Set(fortyThirdEnemies.map((unit) => unit.ai));
+ if (
+ fortyThirdBattleState.camera?.mapWidth !== 86 ||
+ fortyThirdBattleState.camera?.mapHeight !== 68 ||
+ fortyThirdBattleState.victoryConditionLabel !== '여몽 본대 저지' ||
+ fortyThirdEnemies.length < 37 ||
+ !fortyThirdEnemyBehaviors.has('aggressive') ||
+ !fortyThirdEnemyBehaviors.has('guard') ||
+ !fortyThirdEnemyBehaviors.has('hold') ||
+ !fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-leader-lu-meng') ||
+ !fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-lu-xun') ||
+ !fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-fu-shiren') ||
+ !fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-mi-fang') ||
+ !fortyThirdAllies.some((unit) => unit.id === 'guan-yu') ||
+ !fortyThirdAllies.some((unit) => unit.id === 'ma-liang') ||
+ !fortyThirdAllies.some((unit) => unit.id === 'mi-zhu') ||
+ !fortyThirdAllies.some((unit) => unit.id === 'huang-quan') ||
+ !fortyThirdAllies.some((unit) => unit.id === 'wang-ping')
+ ) {
+ throw new Error(`Expected forty-third battle to use Gongan collapse map, Lu Meng objective, selected rear-collapse officers, and mixed Wu AI: ${JSON.stringify(fortyThirdBattleState)}`);
+ }
+
+ 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 fortyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ fortyThirdCampState?.campBattleId !== 'forty-third-battle-gongan-collapse' ||
+ fortyThirdCampState.campTitle !== '공안 성문 변고 후 군영' ||
+ fortyThirdCampState.availableDialogueIds?.length !== 3 ||
+ !fortyThirdCampState.availableDialogueIds.every((id) => id.includes('gongan-collapse')) ||
+ fortyThirdCampState.availableVisitIds?.length !== 2 ||
+ !fortyThirdCampState.availableVisitIds.every((id) => id.includes('gongan-collapse')) ||
+ fortyThirdCampState.rosterCollection?.total < 22
+ ) {
+ throw new Error(`Expected forty-third camp to expose Gongan collapse dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyThirdCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-gongan-collapse-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postGonganProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postGonganProgressState?.activeTab !== 'progress' ||
+ postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
+ postGonganProgressState.campaignProgress?.totalKnown !== 43 ||
+ postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
+ postGonganProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Gongan progress tab to complete the forty-third battle and leave Maicheng isolation as the next prepared slice: ${JSON.stringify(postGonganProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-gongan-progress.png', fullPage: true });
+
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene.stop('CampScene');
@@ -5626,10 +5781,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 Jing rear crisis: ${JSON.stringify(titleContinueState)}`);
+ throw new Error(`Expected title continue to reopen the latest camp after Gongan collapse: ${JSON.stringify(titleContinueState)}`);
}
- console.log(`Verified title-to-forty-second-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan Castle siege, and Jing rear crisis state at ${targetUrl}`);
+ console.log(`Verified title-to-forty-third-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan Castle siege, Jing rear crisis, and Gongan collapse state at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/forty-third-battle-map.svg b/src/assets/images/battle/forty-third-battle-map.svg
new file mode 100644
index 0000000..c7f035c
--- /dev/null
+++ b/src/assets/images/battle/forty-third-battle-map.svg
@@ -0,0 +1,106 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index aac341c..a933dc3 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -32,6 +32,10 @@ import {
fortySecondBattleMap,
fortySecondBattleUnits,
fortySecondBattleVictoryPages,
+ fortyThirdBattleBonds,
+ fortyThirdBattleMap,
+ fortyThirdBattleUnits,
+ fortyThirdBattleVictoryPages,
firstBattleMap,
firstBattleUnits,
firstBattleVictoryPages,
@@ -215,7 +219,8 @@ export type BattleScenarioId =
| 'thirty-ninth-battle-fan-castle-vanguard'
| 'fortieth-battle-han-river-flood'
| 'forty-first-battle-fan-castle-siege'
- | 'forty-second-battle-jing-rear-crisis';
+ | 'forty-second-battle-jing-rear-crisis'
+ | 'forty-third-battle-gongan-collapse';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -2721,6 +2726,69 @@ export const fortySecondBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const fortyThirdBattleScenario: BattleScenarioDefinition = {
+ id: 'forty-third-battle-gongan-collapse',
+ title: '공안 성문 변고',
+ victoryConditionLabel: '여몽 본대 저지',
+ defeatConditionLabel: '유비 또는 관우 퇴각',
+ openingObjectiveLines: [
+ '강릉 나루의 정찰대는 물러났지만 공안 성문에는 더 큰 균열이 생겼습니다. 강동 본대가 성문 안쪽의 흔들림을 이용하기 전에 여몽을 저지하십시오.',
+ '부사인과 미방의 병사들은 아직 완전히 돌아서지 않았습니다. 성문과 창고를 지키면 후방의 신뢰를 조금이라도 붙들 수 있습니다.',
+ '마량, 미축, 황권, 왕평 같은 후방 수습 담당을 데려가면 장부와 지형, 성문 병사들의 흔들림을 읽기 좋습니다.'
+ ],
+ map: fortyThirdBattleMap,
+ units: fortyThirdBattleUnits,
+ bonds: fortyThirdBattleBonds,
+ mapTextureKey: 'battle-map-forty-third',
+ leaderUnitId: 'jing-collapse-leader-lu-meng',
+ quickVictoryTurnLimit: 70,
+ baseVictoryGold: 8920,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '여몽 본대 저지',
+ rewardGold: 5840,
+ unitId: 'jing-collapse-leader-lu-meng'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 1380,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'guan-yu',
+ kind: 'keep-unit-alive',
+ label: '관우 생존',
+ rewardGold: 2160,
+ unitId: 'guan-yu'
+ },
+ {
+ id: 'gongan-gate',
+ kind: 'secure-terrain',
+ label: '공안 성문 확보',
+ rewardGold: 2820,
+ terrain: 'fort'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '70턴 이내 승리',
+ rewardGold: 2040,
+ maxTurn: 70
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'guan-yu' }
+ ],
+ itemRewards: ['콩 42', '상처약 32', '탁주 13', '공안 성문 기록 1', '부사인 군령서 1'],
+ victoryPages: fortyThirdBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -2765,7 +2833,8 @@ export const battleScenarios: Record
'thirty-ninth-battle-fan-castle-vanguard': thirtyNinthBattleScenario,
'fortieth-battle-han-river-flood': fortiethBattleScenario,
'forty-first-battle-fan-castle-siege': fortyFirstBattleScenario,
- 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario
+ 'forty-second-battle-jing-rear-crisis': fortySecondBattleScenario,
+ 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 4a41961..fc7b91e 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -7,6 +7,7 @@ import {
fortiethBattleScenario,
fortyFirstBattleScenario,
fortySecondBattleScenario,
+ fortyThirdBattleScenario,
fifthBattleScenario,
fifteenthBattleScenario,
fourteenthBattleScenario,
@@ -57,6 +58,8 @@ import {
fortyFirstBattleVictoryPages,
fortySecondBattleIntroPages,
fortySecondBattleVictoryPages,
+ fortyThirdBattleIntroPages,
+ fortyThirdBattleVictoryPages,
fifthBattleIntroPages,
fifthBattleVictoryPages,
fifteenthBattleIntroPages,
@@ -608,13 +611,24 @@ const sortieFlows: Record = {
},
[fortySecondBattleScenario.id]: {
afterBattleId: fortySecondBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '형주 균열 심화',
+ eyebrow: '다음 전장',
+ title: fortyThirdBattleScenario.title,
description:
- '강릉 나루는 지켰지만 강동의 침투는 이미 더 넓은 후방을 살피고 있습니다. 다음 장에서는 강릉과 공안의 흔들림, 장수들의 불신, 오군의 본격적인 움직임을 다룹니다.',
- rewardHint: '다음 장: 형주 후방 붕괴와 오군 침투 준비 중',
- pages: fortySecondBattleVictoryPages,
- unavailableNotice: '형주 후방 붕괴와 오군 침투는 다음 작업에서 이어집니다. 지금은 군영에서 나루 경계의 성장, 공명도, 보급을 정비할 수 있습니다.'
+ '강릉 나루는 지켰지만 공안 성문과 창고 장부의 균열은 더 커졌습니다. 다음 출진에서는 여몽의 본대와 흔들리는 공안 수비대를 동시에 상대하며 형주 후방 붕괴를 늦춰야 합니다.',
+ rewardHint: `예상 보상: ${fortyThirdBattleScenario.title} 개방 / 공안 성문 기록`,
+ nextBattleId: fortyThirdBattleScenario.id,
+ campaignStep: 'forty-third-battle',
+ pages: [...fortySecondBattleVictoryPages, ...fortyThirdBattleIntroPages]
+ },
+ [fortyThirdBattleScenario.id]: {
+ afterBattleId: fortyThirdBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '형주 고립 심화',
+ description:
+ '공안 성문은 잠시 붙들었지만 미방과 부사인의 흔들림은 완전히 멈추지 않았습니다. 다음 장에서는 관우의 번성 전선이 후방 상실과 맞물려 고립되는 흐름을 다룹니다.',
+ rewardHint: '다음 장: 맥성 고립과 형주 상실 준비 중',
+ pages: fortyThirdBattleVictoryPages,
+ unavailableNotice: '맥성 고립과 형주 상실 흐름은 다음 작업에서 이어집니다. 지금은 공안 성문 변고 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 6897dc7..3ebd852 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -2780,6 +2780,68 @@ export const fortySecondBattleVictoryPages: StoryPage[] = [
}
];
+export const fortyThirdBattleIntroPages: StoryPage[] = [
+ {
+ id: 'forty-third-gongan-gate',
+ bgm: 'story-dark',
+ chapter: '공안의 성문',
+ background: 'story-militia',
+ speaker: '미축',
+ text: '강릉 창고 장부를 대조하니 공안으로 보낸 군량이 사라진 흔적이 있습니다. 성문을 지키는 병사들은 명령을 기다린다 하면서도 강동 배를 그냥 들이고 있습니다.'
+ },
+ {
+ id: 'forty-third-fu-shiren-wavers',
+ bgm: 'story-dark',
+ chapter: '흔들리는 수비장',
+ background: 'story-three-heroes',
+ speaker: '마량',
+ text: '부사인은 관우 장군의 엄한 군령을 두려워하고, 미방은 강동의 말에 마음이 흔들립니다. 이 싸움은 적을 베는 일보다 성문 안의 믿음을 붙드는 일이 더 어렵습니다.'
+ },
+ {
+ id: 'forty-third-lu-meng-infiltration',
+ bgm: 'battle-prep',
+ chapter: '강동의 본대',
+ background: 'story-liu-bei',
+ speaker: '제갈량',
+ text: '여몽은 더는 작은 정찰선만 보내지 않을 것입니다. 흰 옷의 상인이 길을 열고, 육손의 글이 의심을 누그러뜨린 뒤, 본대가 공안 성문을 향해 움직일 것입니다.'
+ },
+ {
+ id: 'forty-third-sortie',
+ bgm: 'battle-prep',
+ chapter: '공안 성문 변고',
+ background: 'story-sortie',
+ speaker: '관우',
+ portrait: 'guanYu',
+ text: '번성 앞 깃발을 거둘 수 없다면, 후방의 성문을 먼저 다시 잠가야 하오. 공안 성문을 붙들고 부사인의 병사들을 안정시켜 형주가 한순간에 무너지지 않게 하시오.'
+ }
+];
+
+export const fortyThirdBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'forty-third-victory-gate-held',
+ bgm: 'militia-theme',
+ chapter: '잠긴 성문',
+ background: 'story-sortie',
+ text: '공안 성문은 다시 잠겼고, 강동의 선봉은 물러났습니다. 그러나 부사인의 얼굴에는 안도보다 두려움이 더 짙게 남았습니다.'
+ },
+ {
+ id: 'forty-third-mi-fang-silence',
+ bgm: 'story-dark',
+ chapter: '말 없는 장부',
+ background: 'story-militia',
+ speaker: '미축',
+ text: '미방은 끝내 분명한 대답을 하지 않았습니다. 창고 장부는 맞춰 놓았으나, 사람의 마음은 장부처럼 줄을 맞추기 어렵습니다.'
+ },
+ {
+ id: 'forty-third-next-shadow',
+ bgm: 'story-dark',
+ chapter: '맥성으로 드리우는 그림자',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '성문 하나를 지켰지만 형주는 이미 바깥과 안쪽에서 함께 갈라지고 있습니다. 다음 장에서는 관우의 번성 전선이 고립으로 바뀌는 순간을 다루어야 합니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -3177,6 +3239,12 @@ export const fortySecondBattleMap: BattleMap = {
terrain: createFortySecondBattleTerrain()
};
+export const fortyThirdBattleMap: BattleMap = {
+ width: 86,
+ height: 68,
+ terrain: createFortyThirdBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -14800,6 +14868,91 @@ export const fortySecondBattleUnits: UnitData[] = [
createThirtyFirstEnemyUnit('jing-rear-leader-lu-meng', '여몽', '강동 도독', 'rebelLeader', 100, 690, 176, 80, 26)
];
+const fortyThirdBattleAllyPositions: Record = {
+ 'liu-bei': { x: 8, y: 61 },
+ 'guan-yu': { x: 15, y: 56 },
+ 'zhang-fei': { x: 8, y: 63 },
+ 'jian-yong': { x: 6, y: 64 },
+ 'mi-zhu': { x: 11, y: 61 },
+ 'sun-qian': { x: 12, y: 64 },
+ 'zhao-yun': { x: 18, y: 58 },
+ 'zhuge-liang': { x: 12, y: 57 },
+ 'ma-liang': { x: 17, y: 61 },
+ 'yi-ji': { x: 19, y: 64 },
+ 'gong-zhi': { x: 20, y: 62 },
+ 'huang-zhong': { x: 22, y: 56 },
+ 'wei-yan': { x: 19, y: 54 },
+ 'pang-tong': { x: 15, y: 59 },
+ 'fa-zheng': { x: 23, y: 61 },
+ 'wu-yi': { x: 24, y: 57 },
+ 'yan-yan': { x: 25, y: 59 },
+ 'li-yan': { x: 22, y: 53 },
+ 'huang-quan': { x: 26, y: 62 },
+ 'ma-chao': { x: 27, y: 55 },
+ 'ma-dai': { x: 28, y: 57 },
+ 'wang-ping': { x: 25, y: 52 }
+};
+
+export const fortyThirdBattleUnits: 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, fortyThirdBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ createThirtyFirstEnemyUnit('jing-collapse-scout-a', '강동 척후', '백의 척후', 'bandit', 97, 400, 132, 34, 58),
+ createThirtyFirstEnemyUnit('jing-collapse-scout-b', '강동 척후', '성문 척후', 'bandit', 97, 400, 132, 48, 52),
+ createThirtyFirstEnemyUnit('jing-collapse-scout-c', '강동 척후', '창고 척후', 'bandit', 98, 408, 134, 61, 46),
+ createThirtyFirstEnemyUnit('jing-collapse-scout-d', '강동 척후', '내문 척후', 'bandit', 98, 408, 134, 75, 38),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-a', '강동 보병', '나루 보병', 'yellowTurban', 98, 590, 158, 42, 56),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-b', '강동 보병', '창고 보병', 'yellowTurban', 98, 590, 158, 55, 50),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-c', '강동 보병', '외성 보병', 'yellowTurban', 99, 602, 160, 66, 43),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-d', '강동 보병', '공안 보병', 'yellowTurban', 99, 602, 160, 76, 31),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-e', '강동 보병', '내성 보병', 'yellowTurban', 100, 614, 162, 82, 22),
+ createThirtyFirstEnemyUnit('jing-collapse-infantry-f', '강동 보병', '동문 보병', 'yellowTurban', 100, 614, 162, 80, 13),
+ createThirtyFirstEnemyUnit('jing-collapse-archer-a', '강동 궁병', '나루 궁병', 'archer', 98, 430, 138, 38, 60),
+ createThirtyFirstEnemyUnit('jing-collapse-archer-b', '강동 궁병', '창고 궁병', 'archer', 99, 438, 140, 52, 54),
+ createThirtyFirstEnemyUnit('jing-collapse-archer-c', '강동 궁병', '성루 궁병', 'archer', 99, 438, 140, 67, 46),
+ createThirtyFirstEnemyUnit('jing-collapse-archer-d', '강동 궁병', '공안 궁병', 'archer', 100, 446, 142, 79, 34),
+ createThirtyFirstEnemyUnit('jing-collapse-archer-e', '강동 궁병', '동문 궁병', 'archer', 100, 446, 142, 83, 18),
+ createThirtyFirstEnemyUnit('jing-collapse-cavalry-a', '강동 기병', '나루 기병', 'cavalry', 99, 606, 160, 39, 63),
+ createThirtyFirstEnemyUnit('jing-collapse-cavalry-b', '강동 기병', '외성 기병', 'cavalry', 99, 606, 160, 58, 55),
+ createThirtyFirstEnemyUnit('jing-collapse-cavalry-c', '강동 기병', '동림 기병', 'cavalry', 100, 618, 162, 72, 43),
+ createThirtyFirstEnemyUnit('jing-collapse-cavalry-d', '강동 기병', '공안 기병', 'cavalry', 100, 618, 162, 78, 28),
+ createThirtyFirstEnemyUnit('jing-collapse-guard-a', '공안 수비대', '흔들린 수비대', 'yellowTurban', 101, 652, 168, 61, 44),
+ createThirtyFirstEnemyUnit('jing-collapse-guard-b', '공안 수비대', '성문 수비대', 'yellowTurban', 101, 652, 168, 70, 36),
+ createThirtyFirstEnemyUnit('jing-collapse-guard-c', '공안 수비대', '부사인 친위대', 'yellowTurban', 102, 664, 170, 77, 25),
+ createThirtyFirstEnemyUnit('jing-collapse-guard-d', '공안 수비대', '미방 친위대', 'yellowTurban', 102, 664, 170, 81, 17),
+ createThirtyFirstEnemyUnit('jing-collapse-strategist-a', '강동 설객', '육손 서리', 'strategist', 99, 448, 142, 45, 57),
+ createThirtyFirstEnemyUnit('jing-collapse-strategist-b', '강동 설객', '공안 설객', 'strategist', 100, 456, 144, 66, 39),
+ createThirtyFirstEnemyUnit('jing-collapse-strategist-c', '강동 설객', '내성 설객', 'strategist', 101, 464, 146, 79, 23),
+ createThirtyFirstEnemyUnit('jing-collapse-ambusher-a', '백의 복병', '나루 복병', 'bandit', 98, 410, 134, 31, 64),
+ createThirtyFirstEnemyUnit('jing-collapse-ambusher-b', '백의 복병', '창고 복병', 'bandit', 99, 418, 136, 56, 59),
+ createThirtyFirstEnemyUnit('jing-collapse-ambusher-c', '백의 복병', '성문 복병', 'bandit', 99, 418, 136, 73, 47),
+ createThirtyFirstEnemyUnit('jing-collapse-ambusher-d', '백의 복병', '북문 복병', 'bandit', 100, 426, 138, 84, 29),
+ createThirtyFirstEnemyUnit('jing-collapse-marine-a', '강동 수군', '상선 수군', 'yellowTurban', 99, 606, 160, 44, 65),
+ createThirtyFirstEnemyUnit('jing-collapse-marine-b', '강동 수군', '나루 수군', 'yellowTurban', 100, 618, 162, 50, 61),
+ createThirtyFirstEnemyUnit('jing-collapse-marine-c', '강동 수군', '서문 수군', 'yellowTurban', 100, 618, 162, 63, 56),
+ createThirtyFirstEnemyUnit('jing-collapse-officer-lu-xun', '육손', '강동 설객', 'strategist', 102, 488, 150, 72, 30),
+ createThirtyFirstEnemyUnit('jing-collapse-officer-fu-shiren', '부사인', '흔들리는 공안장', 'rebelLeader', 101, 656, 170, 76, 21),
+ createThirtyFirstEnemyUnit('jing-collapse-officer-mi-fang', '미방', '흔들리는 강릉장', 'rebelLeader', 101, 656, 170, 67, 48),
+ createThirtyFirstEnemyUnit('jing-collapse-leader-lu-meng', '여몽', '백의도강 도독', 'rebelLeader', 103, 720, 182, 83, 24)
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -15503,6 +15656,35 @@ export const fortyFirstBattleBonds: BattleBond[] = [...fortiethBattleBonds, ...f
export const fortySecondBattleBonds: BattleBond[] = [...fortyFirstBattleBonds, ...jingRearCrisisBonds].map(
cloneBattleBondForScenario
);
+export const jingCollapseBonds: BattleBond[] = [
+ {
+ id: 'guan-yu__ma-liang_collapse',
+ unitIds: ['guan-yu', 'ma-liang'],
+ title: '형주 수습',
+ level: 42,
+ exp: 0,
+ description: '마량은 흔들리는 공안 병사들의 마음을 읽고, 관우는 전선의 군령으로 후방을 다시 묶는다.'
+ },
+ {
+ id: 'mi-zhu__huang-quan_collapse',
+ unitIds: ['mi-zhu', 'huang-quan'],
+ title: '장부와 신중함',
+ level: 36,
+ exp: 0,
+ description: '미축의 장부와 황권의 신중한 판단은 배신으로 이어지는 작은 숫자의 틈을 찾아낸다.'
+ },
+ {
+ id: 'zhao-yun__wang-ping_collapse',
+ unitIds: ['zhao-yun', 'wang-ping'],
+ title: '나루 차단',
+ level: 34,
+ exp: 0,
+ description: '조운의 기동과 왕평의 지형 판단은 강동 수군이 성문 뒤로 파고드는 길을 끊는다.'
+ }
+];
+export const fortyThirdBattleBonds: BattleBond[] = [...fortySecondBattleBonds, ...jingCollapseBonds].map(
+ cloneBattleBondForScenario
+);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -17549,6 +17731,84 @@ function createFortySecondBattleTerrain(): TerrainType[][] {
);
}
+function createFortyThirdBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 68 }, (_, y) =>
+ Array.from({ length: 86 }, (_, x): TerrainType => {
+ if (x <= 8 && y >= 60) {
+ return 'camp';
+ }
+ if (
+ (x >= 58 && x <= 70 && y >= 41 && y <= 54) ||
+ (x >= 66 && x <= 79 && y >= 29 && y <= 41) ||
+ (x >= 74 && x <= 85 && y >= 16 && y <= 31) ||
+ (x >= 78 && x <= 85 && y >= 7 && y <= 17) ||
+ (x >= 70 && x <= 76 && y >= 45 && y <= 54)
+ ) {
+ return 'fort';
+ }
+ if (
+ (x >= 22 && x <= 28 && y >= 57 && y <= 62) ||
+ (x >= 44 && x <= 51 && y >= 51 && y <= 55) ||
+ (x >= 60 && x <= 66 && y >= 45 && y <= 49) ||
+ (x >= 74 && x <= 80 && y >= 37 && y <= 42) ||
+ (x >= 78 && x <= 84 && y >= 23 && y <= 27)
+ ) {
+ return 'village';
+ }
+ if (
+ (x >= 15 && x <= 22 && y >= 0 && y <= 59) ||
+ (x >= 36 && x <= 41 && y >= 8 && y <= 66) ||
+ (x >= 55 && x <= 60 && y >= 3 && y <= 55) ||
+ (x >= 71 && x <= 75 && y >= 17 && y <= 48) ||
+ (y >= 35 && y <= 39 && x >= 22 && x <= 74) ||
+ (y >= 53 && y <= 56 && x >= 38 && x <= 69)
+ ) {
+ return 'river';
+ }
+ if (
+ (y >= 62 && x >= 8 && x <= 34) ||
+ (x >= 33 && x <= 49 && y >= 57 && y <= 62) ||
+ (x >= 48 && x <= 64 && y >= 51 && y <= 57) ||
+ (x >= 63 && x <= 75 && y >= 44 && y <= 51) ||
+ (x >= 74 && x <= 84 && y >= 35 && y <= 44) ||
+ (x >= 77 && x <= 85 && y >= 21 && y <= 35) ||
+ (x >= 59 && x <= 83 && y >= 18 && y <= 22) ||
+ (y >= 29 && y <= 32 && x >= 22 && x <= 77) ||
+ (x >= 49 && x <= 68 && y >= 40 && y <= 43)
+ ) {
+ return 'road';
+ }
+ if (
+ (x >= 4 && x <= 31 && y >= 14 && y <= 49) ||
+ (x >= 30 && x <= 51 && y >= 21 && y <= 54) ||
+ (x >= 54 && x <= 72 && y >= 50 && y <= 67) ||
+ (x >= 68 && x <= 85 && y >= 44 && y <= 66)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 14 && y <= 31) ||
+ (x >= 28 && x <= 52 && y <= 21) ||
+ (x >= 51 && x <= 72 && y >= 0 && y <= 23) ||
+ (x >= 73 && x <= 85 && y <= 15) ||
+ (x >= 34 && x <= 56 && y >= 60 && y <= 67)
+ ) {
+ return 'hill';
+ }
+ if (
+ (x <= 2 && y <= 59) ||
+ (x >= 83 && y >= 39) ||
+ (x >= 64 && x <= 69 && y <= 11) ||
+ (x >= 27 && x <= 35 && y >= 31 && y <= 48) ||
+ (x >= 69 && x <= 71 && y >= 49 && y <= 60)
+ ) {
+ 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 8d2f203..a564e0f 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -745,7 +745,44 @@ const unitTexture: Record = {
'jing-rear-marine-b': 'unit-rebel',
'jing-rear-officer-lu-xun': 'unit-rebel-archer',
'jing-rear-officer-mi-fang': 'unit-rebel-leader',
- 'jing-rear-leader-lu-meng': 'unit-rebel-leader'
+ 'jing-rear-leader-lu-meng': 'unit-rebel-leader',
+ 'jing-collapse-scout-a': 'unit-rebel',
+ 'jing-collapse-scout-b': 'unit-rebel',
+ 'jing-collapse-scout-c': 'unit-rebel',
+ 'jing-collapse-scout-d': 'unit-rebel',
+ 'jing-collapse-infantry-a': 'unit-rebel',
+ 'jing-collapse-infantry-b': 'unit-rebel',
+ 'jing-collapse-infantry-c': 'unit-rebel',
+ 'jing-collapse-infantry-d': 'unit-rebel',
+ 'jing-collapse-infantry-e': 'unit-rebel',
+ 'jing-collapse-infantry-f': 'unit-rebel',
+ 'jing-collapse-archer-a': 'unit-rebel-archer',
+ 'jing-collapse-archer-b': 'unit-rebel-archer',
+ 'jing-collapse-archer-c': 'unit-rebel-archer',
+ 'jing-collapse-archer-d': 'unit-rebel-archer',
+ 'jing-collapse-archer-e': 'unit-rebel-archer',
+ 'jing-collapse-cavalry-a': 'unit-rebel-cavalry',
+ 'jing-collapse-cavalry-b': 'unit-rebel-cavalry',
+ 'jing-collapse-cavalry-c': 'unit-rebel-cavalry',
+ 'jing-collapse-cavalry-d': 'unit-rebel-cavalry',
+ 'jing-collapse-guard-a': 'unit-rebel',
+ 'jing-collapse-guard-b': 'unit-rebel',
+ 'jing-collapse-guard-c': 'unit-rebel',
+ 'jing-collapse-guard-d': 'unit-rebel',
+ 'jing-collapse-strategist-a': 'unit-rebel-archer',
+ 'jing-collapse-strategist-b': 'unit-rebel-archer',
+ 'jing-collapse-strategist-c': 'unit-rebel-archer',
+ 'jing-collapse-ambusher-a': 'unit-rebel',
+ 'jing-collapse-ambusher-b': 'unit-rebel',
+ 'jing-collapse-ambusher-c': 'unit-rebel',
+ 'jing-collapse-ambusher-d': 'unit-rebel',
+ 'jing-collapse-marine-a': 'unit-rebel',
+ 'jing-collapse-marine-b': 'unit-rebel',
+ 'jing-collapse-marine-c': 'unit-rebel',
+ 'jing-collapse-officer-lu-xun': 'unit-rebel-archer',
+ 'jing-collapse-officer-fu-shiren': 'unit-rebel-leader',
+ 'jing-collapse-officer-mi-fang': 'unit-rebel-leader',
+ 'jing-collapse-leader-lu-meng': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -1948,7 +1985,44 @@ const enemyAiByUnitId: Record = {
'jing-rear-marine-b': 'guard',
'jing-rear-officer-lu-xun': 'hold',
'jing-rear-officer-mi-fang': 'guard',
- 'jing-rear-leader-lu-meng': 'guard'
+ 'jing-rear-leader-lu-meng': 'guard',
+ 'jing-collapse-scout-a': 'aggressive',
+ 'jing-collapse-scout-b': 'aggressive',
+ 'jing-collapse-scout-c': 'aggressive',
+ 'jing-collapse-scout-d': 'aggressive',
+ 'jing-collapse-infantry-a': 'guard',
+ 'jing-collapse-infantry-b': 'guard',
+ 'jing-collapse-infantry-c': 'guard',
+ 'jing-collapse-infantry-d': 'guard',
+ 'jing-collapse-infantry-e': 'guard',
+ 'jing-collapse-infantry-f': 'guard',
+ 'jing-collapse-archer-a': 'hold',
+ 'jing-collapse-archer-b': 'hold',
+ 'jing-collapse-archer-c': 'hold',
+ 'jing-collapse-archer-d': 'hold',
+ 'jing-collapse-archer-e': 'hold',
+ 'jing-collapse-cavalry-a': 'aggressive',
+ 'jing-collapse-cavalry-b': 'aggressive',
+ 'jing-collapse-cavalry-c': 'aggressive',
+ 'jing-collapse-cavalry-d': 'aggressive',
+ 'jing-collapse-guard-a': 'guard',
+ 'jing-collapse-guard-b': 'guard',
+ 'jing-collapse-guard-c': 'guard',
+ 'jing-collapse-guard-d': 'guard',
+ 'jing-collapse-strategist-a': 'hold',
+ 'jing-collapse-strategist-b': 'hold',
+ 'jing-collapse-strategist-c': 'hold',
+ 'jing-collapse-ambusher-a': 'aggressive',
+ 'jing-collapse-ambusher-b': 'aggressive',
+ 'jing-collapse-ambusher-c': 'aggressive',
+ 'jing-collapse-ambusher-d': 'aggressive',
+ 'jing-collapse-marine-a': 'aggressive',
+ 'jing-collapse-marine-b': 'aggressive',
+ 'jing-collapse-marine-c': 'guard',
+ 'jing-collapse-officer-lu-xun': 'hold',
+ 'jing-collapse-officer-fu-shiren': 'guard',
+ 'jing-collapse-officer-mi-fang': 'guard',
+ 'jing-collapse-leader-lu-meng': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index bd52b0f..dd64cb3 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -8,6 +8,7 @@ import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
import fortiethBattleMapUrl from '../../assets/images/battle/fortieth-battle-map.svg';
import fortyFirstBattleMapUrl from '../../assets/images/battle/forty-first-battle-map.svg';
import fortySecondBattleMapUrl from '../../assets/images/battle/forty-second-battle-map.svg';
+import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-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';
@@ -142,6 +143,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-fortieth', fortiethBattleMapUrl);
this.load.image('battle-map-forty-first', fortyFirstBattleMapUrl);
this.load.image('battle-map-forty-second', fortySecondBattleMapUrl);
+ this.load.image('battle-map-forty-third', fortyThirdBattleMapUrl);
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 baefa72..55baf98 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -29,6 +29,7 @@ import {
hanzhongScoutRecruitUnits,
fanCastleBonds,
fanCastleSiegeBonds,
+ jingCollapseBonds,
jingRearCrisisBonds,
jingDefenseBonds,
jingzhouRecruitBonds,
@@ -370,7 +371,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'thirty-ninth-battle-fan-castle-vanguard',
'fortieth-battle-han-river-flood',
'forty-first-battle-fan-castle-siege',
- 'forty-second-battle-jing-rear-crisis'
+ 'forty-second-battle-jing-rear-crisis',
+ 'forty-third-battle-gongan-collapse'
],
nextHints: ['왕업 선언', '번성 포위', '형주 위기']
}
@@ -418,7 +420,8 @@ const campBattleIds = {
thirtyNinth: 'thirty-ninth-battle-fan-castle-vanguard',
fortieth: 'fortieth-battle-han-river-flood',
fortyFirst: 'forty-first-battle-fan-castle-siege',
- fortySecond: 'forty-second-battle-jing-rear-crisis'
+ fortySecond: 'forty-second-battle-jing-rear-crisis',
+ fortyThird: 'forty-third-battle-gongan-collapse'
} as const;
const defaultRequiredSortieUnitIds = ['liu-bei'];
@@ -909,6 +912,20 @@ const sortieRulesByBattleId: Partial