543 lines
31 KiB
JavaScript
543 lines
31 KiB
JavaScript
// Per-game asset manifest for lazy loading.
|
||
//
|
||
// Assets listed here are NOT loaded by PreloadScene at startup. Instead,
|
||
// GameRoomScene loads a game's entry (via services/assetLoader.js) the first
|
||
// time the user enters that game; already-loaded keys are skipped, so repeat
|
||
// visits and cross-game shared assets (e.g. the mahjong tiles) are free.
|
||
//
|
||
// Descriptor shape:
|
||
// { type: 'image'|'spritesheet'|'audio'|'json', key, path, frameWidth?, frameHeight? }
|
||
//
|
||
// A slug's entry is an array of descriptors, a resolver `(scene) => [descriptors]`,
|
||
// or a mixed array of both. Resolvers exist for the drop-in artwork sheets whose
|
||
// paths live in the (always-preloaded) data/*-artwork.json manifests — a null
|
||
// `path` there means "not painted yet" and the game renders procedurally.
|
||
|
||
// Mahjong tile-label overlays (128×178 transparent PNGs), shared by the
|
||
// traditional Mahjong game and Mahjong Match solitaire. The White Dragon has
|
||
// no art — the scenes draw its traditional empty frame.
|
||
const MAHJONG_TILES = [
|
||
...Array.from({ length: 9 }, (_, i) => `bamboo${i + 1}`),
|
||
...Array.from({ length: 9 }, (_, i) => `circle${i + 1}`),
|
||
...Array.from({ length: 15 }, (_, i) => `pinyin${i + 1}`),
|
||
'orchid', 'peony', 'chrysanthemum', 'lotus',
|
||
'spring', 'summer', 'fall', 'winter',
|
||
].map((name) => ({ type: 'image', key: `mahjong-${name}`, path: `assets/images/mahjong/${name}.png` }));
|
||
|
||
// Drop-in spritesheets declared in a cached artwork JSON. Only entries with a
|
||
// `path` set are returned (until then the game renders procedurally) — the
|
||
// same contract PreloadScene used when it loaded these at startup.
|
||
function sheetsFrom(scene, jsonKey, fields) {
|
||
const art = scene.cache.json.get(jsonKey);
|
||
if (!art) return [];
|
||
const sheets = fields.flatMap((f) => {
|
||
const v = art[f];
|
||
return v && typeof v === 'object' && !('key' in v) ? Object.values(v) : [v];
|
||
});
|
||
return sheets
|
||
.filter((s) => s && s.key && s.path)
|
||
.map((s) => ({ type: 'spritesheet', key: s.key, path: s.path, frameWidth: s.frameWidth, frameHeight: s.frameHeight }));
|
||
}
|
||
|
||
// Drop-in standalone images declared in a cached artwork JSON's `artwork` array.
|
||
// Pull video descriptors out of an artwork manifest block keyed by id, e.g.
|
||
// mastervega's portraitVideos. An entry with `path: null` is one that has not
|
||
// been recorded yet and is skipped, exactly like a sheet with no PNG.
|
||
function videosFrom(scene, jsonKey, field) {
|
||
const art = scene.cache.json.get(jsonKey);
|
||
if (!art || !art[field]) return [];
|
||
return Object.entries(art[field])
|
||
.filter(([id, v]) => !id.startsWith('_') && v && v.key && v.path)
|
||
.map(([, v]) => ({ type: 'video', key: v.key, path: v.path }));
|
||
}
|
||
|
||
// Same contract as videosFrom, one level deeper: a block keyed
|
||
// group -> member -> entry, e.g. mastervega's shipVideos (species -> hull).
|
||
// A group whose value is null has nothing recorded yet and is skipped whole,
|
||
// which is what keeps the manifest short while nine species are outstanding.
|
||
function nestedVideosFrom(scene, jsonKey, field) {
|
||
const art = scene.cache.json.get(jsonKey);
|
||
if (!art || !art[field]) return [];
|
||
const out = [];
|
||
for (const [group, members] of Object.entries(art[field])) {
|
||
if (group.startsWith('_') || !members) continue;
|
||
for (const [id, v] of Object.entries(members)) {
|
||
if (id.startsWith('_') || !v || !v.key || !v.path) continue;
|
||
out.push({ type: 'video', key: v.key, path: v.path });
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Same shape as videosFrom, for image entries keyed by id.
|
||
function imagesFromMap(scene, jsonKey, field) {
|
||
const art = scene.cache.json.get(jsonKey);
|
||
if (!art || !art[field]) return [];
|
||
return Object.entries(art[field])
|
||
.filter(([id, v]) => !id.startsWith('_') && v && v.key && v.path)
|
||
.map(([, v]) => ({ type: 'image', key: v.key, path: v.path }));
|
||
}
|
||
|
||
function imagesFrom(scene, jsonKey) {
|
||
const art = scene.cache.json.get(jsonKey);
|
||
return (art?.artwork ?? [])
|
||
.filter((a) => a && a.key && a.path)
|
||
.map((a) => ({ type: 'image', key: a.key, path: a.path }));
|
||
}
|
||
|
||
// Drop-in per-game soundtrack tracks declared in a cached `<name>-music.json`
|
||
// (see services/soundtrack.js). Audio bytes lazy-load only when the owning
|
||
// game is entered — never part of the shared default-soundtrack preload.
|
||
function musicFrom(scene, jsonKey) {
|
||
const data = scene.cache.json.get(jsonKey);
|
||
return (data?.tracks ?? [])
|
||
.filter((t) => t && t.file)
|
||
.map((t, i) => ({ type: 'audio', key: `${jsonKey}-${i}`, path: `assets/music/${t.file}` }));
|
||
}
|
||
|
||
// Master of Vega's soundtrack is condition-driven (menu/peace/combat/one pool
|
||
// per diplomacy race — see VegaMusic.js) rather than one flat shuffled list,
|
||
// so its `<name>-music.json` nests tracks under named pools instead of a
|
||
// single top-level `tracks` array. Walk every pool and flatten them into the
|
||
// same {type:'audio', key, path} shape musicFrom produces. VegaMusic.js reads
|
||
// track file paths straight out of the cached JSON at playback time (it plays
|
||
// via plain `Audio` elements, not these cache keys) — this is purely so the
|
||
// mp3 bytes for the pools every game is guaranteed to hit soon (menu, peace,
|
||
// combat, the generic diplomacy fallback) are already warm.
|
||
//
|
||
// `diplomacy.bySpecies` is deliberately excluded, same reasoning as
|
||
// `colonyVideos` a few lines below: a given playthrough may contact only a
|
||
// handful of the game's nine species, if any, so pre-fetching all nine
|
||
// tracks at room entry pays for eight-plus that are never played. VegaMusic's
|
||
// own `new Audio(...)` at setDiplomacy() time is already the fetch — this
|
||
// manifest was only ever a race to get there first, and racing every race's
|
||
// track just to win a few of them is not worth the up-front weight.
|
||
function vegaMusicFrom(scene, jsonKey) {
|
||
const data = scene.cache.json.get(jsonKey);
|
||
if (!data) return [];
|
||
const out = [];
|
||
const addPool = (pool, keyPrefix) => {
|
||
(pool?.tracks ?? [])
|
||
.filter((t) => t && t.file)
|
||
.forEach((t, i) => out.push({ type: 'audio', key: `${jsonKey}-${keyPrefix}-${i}`, path: `assets/music/${t.file}` }));
|
||
};
|
||
addPool(data.fallback, 'fallback');
|
||
addPool(data.menu, 'menu');
|
||
addPool(data.peace, 'peace');
|
||
addPool(data.combat, 'combat');
|
||
addPool(data.diplomacy?.default, 'diplomacy-default');
|
||
return out;
|
||
}
|
||
|
||
const sheet = (key, path, frameWidth, frameHeight) => ({ type: 'spritesheet', key, path, frameWidth, frameHeight });
|
||
const image = (key, path) => ({ type: 'image', key, path });
|
||
|
||
export const MANIFEST = {
|
||
gootower: [
|
||
image('gootower-bg', 'assets/images/gootower/background-1.png'),
|
||
image('gootower-pipe', 'assets/images/gootower/pipe.png'),
|
||
image('gootower-gear', 'assets/images/gootower/gear.png'),
|
||
],
|
||
// Excitebike draws every pixel itself (see ExcitebikeRaster.js), so there is
|
||
// no art to fetch — only its soundtrack.
|
||
excitebike: [
|
||
(scene) => musicFrom(scene, 'nintendo-music'),
|
||
],
|
||
// Every sheet in mastervega-artwork.json starts with path:null and is painted
|
||
// procedurally by VegaArt.js, so this entry needs no edit when art arrives —
|
||
// only the JSON does.
|
||
mastervega: [
|
||
{ type: 'json', key: 'mastervega-rules', path: 'data/mastervega-rules.json' },
|
||
(scene) => sheetsFrom(scene, 'mastervega-artwork', ['sheets']),
|
||
(scene) => videosFrom(scene, 'mastervega-artwork', 'portraitVideos'),
|
||
(scene) => nestedVideosFrom(scene, 'mastervega-artwork', 'shipVideos'),
|
||
(scene) => imagesFromMap(scene, 'mastervega-artwork', 'portraitStills'),
|
||
(scene) => imagesFromMap(scene, 'mastervega-artwork', 'worldBackgrounds'),
|
||
// The artwork JSON's `colonyVideos` block is deliberately NOT resolved here.
|
||
// Note what that does and does not buy: Phaser's video loader downloads
|
||
// nothing (VideoFile.load() just records the URL), so this was never about
|
||
// 19 MB of bytes at game-room entry — it is about WHERE the fetch is
|
||
// decided. VegaColonyIntro.ensureColonyVideo() registers a clip and warms
|
||
// its bytes when a colony ship reaches a settleable world, which is a
|
||
// better moment than "entered the room" and a far better one than "the
|
||
// vignette is already on screen".
|
||
image('vega-menu-bg', 'assets/images/vega/background-menu.png'),
|
||
image('vega-menu-title', 'assets/images/vega/menu-title.png'),
|
||
// The Galactic News Network anchor desk loop — one global clip (not
|
||
// per-species like portraitVideos), so it's a plain literal here rather
|
||
// than a mastervega-artwork.json map entry. Eager-loaded so the first
|
||
// post-turn GNN open never stalls on a JIT fetch.
|
||
{ type: 'video', key: 'vega-gnn-anchor', path: 'assets/videos/vega/gnn.mp4' },
|
||
// The Galactic Council session ceremony's persistent anchor visual, same
|
||
// 2:3 eager-load treatment as the GNN clip above (VegaCouncilSession.js).
|
||
{ type: 'video', key: 'vega-council-anchor', path: 'assets/videos/vega/galactic-council.mp4' },
|
||
// Bombard/Invade popup clips are JIT-loaded per planet type instead —
|
||
// see bombardVideos in mastervega-artwork.json and VegaBombardScreen.js
|
||
// — same convention as colonyVideos/researchVideos/advisorVideos, none
|
||
// of which are listed here either.
|
||
// The colony-founding cue. It is not part of the shuffled soundtrack —
|
||
// VegaColonyIntro.js ducks that and plays this over the vignette instead.
|
||
// Small, and it has to be ready the instant the vignette opens.
|
||
{ type: 'audio', key: 'vega-colony-cue', path: 'assets/music/vega/colony.mp3' },
|
||
{ type: 'audio', key: 'laser-zap', path: 'assets/fx/laser-zap.mp3' },
|
||
{ type: 'audio', key: 'scifi-explode', path: 'assets/fx/scifi-explode.mp3' },
|
||
{ type: 'audio', key: 'ta-rocket-1', path: 'assets/fx/ta-rocket-1.mp3' },
|
||
{ type: 'audio', key: 'ta-rocket-2', path: 'assets/fx/ta-rocket-2.mp3' },
|
||
// UI sound effects (src/ui/Sounds.js's SFX.VEGA_*). Used to be loaded
|
||
// eagerly by PreloadScene for every game; moved here 2026-08-09 alongside
|
||
// Brian's own move of the files into assets/fx/vega/.
|
||
{ type: 'audio', key: 'sfx-vega-select', path: 'assets/fx/vega/vega-select.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-build', path: 'assets/fx/vega/vega-build.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-close', path: 'assets/fx/vega/vega-close.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-newturn', path: 'assets/fx/vega/vega-newturn.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-unit', path: 'assets/fx/vega/vega-unit.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-view', path: 'assets/fx/vega/vega-view.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-warp', path: 'assets/fx/vega/vega-warp.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-star', path: 'assets/fx/vega/vega-star.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-zoomin', path: 'assets/fx/vega/vega-zoomin.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-zoomout', path: 'assets/fx/vega/vega-zoomout.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-viewcolony', path: 'assets/fx/vega/vega-viewcolony.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-endturn', path: 'assets/fx/vega/vega-endturn.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-counter', path: 'assets/fx/vega/vega-counter.mp3' },
|
||
// GNN's opening sting, and the looping ambience that takes over once it
|
||
// finishes and plays for as long as the GNN screen stays open (see
|
||
// VegaGnnScreen.js's openGnnScreen) — ducking the regular soundtrack
|
||
// the same way VegaColonyIntro.js's founding cue already does.
|
||
{ type: 'audio', key: 'sfx-vega-gnn-sting', path: 'assets/fx/vega/vega-gnn.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-gnn-loop', path: 'assets/music/vega/vega-gnn.mp3' },
|
||
// Same sting-then-loop shape for the Council Session ceremony
|
||
// (VegaCouncilSession.js's openCouncilSessionScreen).
|
||
{ type: 'audio', key: 'sfx-vega-council-intro', path: 'assets/fx/vega/vega-council-intro.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-council-loop', path: 'assets/music/vega/vega-council.mp3' },
|
||
// Combat weapon cues, Mark-banded (see VegaCombat.js's weaponSfxKey()).
|
||
{ type: 'audio', key: 'sfx-vega-weapon-123', path: 'assets/fx/vega/vega-weapon-123.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-weapon-45', path: 'assets/fx/vega/vega-weapon-45.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-weapon-67', path: 'assets/fx/vega/vega-weapon-67.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-missle-launch-12345', path: 'assets/fx/vega/vega-missle-launch-12345.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-missle-launch-67', path: 'assets/fx/vega/vega-missle-launch-67.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-missle-hit-12345', path: 'assets/fx/vega/vega-missle-hit-12345.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-missle-hit-67', path: 'assets/fx/vega/vega-missle-hit-67.mp3' },
|
||
// Planetary defence battery fire cue — a colony has no weapon mount to
|
||
// Mark-band like a ship (VegaCombatV2.js's planet entity is a flat
|
||
// damage scalar), so this is the one cue for every planet fire event.
|
||
{ type: 'audio', key: 'sfx-vega-planet-beam', path: 'assets/fx/vega/vega-planet-beam.mp3' },
|
||
// Per-warship-class destruction cues. Filename has a typo ("friggate")
|
||
// that the cache key does not repeat — see Sounds.js.
|
||
{ type: 'audio', key: 'sfx-vega-destroy-frigate', path: 'assets/fx/vega/vega-destroy-friggate.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-destroy-destroyer', path: 'assets/fx/vega/vega-destroy-destroyer.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-destroy-cruiser', path: 'assets/fx/vega/vega-destroy-cruiser.mp3' },
|
||
{ type: 'audio', key: 'sfx-vega-destroy-battleship', path: 'assets/fx/vega/vega-destroy-battleship.mp3' },
|
||
(scene) => vegaMusicFrom(scene, 'masterofvega-music'),
|
||
],
|
||
forbiddenisland: [
|
||
// Tiles: 2 cols (dry, flooded) × 24 rows. Row i → dry frame 2i, flooded
|
||
// frame 2i+1 (see IslandData.TILE_FRAME_ROW).
|
||
sheet('forbiddenisland-tiles', 'assets/images/forbiddenisland-tiles.png', 200, 200),
|
||
// Treasure-deck cards: 4 cols × 2 rows of 320×420 cells. Frame order
|
||
// documented in IslandData.CARD_FRAME.
|
||
sheet('forbiddenisland-cards', 'assets/images/forbiddenisland-cards.png', 320, 420),
|
||
],
|
||
catan: [
|
||
sheet('catan-cards', 'assets/images/catancards.png', 270, 390),
|
||
sheet('catan-tiles', 'assets/images/catantiles.png', 312, 312),
|
||
sheet('catan-special-cards', 'assets/images/catan-special-cards.png', 270, 390),
|
||
image('catan-robber', 'assets/images/catan-robber.png'),
|
||
image('catan-pirate', 'assets/images/catan-pirate.png'),
|
||
],
|
||
risk: [image('risk-board', 'assets/images/risk-board.png')],
|
||
bloxorz: [
|
||
image('bloxorz-bg-menu', 'assets/images/bloxorz/background-menu.png'),
|
||
...Array.from({ length: 6 }, (_, i) =>
|
||
image(`bloxorz-bg-${i + 1}`, `assets/images/bloxorz/background-${i + 1}.png`)),
|
||
],
|
||
jewelquest: [
|
||
image('bg-jewelquest-battle', 'assets/images/background-jewelquest.png'),
|
||
image('bg-jewelquest-menu', 'assets/images/background-jewelquest-menu.png'),
|
||
],
|
||
bejeweled: [image('bg-bejeweled', 'assets/images/background-bejeweledblitz.png')],
|
||
rushhour: [
|
||
// Carries its own title, so the level-select screen draws no heading.
|
||
// In-play art is procedural (see games/rushhour/RushHourArt.js).
|
||
image('bg-rushhour-menu', 'assets/images/background-rushhour.png'),
|
||
],
|
||
dominion: [
|
||
// Dominion card art. One 270×390 cell per card (art fills the top ~60%;
|
||
// the title/icon band is drawn at runtime). Optional — the scene falls
|
||
// back to procedural placeholders when the sheet is absent.
|
||
sheet('dominion-cards', 'assets/images/dominioncards.png', 270, 390),
|
||
// Prosperity expansion art (frame order documented in expansions/prosperity.js).
|
||
// Optional — same procedural fallback applies when the sheet is absent.
|
||
sheet('dominion-prosperity', 'assets/images/dominion-prosperity.png', 270, 390),
|
||
// Prosperity token sprites (1 VP, 5 VP, Gold) — 150×150 each.
|
||
sheet('dominion-tokens', 'assets/images/dominion-tokens.png', 150, 150),
|
||
],
|
||
splendor: [
|
||
// 28 frames at 270×390. 0-14 = tier×bonus backgrounds, 15-24 = nobles,
|
||
// 25-27 = deck backs. Frame order documented in SplendorData.js.
|
||
// Optional — vectors are drawn when absent.
|
||
sheet('splendor-cards', 'assets/images/splendor-cards.png', 270, 390),
|
||
// Gems: 6 frames at 64×64. white(0) blue(1) green(2) red(3) black(4) gold(5).
|
||
sheet('splendor-gems', 'assets/images/splendor-gems.png', 64, 64),
|
||
],
|
||
// Azul tiles: 6 frames at 96×96. blue(0) yellow(1) red(2) black(3) white(4)
|
||
// first-player(5). Frame order documented in AzulData.js. A placeholder
|
||
// sheet ships; vectors draw when absent.
|
||
azul: [sheet('azul-tiles', 'assets/images/azul-tiles.png', 96, 96)],
|
||
tickettoride: [sheet('ttr-cards', 'assets/images/tickettoride-cards.png', 270, 390)],
|
||
gofish: [sheet('gofish-cards', 'assets/images/gofish-cards.png', 270, 390)],
|
||
oldmaid: [sheet('oldmaid-cards', 'assets/images/oldmaid-cards.png', 270, 390)],
|
||
labyrinth: [
|
||
// Tile backgrounds: frame 0 = movable, 1 = fixed (corridors are drawn in
|
||
// code). Treasure overlays & cards share the treasure index as their
|
||
// frame. All optional — the scene draws vector fallbacks when absent.
|
||
sheet('labyrinth-tiles', 'assets/images/labyrinth-tiles.png', 200, 200),
|
||
sheet('labyrinth-treasures', 'assets/images/labyrinth-treasures.png', 100, 100),
|
||
sheet('labyrinth-cards', 'assets/images/labyrinth-cards.png', 270, 390),
|
||
],
|
||
// Stratego unit art: 12 transparent frames (0=Flag, 1..10=rank, 11=Bomb),
|
||
// 6 cols × 2 rows. Optional — the scene draws vector glyphs when absent.
|
||
stratego: [sheet('stratego-pieces', 'assets/images/stratego-pieces.png', 140, 140)],
|
||
monopoly: [
|
||
// Pawns: 4 frames (one per seat) at 80×80. Optional — falls back to colored circles.
|
||
sheet('monopoly-pawns', 'assets/images/monopoly-pawns.png', 80, 80),
|
||
// Card art: frame 0 = Chance, frame 1 = Community Chest, at 200×300.
|
||
sheet('monopoly-cards', 'assets/images/monopoly-cards.png', 200, 300),
|
||
],
|
||
puddingmonsters: [
|
||
sheet('jello-items', 'assets/images/jello-items.png', 132, 132),
|
||
image('bg-puddingmonsters-menu', 'assets/images/background-jello.png'),
|
||
],
|
||
mahjong: [
|
||
...MAHJONG_TILES,
|
||
// chinese soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Mahjong is actually entered.
|
||
(scene) => musicFrom(scene, 'chinese-music'),
|
||
],
|
||
mahjongmatch: [
|
||
...MAHJONG_TILES,
|
||
// Carries its own title, so the layout-select screen draws no heading.
|
||
image('bg-mahjongmatch-menu', 'assets/images/background-mahjongmatch.png'),
|
||
// In-play backdrops — one is picked at random per game.
|
||
...['01', '02', '03'].map((n) => image(`bg-mm-${n}`, `assets/images/background-mm-${n}.png`)),
|
||
// chinese soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Mahjong Match is actually entered.
|
||
(scene) => musicFrom(scene, 'chinese-music'),
|
||
],
|
||
spireclimb: [
|
||
image('spireclimb-act1', 'assets/images/spireclimb-act1.png'),
|
||
image('spireclimb-act2', 'assets/images/spireclimb-act2.png'),
|
||
image('spireclimb-act3', 'assets/images/spireclimb-act3.png'),
|
||
(scene) => sheetsFrom(scene, 'spireclimb-artwork', ['cardSheet', 'creatureSheet', 'playerSheets']),
|
||
],
|
||
shift: [
|
||
image('shift-background', 'assets/images/background-shift.png'),
|
||
(scene) => imagesFrom(scene, 'shift-artwork'),
|
||
],
|
||
slots: (scene) => imagesFrom(scene, 'slots-artwork'),
|
||
dungeonboss: (scene) => sheetsFrom(scene, 'dungeonboss-artwork',
|
||
['roomSheet', 'heroSheet', 'spellSheet', 'bossSheet', 'deckBackSheet', 'iconSheet']),
|
||
swdbg: (scene) => sheetsFrom(scene, 'swdbg-artwork', ['cardSheet', 'baseSheet']),
|
||
balatro: [
|
||
(scene) => sheetsFrom(scene, 'balatro-artwork',
|
||
['jokerSheet', 'tarotSheet', 'planetSheet', 'spectralSheet', 'voucherSheet', 'packSheet', 'deckBackSheet', 'bossIconSheet']),
|
||
// hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Balatro is actually entered.
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
// Optional drop-in art — spec in assets/gamedata/peggle/sprites.md. Missing
|
||
// files 404 harmlessly and the game renders procedurally: circles for
|
||
// pegs/ball, gradient starfield for the board background.
|
||
// Ship rotation sheets are described by each ship's `sprite` blocks in
|
||
// data/star-control-ships.json (written by tools/fetchStarControlAssets.js).
|
||
// Ships without a sheet render as procedural vector chevrons, so a missing
|
||
// file is harmless and new JSON ships need no manifest edit.
|
||
starcontrol: (scene) => {
|
||
const cfg = scene.cache.json.get('star-control-ships');
|
||
return Object.values(cfg?.ships ?? {})
|
||
.flatMap((s) => [s.sprite, s.projectileSprite, ...(s.forms ?? []).map((f) => f.sprite)])
|
||
.filter((sp) => sp?.sheet && sp.frameWidth > 0 && sp.frameHeight > 0)
|
||
.map((sp) => sheet(sp.sheet, `assets/images/starcontrol/${sp.sheet}.png`, sp.frameWidth, sp.frameHeight));
|
||
},
|
||
peggle: [
|
||
sheet('peggle-sprites', 'assets/images/peggle-sprites.png', 64, 64),
|
||
image('bg-peggle-menu', 'assets/images/background-peggle.png'),
|
||
// One board background per master, named by opponent id
|
||
// (assets/images/peggle/<masterId>.png, 1200×900).
|
||
(scene) => {
|
||
const levels = scene.cache.json.get('peggle-levels')?.levels ?? [];
|
||
const ids = [...new Set(levels.map((l) => l.masterId).filter(Boolean))];
|
||
return ids.map((id) => image(`peggle-bg-${id}`, `assets/images/peggle/${id}.png`));
|
||
},
|
||
],
|
||
// Rules data always loads; sprite sheets are optional drop-ins declared in
|
||
// data/civilization-artwork.json (spec: src/games/civilization/sprites.md).
|
||
// Sheets with path:null stay procedural. citySheets is a map keyed by theme
|
||
// (classic today; asian etc. drop in later with no code change).
|
||
civilization: [
|
||
{ type: 'json', key: 'civilization-rules', path: 'data/civilization-rules.json' },
|
||
image('civilization-setup-bg', 'assets/images/civilization/background.png'),
|
||
// Battle sounds keyed by each unit's `battleSound` in civilization-rules.json
|
||
// (played on attack by CivilizationMapView.playCombatEvent).
|
||
...['arrows', 'cannon', 'catapult', 'gunfire-classic', 'gunfire-modern',
|
||
'helicopter', 'missle', 'submarine', 'sword', 'tank']
|
||
.map((n) => ({ type: 'audio', key: `sfx-battle-${n}`, path: `assets/fx/battle-${n}.mp3` })),
|
||
(scene) => sheetsFrom(scene, 'civilization-artwork',
|
||
['terrainSheet', 'resourceSheet', 'improvementSheet', 'unitSheet', 'iconSheet',
|
||
'barbarianSheet', 'citySheets']),
|
||
],
|
||
// Cups/tracks index always loads (individual track-NNN.json files are
|
||
// fetched lazily at race start). Racer/theme/item sheets and backdrops are
|
||
// optional drop-ins declared in data/superkart-artwork.json — path:null
|
||
// entries stay procedural (tinted kart silhouettes, painted horizon).
|
||
superkart: [
|
||
{ type: 'json', key: 'superkart-cups', path: 'assets/gamedata/superkart/cups.json' },
|
||
image('superkart-background', 'assets/images/superkart-background.png'),
|
||
(scene) => sheetsFrom(scene, 'superkart-artwork', ['racerSheets', 'themeSheets', 'itemSheet']),
|
||
(scene) => {
|
||
const art = scene.cache.json.get('superkart-artwork');
|
||
return Object.values(art?.backdrops ?? {})
|
||
.filter((b) => b && b.key && b.path)
|
||
.map((b) => image(b.key, b.path));
|
||
},
|
||
// nintendo soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Super Kart is actually entered.
|
||
(scene) => musicFrom(scene, 'nintendo-music'),
|
||
],
|
||
// Rules + campaign always load; the painted sheet is an optional drop-in
|
||
// declared in data/advancewars-artwork.json (spec: src/games/advancewars/
|
||
// sprites.md) — path:null stays procedural. Shares Super Kart's nintendo
|
||
// soundtrack (see services/soundtrack.js).
|
||
advancewars: [
|
||
{ type: 'json', key: 'advancewars-rules', path: 'data/advancewars-rules.json' },
|
||
{ type: 'json', key: 'advancewars-campaign', path: 'data/advancewars-campaign.json' },
|
||
(scene) => sheetsFrom(scene, 'advancewars-artwork',
|
||
['terrainSheet', 'buildingSheet', 'unitSheet', 'uiSheet', 'battleBgSheet']),
|
||
// Battle sounds keyed by each unit's `battleSound` in advancewars-rules.json
|
||
// (played on attack by playBattleAnim in AdvanceWarsBattleAnim.js).
|
||
...['gunfire-modern', 'tank', 'cannon', 'helicopter', 'missle', 'submarine']
|
||
.map((n) => ({ type: 'audio', key: `sfx-battle-${n}`, path: `assets/fx/battle-${n}.mp3` })),
|
||
// Foot-unit march (infantry/mech), played on move in AdvanceWarsMapView.js.
|
||
{ type: 'audio', key: 'sfx-march', path: 'assets/fx/march.mp3' },
|
||
(scene) => musicFrom(scene, 'nintendo-music'),
|
||
],
|
||
// Config + puzzle bank always load; panel/character sheets are optional
|
||
// drop-ins declared in data/tetrisattack-artwork.json (spec: src/games/
|
||
// tetrisattack/sprites.md) — path:null stays procedural. Shares the nintendo
|
||
// soundtrack (see services/soundtrack.js).
|
||
tetrisattack: [
|
||
{ type: 'json', key: 'tetrisattack', path: 'data/tetrisattack.json' },
|
||
{ type: 'json', key: 'tetrisattack-puzzles', path: 'data/tetrisattack-puzzles.json' },
|
||
image('tetrisattack-menu-bg', 'assets/images/tetrisattack/main-menu.png'),
|
||
(scene) => sheetsFrom(scene, 'tetrisattack-artwork', ['panelSheet', 'characterSheet']),
|
||
// Stage Clear backgrounds (background-{hero}-r{stage}.png, 6 friends × 5
|
||
// stages) and character voice clips (assets/fx/tetrisattack/{hero}-*.mp3)
|
||
// are NOT declared here — 30 full-screen images is far too much to pull on
|
||
// entering the game room, and the voice sets are still being filled in.
|
||
// TetrisAttackGame.ensureRoundAssets() loads one character's set as their
|
||
// round begins; anything missing just never plays.
|
||
(scene) => musicFrom(scene, 'nintendo-music'),
|
||
],
|
||
// Rules always load; every unit/structure/terrain sheet is an optional drop-in
|
||
// declared in data/totalannihilation-artwork.json (spec: src/games/
|
||
// totalannihilation/sprites.md) — each is path:null today and paints
|
||
// procedurally, so the game is fully playable with zero PNGs. `sheets` is a MAP
|
||
// of sheets rather than a list of field names, which is why one resolver line
|
||
// covers every sheet ever added: sheetsFrom() Object.values() anything without a
|
||
// `key`. Uses the hacker soundtrack (see services/soundtrack.js).
|
||
totalannihilation: [
|
||
{ type: 'json', key: 'totalannihilation-rules', path: 'data/totalannihilation-rules.json' },
|
||
{ type: 'json', key: 'totalannihilation-campaign', path: 'data/totalannihilation-campaign.json' },
|
||
(scene) => sheetsFrom(scene, 'totalannihilation-artwork', ['sheets']),
|
||
image('ta-background', 'assets/images/ta/background.png'),
|
||
// Weapon sounds keyed by each weapon's `sound` in totalannihilation-rules.json.
|
||
...['tank'].map((n) => ({ type: 'audio', key: `sfx-battle-${n}`, path: `assets/fx/battle-${n}.mp3` })),
|
||
{ type: 'audio', key: 'sfx-laser-zap', path: 'assets/fx/laser-zap.mp3' },
|
||
{ type: 'audio', key: 'sfx-scifi-launch', path: 'assets/fx/scifi-launch.mp3' },
|
||
// TA-specific sfx: machine guns per weapon.sound, rocket variants (random pick between the
|
||
// two on fire, see TotalAnnihilationGame._onSimEvent), and unit/vehicle death cues.
|
||
...['50cal', 'machinegun', 'rocket-1', 'rocket-2', 'nuclear', 'unit-loss', 'vehicle-loss']
|
||
.map((n) => ({ type: 'audio', key: `sfx-ta-${n}`, path: `assets/fx/ta-${n}.mp3` })),
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
wolfenstein: [
|
||
{ type: 'json', key: 'wolfenstein-rules', path: 'data/wolfenstein-rules.json' },
|
||
{ type: 'json', key: 'wolfenstein-campaigns', path: 'data/wolfenstein-campaigns.json' },
|
||
(scene) => sheetsFrom(scene, 'wolfenstein-artwork', ['sheets']),
|
||
(scene) => imagesFrom(scene, 'wolfenstein-artwork'),
|
||
// Weapon/enemy sfx (fists swing/hit, pistol shot/reload/empty, guard alert/
|
||
// melee/death, door open/close, pickup) will be added here once clips
|
||
// exist — WolfensteinGame plays no sound yet, same "add the row when the
|
||
// clip lands" approach as totalannihilation's weapon-sound block above.
|
||
],
|
||
coloradodefense: [
|
||
// arcadedark soundtrack (see services/soundtrack.js) — lazy-loaded here
|
||
// so its audio only downloads once Colorado Defense is actually entered.
|
||
(scene) => musicFrom(scene, 'arcadedark-music'),
|
||
],
|
||
tempest: [
|
||
// arcadedark soundtrack (see services/soundtrack.js) — lazy-loaded here
|
||
// so its audio only downloads once Tempest is actually entered.
|
||
(scene) => musicFrom(scene, 'arcadedark-music'),
|
||
],
|
||
mastermind: [
|
||
// hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Mastermind is actually entered.
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
hexsweeper: [
|
||
// hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Hexsweeper is actually entered.
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
dotlink: [
|
||
// hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once Dot Link is actually entered.
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
zuma: [
|
||
image('zuma-menu-bg', 'assets/images/zuma/background-menu.png'),
|
||
// In-level backgrounds, alternated per level by ZumaLogic's `background`
|
||
// field (see tools/genZuma.js) — more can be dropped in later.
|
||
image('zuma-background-1', 'assets/images/zuma/background-01.png'),
|
||
image('zuma-background-2', 'assets/images/zuma/background-02.png'),
|
||
// frame 0 = the whole stone frog, frame 1 = the same disc with the mouth
|
||
// slot cut out; the ready marble is drawn between them.
|
||
sheet('zuma-frog', 'assets/images/zuma/frog.png', 200, 200),
|
||
// zuma soundtrack (see services/soundtrack.js) — lazy-loaded here so its
|
||
// audio only downloads once Zuma is actually entered.
|
||
(scene) => musicFrom(scene, 'zuma-music'),
|
||
// Fired-shot whoosh, chain-collision hit, and the level-start fanfare
|
||
// (ZumaGame.playStartFanfare ducks the soundtrack while it plays).
|
||
{ type: 'audio', key: 'sfx-zuma-shoot', path: 'assets/fx/zuma-shoot.mp3' },
|
||
{ type: 'audio', key: 'sfx-zuma-hit', path: 'assets/fx/zuma-hit.mp3' },
|
||
{ type: 'audio', key: 'sfx-zuma-start', path: 'assets/fx/zuma-start.mp3' },
|
||
{ type: 'audio', key: 'sfx-zuma-explode', path: 'assets/fx/zuma-explode.mp3' },
|
||
{ type: 'audio', key: 'sfx-zuma-win', path: 'assets/fx/zuma-win.mp3' },
|
||
{ type: 'audio', key: 'sfx-zuma-lose', path: 'assets/fx/zuma-lose.mp3' },
|
||
],
|
||
tents: [
|
||
// Tent placement/removal cues and the win jingle (played by TentsGame
|
||
// via the SFX.TENTS_* keys in src/ui/Sounds.js).
|
||
{ type: 'audio', key: 'sfx-tents-place', path: 'assets/fx/tents-place.mp3' },
|
||
{ type: 'audio', key: 'sfx-tents-remove', path: 'assets/fx/tents-remove.mp3' },
|
||
{ type: 'audio', key: 'sfx-tents-win', path: 'assets/fx/tents-win.mp3' },
|
||
// Puzzle-start intro jingle — TentsGame.startGame ducks the soundtrack
|
||
// for its duration (same pattern as Zuma's level-start fanfare).
|
||
{ type: 'audio', key: 'sfx-tents-intro', path: 'assets/music/tents-intro.mp3' },
|
||
],
|
||
'2048': [
|
||
// hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so
|
||
// its audio only downloads once 2048 is actually entered.
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
};
|
||
|
||
// Returns the full (normalized) descriptor list for a slug — including assets
|
||
// that may already be loaded. Callers filter with the loader's cache checks.
|
||
export function resolveGameAssets(scene, slug) {
|
||
const entry = MANIFEST[slug];
|
||
if (!entry) return [];
|
||
const parts = typeof entry === 'function' ? entry(scene) : entry;
|
||
return parts.flatMap((p) => (typeof p === 'function' ? p(scene) : [p]));
|
||
}
|