Files

167 lines
6.3 KiB
JavaScript

// ---- music manager: crossfading loops mapped to game moods ----
const Music = {
current: null, // { audio, name }
fading: null,
bossCount: 0,
TRACKS: {
menu: 'Blood Arcade.mp3',
floors: [
'Rust Hollow Loop.mp3', 'Blood Dice Loop.mp3', 'Crimsonland Requiem.mp3',
'Crimsonland Tool.mp3', 'Blood Arcade_Alt.mp3', 'Rust Hollow Loop_Alt.mp3',
'Blood Dice Loop_alt.mp3', 'Crimsonland Requiem_alt.mp3', 'Crimsonland Tool_alt.mp3',
'Blood Arcade.mp3',
],
bosses: {
bonesaw: ['Bone Saw Loop_Boss.mp3', 'Bone Saw Loop_Alt_Boss.mp3'],
gorecrown: ['Gore Crown Loop_Boss.mp3', 'Gore Crown Loop_Alt_Boss.mp3'],
knifecrawl: ['Knife Floor Crawl_Boss.mp3', 'Knife Floor Crawl_Alt_Boss.mp3'],
vealmother: ['Veal Mother Loop_Boss.mp3', 'Veal Mother Loop_Alt_Boss.mp3'],
flenser: ['Flenser Loop_Boss.mp3', 'Flenser Loop_Alt_Boss.mp3'],
hookchoir: ['Hook Choir Loop_Boss.mp3', 'Hook Choir Loop_Alt_Boss.mp3'],
platefather: ['Plate Father Loop_Boss.mp3', 'Plate Father Loop_Alt_Boss.mp3'],
augerprime: ['Auger Prime Loop_Boss.mp3', 'Auger Prime Loop_Alt_Boss.mp3'],
scald: ['Scald Loop_Boss.mp3', 'Scald Loop_Alt_Boss.mp3'],
},
},
// jukebox: every non-boss track, selectable by the player
PLAYLIST: [
'Blood Arcade.mp3', 'Blood Arcade_Alt.mp3',
'Rust Hollow Loop.mp3', 'Rust Hollow Loop_Alt.mp3',
'Blood Dice Loop.mp3', 'Blood Dice Loop_alt.mp3',
'Crimsonland Requiem.mp3', 'Crimsonland Requiem_alt.mp3',
'Crimsonland Tool.mp3', 'Crimsonland Tool_alt.mp3',
'Bone Saw Loop_Boss.mp3', 'Bone Saw Loop_Alt_Boss.mp3',
'Gore Crown Loop_Boss.mp3', 'Gore Crown Loop_Alt_Boss.mp3',
'Knife Floor Crawl_Boss.mp3', 'Knife Floor Crawl_Alt_Boss.mp3',
'Veal Mother Loop_Boss.mp3', 'Veal Mother Loop_Alt_Boss.mp3',
'Flenser Loop_Boss.mp3', 'Flenser Loop_Alt_Boss.mp3',
'Hook Choir Loop_Boss.mp3', 'Hook Choir Loop_Alt_Boss.mp3',
'Plate Father Loop_Boss.mp3', 'Plate Father Loop_Alt_Boss.mp3',
'Auger Prime Loop_Boss.mp3', 'Auger Prime Loop_Alt_Boss.mp3',
'Scald Loop_Boss.mp3', 'Scald Loop_Alt_Boss.mp3',
],
override: null, // filename when the player picked a track manually
pretty(file) {
return file.replace(/\.mp3$/i, '').replace(/_Boss$/i, '').replace(/_Alt$/i, ' (Alt)').replace(/_alt$/i, ' (Alt)');
},
// the track the game *should* be playing right now (respects manual override)
requestFloorMusic() {
if (this.override) this.play(this.override);
else this.playFloor(G.floor);
},
makeAudio(name) {
const a = new Audio('mp3-music/' + encodeURIComponent(name));
a.loop = true;
a.volume = 0;
a.preload = 'auto';
return a;
},
floorTrack(floor) { return this.TRACKS.floors[(floor - 1) % this.TRACKS.floors.length]; },
bossTrack(kind) {
const pair = this.TRACKS.bosses[kind] || this.TRACKS.bosses.bonesaw;
return pair[this.bossCount % 2];
},
play(name) {
if (this.current && this.current.name === name) return;
if (this.fading) {
this.fading.audio.pause();
if (typeof Sfx !== 'undefined') Sfx.detachMusic(this.fading.audio);
this.fading = null;
}
if (this.current) this.fading = this.current;
const a = this.makeAudio(name);
a.muted = G.muted;
if (typeof Sfx !== 'undefined') Sfx.attachMusic(a);
a.play().catch(() => {}); // browser may block until user gesture; retried on next call
this.current = { audio: a, name, retryT: 0 };
},
playMenu() { this.play(this.TRACKS.menu); },
playFloor(floor) { this.play(this.floorTrack(floor)); },
playBoss(kind) { this.play(this.bossTrack(kind)); this.bossCount++; },
stop() {
if (this.current) {
this.current.audio.pause();
if (typeof Sfx !== 'undefined') Sfx.detachMusic(this.current.audio);
this.current = null;
}
if (this.fading) {
this.fading.audio.pause();
if (typeof Sfx !== 'undefined') Sfx.detachMusic(this.fading.audio);
this.fading = null;
}
},
setMuted(m) {
if (this.current) this.current.audio.muted = m;
if (this.fading) this.fading.audio.muted = m;
if (typeof Sfx !== 'undefined') Sfx.setMusicMuted(m);
},
setVolume(v) {
G.musicVol = clamp(v, 0, 1);
try { localStorage.setItem('meatslicer_music_volume', String(G.musicVol)); } catch (e) {}
if (this.current) this.current.audio.volume = Math.min(this.current.audio.volume, G.musicVol);
if (this.fading) this.fading.audio.volume = Math.min(this.fading.audio.volume, G.musicVol);
},
// cycle the jukebox selection: Auto -> track 0 -> ... -> track N -> Auto
cycle(dir) {
const n = this.PLAYLIST.length;
if (G.musicSel === undefined || G.musicSel === null) G.musicSel = -1;
G.musicSel += dir;
if (G.musicSel >= n) G.musicSel = -1;
if (G.musicSel < -1) G.musicSel = n - 1;
if (G.musicSel === -1) {
this.override = null;
addToast('MUSIC: AUTO', 'floor rotation');
} else {
this.override = this.PLAYLIST[G.musicSel];
addToast('MUSIC: ' + this.pretty(this.override));
}
// never interrupt a live boss track — it hands back when the boss dies
if (!G.boss) this.requestFloorMusic();
Sfx.pickup();
},
update(dt) {
const target = G.musicVol === undefined ? 0.55 : G.musicVol;
// Tracks created before the AudioContext resumed keep playing directly;
// attach once the context is running so large SFX can duck the music bus.
if (typeof Sfx !== 'undefined') {
if (this.current) Sfx.attachMusic(this.current.audio);
if (this.fading) Sfx.attachMusic(this.fading.audio);
}
if (this.current && this.current.audio.volume < target) {
this.current.audio.volume = clamp(this.current.audio.volume + dt * 0.8, 0, target);
// If autoplay was blocked, retry at a gentle cadence rather than once
// per frame (which can flood promises and the browser media pipeline).
this.current.retryT -= dt;
if (this.current.audio.paused && this.current.retryT <= 0) {
this.current.retryT = 1;
this.current.audio.play().catch(() => {});
}
}
if (this.fading) {
const v = this.fading.audio.volume - dt * 0.8;
if (v <= 0.01) {
this.fading.audio.volume = 0;
this.fading.audio.pause();
if (typeof Sfx !== 'undefined') Sfx.detachMusic(this.fading.audio);
this.fading = null;
} else {
this.fading.audio.volume = v;
}
}
},
};