diff --git a/docs/asset-backlog.md b/docs/asset-backlog.md index 8430566..5e61b59 100644 --- a/docs/asset-backlog.md +++ b/docs/asset-backlog.md @@ -6,9 +6,9 @@ generated, connected, validated, built, flow-tested, committed, and pushed. ## Current Baseline -- Story images: 54 files after Batch 2. -- Portraits: 26 files after Batch 2. -- Unit sheets: 114 files, 57 complete base/action design pairs after Batch 3. +- Story images: 64 files after Batch 4. +- Portraits: 31 files after Batch 4. +- Unit sheets: 122 files, 61 complete base/action design pairs after Batch 4. - Target direction: - Story: about 5 images per major campaign chapter across 65 campaign stages. - Portraits: one portrait for every important `scenario.ts` speaker. @@ -113,4 +113,40 @@ Keep the existing texture keys so code paths and saved scenarios continue to wor ### Remaining Portraits +- None. All currently mapped named speakers have portrait coverage after Batch 4. + +## Batch 4 Priority + +Status: complete. Image dimensions, unit base/action pairs, `pnpm build`, and `pnpm verify:flow` passed. + +This batch finishes the named-speaker portrait backlog, adds portrait variants that +are actually selected in story scenes, and continues expanding high-repeat story +backgrounds and class/faction unit variety. + +### Story + +- `55-yellow-pursuit-ambush.png` +- `56-yellow-pursuit-village-relief.png` +- `57-wolong-straw-boats.png` +- `58-red-cliffs-wind-prayer.png` +- `59-yiling-baidi-mountain-retreat.png` +- `60-maicheng-isolation-snow-road.png` +- `61-yiling-fire-attack-riverbank.png` +- `62-hanzhong-rain-night-watch.png` +- `63-weishui-northbank-cavalry-screen.png` +- `64-nanzhong-forest-council.png` + +### Portraits + - `ma-dai.png` +- `liu-bei-campaign.png` +- `guan-yu-campaign.png` +- `zhang-fei-campaign.png` +- `zhuge-liang-campaign.png` + +### Units + +- `unit-shu-spearman-veteran.png` + `unit-shu-spearman-veteran-actions.png` +- `unit-wei-strategist-blue.png` + `unit-wei-strategist-blue-actions.png` +- `unit-wu-officer-river.png` + `unit-wu-officer-river-actions.png` +- `unit-nanman-officer-warlord.png` + `unit-nanman-officer-warlord-actions.png` diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 4747147..7f74616 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -9147,7 +9147,7 @@ try { weishuiCampsStoryState?.background !== 'story-weishui-camps' || weishuiCampsStoryState?.backgroundReady !== true || weishuiCampsStoryState?.portraitKey !== 'zhugeLiang' || - weishuiCampsStoryState?.portraitTextureKey !== 'portrait-zhuge-liang' + !weishuiCampsStoryState?.portraitTextureKey?.startsWith('portrait-zhuge-liang') ) { throw new Error(`Expected Weishui camps story to lazy-load its new generated background and show Zhuge Liang portrait by speaker fallback: ${JSON.stringify(weishuiCampsStoryState)}`); } @@ -9324,13 +9324,20 @@ try { const northbankStoryState = await page.evaluate(() => window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.() ); + const northbankExpectedPortraits = { + 'sixty-fifth-northbank-council': ['zhugeLiang', 'portrait-zhuge-liang'], + 'sixty-fifth-forward-ledger': ['huangQuan', 'portrait-huang-quan'], + 'sixty-fifth-rearguard-threat': ['jiangWei', 'portrait-jiang-wei'] + }; + const northbankExpectedPortrait = northbankExpectedPortraits[northbankStoryState?.currentPageId]; if ( northbankStoryState?.background !== 'story-weishui-northbank' || northbankStoryState?.backgroundReady !== true || - northbankStoryState?.portraitKey !== 'zhugeLiang' || - northbankStoryState?.portraitTextureKey !== 'portrait-zhuge-liang' + !northbankExpectedPortrait || + northbankStoryState?.portraitKey !== northbankExpectedPortrait[0] || + !northbankStoryState?.portraitTextureKey?.startsWith(northbankExpectedPortrait[1]) ) { - throw new Error(`Expected Weishui north bank story to lazy-load its new generated background and show Zhuge Liang portrait by speaker fallback: ${JSON.stringify(northbankStoryState)}`); + throw new Error(`Expected Weishui north bank story to lazy-load its new generated background and show the current speaker portrait by fallback: ${JSON.stringify(northbankStoryState)}`); } await page.screenshot({ path: 'dist/verification-sixty-fifth-story.png', fullPage: true }); diff --git a/src/assets/images/portraits/guan-yu-campaign.png b/src/assets/images/portraits/guan-yu-campaign.png new file mode 100644 index 0000000..6bb8dc3 Binary files /dev/null and b/src/assets/images/portraits/guan-yu-campaign.png differ diff --git a/src/assets/images/portraits/liu-bei-campaign.png b/src/assets/images/portraits/liu-bei-campaign.png new file mode 100644 index 0000000..ee5709f Binary files /dev/null and b/src/assets/images/portraits/liu-bei-campaign.png differ diff --git a/src/assets/images/portraits/ma-dai.png b/src/assets/images/portraits/ma-dai.png new file mode 100644 index 0000000..34ada3c Binary files /dev/null and b/src/assets/images/portraits/ma-dai.png differ diff --git a/src/assets/images/portraits/zhang-fei-campaign.png b/src/assets/images/portraits/zhang-fei-campaign.png new file mode 100644 index 0000000..4c3cb4b Binary files /dev/null and b/src/assets/images/portraits/zhang-fei-campaign.png differ diff --git a/src/assets/images/portraits/zhuge-liang-campaign.png b/src/assets/images/portraits/zhuge-liang-campaign.png new file mode 100644 index 0000000..fdd151f Binary files /dev/null and b/src/assets/images/portraits/zhuge-liang-campaign.png differ diff --git a/src/assets/images/story/55-yellow-pursuit-ambush.png b/src/assets/images/story/55-yellow-pursuit-ambush.png new file mode 100644 index 0000000..7d63891 Binary files /dev/null and b/src/assets/images/story/55-yellow-pursuit-ambush.png differ diff --git a/src/assets/images/story/56-yellow-pursuit-village-relief.png b/src/assets/images/story/56-yellow-pursuit-village-relief.png new file mode 100644 index 0000000..41a87a3 Binary files /dev/null and b/src/assets/images/story/56-yellow-pursuit-village-relief.png differ diff --git a/src/assets/images/story/57-wolong-straw-boats.png b/src/assets/images/story/57-wolong-straw-boats.png new file mode 100644 index 0000000..b6ddeb6 Binary files /dev/null and b/src/assets/images/story/57-wolong-straw-boats.png differ diff --git a/src/assets/images/story/58-red-cliffs-wind-prayer.png b/src/assets/images/story/58-red-cliffs-wind-prayer.png new file mode 100644 index 0000000..ed9da95 Binary files /dev/null and b/src/assets/images/story/58-red-cliffs-wind-prayer.png differ diff --git a/src/assets/images/story/59-yiling-baidi-mountain-retreat.png b/src/assets/images/story/59-yiling-baidi-mountain-retreat.png new file mode 100644 index 0000000..94c81ed Binary files /dev/null and b/src/assets/images/story/59-yiling-baidi-mountain-retreat.png differ diff --git a/src/assets/images/story/60-maicheng-isolation-snow-road.png b/src/assets/images/story/60-maicheng-isolation-snow-road.png new file mode 100644 index 0000000..e6ddc4e Binary files /dev/null and b/src/assets/images/story/60-maicheng-isolation-snow-road.png differ diff --git a/src/assets/images/story/61-yiling-fire-attack-riverbank.png b/src/assets/images/story/61-yiling-fire-attack-riverbank.png new file mode 100644 index 0000000..7d4da4d Binary files /dev/null and b/src/assets/images/story/61-yiling-fire-attack-riverbank.png differ diff --git a/src/assets/images/story/62-hanzhong-rain-night-watch.png b/src/assets/images/story/62-hanzhong-rain-night-watch.png new file mode 100644 index 0000000..2032cfc Binary files /dev/null and b/src/assets/images/story/62-hanzhong-rain-night-watch.png differ diff --git a/src/assets/images/story/63-weishui-northbank-cavalry-screen.png b/src/assets/images/story/63-weishui-northbank-cavalry-screen.png new file mode 100644 index 0000000..286dbef Binary files /dev/null and b/src/assets/images/story/63-weishui-northbank-cavalry-screen.png differ diff --git a/src/assets/images/story/64-nanzhong-forest-council.png b/src/assets/images/story/64-nanzhong-forest-council.png new file mode 100644 index 0000000..edffd2a Binary files /dev/null and b/src/assets/images/story/64-nanzhong-forest-council.png differ diff --git a/src/assets/images/units/unit-nanman-officer-warlord-actions.png b/src/assets/images/units/unit-nanman-officer-warlord-actions.png new file mode 100644 index 0000000..3a26e1c Binary files /dev/null and b/src/assets/images/units/unit-nanman-officer-warlord-actions.png differ diff --git a/src/assets/images/units/unit-nanman-officer-warlord.png b/src/assets/images/units/unit-nanman-officer-warlord.png new file mode 100644 index 0000000..8735f8f Binary files /dev/null and b/src/assets/images/units/unit-nanman-officer-warlord.png differ diff --git a/src/assets/images/units/unit-shu-spearman-veteran-actions.png b/src/assets/images/units/unit-shu-spearman-veteran-actions.png new file mode 100644 index 0000000..fdfe81c Binary files /dev/null and b/src/assets/images/units/unit-shu-spearman-veteran-actions.png differ diff --git a/src/assets/images/units/unit-shu-spearman-veteran.png b/src/assets/images/units/unit-shu-spearman-veteran.png new file mode 100644 index 0000000..cc49264 Binary files /dev/null and b/src/assets/images/units/unit-shu-spearman-veteran.png differ diff --git a/src/assets/images/units/unit-wei-strategist-blue-actions.png b/src/assets/images/units/unit-wei-strategist-blue-actions.png new file mode 100644 index 0000000..41724c0 Binary files /dev/null and b/src/assets/images/units/unit-wei-strategist-blue-actions.png differ diff --git a/src/assets/images/units/unit-wei-strategist-blue.png b/src/assets/images/units/unit-wei-strategist-blue.png new file mode 100644 index 0000000..c701a16 Binary files /dev/null and b/src/assets/images/units/unit-wei-strategist-blue.png differ diff --git a/src/assets/images/units/unit-wu-officer-river-actions.png b/src/assets/images/units/unit-wu-officer-river-actions.png new file mode 100644 index 0000000..2f5dc36 Binary files /dev/null and b/src/assets/images/units/unit-wu-officer-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-officer-river.png b/src/assets/images/units/unit-wu-officer-river.png new file mode 100644 index 0000000..66ea6a3 Binary files /dev/null and b/src/assets/images/units/unit-wu-officer-river.png differ diff --git a/src/game/data/portraitAssets.ts b/src/game/data/portraitAssets.ts index ac43ace..63f07c7 100644 --- a/src/game/data/portraitAssets.ts +++ b/src/game/data/portraitAssets.ts @@ -65,6 +65,16 @@ export function portraitTextureKey(key: string) { return portraitAssets[slug] ? `portrait-${slug}` : undefined; } +export function portraitTextureKeysForKey(key: string) { + const slug = portraitSlugForKey(key); + const prefix = `${slug}-`; + const slugs = Object.keys(portraitAssets) + .filter((candidate) => candidate === slug || candidate.startsWith(prefix)) + .sort(); + + return Array.from(new Set(slugs.map((candidate) => `portrait-${candidate}`))); +} + export function portraitKeyForSpeaker(speaker?: string) { return speaker ? speakerPortraitKeys[speaker] : undefined; } diff --git a/src/game/scenes/StoryScene.ts b/src/game/scenes/StoryScene.ts index dbadea0..e4a98f0 100644 --- a/src/game/scenes/StoryScene.ts +++ b/src/game/scenes/StoryScene.ts @@ -1,6 +1,6 @@ import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; -import { portraitKeyForSpeaker, portraitTextureKey } from '../data/portraitAssets'; +import { portraitKeyForSpeaker, portraitTextureKeysForKey } from '../data/portraitAssets'; import { storyBackgroundAssets, storyBackgroundKeysFor } from '../data/storyAssets'; import { prologuePages, type PortraitKey, type StoryPage } from '../data/scenario'; import { palette } from '../ui/palette'; @@ -238,8 +238,7 @@ export class StoryScene extends Phaser.Scene { this.applyBackground(page); this.chapterText?.setText(page.chapter); - const portraitKey = this.pagePortraitKey(page); - const textureKey = portraitKey ? portraitTextureKey(portraitKey) : undefined; + const textureKey = this.pagePortraitTextureKey(page); if (textureKey && this.textures.exists(textureKey)) { this.portraitFrame?.setVisible(true); @@ -268,6 +267,20 @@ export class StoryScene extends Phaser.Scene { return page.portrait ?? portraitKeyForSpeaker(page.speaker); } + private pagePortraitTextureKey(page: StoryPage) { + const portraitKey = this.pagePortraitKey(page); + if (!portraitKey) { + return undefined; + } + + const keys = portraitTextureKeysForKey(portraitKey).filter((key) => this.textures.exists(key)); + if (keys.length <= 1) { + return keys[0]; + } + + return keys[this.hashString(page.id) % keys.length]; + } + private pageBackgroundKey(page: StoryPage) { const keys = storyBackgroundKeysFor(page.background).filter((key) => this.textures.exists(key)); if (keys.length <= 1) {