From 697ce8837d679313a059e99be2707b4c09bddae3 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 06:33:37 +0900 Subject: [PATCH] Add Changban refuge sortie chapter --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 148 ++++- .../images/battle/nineteenth-battle-map.svg | 95 ++++ src/game/data/battles.ts | 73 ++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 504 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 38 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 163 +++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1047 insertions(+), 23 deletions(-) create mode 100644 src/assets/images/battle/nineteenth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 60d62f5..0ed1628 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -40,15 +40,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Seventeenth battle Wolong visit road, gated by Wolong clue visits, opening the Longzhong approach and recruiting Zhuge Liang into the roster after victory - 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 - 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 eighteenth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang camp state, campaign timeline state, and camp save state +- 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 ## Next Steps -1. Continue the Red Cliffs preparation chapter into Changban and the Jiangdong envoy route +1. Continue the Red Cliffs preparation chapter into the Jiangdong envoy route 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 e120248..7bfdca2 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 !== 18 || + progressTabState.campaignProgress?.totalKnown !== 19 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' @@ -1887,15 +1887,153 @@ try { const postBowangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postBowangProgressState?.activeTab !== 'progress' || - postBowangProgressState.campaignProgress?.completedKnown !== postBowangProgressState.campaignProgress?.totalKnown || + postBowangProgressState.campaignProgress?.completedKnown !== 18 || + postBowangProgressState.campaignProgress?.totalKnown !== 19 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || - postBowangProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' ) { - throw new Error(`Expected post-Bowang progress tab to keep Red Cliffs active while next scenario is pending: ${JSON.stringify(postBowangProgressState?.campaignProgress)}`); + throw new Error(`Expected post-Bowang progress tab to point toward Changban refuge route: ${JSON.stringify(postBowangProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-bowang-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const changbanSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !changbanSortieState?.sortieVisible || + !changbanSortieState.sortiePlan?.objectiveLine?.includes('장판파 피난로') || + !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || + !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + changbanSortieState.sortieRoster?.length < 8 || + changbanSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected eighteenth camp sortie prep to target Changban with Zhao Yun, Zhang Fei, and Zhuge Liang recommendations: ${JSON.stringify(changbanSortieState)}`); + } + assertSortieTacticalRoster(changbanSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang' + ]); + + const changbanPriorityUnits = ['zhao-yun', 'zhang-fei', 'zhuge-liang']; + for (const unitId of changbanPriorityUnits) { + 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' && !changbanPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const changbanSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !changbanPriorityUnits.every((unitId) => + changbanSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + changbanSortieReadyState.sortiePlan?.selectedCount !== 6 || + changbanSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 + ) { + throw new Error(`Expected Changban sortie to deploy priority officers and preserve six-officer choice pressure: ${JSON.stringify(changbanSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-changban-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-changban-story.png', fullPage: true }); + + for (let i = 0; i < 30; i += 1) { + const enteredNineteenthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'nineteenth-battle-changban-refuge'; + }); + if (enteredNineteenthBattle) { + 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 === 'nineteenth-battle-changban-refuge' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-nineteenth-battle.png', fullPage: true }); + + const nineteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const nineteenthEnemies = nineteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const nineteenthAllies = nineteenthBattleState.units.filter((unit) => unit.faction === 'ally'); + const nineteenthEnemyBehaviors = new Set(nineteenthEnemies.map((unit) => unit.ai)); + if ( + nineteenthBattleState.camera?.mapWidth !== 38 || + nineteenthBattleState.camera?.mapHeight !== 28 || + nineteenthBattleState.victoryConditionLabel !== '조순 퇴각' || + nineteenthEnemies.length < 17 || + !nineteenthEnemyBehaviors.has('aggressive') || + !nineteenthEnemyBehaviors.has('guard') || + !nineteenthEnemyBehaviors.has('hold') || + !nineteenthEnemies.some((unit) => unit.id === 'changban-leader-cao-chun') || + !nineteenthAllies.some((unit) => unit.id === 'zhao-yun') || + !nineteenthAllies.some((unit) => unit.id === 'zhuge-liang') + ) { + throw new Error(`Expected nineteenth battle to use Changban map, Cao Chun objective, mixed AI, Zhao Yun, and Zhuge Liang: ${JSON.stringify(nineteenthBattleState)}`); + } + + 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 nineteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + nineteenthCampState?.campBattleId !== 'nineteenth-battle-changban-refuge' || + nineteenthCampState.campTitle !== '장판파 돌파 후 군영' || + nineteenthCampState.availableDialogueIds?.length !== 3 || + !nineteenthCampState.availableDialogueIds.every((id) => id.endsWith('changban-refuge')) || + nineteenthCampState.availableVisitIds?.length !== 2 || + !nineteenthCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhao-yun') || + !nineteenthCampState.report?.bonds?.some((bond) => bond.id === 'zhang-fei__zhuge-liang') || + nineteenthCampState.rosterCollection?.total < 8 || + nineteenthCampState.rosterCollection?.reserveTrainingCount < 2 || + nineteenthCampState.rosterCollection?.reserveTrainingExp < 12 + ) { + throw new Error(`Expected nineteenth camp to expose Changban dialogue/visit sets, Zhao Yun bonds, and reserve growth: ${JSON.stringify(nineteenthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-changban-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postChangbanProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postChangbanProgressState?.activeTab !== 'progress' || + postChangbanProgressState.campaignProgress?.completedKnown !== postChangbanProgressState.campaignProgress?.totalKnown || + postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || + postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || + 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)}`); + } + await page.screenshot({ path: 'dist/verification-post-changban-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -1907,7 +2045,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-eighteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-nineteenth-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/nineteenth-battle-map.svg b/src/assets/images/battle/nineteenth-battle-map.svg new file mode 100644 index 0000000..5780372 --- /dev/null +++ b/src/assets/images/battle/nineteenth-battle-map.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 20914ec..5856c81 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -27,6 +27,10 @@ import { ninthBattleMap, ninthBattleUnits, ninthBattleVictoryPages, + nineteenthBattleBonds, + nineteenthBattleMap, + nineteenthBattleUnits, + nineteenthBattleVictoryPages, tenthBattleBonds, tenthBattleMap, tenthBattleUnits, @@ -95,7 +99,8 @@ export type BattleScenarioId = | 'fifteenth-battle-yuan-refuge-road' | 'sixteenth-battle-liu-biao-refuge' | 'seventeenth-battle-wolong-visit-road' - | 'eighteenth-battle-bowang-ambush'; + | 'eighteenth-battle-bowang-ambush' + | 'nineteenth-battle-changban-refuge'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1088,6 +1093,69 @@ export const eighteenthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const nineteenthBattleScenario: BattleScenarioDefinition = { + id: 'nineteenth-battle-changban-refuge', + title: '장판파 피난로', + victoryConditionLabel: '조순 퇴각', + defeatConditionLabel: '유비 또는 조운 퇴각', + openingObjectiveLines: [ + '조조군 호표기가 피난 행렬을 따라 장판파로 들이칩니다. 백성을 버리지 않으려면 남쪽 피난로와 다리목을 동시에 지켜야 합니다.', + '조운은 넓은 길을 오가며 흩어진 병력을 모을 수 있고, 장비는 다리목을 막아 추격 기병을 늦추기 좋습니다. 제갈량과 손건은 전후 강동 사절 명분을 정리합니다.', + '조순을 퇴각시키고 유비와 조운을 지키십시오. 피난민 마을을 확보하면 다음 강동 사절 준비가 안정됩니다.' + ], + map: nineteenthBattleMap, + units: nineteenthBattleUnits, + bonds: nineteenthBattleBonds, + mapTextureKey: 'battle-map-nineteenth', + leaderUnitId: 'changban-leader-cao-chun', + quickVictoryTurnLimit: 28, + baseVictoryGold: 2220, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '조순 퇴각', + rewardGold: 1440, + unitId: 'changban-leader-cao-chun' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 480, + unitId: 'liu-bei' + }, + { + id: 'zhao-yun', + kind: 'keep-unit-alive', + label: '조운 생존', + rewardGold: 620, + unitId: 'zhao-yun' + }, + { + id: 'refugee-village', + kind: 'secure-terrain', + label: '피난민 마을 확보', + rewardGold: 820, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '28턴 이내 승리', + rewardGold: 620, + maxTurn: 28 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhao-yun' } + ], + itemRewards: ['콩 7', '상처약 5', '탁주 2', '피난로 지도 +1', '강동 풍문 +1'], + victoryPages: nineteenthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1108,7 +1176,8 @@ export const battleScenarios: Record 'fifteenth-battle-yuan-refuge-road': fifteenthBattleScenario, 'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario, 'seventeenth-battle-wolong-visit-road': seventeenthBattleScenario, - 'eighteenth-battle-bowang-ambush': eighteenthBattleScenario + 'eighteenth-battle-bowang-ambush': eighteenthBattleScenario, + 'nineteenth-battle-changban-refuge': nineteenthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 7629828..37f7f1c 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -9,6 +9,7 @@ import { fourteenthBattleScenario, fourthBattleScenario, ninthBattleScenario, + nineteenthBattleScenario, secondBattleScenario, seventhBattleScenario, sixthBattleScenario, @@ -38,6 +39,8 @@ import { fourthBattleVictoryPages, ninthBattleIntroPages, ninthBattleVictoryPages, + nineteenthBattleIntroPages, + nineteenthBattleVictoryPages, secondBattleIntroPages, secondBattleVictoryPages, seventhBattleIntroPages, @@ -249,13 +252,24 @@ const sortieFlows: Record = { }, [eighteenthBattleScenario.id]: { afterBattleId: eighteenthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '강동으로 부는 바람', + eyebrow: '다음 전장', + title: nineteenthBattleScenario.title, description: - '박망파에서 조조군 선봉을 늦춘 유비군은 더 큰 남하를 막기 위해 강동과 손잡을 길을 찾아야 합니다. 다음 흐름은 장판파와 강동 사절, 그리고 적벽대전으로 이어집니다.', - rewardHint: '다음 장: 장판파와 강동 사절 준비 중', - pages: eighteenthBattleVictoryPages, - unavailableNotice: '장판파와 강동 사절 장은 다음 작업에서 이어집니다. 군영에서 제갈량의 책략과 장수들의 출전 구성을 정비하십시오.' + '박망파에서 조조군 선봉을 늦췄지만 대군의 남하는 계속됩니다. 신야의 백성을 데리고 장판파 피난로를 열어야 강동 사절로 이어질 시간을 벌 수 있습니다.', + rewardHint: `예상 보상: ${nineteenthBattleScenario.title} 개방 / 장판파 피난로 확보`, + nextBattleId: nineteenthBattleScenario.id, + campaignStep: 'nineteenth-battle', + pages: [...eighteenthBattleVictoryPages, ...nineteenthBattleIntroPages] + }, + [nineteenthBattleScenario.id]: { + afterBattleId: nineteenthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '강동 사절 준비', + description: + '장판파에서 피난로를 열어 백성과 군을 다시 모았습니다. 이제 제갈량과 손건은 강동으로 보낼 말과 증거를 정리하고, 손권과 마주할 준비를 해야 합니다.', + rewardHint: '다음 장: 강동 사절과 적벽대전 준비 중', + pages: nineteenthBattleVictoryPages, + unavailableNotice: '강동 사절과 적벽대전 장은 다음 작업에서 이어집니다. 군영에서 출전 후보, 공명, 보급을 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 5169475..8ab6fa8 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1197,6 +1197,69 @@ export const eighteenthBattleVictoryPages: StoryPage[] = [ } ]; +export const nineteenthBattleIntroPages: StoryPage[] = [ + { + id: 'nineteenth-cao-army-south', + bgm: 'story-dark', + chapter: '조조의 남하', + background: 'story-militia', + speaker: '손건', + text: '박망파에서 시간을 벌었지만 조조의 대군은 멈추지 않았습니다. 신야의 백성들이 남쪽으로 몰리고, 장판파 길목에는 추격 기병이 먼저 닿고 있습니다.' + }, + { + id: 'nineteenth-zhao-yun-rescue', + bgm: 'battle-prep', + chapter: '백마의 구원', + background: 'story-three-heroes', + speaker: '조운', + text: '주공, 피난 행렬이 흩어지면 백성도 군도 모두 잃습니다. 제가 앞뒤를 오가며 길을 묶겠습니다. 다리와 마을을 먼저 지켜 주십시오.' + }, + { + id: 'nineteenth-zhang-fei-bridge', + bgm: 'battle-prep', + chapter: '장판교의 호령', + background: 'story-sortie', + speaker: '장비', + portrait: 'zhangFei', + text: '형님은 백성을 이끌고 남쪽 길을 보시오. 뒤쫓는 놈들은 제가 다리목에서 한 번 꺾어 놓겠습니다. 다만 누가 옆을 받쳐 줄지는 잘 골라야겠소!' + }, + { + id: 'nineteenth-changban-sortie', + bgm: 'battle-prep', + chapter: '장판파 피난로', + background: 'story-liu-bei', + speaker: '유비', + portrait: 'liuBei', + text: '오늘의 싸움은 이기는 것만으로 끝나지 않소. 백성이 지나갈 길을 열고, 조운과 익덕이 무너지지 않게 서로 받쳐야 하오. 함께할 무장을 신중히 정합시다.' + } +]; + +export const nineteenthBattleVictoryPages: StoryPage[] = [ + { + id: 'nineteenth-victory-refuge-road', + bgm: 'militia-theme', + chapter: '피난로가 열리다', + background: 'story-militia', + text: '장판파의 먼지 속에서 유비군은 피난 행렬을 가까스로 남쪽 길로 흘려보냈습니다. 조운의 창과 장비의 호령이 무너질 뻔한 길목을 붙들었습니다.' + }, + { + id: 'nineteenth-zhao-yun-returns', + bgm: 'battle-prep', + chapter: '흩어진 깃발을 모아', + background: 'story-three-heroes', + speaker: '조운', + text: '흩어진 병사와 백성을 모두 되찾지는 못했습니다. 그러나 주공의 깃발이 뒤돌아보았기에 사람들은 다시 모일 곳을 알게 되었습니다.' + }, + { + id: 'nineteenth-jiangdong-envoy', + bgm: 'story-dark', + chapter: '강동으로 보낼 사절', + background: 'story-liu-bei', + speaker: '제갈량', + text: '조조의 남하는 이제 강동도 외면할 수 없습니다. 장판파에서 지켜낸 백성의 길과 조조군의 속도를 함께 전하면, 손권과 마주 앉을 명분이 생깁니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1450,6 +1513,12 @@ export const eighteenthBattleMap: BattleMap = { terrain: createEighteenthBattleTerrain() }; +export const nineteenthBattleMap: BattleMap = { + width: 38, + height: 28, + terrain: createNineteenthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -6792,6 +6861,384 @@ export const eighteenthBattleUnits: UnitData[] = [ } ]; +const nineteenthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 22 }, + 'guan-yu': { x: 5, y: 20 }, + 'zhang-fei': { x: 6, y: 22 }, + 'jian-yong': { x: 3, y: 23 }, + 'mi-zhu': { x: 4, y: 24 }, + 'sun-qian': { x: 6, y: 24 }, + 'zhao-yun': { x: 8, y: 20 }, + 'zhuge-liang': { x: 5, y: 23 } +}; + +export const nineteenthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits + ].map((unit) => placeScenarioUnit(unit, nineteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'changban-scout-a', + name: '조조군 척후', + faction: 'enemy', + className: '추격 척후', + classKey: 'bandit', + level: 22, + exp: 30, + hp: 66, + maxHp: 66, + attack: 25, + move: 4, + stats: { might: 98, intelligence: 72, leadership: 78, agility: 98, luck: 68 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 22 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 70 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 13, + y: 21 + }, + { + id: 'changban-scout-b', + name: '조조군 척후', + faction: 'enemy', + className: '측면 척후', + classKey: 'bandit', + level: 22, + exp: 30, + hp: 66, + maxHp: 66, + attack: 25, + move: 4, + stats: { might: 98, intelligence: 72, leadership: 78, agility: 98, luck: 68 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 22 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 70 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 12, + y: 24 + }, + { + id: 'changban-cavalry-a', + name: '호표 추격기', + faction: 'enemy', + className: '조조 경기병', + classKey: 'cavalry', + level: 23, + exp: 32, + hp: 88, + maxHp: 88, + attack: 30, + move: 5, + stats: { might: 112, intelligence: 72, leadership: 90, agility: 106, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 28 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 4 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 16, + y: 18 + }, + { + id: 'changban-cavalry-b', + name: '호표 추격기', + faction: 'enemy', + className: '조조 경기병', + classKey: 'cavalry', + level: 23, + exp: 32, + hp: 88, + maxHp: 88, + attack: 30, + move: 5, + stats: { might: 112, intelligence: 72, leadership: 90, agility: 106, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 28 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 4 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 17, + y: 22 + }, + { + id: 'changban-cavalry-c', + name: '조조군 선봉기', + faction: 'enemy', + className: '선봉 기병', + classKey: 'cavalry', + level: 24, + exp: 34, + hp: 92, + maxHp: 92, + attack: 31, + move: 5, + stats: { might: 114, intelligence: 74, leadership: 92, agility: 108, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 34 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 10 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 22, + y: 17 + }, + { + id: 'changban-cavalry-d', + name: '조조군 선봉기', + faction: 'enemy', + className: '선봉 기병', + classKey: 'cavalry', + level: 24, + exp: 34, + hp: 92, + maxHp: 92, + attack: 31, + move: 5, + stats: { might: 114, intelligence: 74, leadership: 92, agility: 108, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 34 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 10 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 21 + }, + { + id: 'changban-infantry-a', + name: '조조 보병', + faction: 'enemy', + className: '추격 보병', + classKey: 'yellowTurban', + level: 22, + exp: 30, + hp: 82, + maxHp: 82, + attack: 27, + move: 3, + stats: { might: 106, intelligence: 74, leadership: 92, agility: 78, luck: 68 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 26 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 2 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 18, + y: 16 + }, + { + id: 'changban-infantry-b', + name: '조조 보병', + faction: 'enemy', + className: '추격 보병', + classKey: 'yellowTurban', + level: 22, + exp: 30, + hp: 82, + maxHp: 82, + attack: 27, + move: 3, + stats: { might: 106, intelligence: 74, leadership: 92, agility: 78, luck: 68 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 26 }, + armor: { itemId: 'lamellar-armor', level: 3, exp: 2 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 20, + y: 20 + }, + { + id: 'changban-infantry-c', + name: '장판 보병', + faction: 'enemy', + className: '다리목 보병', + classKey: 'yellowTurban', + level: 23, + exp: 32, + hp: 86, + maxHp: 86, + attack: 28, + move: 3, + stats: { might: 108, intelligence: 76, leadership: 94, agility: 80, luck: 69 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 30 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 6 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 26, + y: 16 + }, + { + id: 'changban-infantry-d', + name: '장판 보병', + faction: 'enemy', + className: '다리목 보병', + classKey: 'yellowTurban', + level: 23, + exp: 32, + hp: 86, + maxHp: 86, + attack: 28, + move: 3, + stats: { might: 108, intelligence: 76, leadership: 94, agility: 80, luck: 69 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 30 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 6 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 28, + y: 20 + }, + { + id: 'changban-archer-a', + name: '조조 궁병', + faction: 'enemy', + className: '언덕 궁병', + classKey: 'archer', + level: 22, + exp: 30, + hp: 64, + maxHp: 64, + attack: 27, + move: 3, + stats: { might: 94, intelligence: 86, leadership: 84, agility: 92, luck: 70 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 24 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 0 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 21, + y: 14 + }, + { + id: 'changban-archer-b', + name: '조조 궁병', + faction: 'enemy', + className: '강변 궁병', + classKey: 'archer', + level: 22, + exp: 30, + hp: 64, + maxHp: 64, + attack: 27, + move: 3, + stats: { might: 94, intelligence: 86, leadership: 84, agility: 92, luck: 70 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 24 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 0 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 24, + y: 23 + }, + { + id: 'changban-archer-c', + name: '조조 궁병', + faction: 'enemy', + className: '후방 궁병', + classKey: 'archer', + level: 23, + exp: 32, + hp: 66, + maxHp: 66, + attack: 28, + move: 3, + stats: { might: 96, intelligence: 88, leadership: 86, agility: 94, luck: 70 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 30 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 4 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 31, + y: 18 + }, + { + id: 'changban-guard-a', + name: '조조 친위', + faction: 'enemy', + className: '호표 친위', + classKey: 'yellowTurban', + level: 24, + exp: 34, + hp: 94, + maxHp: 94, + attack: 30, + move: 3, + stats: { might: 114, intelligence: 78, leadership: 102, agility: 84, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 38 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 33, + y: 16 + }, + { + id: 'changban-guard-b', + name: '조조 친위', + faction: 'enemy', + className: '호표 친위', + classKey: 'yellowTurban', + level: 24, + exp: 34, + hp: 94, + maxHp: 94, + attack: 30, + move: 3, + stats: { might: 114, intelligence: 78, leadership: 102, agility: 84, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 38 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 14 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 33, + y: 20 + }, + { + id: 'changban-strategist-a', + name: '조조군 군리', + faction: 'enemy', + className: '군리', + classKey: 'archer', + level: 23, + exp: 32, + hp: 68, + maxHp: 68, + attack: 27, + move: 3, + stats: { might: 76, intelligence: 96, leadership: 88, agility: 84, luck: 72 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 34 }, + armor: { itemId: 'cloth-armor', level: 3, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 31, + y: 14 + }, + { + id: 'changban-leader-cao-chun', + name: '조순', + faction: 'enemy', + className: '호표기장', + classKey: 'rebelLeader', + level: 25, + exp: 38, + hp: 126, + maxHp: 126, + attack: 33, + move: 5, + stats: { might: 118, intelligence: 82, leadership: 110, agility: 96, luck: 74 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 3, exp: 52 }, + armor: { itemId: 'reinforced-lamellar', level: 3, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 35, + y: 18 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -6942,6 +7389,7 @@ export const seventeenthBattleBonds: BattleBond[] = sixteenthBattleBonds.map(clo export const eighteenthBattleBonds: BattleBond[] = [...sixteenthBattleBonds, ...zhugeRecruitBonds].map( cloneBattleBondForScenario ); +export const nineteenthBattleBonds: BattleBond[] = [...eighteenthBattleBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -7450,6 +7898,62 @@ function createEighteenthBattleTerrain(): TerrainType[][] { ); } +function createNineteenthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 28 }, (_, y) => + Array.from({ length: 38 }, (_, x): TerrainType => { + if ((x <= 3 && y >= 21 && y <= 26) || (x >= 4 && x <= 7 && y >= 23 && y <= 26)) { + return 'camp'; + } + if ((x >= 29 && x <= 36 && y >= 15 && y <= 22) || (x >= 32 && x <= 37 && y >= 12 && y <= 14)) { + return 'fort'; + } + if ((x >= 8 && x <= 10 && y >= 20 && y <= 22) || (x >= 20 && x <= 22 && y >= 17 && y <= 19)) { + return 'village'; + } + if ((x === 15 || x === 16) && y >= 0 && y <= 17) { + return 'river'; + } + if ((x === 23 || x === 24) && y >= 7 && y <= 27) { + return 'river'; + } + if ( + (y === 22 && x >= 3 && x <= 14) || + (y === 19 && x >= 13 && x <= 23) || + (y === 17 && x >= 23 && x <= 35) || + (x === 31 && y >= 13 && y <= 21) + ) { + return 'road'; + } + if ( + (x >= 4 && x <= 14 && y === 25 - Math.floor(x / 2)) || + (x >= 16 && x <= 30 && y === Math.floor(x / 2) + 8) || + (x >= 7 && x <= 13 && y === 22) + ) { + return 'road'; + } + if ( + (x >= 5 && x <= 14 && y >= 9 && y <= 18) || + (x >= 16 && x <= 22 && y >= 20 && y <= 27) || + (x <= 9 && y <= 8) || + (x >= 26 && x <= 35 && y <= 9) + ) { + return 'forest'; + } + if ( + (x >= 9 && x <= 14 && y >= 2 && y <= 6) || + (x >= 25 && x <= 31 && y >= 22 && y <= 27) || + (x >= 29 && x <= 37 && y >= 3 && y <= 7) + ) { + return 'hill'; + } + if ((x <= 1 && y <= 13) || (x >= 36 && y <= 11) || (x >= 36 && 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 07b36f4..8c9e710 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -171,7 +171,24 @@ const unitTexture: Record = { 'bowang-guard-a': 'unit-rebel', 'bowang-guard-b': 'unit-rebel', 'bowang-strategist-a': 'unit-rebel-archer', - 'bowang-leader-xiahou-dun': 'unit-rebel-leader' + 'bowang-leader-xiahou-dun': 'unit-rebel-leader', + 'changban-scout-a': 'unit-rebel', + 'changban-scout-b': 'unit-rebel', + 'changban-cavalry-a': 'unit-rebel-cavalry', + 'changban-cavalry-b': 'unit-rebel-cavalry', + 'changban-cavalry-c': 'unit-rebel-cavalry', + 'changban-cavalry-d': 'unit-rebel-cavalry', + 'changban-infantry-a': 'unit-rebel', + 'changban-infantry-b': 'unit-rebel', + 'changban-infantry-c': 'unit-rebel', + 'changban-infantry-d': 'unit-rebel', + 'changban-archer-a': 'unit-rebel-archer', + 'changban-archer-b': 'unit-rebel-archer', + 'changban-archer-c': 'unit-rebel-archer', + 'changban-guard-a': 'unit-rebel', + 'changban-guard-b': 'unit-rebel', + 'changban-strategist-a': 'unit-rebel-archer', + 'changban-leader-cao-chun': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -798,7 +815,24 @@ const enemyAiByUnitId: Record = { 'bowang-guard-a': 'guard', 'bowang-guard-b': 'guard', 'bowang-strategist-a': 'hold', - 'bowang-leader-xiahou-dun': 'guard' + 'bowang-leader-xiahou-dun': 'guard', + 'changban-scout-a': 'aggressive', + 'changban-scout-b': 'aggressive', + 'changban-cavalry-a': 'aggressive', + 'changban-cavalry-b': 'aggressive', + 'changban-cavalry-c': 'aggressive', + 'changban-cavalry-d': 'aggressive', + 'changban-infantry-a': 'guard', + 'changban-infantry-b': 'guard', + 'changban-infantry-c': 'guard', + 'changban-infantry-d': 'guard', + 'changban-archer-a': 'hold', + 'changban-archer-b': 'hold', + 'changban-archer-c': 'hold', + 'changban-guard-a': 'guard', + 'changban-guard-b': 'guard', + 'changban-strategist-a': 'hold', + 'changban-leader-cao-chun': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 1c9b80a..c3f77c4 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 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'; +import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg'; import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg'; import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg'; import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg'; @@ -94,6 +95,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl); this.load.image('battle-map-seventeenth', seventeenthBattleMapUrl); this.load.image('battle-map-eighteenth', eighteenthBattleMapUrl); + this.load.image('battle-map-nineteenth', nineteenthBattleMapUrl); 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 9d59c7c..e418ef4 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'], + battleIds: ['eighteenth-battle-bowang-ambush', 'nineteenth-battle-changban-refuge'], nextHints: ['강동 사절', '화공 준비', '조조 남하'] }, { @@ -320,7 +320,8 @@ const campBattleIds = { fifteenth: 'fifteenth-battle-yuan-refuge-road', sixteenth: 'sixteenth-battle-liu-biao-refuge', seventeenth: 'seventeenth-battle-wolong-visit-road', - eighteenth: 'eighteenth-battle-bowang-ambush' + eighteenth: 'eighteenth-battle-bowang-ambush', + nineteenth: 'nineteenth-battle-changban-refuge' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -511,6 +512,18 @@ const sortieRulesByBattleId: Partial