diff --git a/AGENTS.md b/AGENTS.md index 39ffce7..1d5fcd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,3 +6,4 @@ - For this project, whenever source changes are made, commit the changes and push them to the Gitea remote before finishing the task. - The game targets normal operation on PC/desktop browsers; do not prioritize mobile layout or mobile-specific work. - 이미지는 KOEI 삼국지 스타일을 활용하되, 실제 저작권 원본 이미지나 로고를 복제하지 않고 프로젝트 고유 자산으로 제작한다. +- 캠페인 큰 줄기는 유비의 삼국지 서사를 따른다: 황건적 토벌 → 반동탁 흐름 → 공손찬 의탁 → 도겸에게 서주 인수 → 여포에게 서주 상실 → 조조 의탁 → 조조 이탈 → 원소 의탁 → 유표 의탁 → 제갈량 영입 → 적벽대전 → 형주·익주 확보 → 촉한 건국. diff --git a/docs/roadmap.md b/docs/roadmap.md index c6f5057..7c78059 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -35,3 +35,4 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Use original names, portraits, maps, and dialogue inspired by historical tactical RPGs. - Keep copyrighted scripts, sprites, portraits, music, and maps out of the repository. - Prefer data-driven scenarios so new chapters and battlefields can be added without rewriting scene code. +- Follow Liu Bei's long-form campaign arc: Yellow Turban suppression -> anti-Dong Zhuo era -> service under Gongsun Zan -> inheritance of Xu Province from Tao Qian -> loss of Xu Province to Lu Bu -> service under Cao Cao -> break from Cao Cao -> service under Yuan Shao -> refuge under Liu Biao -> recruitment of Zhuge Liang -> Red Cliffs -> securing Jing and Yi Provinces -> founding of Shu Han. diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 4b6872e..7037248 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -358,7 +358,7 @@ try { return activeScenes.includes('StoryScene'); }); - for (let i = 0; i < 8; i += 1) { + for (let i = 0; i < 14; i += 1) { const isSecondBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit'; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts new file mode 100644 index 0000000..7737bd4 --- /dev/null +++ b/src/game/data/campaignFlow.ts @@ -0,0 +1,57 @@ +import type { CampaignStep } from '../state/campaignState'; +import { defaultBattleScenario, secondBattleScenario, thirdBattleScenario, type BattleScenarioId } from './battles'; +import { + firstBattleVictoryPages, + secondBattleIntroPages, + secondBattleVictoryPages, + thirdBattleIntroPages, + type StoryPage +} from './scenario'; + +export type SortieFlow = { + afterBattleId: string; + eyebrow: string; + title: string; + description: string; + rewardHint: string; + nextBattleId?: BattleScenarioId; + campaignStep?: CampaignStep; + pages: StoryPage[]; + unavailableNotice?: string; +}; + +const sortieFlows: Record = { + [defaultBattleScenario.id]: { + afterBattleId: defaultBattleScenario.id, + eyebrow: '다음 전장', + title: secondBattleScenario.title, + description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.', + rewardHint: '예상 보상: 군자금, 소모품, 의용군 명성', + nextBattleId: secondBattleScenario.id, + campaignStep: 'second-battle', + pages: [...firstBattleVictoryPages, ...secondBattleIntroPages] + }, + [secondBattleScenario.id]: { + afterBattleId: secondBattleScenario.id, + eyebrow: '다음 전장', + title: thirdBattleScenario.title, + description: '탁현을 넘어 광종으로 향하는 길목에서 황건 전령이 관군의 움직임을 본대로 전하려 합니다. 강가 요새와 좁은 길을 돌파해야 합니다.', + rewardHint: `예상 보상: ${thirdBattleScenario.title} 개방`, + nextBattleId: thirdBattleScenario.id, + campaignStep: 'third-battle', + pages: [...secondBattleVictoryPages, ...thirdBattleIntroPages] + }, + [thirdBattleScenario.id]: { + afterBattleId: thirdBattleScenario.id, + eyebrow: '다음 장 준비', + title: '광종 본전', + description: '광종 구원로는 열렸지만 황건 본대는 아직 건재합니다. 다음 작업에서 광종 본전과 더 큰 전장 목표를 이어 붙일 수 있습니다.', + rewardHint: '다음 장: 광종 본전 준비 중', + pages: [], + unavailableNotice: '광종 본전은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.' + } +}; + +export function getSortieFlow(latestBattleId?: string) { + return sortieFlows[latestBattleId ?? defaultBattleScenario.id] ?? sortieFlows[defaultBattleScenario.id]; +} diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 39f6c77..67ce459 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -2,8 +2,9 @@ import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems'; import { getUnitClass } from '../data/battleRules'; -import { defaultBattleScenario, secondBattleScenario, thirdBattleScenario } from '../data/battles'; -import { firstBattleBonds, firstBattleUnits, secondBattleIntroPages, thirdBattleIntroPages, type PortraitKey, type UnitData } from '../data/scenario'; +import { defaultBattleScenario } from '../data/battles'; +import { getSortieFlow } from '../data/campaignFlow'; +import { firstBattleBonds, firstBattleUnits, type PortraitKey, type UnitData } from '../data/scenario'; import { applyCampBondExp, getCampaignState, @@ -398,37 +399,17 @@ export class CampScene extends Phaser.Scene { bg.setStrokeStyle(1, palette.gold, 0.4); const completedDialogues = this.completedCampDialogues().length; const inventory = this.inventoryLabels().join(', '); - const reward = - this.campaign?.latestBattleId === thirdBattleScenario.id - ? '다음 장: 광종 본전 준비 중' - : this.campaign?.latestBattleId === secondBattleScenario.id - ? `예상 보상: ${thirdBattleScenario.title} 개방` - : '예상 보상: 군자금, 소모품, 의용군 명성'; + const reward = getSortieFlow(this.campaign?.latestBattleId).rewardHint; this.trackSortie(this.add.text(x + 16, y + 10, reward, this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1); this.trackSortie(this.add.text(x + 16, y + 30, `현재 준비: 대화 ${completedDialogues}/${campDialogues.length} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1); } private nextSortieBriefing() { - if (this.campaign?.latestBattleId === thirdBattleScenario.id) { - return { - eyebrow: '다음 장 준비', - title: '광종 본전', - description: '광종 구원로는 열렸지만 황건 본대는 아직 건재합니다. 다음 작업에서 광종 본전과 더 큰 전장 목표를 이어 붙일 수 있습니다.' - }; - } - - if (this.campaign?.latestBattleId === secondBattleScenario.id) { - return { - eyebrow: '다음 전장', - title: thirdBattleScenario.title, - description: '탁현을 넘어 광종으로 향하는 길목에서 황건 전령이 관군의 움직임을 본대로 전하려 합니다. 강가 요새와 좁은 길을 돌파해야 합니다.' - }; - } - + const flow = getSortieFlow(this.campaign?.latestBattleId); return { - eyebrow: '다음 전장', - title: secondBattleScenario.title, - description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.' + eyebrow: flow.eyebrow, + title: flow.title, + description: flow.description }; } @@ -484,27 +465,19 @@ export class CampScene extends Phaser.Scene { } private startVictoryStory() { - if (this.campaign?.latestBattleId === thirdBattleScenario.id) { + const flow = getSortieFlow(this.campaign?.latestBattleId); + + if (!flow.nextBattleId || !flow.campaignStep || flow.pages.length === 0) { this.hideSortiePrep(); - this.showCampNotice('광종 본전은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'); + this.showCampNotice(flow.unavailableNotice ?? '다음 장은 아직 준비 중입니다. 군영에서 성장 상태를 정비하세요.'); return; } - if (this.campaign?.latestBattleId === secondBattleScenario.id) { - markCampaignStep('third-battle'); - this.scene.start('StoryScene', { - pages: [...secondBattleScenario.victoryPages, ...thirdBattleIntroPages], - nextScene: 'BattleScene', - nextSceneData: { battleId: thirdBattleScenario.id } - }); - return; - } - - markCampaignStep('second-battle'); + markCampaignStep(flow.campaignStep); this.scene.start('StoryScene', { - pages: secondBattleIntroPages, + pages: flow.pages, nextScene: 'BattleScene', - nextSceneData: { battleId: secondBattleScenario.id } + nextSceneData: { battleId: flow.nextBattleId } }); }