diff --git a/docs/roadmap.md b/docs/roadmap.md index 0ed1628..8ef05d4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -41,15 +41,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Post-Wolong camp dialogue and visit events, including Zhuge Liang bond choices and Red Cliffs preparation clues - Eighteenth battle Bowang ambush as the first Red Cliffs preparation battle, with a new large battlefield, Xiahou Dun's vanguard, mixed enemy AI, Zhuge Liang sortie recommendation, and post-battle camp events - Nineteenth battle Changban refuge route, extending Red Cliffs preparation with a larger refugee-road battlefield, Cao Chun's cavalry pursuit, Zhao Yun survival pressure, and a sortie choice that highlights growing roster tradeoffs +- Twentieth battle Jiangdong envoy route, shifting sortie choice toward protecting Zhuge Liang and Sun Qian while opening the diplomatic path to Sun Quan and Zhou Yu - 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 nineteenth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban camp states, campaign timeline state, and camp save state +- Flow verification script from title through the twentieth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue the Red Cliffs preparation chapter into the Jiangdong envoy route and the first Red Cliffs confrontation +1. Continue the Red Cliffs preparation chapter into the Sun Quan/Zhou Yu parley and the first Red Cliffs confrontation 2. Add more recruitable officers as the campaign moves toward Red Cliffs, Jing Province, Yi Province, and Shu Han 3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 7bfdca2..64a9403 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 !== 19 || + progressTabState.campaignProgress?.totalKnown !== 20 || 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 !== 19 || + postBowangProgressState.campaignProgress?.totalKnown !== 20 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' @@ -2025,15 +2025,153 @@ try { const postChangbanProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postChangbanProgressState?.activeTab !== 'progress' || - postChangbanProgressState.campaignProgress?.completedKnown !== postChangbanProgressState.campaignProgress?.totalKnown || + postChangbanProgressState.campaignProgress?.completedKnown !== 19 || + postChangbanProgressState.campaignProgress?.totalKnown !== 20 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || - postChangbanProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' ) { - throw new Error(`Expected post-Changban progress tab to keep Red Cliffs active while Jiangdong envoy is pending: ${JSON.stringify(postChangbanProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Changban progress tab to point toward Jiangdong envoy route: ${JSON.stringify(postChangbanProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-changban-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const jiangdongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jiangdongSortieState?.sortieVisible || + !jiangdongSortieState.sortiePlan?.objectiveLine?.includes('강동 사절로') || + !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || + !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + jiangdongSortieState.sortieRoster?.length < 8 || + jiangdongSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected nineteenth camp sortie prep to target Jiangdong envoy with Zhuge Liang, Sun Qian, and Zhao Yun recommendations: ${JSON.stringify(jiangdongSortieState)}`); + } + assertSortieTacticalRoster(jiangdongSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang' + ]); + + const jiangdongPriorityUnits = ['zhuge-liang', 'sun-qian', 'zhao-yun']; + for (const unitId of jiangdongPriorityUnits) { + const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { + const removable = currentSortieState.sortieRoster?.find( + (unit) => unit.selected && unit.id !== 'liu-bei' && !jiangdongPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const jiangdongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !jiangdongPriorityUnits.every((unitId) => + jiangdongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + jiangdongSortieReadyState.sortiePlan?.selectedCount !== 6 || + jiangdongSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 + ) { + throw new Error(`Expected Jiangdong envoy sortie to deploy priority officers while preserving six-officer pressure: ${JSON.stringify(jiangdongSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-jiangdong-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-jiangdong-story.png', fullPage: true }); + + for (let i = 0; i < 32; i += 1) { + const enteredTwentiethBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twentieth-battle-jiangdong-envoy'; + }); + if (enteredTwentiethBattle) { + 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 === 'twentieth-battle-jiangdong-envoy' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twentieth-battle.png', fullPage: true }); + + const twentiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentiethEnemies = twentiethBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentiethAllies = twentiethBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentiethEnemyBehaviors = new Set(twentiethEnemies.map((unit) => unit.ai)); + if ( + twentiethBattleState.camera?.mapWidth !== 40 || + twentiethBattleState.camera?.mapHeight !== 28 || + twentiethBattleState.victoryConditionLabel !== '문빙 퇴각' || + twentiethEnemies.length < 17 || + !twentiethEnemyBehaviors.has('aggressive') || + !twentiethEnemyBehaviors.has('guard') || + !twentiethEnemyBehaviors.has('hold') || + !twentiethEnemies.some((unit) => unit.id === 'envoy-road-leader-wen-pin') || + !twentiethAllies.some((unit) => unit.id === 'sun-qian') || + !twentiethAllies.some((unit) => unit.id === 'zhuge-liang') + ) { + throw new Error(`Expected twentieth battle to use Jiangdong envoy map, Wen Pin objective, mixed AI, Sun Qian, and Zhuge Liang: ${JSON.stringify(twentiethBattleState)}`); + } + + 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 twentiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentiethCampState?.campBattleId !== 'twentieth-battle-jiangdong-envoy' || + twentiethCampState.campTitle !== '강동 사절 준비 군영' || + twentiethCampState.availableDialogueIds?.length !== 3 || + !twentiethCampState.availableDialogueIds.every((id) => id.endsWith('jiangdong-envoy')) || + twentiethCampState.availableVisitIds?.length !== 2 || + !twentiethCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang') || + !twentiethCampState.report?.bonds?.some((bond) => bond.id === 'zhao-yun__zhuge-liang') || + twentiethCampState.rosterCollection?.total < 8 || + twentiethCampState.rosterCollection?.reserveTrainingCount < 2 || + twentiethCampState.rosterCollection?.reserveTrainingExp < 12 + ) { + throw new Error(`Expected twentieth camp to expose Jiangdong dialogue/visit sets, envoy bonds, and reserve growth: ${JSON.stringify(twentiethCampState)}`); + } + await page.screenshot({ path: 'dist/verification-jiangdong-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postJiangdongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postJiangdongProgressState?.activeTab !== 'progress' || + postJiangdongProgressState.campaignProgress?.completedKnown !== postJiangdongProgressState.campaignProgress?.totalKnown || + postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || + postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || + postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Jiangdong progress tab to keep Red Cliffs active while first Red Cliffs battle is pending: ${JSON.stringify(postJiangdongProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-jiangdong-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -2045,7 +2183,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-nineteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twentieth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twentieth-battle-map.svg b/src/assets/images/battle/twentieth-battle-map.svg new file mode 100644 index 0000000..9f7e0fa --- /dev/null +++ b/src/assets/images/battle/twentieth-battle-map.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 5856c81..ea8f810 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -39,6 +39,10 @@ import { thirteenthBattleMap, thirteenthBattleUnits, thirteenthBattleVictoryPages, + twentiethBattleBonds, + twentiethBattleMap, + twentiethBattleUnits, + twentiethBattleVictoryPages, twelfthBattleBonds, twelfthBattleMap, twelfthBattleUnits, @@ -100,7 +104,8 @@ export type BattleScenarioId = | 'sixteenth-battle-liu-biao-refuge' | 'seventeenth-battle-wolong-visit-road' | 'eighteenth-battle-bowang-ambush' - | 'nineteenth-battle-changban-refuge'; + | 'nineteenth-battle-changban-refuge' + | 'twentieth-battle-jiangdong-envoy'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1156,6 +1161,70 @@ export const nineteenthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentiethBattleScenario: BattleScenarioDefinition = { + id: 'twentieth-battle-jiangdong-envoy', + title: '강동 사절로', + victoryConditionLabel: '문빙 퇴각', + defeatConditionLabel: '유비, 제갈량 또는 손건 퇴각', + openingObjectiveLines: [ + '장판파를 벗어난 유비군은 강동과 손잡을 사절을 보내야 합니다. 문빙의 추격대가 강나루를 닫기 전에 길을 열어야 합니다.', + '이번 전투는 제갈량과 손건을 지키는 것이 핵심입니다. 전열이 다리목을 붙들고, 기동 장수가 추격 기병을 끊어야 사절 문서가 강동으로 향할 수 있습니다.', + '문빙을 퇴각시키고 강나루를 확보하십시오. 제갈량과 손건이 살아남으면 적벽대전으로 향할 외교 명분이 완성됩니다.' + ], + map: twentiethBattleMap, + units: twentiethBattleUnits, + bonds: twentiethBattleBonds, + mapTextureKey: 'battle-map-twentieth', + leaderUnitId: 'envoy-road-leader-wen-pin', + quickVictoryTurnLimit: 29, + baseVictoryGold: 2380, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '문빙 퇴각', + rewardGold: 1540, + unitId: 'envoy-road-leader-wen-pin' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 760, + unitId: 'zhuge-liang' + }, + { + id: 'sun-qian', + kind: 'keep-unit-alive', + label: '손건 생존', + rewardGold: 620, + unitId: 'sun-qian' + }, + { + id: 'river-port', + kind: 'secure-terrain', + label: '강나루 확보', + rewardGold: 860, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '29턴 이내 승리', + rewardGold: 640, + maxTurn: 29 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' }, + { kind: 'unit-defeated', unitId: 'sun-qian' } + ], + itemRewards: ['콩 8', '상처약 5', '탁주 3', '강동 사절 문서 +1', '화공 논의 +1'], + victoryPages: twentiethBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1177,7 +1246,8 @@ export const battleScenarios: Record 'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario, 'seventeenth-battle-wolong-visit-road': seventeenthBattleScenario, 'eighteenth-battle-bowang-ambush': eighteenthBattleScenario, - 'nineteenth-battle-changban-refuge': nineteenthBattleScenario + 'nineteenth-battle-changban-refuge': nineteenthBattleScenario, + 'twentieth-battle-jiangdong-envoy': twentiethBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 37f7f1c..7a8394e 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -17,6 +17,7 @@ import { seventeenthBattleScenario, tenthBattleScenario, thirteenthBattleScenario, + twentiethBattleScenario, twelfthBattleScenario, thirdBattleScenario, type BattleScenarioId @@ -55,6 +56,8 @@ import { tenthBattleVictoryPages, thirteenthBattleIntroPages, thirteenthBattleVictoryPages, + twentiethBattleIntroPages, + twentiethBattleVictoryPages, twelfthBattleIntroPages, twelfthBattleVictoryPages, thirdBattleIntroPages, @@ -263,13 +266,24 @@ const sortieFlows: Record = { }, [nineteenthBattleScenario.id]: { afterBattleId: nineteenthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '강동 사절 준비', + eyebrow: '다음 전장', + title: twentiethBattleScenario.title, description: - '장판파에서 피난로를 열어 백성과 군을 다시 모았습니다. 이제 제갈량과 손건은 강동으로 보낼 말과 증거를 정리하고, 손권과 마주할 준비를 해야 합니다.', - rewardHint: '다음 장: 강동 사절과 적벽대전 준비 중', - pages: nineteenthBattleVictoryPages, - unavailableNotice: '강동 사절과 적벽대전 장은 다음 작업에서 이어집니다. 군영에서 출전 후보, 공명, 보급을 정비하십시오.' + '장판파에서 피난로를 열어 백성과 군을 다시 모았습니다. 이제 제갈량과 손건을 강나루까지 보내 강동 사절의 길을 열어야 합니다.', + rewardHint: `예상 보상: ${twentiethBattleScenario.title} 개방 / 강동 사절 문서 확보`, + nextBattleId: twentiethBattleScenario.id, + campaignStep: 'twentieth-battle', + pages: [...nineteenthBattleVictoryPages, ...twentiethBattleIntroPages] + }, + [twentiethBattleScenario.id]: { + afterBattleId: twentiethBattleScenario.id, + eyebrow: '다음 장 준비', + title: '적벽 첫 전투 준비', + description: + '강동으로 향하는 길이 열렸습니다. 이제 제갈량과 손건은 손권과 주유를 설득해야 하고, 유비군은 장강에서 조조의 대군을 맞을 준비를 시작합니다.', + rewardHint: '다음 장: 강동 회담과 적벽대전 첫 전투 준비 중', + pages: twentiethBattleVictoryPages, + unavailableNotice: '강동 회담과 적벽대전 첫 전투는 다음 작업에서 이어집니다. 군영에서 사절 문서, 화공 준비, 출전 후보를 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 8ab6fa8..22c5f3c 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1260,6 +1260,69 @@ export const nineteenthBattleVictoryPages: StoryPage[] = [ } ]; +export const twentiethBattleIntroPages: StoryPage[] = [ + { + id: 'twentieth-envoy-road-opens', + bgm: 'story-dark', + chapter: '강동으로 가는 길', + background: 'story-militia', + speaker: '손건', + text: '장판파의 피난로를 지나 유비군은 하구로 향합니다. 강동에 사절을 보내려면 조조군 추격대가 강나루를 닫기 전에 길을 열어야 합니다.' + }, + { + id: 'twentieth-zhuge-envoy-plan', + bgm: 'battle-prep', + chapter: '사절의 문서', + background: 'story-liu-bei', + speaker: '제갈량', + text: '강동은 조조를 두려워하지만, 싸울 계산이 서야 움직입니다. 손건의 문서와 장판파의 기록을 나루까지 지켜야 합니다.' + }, + { + id: 'twentieth-generals-protect-envoy', + bgm: 'battle-prep', + chapter: '호위와 돌파', + background: 'story-three-heroes', + speaker: '관우', + portrait: 'guanYu', + text: '이번 길은 적을 많이 베는 것보다 사절을 온전히 보내는 일이 먼저입니다. 전열과 기동, 보급을 골고루 세우는 편성이 필요하겠습니다.' + }, + { + id: 'twentieth-jiangdong-envoy-sortie', + bgm: 'battle-prep', + chapter: '강동 사절로', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '선생과 손건이 강동에 닿아야 조조의 큰 군세를 막을 길이 열리오. 오늘은 칼끝만이 아니라 말과 문서를 지키는 싸움이오.' + } +]; + +export const twentiethBattleVictoryPages: StoryPage[] = [ + { + id: 'twentieth-victory-river-port', + bgm: 'militia-theme', + chapter: '나루가 열리다', + background: 'story-sortie', + text: '문빙의 추격대가 물러서자 강나루의 문이 열렸습니다. 유비군은 피난 행렬을 뒤에 두고, 강동으로 향할 작은 배와 사절 문서를 지켜냈습니다.' + }, + { + id: 'twentieth-sun-qian-letter-ready', + bgm: 'battle-prep', + chapter: '손권에게 보낼 말', + background: 'story-militia', + speaker: '손건', + text: '강동에 전할 말은 이제 분명합니다. 조조의 남하, 장판파의 피난로, 그리고 주공께서 백성을 버리지 않았다는 사실을 함께 적겠습니다.' + }, + { + id: 'twentieth-red-cliffs-near', + bgm: 'story-dark', + chapter: '적벽으로 부는 바람', + background: 'story-three-heroes', + speaker: '제갈량', + text: '이제 강동의 마음을 움직일 차례입니다. 손권과 주유가 계산을 마치면, 조조의 대군은 장강 위에서 처음으로 멈춰 서게 될 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1519,6 +1582,12 @@ export const nineteenthBattleMap: BattleMap = { terrain: createNineteenthBattleTerrain() }; +export const twentiethBattleMap: BattleMap = { + width: 40, + height: 28, + terrain: createTwentiethBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -7239,6 +7308,384 @@ export const nineteenthBattleUnits: UnitData[] = [ } ]; +const twentiethBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 22 }, + 'guan-yu': { x: 5, y: 20 }, + 'zhang-fei': { x: 6, y: 23 }, + 'jian-yong': { x: 3, y: 24 }, + 'mi-zhu': { x: 5, y: 25 }, + 'sun-qian': { x: 7, y: 22 }, + 'zhao-yun': { x: 8, y: 20 }, + 'zhuge-liang': { x: 6, y: 21 } +}; + +export const twentiethBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'envoy-road-scout-a', + name: '조조군 척후', + faction: 'enemy', + className: '나루 척후', + classKey: 'bandit', + level: 24, + exp: 34, + hp: 70, + maxHp: 70, + attack: 27, + move: 4, + stats: { might: 102, intelligence: 74, leadership: 80, agility: 100, luck: 70 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 36 }, + armor: { itemId: 'rebel-vest', level: 3, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 13, + y: 21 + }, + { + id: 'envoy-road-scout-b', + name: '조조군 척후', + faction: 'enemy', + className: '강변 척후', + classKey: 'bandit', + level: 24, + exp: 34, + hp: 70, + maxHp: 70, + attack: 27, + move: 4, + stats: { might: 102, intelligence: 74, leadership: 80, agility: 100, luck: 70 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 36 }, + armor: { itemId: 'rebel-vest', level: 3, exp: 4 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 14, + y: 24 + }, + { + id: 'envoy-road-cavalry-a', + name: '조조 추격기', + faction: 'enemy', + className: '추격 기병', + classKey: 'cavalry', + level: 25, + exp: 36, + hp: 94, + maxHp: 94, + attack: 32, + move: 5, + stats: { might: 116, intelligence: 74, leadership: 92, agility: 110, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 44 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 18 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 17, + y: 19 + }, + { + id: 'envoy-road-cavalry-b', + name: '조조 추격기', + faction: 'enemy', + className: '추격 기병', + classKey: 'cavalry', + level: 25, + exp: 36, + hp: 94, + maxHp: 94, + attack: 32, + move: 5, + stats: { might: 116, intelligence: 74, leadership: 92, agility: 110, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 44 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 18 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 18, + y: 23 + }, + { + id: 'envoy-road-cavalry-c', + name: '문빙 기병', + faction: 'enemy', + className: '나루 기병', + classKey: 'cavalry', + level: 26, + exp: 38, + hp: 98, + maxHp: 98, + attack: 33, + move: 5, + stats: { might: 118, intelligence: 76, leadership: 94, agility: 112, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 50 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 26 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 18 + }, + { + id: 'envoy-road-cavalry-d', + name: '문빙 기병', + faction: 'enemy', + className: '나루 기병', + classKey: 'cavalry', + level: 26, + exp: 38, + hp: 98, + maxHp: 98, + attack: 33, + move: 5, + stats: { might: 118, intelligence: 76, leadership: 94, agility: 112, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 50 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 26 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 28, + y: 21 + }, + { + id: 'envoy-road-infantry-a', + name: '나루 보병', + faction: 'enemy', + className: '강나루 보병', + classKey: 'yellowTurban', + level: 24, + exp: 34, + hp: 88, + maxHp: 88, + attack: 29, + move: 3, + stats: { might: 110, intelligence: 76, leadership: 94, agility: 82, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 40 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 21, + y: 19 + }, + { + id: 'envoy-road-infantry-b', + name: '나루 보병', + faction: 'enemy', + className: '강나루 보병', + classKey: 'yellowTurban', + level: 24, + exp: 34, + hp: 88, + maxHp: 88, + attack: 29, + move: 3, + stats: { might: 110, intelligence: 76, leadership: 94, agility: 82, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 40 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 23, + y: 21 + }, + { + id: 'envoy-road-infantry-c', + name: '문빙 보병', + faction: 'enemy', + className: '포구 보병', + classKey: 'yellowTurban', + level: 25, + exp: 36, + hp: 92, + maxHp: 92, + attack: 30, + move: 3, + stats: { might: 112, intelligence: 78, leadership: 96, agility: 84, luck: 71 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 46 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 31, + y: 16 + }, + { + id: 'envoy-road-infantry-d', + name: '문빙 보병', + faction: 'enemy', + className: '포구 보병', + classKey: 'yellowTurban', + level: 25, + exp: 36, + hp: 92, + maxHp: 92, + attack: 30, + move: 3, + stats: { might: 112, intelligence: 78, leadership: 96, agility: 84, luck: 71 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 46 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 33, + y: 19 + }, + { + id: 'envoy-road-archer-a', + name: '강변 궁병', + faction: 'enemy', + className: '강변 궁병', + classKey: 'archer', + level: 24, + exp: 34, + hp: 68, + maxHp: 68, + attack: 29, + move: 3, + stats: { might: 98, intelligence: 88, leadership: 86, agility: 96, luck: 72 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 42 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 16 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 20, + y: 15 + }, + { + id: 'envoy-road-archer-b', + name: '강변 궁병', + faction: 'enemy', + className: '언덕 궁병', + classKey: 'archer', + level: 24, + exp: 34, + hp: 68, + maxHp: 68, + attack: 29, + move: 3, + stats: { might: 98, intelligence: 88, leadership: 86, agility: 96, luck: 72 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 42 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 16 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 27, + y: 14 + }, + { + id: 'envoy-road-archer-c', + name: '포구 궁병', + faction: 'enemy', + className: '포구 궁병', + classKey: 'archer', + level: 25, + exp: 36, + hp: 70, + maxHp: 70, + attack: 30, + move: 3, + stats: { might: 100, intelligence: 90, leadership: 88, agility: 98, luck: 72 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 48 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 20 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 35, + y: 15 + }, + { + id: 'envoy-road-guard-a', + name: '포구 수비대', + faction: 'enemy', + className: '포구 수비대', + classKey: 'yellowTurban', + level: 26, + exp: 38, + hp: 100, + maxHp: 100, + attack: 32, + move: 3, + stats: { might: 118, intelligence: 80, leadership: 104, agility: 86, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 52 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 28 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 35, + y: 18 + }, + { + id: 'envoy-road-guard-b', + name: '포구 수비대', + faction: 'enemy', + className: '포구 수비대', + classKey: 'yellowTurban', + level: 26, + exp: 38, + hp: 100, + maxHp: 100, + attack: 32, + move: 3, + stats: { might: 118, intelligence: 80, leadership: 104, agility: 86, luck: 72 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 52 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 28 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 36, + y: 20 + }, + { + id: 'envoy-road-strategist-a', + name: '조조군 군리', + faction: 'enemy', + className: '군리', + classKey: 'archer', + level: 25, + exp: 36, + hp: 72, + maxHp: 72, + attack: 29, + move: 3, + stats: { might: 78, intelligence: 98, leadership: 90, agility: 86, luck: 74 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 50 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 34, + y: 14 + }, + { + id: 'envoy-road-leader-wen-pin', + name: '문빙', + faction: 'enemy', + className: '조조군 추격장', + classKey: 'rebelLeader', + level: 27, + exp: 42, + hp: 132, + maxHp: 132, + attack: 34, + move: 4, + stats: { might: 120, intelligence: 86, leadership: 112, agility: 90, luck: 76 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 4, exp: 4 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 36 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 37, + y: 17 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -7390,6 +7837,7 @@ export const eighteenthBattleBonds: BattleBond[] = [...sixteenthBattleBonds, ... cloneBattleBondForScenario ); export const nineteenthBattleBonds: BattleBond[] = [...eighteenthBattleBonds].map(cloneBattleBondForScenario); +export const twentiethBattleBonds: BattleBond[] = [...nineteenthBattleBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -7954,6 +8402,62 @@ function createNineteenthBattleTerrain(): TerrainType[][] { ); } +function createTwentiethBattleTerrain(): TerrainType[][] { + return Array.from({ length: 28 }, (_, y) => + Array.from({ length: 40 }, (_, x): TerrainType => { + if ((x <= 3 && y >= 21 && y <= 26) || (x >= 4 && x <= 7 && y >= 23 && y <= 26)) { + return 'camp'; + } + if ((x >= 34 && x <= 39 && y >= 14 && y <= 21) || (x >= 31 && x <= 34 && y >= 15 && y <= 18)) { + return 'fort'; + } + if ((x >= 13 && x <= 16 && y >= 18 && y <= 20) || (x >= 29 && x <= 31 && y >= 15 && y <= 17)) { + return 'village'; + } + if ( + (y === 22 && x >= 3 && x <= 15) || + (y === 20 && x >= 14 && x <= 26) || + (y === 17 && x >= 26 && x <= 36) || + (x === 34 && y >= 14 && y <= 22) + ) { + return 'road'; + } + if ( + (x >= 6 && x <= 16 && y === 24 - Math.floor(x / 2)) || + (x >= 16 && x <= 31 && y === Math.floor(x / 2) + 8) || + (x >= 5 && x <= 12 && y === 23) + ) { + return 'road'; + } + if ((x === 22 || x === 23) && y >= 0 && y <= 27) { + return 'river'; + } + if ((x === 28 || x === 29) && y >= 0 && y <= 14) { + return 'river'; + } + if ( + (x >= 5 && x <= 15 && y >= 8 && y <= 17) || + (x >= 16 && x <= 21 && y >= 21 && y <= 27) || + (x <= 9 && y <= 8) || + (x >= 29 && x <= 38 && y <= 9) + ) { + return 'forest'; + } + if ( + (x >= 10 && x <= 17 && y >= 2 && y <= 6) || + (x >= 25 && x <= 33 && y >= 22 && y <= 27) || + (x >= 31 && x <= 39 && y >= 3 && y <= 7) + ) { + return 'hill'; + } + if ((x <= 1 && y <= 13) || (x >= 38 && y <= 11) || (x >= 38 && y >= 23)) { + 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 8c9e710..0fb2f78 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -188,7 +188,24 @@ const unitTexture: Record = { 'changban-guard-a': 'unit-rebel', 'changban-guard-b': 'unit-rebel', 'changban-strategist-a': 'unit-rebel-archer', - 'changban-leader-cao-chun': 'unit-rebel-leader' + 'changban-leader-cao-chun': 'unit-rebel-leader', + 'envoy-road-scout-a': 'unit-rebel', + 'envoy-road-scout-b': 'unit-rebel', + 'envoy-road-cavalry-a': 'unit-rebel-cavalry', + 'envoy-road-cavalry-b': 'unit-rebel-cavalry', + 'envoy-road-cavalry-c': 'unit-rebel-cavalry', + 'envoy-road-cavalry-d': 'unit-rebel-cavalry', + 'envoy-road-infantry-a': 'unit-rebel', + 'envoy-road-infantry-b': 'unit-rebel', + 'envoy-road-infantry-c': 'unit-rebel', + 'envoy-road-infantry-d': 'unit-rebel', + 'envoy-road-archer-a': 'unit-rebel-archer', + 'envoy-road-archer-b': 'unit-rebel-archer', + 'envoy-road-archer-c': 'unit-rebel-archer', + 'envoy-road-guard-a': 'unit-rebel', + 'envoy-road-guard-b': 'unit-rebel', + 'envoy-road-strategist-a': 'unit-rebel-archer', + 'envoy-road-leader-wen-pin': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -832,7 +849,24 @@ const enemyAiByUnitId: Record = { 'changban-guard-a': 'guard', 'changban-guard-b': 'guard', 'changban-strategist-a': 'hold', - 'changban-leader-cao-chun': 'guard' + 'changban-leader-cao-chun': 'guard', + 'envoy-road-scout-a': 'aggressive', + 'envoy-road-scout-b': 'aggressive', + 'envoy-road-cavalry-a': 'aggressive', + 'envoy-road-cavalry-b': 'aggressive', + 'envoy-road-cavalry-c': 'aggressive', + 'envoy-road-cavalry-d': 'aggressive', + 'envoy-road-infantry-a': 'guard', + 'envoy-road-infantry-b': 'guard', + 'envoy-road-infantry-c': 'guard', + 'envoy-road-infantry-d': 'guard', + 'envoy-road-archer-a': 'hold', + 'envoy-road-archer-b': 'hold', + 'envoy-road-archer-c': 'hold', + 'envoy-road-guard-a': 'guard', + 'envoy-road-guard-b': 'guard', + 'envoy-road-strategist-a': 'hold', + 'envoy-road-leader-wen-pin': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index c3f77c4..ca22a0a 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -17,6 +17,7 @@ import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-batt import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg'; import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg'; import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; +import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg'; import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg'; import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png'; @@ -96,6 +97,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-seventeenth', seventeenthBattleMapUrl); this.load.image('battle-map-eighteenth', eighteenthBattleMapUrl); this.load.image('battle-map-nineteenth', nineteenthBattleMapUrl); + this.load.image('battle-map-twentieth', twentiethBattleMapUrl); 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 e418ef4..340a086 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -281,7 +281,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ title: '적벽대전', period: '강동의 바람', description: '제갈량의 계책과 강동의 힘이 맞물려 조조의 대군을 막아서는 큰 전환 장입니다.', - battleIds: ['eighteenth-battle-bowang-ambush', 'nineteenth-battle-changban-refuge'], + battleIds: ['eighteenth-battle-bowang-ambush', 'nineteenth-battle-changban-refuge', 'twentieth-battle-jiangdong-envoy'], nextHints: ['강동 사절', '화공 준비', '조조 남하'] }, { @@ -321,7 +321,8 @@ const campBattleIds = { sixteenth: 'sixteenth-battle-liu-biao-refuge', seventeenth: 'seventeenth-battle-wolong-visit-road', eighteenth: 'eighteenth-battle-bowang-ambush', - nineteenth: 'nineteenth-battle-changban-refuge' + nineteenth: 'nineteenth-battle-changban-refuge', + twentieth: 'twentieth-battle-jiangdong-envoy' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -524,6 +525,18 @@ const sortieRulesByBattleId: Partial