146 lines
4.2 KiB
JavaScript
146 lines
4.2 KiB
JavaScript
import * as Phaser from 'phaser';
|
|
import { COLORS, GAME_WIDTH } from '../config.js';
|
|
|
|
const BTN = 32;
|
|
const GAP = 6;
|
|
const PAD = 12;
|
|
const DEPTH = 90;
|
|
|
|
function shuffle(arr) {
|
|
const a = [...arr];
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
export class MusicPlayer {
|
|
constructor(scene, tracks) {
|
|
this.scene = scene;
|
|
this.tracks = shuffle(tracks);
|
|
this.idx = 0;
|
|
this.muted = false;
|
|
this._audio = null;
|
|
this._objs = [];
|
|
|
|
this._buildUI();
|
|
this._play(0);
|
|
|
|
scene.events.once('shutdown', () => this.destroy());
|
|
scene.events.once('destroy', () => this.destroy());
|
|
}
|
|
|
|
_buildUI() {
|
|
const s = this.scene;
|
|
const muteX = GAME_WIDTH - PAD - BTN / 2;
|
|
const skipX = muteX - GAP - BTN;
|
|
const btnY = PAD + BTN / 2;
|
|
const infoY = PAD + BTN + GAP + 8;
|
|
|
|
this._muteX = muteX;
|
|
this._muteY = btnY;
|
|
|
|
const skipBg = s.add.graphics().setDepth(DEPTH);
|
|
this._drawBg(skipBg, skipX, btnY, false);
|
|
skipBg.setInteractive(
|
|
new Phaser.Geom.Rectangle(skipX - BTN / 2, btnY - BTN / 2, BTN, BTN),
|
|
Phaser.Geom.Rectangle.Contains,
|
|
);
|
|
skipBg.input.cursor = 'pointer';
|
|
skipBg.on('pointerover', () => this._drawBg(skipBg, skipX, btnY, true));
|
|
skipBg.on('pointerout', () => this._drawBg(skipBg, skipX, btnY, false));
|
|
skipBg.on('pointerdown', () => this.next());
|
|
|
|
const skipIcon = s.add.text(skipX, btnY, '⏭', {
|
|
fontFamily: 'sans-serif', fontSize: '17px', color: COLORS.textHex,
|
|
}).setOrigin(0.5).setDepth(DEPTH + 1);
|
|
|
|
const muteBg = s.add.graphics().setDepth(DEPTH);
|
|
this._drawBg(muteBg, muteX, btnY, false);
|
|
muteBg.setInteractive(
|
|
new Phaser.Geom.Rectangle(muteX - BTN / 2, btnY - BTN / 2, BTN, BTN),
|
|
Phaser.Geom.Rectangle.Contains,
|
|
);
|
|
muteBg.input.cursor = 'pointer';
|
|
muteBg.on('pointerover', () => this._drawBg(muteBg, muteX, btnY, true));
|
|
muteBg.on('pointerout', () => this._drawBg(muteBg, muteX, btnY, false));
|
|
muteBg.on('pointerdown', () => this.toggleMute());
|
|
|
|
this._muteIcon = s.add.text(muteX, btnY, '♪', {
|
|
fontFamily: 'sans-serif', fontSize: '17px', color: COLORS.textHex,
|
|
}).setOrigin(0.5).setDepth(DEPTH + 1);
|
|
|
|
this._muteLine = s.add.graphics().setDepth(DEPTH + 2);
|
|
|
|
this._infoText = s.add.text(GAME_WIDTH - PAD, infoY, '', {
|
|
fontFamily: 'Righteous', fontSize: '13px', color: COLORS.mutedHex,
|
|
}).setOrigin(1, 0).setDepth(DEPTH);
|
|
|
|
this._objs.push(skipBg, skipIcon, muteBg, this._muteIcon, this._muteLine, this._infoText);
|
|
}
|
|
|
|
_drawBg(g, cx, cy, hover) {
|
|
g.clear();
|
|
g.fillStyle(COLORS.panel, 0.88);
|
|
g.fillRoundedRect(cx - BTN / 2, cy - BTN / 2, BTN, BTN, 6);
|
|
if (hover) {
|
|
g.lineStyle(1.5, COLORS.accent, 0.8);
|
|
g.strokeRoundedRect(cx - BTN / 2, cy - BTN / 2, BTN, BTN, 6);
|
|
}
|
|
}
|
|
|
|
_play(idx) {
|
|
if (this._audio) {
|
|
this._audio.pause();
|
|
this._audio.onended = null;
|
|
}
|
|
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.play().catch(() => { });
|
|
this._audio.onended = () => this.next();
|
|
this._updateInfo();
|
|
}
|
|
|
|
_updateInfo() {
|
|
const t = this.tracks[this.idx];
|
|
this._infoText?.setText(`${t.artist} — ${t.title}`);
|
|
}
|
|
|
|
_updateMuteVisual() {
|
|
this._muteIcon?.setColor(this.muted ? COLORS.mutedHex : COLORS.textHex);
|
|
this._muteLine?.clear();
|
|
if (this.muted) {
|
|
const r = BTN / 2 - 5;
|
|
this._muteLine.lineStyle(2, COLORS.danger, 0.9);
|
|
this._muteLine.beginPath();
|
|
this._muteLine.moveTo(this._muteX + r, this._muteY - r);
|
|
this._muteLine.lineTo(this._muteX - r, this._muteY + r);
|
|
this._muteLine.strokePath();
|
|
}
|
|
}
|
|
|
|
next() {
|
|
this.idx = (this.idx + 1) % this.tracks.length;
|
|
this._play(this.idx);
|
|
}
|
|
|
|
toggleMute() {
|
|
this.muted = !this.muted;
|
|
if (this._audio) this._audio.muted = this.muted;
|
|
this._updateMuteVisual();
|
|
}
|
|
|
|
destroy() {
|
|
if (this._audio) {
|
|
this._audio.pause();
|
|
this._audio.onended = null;
|
|
this._audio = null;
|
|
}
|
|
for (const o of this._objs) { try { o.destroy(); } catch (_) { } }
|
|
this._objs = [];
|
|
}
|
|
}
|