293 lines
16 KiB
JavaScript
293 lines
16 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.
|
||
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}` }));
|
||
}
|
||
|
||
const sheet = (key, path, frameWidth, frameHeight) => ({ type: 'spritesheet', key, path, frameWidth, frameHeight });
|
||
const image = (key, path) => ({ type: 'image', key, path });
|
||
|
||
export const MANIFEST = {
|
||
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')],
|
||
jewelquest: [image('bg-jewelquest-battle', 'assets/images/background-jewelquest.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)],
|
||
mahjong: MAHJONG_TILES,
|
||
mahjongmatch: MAHJONG_TILES,
|
||
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: (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),
|
||
// 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', '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']),
|
||
// Weapon sounds keyed by each weapon's `sound` in totalannihilation-rules.json.
|
||
...['gunfire-modern', 'tank', 'missle'].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' },
|
||
(scene) => musicFrom(scene, 'hacker-music'),
|
||
],
|
||
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'),
|
||
],
|
||
'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]));
|
||
}
|