164 lines
8.9 KiB
JavaScript
164 lines
8.9 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 }));
|
||
}
|
||
|
||
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']),
|
||
// 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`));
|
||
},
|
||
],
|
||
};
|
||
|
||
// 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]));
|
||
}
|