studio/src/editor/engine/SoundLibrary.js
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +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();
}
}