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 = []; } }