feat: add per-game soundtracks for multiple games

Introduce game-specific music overrides using the soundtrack service.
Adds arcadedark and hacker music packs, and wires them to Balatro,
Colorado Defense, Tempest, Mastermind, Hexsweeper, Dot Link, and 2048.
Games now lazy-load their soundtrack assets on entry instead of sharing
a single generic music cache.
This commit is contained in:
Brian Fertig 2026-07-18 17:21:32 -06:00
parent e1a5c21180
commit 7713089466
13 changed files with 106 additions and 14 deletions

Binary file not shown.

View File

@ -0,0 +1,15 @@
{
"tracks": [
{
"file": "arcadedark-track01.mp3",
"artist": "Cthulu",
"title": "Press the Button"
},
{
"file": "arcadedark-track02.mp3",
"artist": "The Backrooms",
"title": "Subconscious Memories"
}
],
"volume": 0.8
}

20
data/hacker-music.json Normal file
View File

@ -0,0 +1,20 @@
{
"tracks": [
{
"file": "hacker-track01.mp3",
"artist": "Cy-Bro",
"title": "Rock the Anomaly"
},
{
"file": "hacker-track02.mp3",
"artist": "Nadia",
"title": "A time after now, but also before"
},
{
"file": "hacker-track03.mp3",
"artist": "DV-8-2303",
"title": "Cybercrimes"
}
],
"volume": 0.3
}

View File

@ -135,8 +135,13 @@ export const MANIFEST = {
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']),
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.
@ -194,6 +199,36 @@ export const MANIFEST = {
// its audio only downloads once Super Kart is actually entered.
(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

View File

@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import {
@ -55,8 +56,8 @@ export default class Game2048 extends Phaser.Scene {
create() {
try {
const music = this.cache.json.get('music');
if (music?.tracks) new MusicPlayer(this, music.tracks);
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) new MusicPlayer(this, tracks, volume);
} catch (_) {}
this.schemes = this.cache.json.get('colored-playfields')?.schemes ?? [];

View File

@ -11,6 +11,7 @@ import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { api } from '../../services/api.js';
import * as store from '../../services/localStore.js';
import {
@ -85,7 +86,10 @@ export default class BalatroGame extends Phaser.Scene {
}
create() {
try { const m = this.cache.json.get('music'); if (m?.tracks) new MusicPlayer(this, m.tracks); } catch (_) {}
try {
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) new MusicPlayer(this, tracks, volume);
} catch (_) {}
this.art = this.cache.json.get('balatro-artwork') || {};
if (!this.textures.exists('balatro-spark')) {

View File

@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import { applyArcadeCRTOverlay } from '../../ui/ArcadeCRTOverlay.js';
@ -50,8 +51,8 @@ export default class ColoradoDefenseGame extends Phaser.Scene {
create() {
try {
const music = this.cache.json.get('music');
if (music?.tracks) this.music = new MusicPlayer(this, music.tracks);
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) this.music = new MusicPlayer(this, tracks, volume);
} catch (_) { /* optional */ }
this.input.mouse?.disableContextMenu();

View File

@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX, playDotLinkSound, playScifiLaunch } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import {
@ -94,8 +95,8 @@ export default class DotLinkGame extends Phaser.Scene {
async create() {
try {
const music = this.cache.json.get('music');
if (music?.tracks) new MusicPlayer(this, music.tracks);
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) new MusicPlayer(this, tracks, volume);
} catch (_) { /* optional */ }
this.buildTextures();

View File

@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import {
@ -44,8 +45,8 @@ export default class HexsweeperGame extends Phaser.Scene {
create() {
try {
const music = this.cache.json.get('music');
if (music?.tracks) new MusicPlayer(this, music.tracks);
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) new MusicPlayer(this, tracks, volume);
} catch (_) { /* optional */ }
// Right-click should flag, not open the browser context menu.

View File

@ -4,6 +4,7 @@ import { Button } from '../../ui/Button.js';
import { createOpponentPortrait } from '../../ui/Portrait.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import {
makeConfig, createInitialState, setPlayerCode, applyGuess, isGameOver,
} from './MastermindLogic.js';
@ -73,7 +74,10 @@ export default class MastermindGame extends Phaser.Scene {
}
create() {
try { new MusicPlayer(this, this.cache.json.get('music')?.tracks ?? []); } catch (_) {}
try {
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) new MusicPlayer(this, tracks, volume);
} catch (_) {}
this.gs = createInitialState(this.config);
this.buildTextures();

View File

@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
import { Button } from '../../ui/Button.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX } from '../../ui/Sounds.js';
import { api } from '../../services/api.js';
import { applyArcadeCRTOverlay } from '../../ui/ArcadeCRTOverlay.js';
@ -43,8 +44,8 @@ export default class TempestGame extends Phaser.Scene {
create() {
try {
const music = this.cache.json.get('music');
if (music?.tracks) this.music = new MusicPlayer(this, music.tracks);
const { tracks, volume } = getGameSoundtrack(this);
if (tracks.length) this.music = new MusicPlayer(this, tracks, volume);
} catch (_) { /* optional */ }
this.input.mouse?.disableContextMenu();

View File

@ -44,6 +44,8 @@ export default class PreloadScene extends Phaser.Scene {
this.load.json('card-backs', 'data/card-backs.json');
this.load.json('music', 'data/music.json');
this.load.json('nintendo-music', 'data/nintendo-music.json');
this.load.json('arcadedark-music', 'data/arcadedark-music.json');
this.load.json('hacker-music', 'data/hacker-music.json');
this.load.json('rushhour', 'data/rushhour.json');
this.load.json('puddingmonsters', 'data/puddingmonsters.json');
this.load.json('shift-artwork', 'data/shift-artwork.json');

View File

@ -18,6 +18,13 @@
// if (tracks.length) this.music = new MusicPlayer(this, tracks, volume);
export const GAME_SOUNDTRACK_OVERRIDES = {
superkart: 'nintendo',
coloradodefense: 'arcadedark',
tempest: 'arcadedark',
mastermind: 'hacker',
balatro: 'hacker',
hexsweeper: 'hacker',
dotlink: 'hacker',
'2048': 'hacker',
};
// Resolve the track list (and optional volume override) a game scene's