// 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 `-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/.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']), (scene) => musicFrom(scene, 'nintendo-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])); }