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