fertig-classic-games/src/data/assetManifest.js

293 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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]));
}