diff --git a/docs/roadmap.md b/docs/roadmap.md index 3bccf0c..d9ea71d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -60,16 +60,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirty-sixth battle Dingjun Mountain vanguard route, using Huang Zhong and Fa Zheng's high-ground plan across a 72x54 Hanzhong battlefield and recruiting Wang Ping after the ridge is secured - Thirty-seventh battle Hanzhong decisive route, using Wang Ping's terrain knowledge and a seven-officer sortie choice across a 74x56 Hanzhong battlefield to push Cao Cao out and prepare Liu Bei's King of Hanzhong step - Post-Hanzhong King of Hanzhong council flow, where the sortie button can advance a story-only political camp milestone and persist a dedicated campaign step before Shu-Han foundation +- Shu-Han foundation story milestone, where the King of Hanzhong council advances into Liu Bei's formal state-building scene and points the next arc toward Guan Yu's Jing Province defense - Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter - Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer - Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers - Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses -- Flow verification script from title through the thirty-seventh battle victory and King of Hanzhong council 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 camp states, campaign timeline state, and camp save state +- Flow verification script from title through the thirty-seventh battle victory, King of Hanzhong council milestone, and 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 camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue from the King of Hanzhong council into the formal Shu-Han foundation scene, making Liu Bei's political transition feel like a payoff for the campaign arc -2. Add the next tension around Guan Yu's Jing Province defense so the Shu-Han foundation has an immediate strategic cost +1. Add the next tension around Guan Yu's Jing Province defense so the Shu-Han foundation has an immediate strategic cost +2. Build the first post-foundation Jing Province defense battle, using Guan Yu's isolated front and Wu/Wei pressure as the new tactical axis 3. Expand sortie selection into richer pre-battle officer planning, including explicit reserve drills, class practice, and bond-focused camp assignments 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 7acf61e..a4d6c67 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -4751,12 +4751,72 @@ try { postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || postKingCouncilProgressState.campaignProgress?.totalKnown !== 37 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || - postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 준비' + postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { throw new Error(`Expected King of Hanzhong council to move the progress tab toward Shu-Han foundation prep: ${JSON.stringify(postKingCouncilProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-king-council-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const shuHanFoundationSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !shuHanFoundationSortieState?.sortieVisible || + shuHanFoundationSortieState.campaign?.step !== 'hanzhong-king-camp' || + shuHanFoundationSortieState.campTitle !== '한중왕 즉위 준비 군영' + ) { + throw new Error(`Expected Shu-Han foundation story to be available from the King of Hanzhong camp: ${JSON.stringify(shuHanFoundationSortieState)}`); + } + + 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-shu-han-foundation-story.png', fullPage: true }); + + for (let i = 0; i < 16; i += 1) { + const returnedToCamp = await page.evaluate(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); + }); + if (returnedToCamp) { + break; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(320); + } + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); + }); + + const shuHanFoundationCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + shuHanFoundationCampState?.campaign?.step !== 'shu-han-foundation-camp' || + shuHanFoundationCampState.campTitle !== '촉한 건국 후 군영' || + shuHanFoundationCampState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' || + shuHanFoundationCampState.availableDialogueIds?.length !== 3 || + shuHanFoundationCampState.availableVisitIds?.length !== 2 + ) { + throw new Error(`Expected Shu-Han foundation story to persist the foundation camp step: ${JSON.stringify(shuHanFoundationCampState)}`); + } + await page.screenshot({ path: 'dist/verification-shu-han-foundation-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postFoundationProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postFoundationProgressState?.activeTab !== 'progress' || + postFoundationProgressState.campaignProgress?.completedKnown !== 37 || + postFoundationProgressState.campaignProgress?.totalKnown !== 37 || + postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || + postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 긴장' + ) { + throw new Error(`Expected Shu-Han foundation camp to point the progress tab toward Jing Province defense: ${JSON.stringify(postFoundationProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-foundation-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -4775,7 +4835,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirty-seventh-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, and Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, and King of Hanzhong council state at ${targetUrl}`); + console.log(`Verified title-to-thirty-seventh-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, and Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, and Shu-Han foundation state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 1f9f0d2..9e32bba 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -70,6 +70,7 @@ import { sixteenthBattleVictoryPages, seventeenthBattleIntroPages, seventeenthBattleVictoryPages, + shuHanFoundationPages, tenthBattleIntroPages, tenthBattleVictoryPages, thirteenthBattleIntroPages, @@ -523,9 +524,33 @@ const sortieFlows: Record = { campaignStep: 'hanzhong-king-camp', pages: hanzhongKingCouncilPages, unavailableNotice: '한중왕 즉위 준비가 완료되었습니다. 다음 작업에서는 촉한 건국 선포와 형주 방면의 긴장을 이어 준비합니다.' + }, + 'hanzhong-king-camp': { + afterBattleId: thirtySeventhBattleScenario.id, + eyebrow: '건국 선포', + title: '촉한 건국 선포', + description: + '한중왕 즉위 논의를 마쳤습니다. 이제 유비가 촉한의 이름을 세우고, 관우가 지키는 형주가 다음 긴장의 중심으로 떠오르는 정치 전환을 진행합니다.', + rewardHint: '군영 목표: 촉한 건국 선포 / 형주 방위 긴장 개방', + campaignStep: 'shu-han-foundation-camp', + pages: shuHanFoundationPages, + unavailableNotice: '촉한 건국 선포가 완료되었습니다. 다음 작업에서는 형주 방위와 관우 전선의 긴장을 전투 흐름으로 이어갑니다.' + }, + 'shu-han-foundation-camp': { + afterBattleId: thirtySeventhBattleScenario.id, + eyebrow: '다음 장 준비', + title: '형주 방위 긴장', + description: + '촉한의 이름이 세워졌습니다. 다음은 관우가 지키는 형주에서 손권과 조조의 시선이 겹치는 전선을 준비해야 합니다.', + rewardHint: '다음 장: 형주 방위 준비 중', + pages: [], + unavailableNotice: '형주 방위와 관우 전선은 다음 작업에서 이어집니다. 촉한 건국 이후의 첫 전략적 대가를 준비하십시오.' } }; -export function getSortieFlow(latestBattleId?: string) { +export function getSortieFlow(latestBattleId?: string, campaignStep?: CampaignStep) { + if (campaignStep && campaignStep in sortieFlows) { + return sortieFlows[campaignStep]; + } return sortieFlows[latestBattleId ?? defaultBattleScenario.id] ?? sortieFlows[defaultBattleScenario.id]; } diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 1c016fb..4c82c3d 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -2424,6 +2424,43 @@ export const hanzhongKingCouncilPages: StoryPage[] = [ } ]; +export const shuHanFoundationPages: StoryPage[] = [ + { + id: 'shu-han-foundation-banner', + bgm: 'militia-theme', + chapter: '한중왕 즉위', + background: 'story-three-heroes', + speaker: '제갈량', + text: '한중의 깃발 아래 장수들의 이름이 모였습니다. 유비는 한중왕의 이름을 받아들이며, 이 이름이 욕심이 아니라 한실을 잇는 약속임을 밝히려 합니다.' + }, + { + id: 'shu-han-foundation-oath', + bgm: 'battle-prep', + chapter: '촉한의 이름', + background: 'story-liu-bei', + speaker: '유비', + portrait: 'liuBei', + text: '우리가 세우려는 나라는 나 한 사람의 나라가 아니오. 도원에서 맺은 뜻, 전장을 지나 모인 장수들의 뜻, 그리고 백성을 지키려는 뜻을 촉한의 이름으로 세우겠소.' + }, + { + id: 'shu-han-foundation-generals', + bgm: 'battle-prep', + chapter: '오호의 깃발', + background: 'story-sortie', + speaker: '관우', + portrait: 'guanYu', + text: '형님께서 이름을 세우셨으니 장수들도 각자의 자리를 지켜야 합니다. 서쪽은 안정되었으나 형주의 바람은 점점 거칠어지고 있습니다.' + }, + { + id: 'shu-han-foundation-next-front', + bgm: 'story-dark', + chapter: '형주의 먹구름', + background: 'story-militia', + speaker: '제갈량', + text: '촉한의 이름은 세워졌지만, 그 이름은 새로운 책임을 부릅니다. 관우가 지키는 형주에는 손권과 조조의 시선이 함께 모이고 있습니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index ea68483..8c34030 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -5854,6 +5854,9 @@ export class CampScene extends Phaser.Scene { private currentCampTitle() { const battleId = this.currentCampBattleId(); + if (this.campaign?.step === 'shu-han-foundation-camp') { + return '촉한 건국 후 군영'; + } if (this.campaign?.step === 'hanzhong-king-camp') { return '한중왕 즉위 준비 군영'; } @@ -6380,7 +6383,7 @@ export class CampScene extends Phaser.Scene { } private nextSortieScenario() { - const nextBattleId = getSortieFlow(this.campaign?.latestBattleId).nextBattleId; + const nextBattleId = this.currentSortieFlow().nextBattleId; return nextBattleId ? getBattleScenario(nextBattleId) : undefined; } @@ -6581,7 +6584,7 @@ export class CampScene extends Phaser.Scene { const selectedSummary = selectedUnits.length > 0 ? `${selectedUnits.length}명 (${selectedPreview}${selectedUnits.length > 3 ? ' 외' : ''})` : '없음'; const inventorySummary = inventoryLabels.length > 0 ? `${inventoryLabels.slice(0, 4).join(', ')}${inventoryLabels.length > 4 ? ' 외' : ''}` : '없음'; - const reward = getSortieFlow(this.campaign?.latestBattleId).rewardHint; + const reward = this.currentSortieFlow().rewardHint; this.trackSortie(this.add.text(x + 16, y + 10, reward, this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1); this.trackSortie( this.add.text( @@ -6594,7 +6597,7 @@ export class CampScene extends Phaser.Scene { } private nextSortieBriefing() { - const flow = getSortieFlow(this.campaign?.latestBattleId); + const flow = this.currentSortieFlow(); return { eyebrow: flow.eyebrow, title: flow.title, @@ -6602,6 +6605,10 @@ export class CampScene extends Phaser.Scene { }; } + private currentSortieFlow() { + return getSortieFlow(this.campaign?.latestBattleId, this.campaign?.step); + } + private sortieChecklist(): SortieChecklistItem[] { const units = this.currentUnits().filter((unit) => unit.faction === 'ally'); const liuBei = units.find((unit) => unit.id === 'liu-bei'); @@ -6668,7 +6675,7 @@ export class CampScene extends Phaser.Scene { } private startVictoryStory() { - const flow = getSortieFlow(this.campaign?.latestBattleId); + const flow = this.currentSortieFlow(); if (!this.ensureSortieSelectionSaved()) { this.showCampNotice('출전할 무장을 선택하세요. 유비는 반드시 출전해야 합니다.'); return; @@ -6966,7 +6973,7 @@ export class CampScene extends Phaser.Scene { const activeChapterIndex = this.activeTimelineChapterIndex(completedIds); const activeChapter = campaignTimelineChapters[activeChapterIndex]; const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId; - const flow = getSortieFlow(this.campaign?.latestBattleId); + const flow = this.currentSortieFlow(); const nextBattleId = flow.nextBattleId; return { completedKnown, @@ -6974,8 +6981,8 @@ export class CampScene extends Phaser.Scene { activeChapterIndex, activeChapter, latestBattleTitle: latestBattleId ? getBattleScenario(latestBattleId).title : '없음', - nextBattleTitle: this.campaign?.step === 'hanzhong-king-camp' - ? '촉한 건국 준비' + nextBattleTitle: this.campaign?.step === 'shu-han-foundation-camp' + ? '형주 방위 긴장' : nextBattleId ? getBattleScenario(nextBattleId).title : flow.campaignStep @@ -6986,7 +6993,7 @@ export class CampScene extends Phaser.Scene { private activeTimelineChapterIndex(completedIds = this.completedBattleIds()) { const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId; - const nextBattleId = getSortieFlow(this.campaign?.latestBattleId).nextBattleId; + const nextBattleId = this.currentSortieFlow().nextBattleId; if (!nextBattleId && latestBattleId) { const latestChapterIndex = campaignTimelineChapters.findIndex((chapter) => chapter.battleIds.includes(latestBattleId as BattleScenarioId) diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index b739fe4..deb26cf 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -374,7 +374,8 @@ export class TitleScene extends Phaser.Scene { campaign.step === 'thirty-fifth-camp' || campaign.step === 'thirty-sixth-camp' || campaign.step === 'thirty-seventh-camp' || - campaign.step === 'hanzhong-king-camp' + campaign.step === 'hanzhong-king-camp' || + campaign.step === 'shu-han-foundation-camp' ) { this.scene.start('CampScene'); return; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index cbe7e5a..a9bfdc1 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -116,7 +116,8 @@ export type CampaignStep = | 'thirty-sixth-camp' | 'thirty-seventh-battle' | 'thirty-seventh-camp' - | 'hanzhong-king-camp'; + | 'hanzhong-king-camp' + | 'shu-han-foundation-camp'; export type CampaignUnitProgressSnapshot = { unitId: string;