diff --git a/assets/fx/kart-spinner.mp3 b/assets/fx/kart-spinner.mp3 new file mode 100644 index 0000000..f3484bd Binary files /dev/null and b/assets/fx/kart-spinner.mp3 differ diff --git a/assets/images/superkart/icons.png b/assets/images/superkart/icons.png new file mode 100644 index 0000000..cc74895 Binary files /dev/null and b/assets/images/superkart/icons.png differ diff --git a/assets/images/superkart/icons.psd b/assets/images/superkart/icons.psd new file mode 100644 index 0000000..b7f95ba Binary files /dev/null and b/assets/images/superkart/icons.psd differ diff --git a/assets/music/arcadedark-track01.mp3 b/assets/music/arcadedark-track01.mp3 new file mode 100644 index 0000000..931ed9c Binary files /dev/null and b/assets/music/arcadedark-track01.mp3 differ diff --git a/assets/music/arcadedark-track02.mp3 b/assets/music/arcadedark-track02.mp3 new file mode 100644 index 0000000..e70a762 Binary files /dev/null and b/assets/music/arcadedark-track02.mp3 differ diff --git a/assets/music/hacker-track01.mp3 b/assets/music/hacker-track01.mp3 new file mode 100644 index 0000000..911b5e6 Binary files /dev/null and b/assets/music/hacker-track01.mp3 differ diff --git a/assets/music/hacker-track02.mp3 b/assets/music/hacker-track02.mp3 new file mode 100644 index 0000000..92855d0 Binary files /dev/null and b/assets/music/hacker-track02.mp3 differ diff --git a/assets/music/hacker-track03.mp3 b/assets/music/hacker-track03.mp3 new file mode 100644 index 0000000..8e166e3 Binary files /dev/null and b/assets/music/hacker-track03.mp3 differ diff --git a/assets/music/nintendo-track01.mp3 b/assets/music/nintendo-track01.mp3 new file mode 100644 index 0000000..2f870b0 Binary files /dev/null and b/assets/music/nintendo-track01.mp3 differ diff --git a/assets/music/nintendo-track02.mp3 b/assets/music/nintendo-track02.mp3 new file mode 100644 index 0000000..669dce5 Binary files /dev/null and b/assets/music/nintendo-track02.mp3 differ diff --git a/data/music.json b/data/music.json index d38da32..02539e2 100644 --- a/data/music.json +++ b/data/music.json @@ -100,5 +100,10 @@ "artist": "Relaxin' Playing Fertig Games", "title": "Sublime" } - ] + ], + "soundtracks": { + "main_menu": [ + { "file": "mainMenu.mp3" } + ] + } } \ No newline at end of file diff --git a/data/nintendo-music.json b/data/nintendo-music.json new file mode 100644 index 0000000..a6bca6c --- /dev/null +++ b/data/nintendo-music.json @@ -0,0 +1,15 @@ +{ + "tracks": [ + { + "file": "nintendo-track01.mp3", + "artist": "Fireball", + "title": "Supernova Around the Sun" + }, + { + "file": "nintendo-track02.mp3", + "artist": "Kona", + "title": "Pet me in the Scruffy" + } + ], + "volume": 0.8 +} diff --git a/data/superkart-artwork.json b/data/superkart-artwork.json index 84b79a8..e203f8a 100644 --- a/data/superkart-artwork.json +++ b/data/superkart-artwork.json @@ -56,5 +56,5 @@ "speedway": { "key": "superkart-backdrop-speedway", "path": "assets/images/superkart/backdrop-speedway.png" }, "nightcity": { "key": "superkart-backdrop-nightcity", "path": "assets/images/superkart/backdrop-nightcity.png" } }, - "itemSheet": { "key": "superkart-items", "path": null, "frameWidth": 48, "frameHeight": 48 } + "itemSheet": { "key": "superkart-items", "path": "assets/images/superkart/icons.png", "frameWidth": 48, "frameHeight": 48 } } diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 9663a8b..0e4afd8 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -47,6 +47,16 @@ function imagesFrom(scene, jsonKey) { .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 }); @@ -180,6 +190,9 @@ export const MANIFEST = { .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'), ], }; diff --git a/src/games/superkart/SuperKartGame.js b/src/games/superkart/SuperKartGame.js index e744663..eb7d7c4 100644 --- a/src/games/superkart/SuperKartGame.js +++ b/src/games/superkart/SuperKartGame.js @@ -14,6 +14,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 { applyArcadeCRTOverlay } from '../../ui/ArcadeCRTOverlay.js'; import { enqueue as enqueueSpeech, resetQueue } from '../../ui/SpeechQueue.js'; @@ -83,6 +84,7 @@ export default class SuperKartGame extends Phaser.Scene { this.engineStartSound = null; this.engineTopSpeed = 0; this.starSound = null; + this.advanceBtn = null; } create() { @@ -92,8 +94,8 @@ export default class SuperKartGame extends Phaser.Scene { this.cups = this.cache.json.get('superkart-cups') ?? { cups: [], tracks: [] }; 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.crt = applyArcadeCRTOverlay(this, { accentTint: 0xffd028, curveAmount: 0.35 }); @@ -235,6 +237,8 @@ export default class SuperKartGame extends Phaser.Scene { this.input.keyboard.on('keydown-ENTER', () => { if (this.victoryCamActive) this.skipVictoryCam(); }); + this.input.keyboard.on('keydown-SPACE', () => this.advanceBtn?.emit('pointerup')); + this.input.keyboard.on('keydown-X', () => this.advanceBtn?.emit('pointerup')); } readInputs() { @@ -253,6 +257,7 @@ export default class SuperKartGame extends Phaser.Scene { clearView() { for (const o of this.viewObjs) o?.destroy(); this.viewObjs = []; + this.advanceBtn = null; } vAdd(obj) { @@ -1024,7 +1029,9 @@ export default class SuperKartGame extends Phaser.Scene { for (const ev of events) { switch (ev.type) { case 'go': playSound(this, SFX.COUNTDOWN_GO); break; - case 'item-box': if (ev.kart === pIdx) playSound(this, SFX.UI_FLIP); break; + case 'item-box': + if (ev.kart === pIdx) { playSound(this, SFX.UI_FLIP); playSound(this, SFX.KART_SPINNER); } + break; case 'item-get': if (ev.kart === pIdx) playSound(this, SFX.UI_CHIME); break; case 'item-use': if (ev.kart === pIdx) { @@ -1298,11 +1305,13 @@ export default class SuperKartGame extends Phaser.Scene { const lastRace = this.gp.raceIdx >= this.gp.cup.tracks.length - 1; const btn = this.vAdd(new Button(this, GAME_WIDTH / 2, GAME_HEIGHT - 90, lastRace ? 'Final Standings' : 'Next Race', () => { + this.advanceBtn = null; if (lastRace) this.showPodium(); else { this.gp.raceIdx += 1; this.startCupRace(); } }, { width: 340 })); btn.setAlpha(0); this.tweens.add({ targets: btn, alpha: 1, duration: 300 }); + this.advanceBtn = btn; } // Small outward-bursting rectangle particles — shared by showCupStandings' diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index 5d8adbf..bc5b3ff 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -2,7 +2,7 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js'; import { auth } from '../services/auth.js'; import { getGame } from '../data/gamesRegistry.js'; -import { stopMenuMusic } from '../ui/MenuMusic.js'; +import { stopMenuMusic, configureMainMenuTrack } from '../ui/MenuMusic.js'; export default class PreloadScene extends Phaser.Scene { constructor() { super('Preload'); } @@ -43,6 +43,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.json('colored-playfields', 'data/colored-playfields.json'); 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('rushhour', 'data/rushhour.json'); this.load.json('puddingmonsters', 'data/puddingmonsters.json'); this.load.json('shift-artwork', 'data/shift-artwork.json'); @@ -80,6 +81,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio('sfx-kart-hit', 'assets/fx/kart-hit.mp3'); this.load.audio('sfx-kart-shell', 'assets/fx/kart-shell.mp3'); this.load.audio('sfx-kart-star', 'assets/fx/kart-star.mp3'); + this.load.audio('sfx-kart-spinner', 'assets/fx/kart-spinner.mp3'); this.load.audio('sfx-water-splash', 'assets/fx/water-splash.mp3'); this.load.audio('sfx-water-sink', 'assets/fx/water-sink.mp3'); this.load.audio('sfx-water-raise', 'assets/fx/water-raise.mp3'); @@ -174,6 +176,10 @@ export default class PreloadScene extends Phaser.Scene { // thumbnails in OpponentSelect, so they load at startup. Per-game drop-in // artwork (shift/slots/spireclimb/dungeonboss/swdbg/balatro) is handled by // data/assetManifest.js and lazy-loaded on game entry instead. + const musicData = this.cache.json.get('music'); + const menuTrack = musicData?.soundtracks?.main_menu?.[0]?.file; + if (menuTrack) configureMainMenuTrack(menuTrack); + const pfd = this.cache.json.get('playfields'); const cbd = this.cache.json.get('card-backs'); const toLoad = [ diff --git a/src/services/soundtrack.js b/src/services/soundtrack.js new file mode 100644 index 0000000..63d3fb4 --- /dev/null +++ b/src/services/soundtrack.js @@ -0,0 +1,31 @@ +// Per-game music soundtrack overrides. Empty by default — every game plays +// the shared `default` soundtrack (data/music.json's `tracks` array), +// preloaded eagerly in PreloadScene exactly as before. To give a specific +// game its own soundtrack instead: +// 1. Add data/-music.json: { "tracks": [{file, artist, title}, ...], +// "volume": 0.15 } — "volume" is optional (0-1, MusicPlayer's own +// default applies if omitted) and only affects this named soundtrack; +// the default soundtrack's volume is never touched by this mechanism. +// Actual mp3 files go at assets/music//... +// 2. Eager-load that JSON in PreloadScene (tiny metadata, mirrors the +// existing *-artwork.json convention) under cache key `-music`. +// 3. In src/data/assetManifest.js, add `musicFrom(scene, '-music')` +// to that game's MANIFEST entry so its audio bytes lazy-load only when +// the game is entered — never part of the shared default preload. +// 4. Add `: ''` to GAME_SOUNDTRACK_OVERRIDES below. +// 5. In that game's scene, swap its MusicPlayer track-source line for: +// const { tracks, volume } = getGameSoundtrack(this); +// if (tracks.length) this.music = new MusicPlayer(this, tracks, volume); +export const GAME_SOUNDTRACK_OVERRIDES = { + superkart: 'nintendo', +}; + +// Resolve the track list (and optional volume override) a game scene's +// MusicPlayer should shuffle through. `volume` is undefined for the default +// soundtrack, so MusicPlayer's own default kicks in unchanged. +export function getGameSoundtrack(scene) { + const name = GAME_SOUNDTRACK_OVERRIDES[scene.gameDef?.slug]; + if (!name) return { tracks: scene.cache.json.get('music')?.tracks ?? [], volume: undefined }; + const data = scene.cache.json.get(`${name}-music`); + return { tracks: data?.tracks ?? [], volume: data?.volume }; +} diff --git a/src/ui/MenuMusic.js b/src/ui/MenuMusic.js index a720032..34c792f 100644 --- a/src/ui/MenuMusic.js +++ b/src/ui/MenuMusic.js @@ -1,8 +1,16 @@ let _audio = null; +let _trackFile = 'mainMenu.mp3'; + +// Sourced from data/music.json's `soundtracks.main_menu` entry (see +// PreloadScene) so the menu theme is a real, named soundtrack rather than a +// hardcoded path. Falls back to the historical default if never configured. +export function configureMainMenuTrack(file) { + _trackFile = file; +} function _getAudio() { if (!_audio) { - _audio = new Audio('assets/music/mainMenu.mp3'); + _audio = new Audio(`assets/music/${_trackFile}`); _audio.loop = true; _audio.volume = 0.6; } diff --git a/src/ui/MusicPlayer.js b/src/ui/MusicPlayer.js index ca4a16f..4d3fefc 100644 --- a/src/ui/MusicPlayer.js +++ b/src/ui/MusicPlayer.js @@ -16,11 +16,12 @@ function shuffle(arr) { } export class MusicPlayer { - constructor(scene, tracks) { + constructor(scene, tracks, volume = 0.15) { this.scene = scene; this.tracks = shuffle(tracks); this.idx = 0; this.muted = false; + this.volume = volume; this._audio = null; this._objs = []; @@ -98,7 +99,7 @@ export class MusicPlayer { const track = this.tracks[idx]; this._audio = new Audio(`assets/music/${track.file}`); this._audio.muted = this.muted; - this._audio.volume = 0.15; + this._audio.volume = this.volume; this._audio.play().catch(() => { }); this._audio.onended = () => this.next(); this._updateInfo(); diff --git a/src/ui/Sounds.js b/src/ui/Sounds.js index d24da56..1c7c990 100644 --- a/src/ui/Sounds.js +++ b/src/ui/Sounds.js @@ -89,6 +89,7 @@ export const SFX = { KART_HIT: 'sfx-kart-hit', KART_SHELL: 'sfx-kart-shell', KART_STAR: 'sfx-kart-star', + KART_SPINNER: 'sfx-kart-spinner', }; export function playSound(scene, key) { diff --git a/src/ui/SpeechQueue.js b/src/ui/SpeechQueue.js index 3820b88..e739cf4 100644 --- a/src/ui/SpeechQueue.js +++ b/src/ui/SpeechQueue.js @@ -16,7 +16,7 @@ function _playNext() { const entry = _queue.shift(); _currentCbs = entry; _currentAudio = new Audio(`assets/speech/${entry.filename}.mp3`); - _currentAudio.volume = 0.8; + _currentAudio.volume = 1; const done = () => { entry.onEnd?.(); _playing = false;