player/src/engine/SoundLibrary.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

136 lines
4.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.

/**
* SoundLibrary — библиотека пользовательских звуков проекта (Фаза 5.5).
*
* Зачем: даёт автору загрузить свои короткие звуки (mp3/wav/ogg) и
* проигрывать их в игре через game.sound.* — в т.ч. как 3D-позиционный
* звук от точки в мире.
*
* Как хранится: каждый звук — { id, name, dataUrl }, где dataUrl —
* base64 аудио. Сериализуется в scene.sounds, едет в БД с проектом
* (своего файлового хранилища у Рублокс-веба нет — как картинки в
* AssetManager).
*
* Ограничения (звук в base64 раздувает JSON проекта):
* MAX_SOUNDS — не больше 8 звуков на проект;
* MAX_BYTES — не больше 2 МБ на звук (mp3 ~30-60 сек; для коротких
* фоновых треков и эффектов хватает).
*/
export const MAX_SOUNDS = 8;
export const MAX_SOUND_BYTES = 2 * 1024 * 1024;
let _soundIdSeq = 1;
export class SoundLibrary {
constructor() {
// Map id → { id, name, dataUrl }
this.sounds = new Map();
}
/** Все звуки массивом — для UI-панели. */
list() {
return [...this.sounds.values()];
}
get(id) {
return this.sounds.get(id) || null;
}
/** dataURL по id — то, что отдаём в Audio / decodeAudioData. */
getDataUrl(id) {
const s = this.sounds.get(id);
return s ? s.dataUrl : null;
}
count() {
return this.sounds.size;
}
/**
* Загрузить звук из File (input type=file).
* Возвращает Promise<{ ok, id?, error? }>.
*/
addFromFile(file) {
return new Promise((resolve) => {
if (this.sounds.size >= MAX_SOUNDS) {
resolve({ ok: false, error: `Лимит ${MAX_SOUNDS} звуков на проект` });
return;
}
if (!file || !/^audio\//.test(file.type)) {
resolve({ ok: false, error: 'Это не звуковой файл' });
return;
}
// Грубая проверка размера ДО чтения (base64 ~+33% сверху).
if (file.size > MAX_SOUND_BYTES) {
resolve({
ok: false,
error: `Звук слишком большой (макс ${Math.round(MAX_SOUND_BYTES / 1024)} КБ)`,
});
return;
}
const reader = new FileReader();
reader.onerror = () => resolve({ ok: false, error: 'Не удалось прочитать файл' });
reader.onload = () => {
const dataUrl = reader.result;
if (typeof dataUrl !== 'string') {
resolve({ ok: false, error: 'Битый файл' });
return;
}
if (dataUrl.length > MAX_SOUND_BYTES * 1.4) {
resolve({ ok: false, error: 'Звук слишком большой' });
return;
}
const name = (file.name || 'звук').replace(/\.[^.]+$/, '');
const id = this.add(name, dataUrl);
resolve({ ok: true, id });
};
reader.readAsDataURL(file);
});
}
/** Добавить звук с готовым dataUrl. Возвращает id. */
add(name, dataUrl) {
const id = `sound_${_soundIdSeq++}`;
this.sounds.set(id, { id, name: name || 'звук', dataUrl });
return id;
}
rename(id, name) {
const s = this.sounds.get(id);
if (s) s.name = name || s.name;
}
remove(id) {
this.sounds.delete(id);
}
// ============ STATE ============
serialize() {
return this.list().map(s => ({
id: s.id, name: s.name, dataUrl: s.dataUrl,
}));
}
load(data) {
this.sounds.clear();
if (!Array.isArray(data)) return;
let maxNum = 0;
for (const s of data) {
if (!s || typeof s.id !== 'string' || typeof s.dataUrl !== 'string') continue;
this.sounds.set(s.id, {
id: s.id,
name: typeof s.name === 'string' ? s.name : 'звук',
dataUrl: s.dataUrl,
});
const m = /^sound_(\d+)$/.exec(s.id);
if (m) maxNum = Math.max(maxNum, Number(m[1]));
}
if (maxNum >= _soundIdSeq) _soundIdSeq = maxNum + 1;
}
dispose() {
this.sounds.clear();
}
}