player/src/engine/GameAudioManager.js
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
Open-source web player for Rublox games, dual-licensed under
AGPL-3.0 + Commercial.

Highlights:
- Babylon.js 7 + React 18 + Vite 5 stack
- Self-contained engine (~46k lines): BlockManager, ModelManager,
  PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD
  gamemodes
- Configurable backend via VITE_API_BASE and friends — works against
  staging (dev-api.rublox.pro) out of the box
- Standalone mode (VITE_STANDALONE=true) loads a bundled sample game
  for first-run without any backend
- Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG
- Lint + format scaffolding (ESLint + Prettier + EditorConfig)
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Removed before public release:
- frontend_deploy.py (contained production SSH credentials)
- ~27 admin endpoints (kept in private repo)
- Hard-coded internal URLs and IPs
- All previous git history (clean repo init)
2026-05-27 23:04:04 +03:00

185 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* GameAudioManager — менеджер аудио для проектов в плеере (GD и др.).
*
* Хранит один AudioContext на сессию плеера, поддерживает:
* - playSfx(name) — проигрывает один из 9 SFX (jump/death/orb_tap/...)
* - playMusic(trackId) — играет фоновую музыку (синтез fallback / mp3 если есть)
* - stopMusic()
* - muteMusic(bool)
*
* AudioContext создаётся лениво при первом playSfx/playMusic. Чтобы он не был
* suspended из-за autoplay policy, плеер вызывает unlock() при первом клике
* (см. KubikonPlayer.jsx).
*
* SFX и музыка — те же фабрики что в /admin-preview/gd-sfx и /admin-preview/gd-music.
* Реальные mp3 (если положены в /public/music/gd/) играются вместо fallback-синтеза.
*/
import { SFX_CATALOG } from '../AdminPreview/gdSfx/sfxFactories';
import { TRACKS } from '../AdminPreview/gdMusic/musicCatalog';
import { SynthPlayer, trackFileExists } from '../AdminPreview/gdMusic/musicSynth';
export class GameAudioManager {
constructor() {
this.ctx = null;
this.masterGain = null;
this.sfxIndex = {};
for (const s of SFX_CATALOG) this.sfxIndex[s.id] = s;
this.trackIndex = {};
for (const t of TRACKS) this.trackIndex[t.id] = t;
this.currentTrackId = null;
this.audioEl = null; // <audio> для mp3 трека (если файл есть)
this.synth = null; // SynthPlayer fallback
this.muted = false;
this.fileStatusCache = new Map();
this._pendingMusic = null; // если playMusic вызван до unlock — отложим
// Глобальный first-gesture listener — браузеры блокируют AudioContext
// до первого клика. Слушаем pointerdown/keydown/touchstart на window.
this._unlockHandler = () => {
this.unlock().then((ok) => {
if (ok && this._pendingMusic) {
const tid = this._pendingMusic;
this._pendingMusic = null;
this.playMusic(tid);
}
});
window.removeEventListener('pointerdown', this._unlockHandler);
window.removeEventListener('keydown', this._unlockHandler);
window.removeEventListener('touchstart', this._unlockHandler);
};
try {
window.addEventListener('pointerdown', this._unlockHandler, { once: false });
window.addEventListener('keydown', this._unlockHandler, { once: false });
window.addEventListener('touchstart', this._unlockHandler, { once: false });
} catch (e) { /* SSR / no window */ }
}
/** Создать AudioContext (или вернуть существующий). Должен вызываться внутри
* user-gesture цепочки (например onClick). */
async unlock() {
if (!this.ctx) {
const Ctor = window.AudioContext || window.webkitAudioContext;
if (!Ctor) return false;
this.ctx = new Ctor();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.21; // 0.7 × 0.3 (70% по запросу юзера)
this.masterGain.connect(this.ctx.destination);
}
if (this.ctx.state === 'suspended') {
try { await this.ctx.resume(); } catch (e) { /* ignore */ }
}
return this.ctx.state === 'running';
}
/** Проиграть короткий SFX (синхронно, без задержки). */
playSfx(name) {
if (this.muted) return;
const sfx = this.sfxIndex[name];
if (!sfx || !this.ctx || !this.masterGain) return;
try {
sfx.play(this.ctx, this.masterGain);
} catch (e) {
console.warn('[GameAudio] sfx failed', name, e);
}
}
/** Запустить фоновую музыку. Если уже играет тот же трек — ничего.
* Если AudioContext ещё не разблокирован (нет user-gesture) — откладываем
* до первого клика (см. _unlockHandler). */
async playMusic(trackId) {
if (this.currentTrackId === trackId) return;
if (!this.ctx) {
// AudioContext ещё не создан — ждём первого user-gesture
this._pendingMusic = trackId;
return;
}
this.stopMusic();
const track = this.trackIndex[trackId];
if (!track) {
console.warn('[GameAudio] unknown track:', trackId);
return;
}
this.currentTrackId = trackId;
if (this.muted) return;
// Проверка наличия mp3-файла (с кешем)
let fileExists = this.fileStatusCache.get(trackId);
if (fileExists === undefined) {
try { fileExists = await trackFileExists(track.file); }
catch (e) { fileExists = false; }
this.fileStatusCache.set(trackId, fileExists);
}
if (fileExists) {
// Играем настоящий файл
try {
const a = new Audio(track.file);
a.loop = true;
a.volume = 0.165; // 0.55 × 0.3 (70%)
await a.play();
this.audioEl = a;
} catch (e) {
console.warn('[GameAudio] audio.play() failed, fallback to synth', e);
this._startSynth(track);
}
} else {
this._startSynth(track);
}
}
_startSynth(track) {
try {
this.synth = new SynthPlayer(track.fallbackSynth, track.bpm);
// SynthPlayer создаёт свой AudioContext — ок, не конфликтует с нашим
this.synth.start();
} catch (e) {
console.warn('[GameAudio] synth.start() failed', e);
}
}
stopMusic() {
if (this.audioEl) {
try { this.audioEl.pause(); } catch (e) {}
this.audioEl = null;
}
if (this.synth) {
try { this.synth.stop(); } catch (e) {}
this.synth = null;
}
this.currentTrackId = null;
}
setMuted(mute) {
this.muted = !!mute;
if (this.muted) {
// временно глушим — но не стираем currentTrackId
if (this.audioEl) {
try { this.audioEl.volume = 0; } catch (e) {}
}
if (this.synth) {
const wasId = this.currentTrackId;
this.stopMusic();
this.currentTrackId = wasId;
}
} else if (this.currentTrackId) {
const tid = this.currentTrackId;
this.currentTrackId = null;
this.playMusic(tid);
}
}
dispose() {
this.stopMusic();
try {
window.removeEventListener('pointerdown', this._unlockHandler);
window.removeEventListener('keydown', this._unlockHandler);
window.removeEventListener('touchstart', this._unlockHandler);
} catch (e) {}
if (this.ctx) {
try { this.ctx.close(); } catch (e) {}
this.ctx = null;
}
}
}