167 lines
6.3 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
},
|
|
};
|