studio/src/editor/engine/GdDiamond.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

193 lines
8.0 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.

/**
* GdDiamond — визуал алмазов и эффект подбора для GD-уровней.
*
* 1. Находит все primitive id ∈ [14001, 14002] и заменяет sphere
* на красивый **октаэдр-кристалл** с emissive свечением и точечным светом.
* 2. Idle-анимация: вращение + плавание вверх-вниз.
* 3. При исчезновении (setEnabled(false) или setVisible(false)) — взрыв ParticleSystem
* на 1 секунду на месте алмаза.
*
* Подключается в BabylonScene.enterPlayMode.
*/
import {
Vector3, Color3, Color4, StandardMaterial, MeshBuilder, PointLight,
ParticleSystem, Texture,
} from '@babylonjs/core';
const DIAMOND_IDS = [14001, 14002];
export class GdDiamond {
constructor() {
this.scene = null;
this._scene3d = null;
this._items = []; // [{ primId, root, mesh, light, baseY, phase }]
this._onBeforeRender = null;
this._t = 0;
}
attach(scene, scene3d) {
if (!scene || !scene3d) return;
this.scene = scene;
this._scene3d = scene3d;
// Отложенно — primitives могут не быть загружены
let attempts = 0;
const tryAttach = () => {
attempts++;
const pm = scene3d.primitiveManager;
if (!pm || !pm.instances) {
if (attempts < 10) setTimeout(tryAttach, 200);
return;
}
this._replaceDiamonds();
this._setupAnim();
};
tryAttach();
}
_replaceDiamonds() {
const pm = this._scene3d.primitiveManager;
let n = 0;
for (const id of DIAMOND_IDS) {
const data = pm.instances.get(id);
if (!data || !data.mesh) continue;
const orig = data.mesh;
const x = data.x, y = data.y, z = data.z || 0;
// Сделать красивый октаэдр-кристалл вместо sphere
const crystal = MeshBuilder.CreatePolyhedron(`gd_diamond_${id}`,
{ type: 1, size: 0.55 }, this.scene); // type=1 = octahedron
crystal.position.set(x, y, z);
// Материал — голубой стеклянный с свечением
const mat = new StandardMaterial(`gd_diamond_mat_${id}`, this.scene);
mat.diffuseColor = new Color3(0.5, 0.85, 1.0);
mat.emissiveColor = new Color3(0.3, 0.7, 1.0);
mat.specularColor = new Color3(1, 1, 1);
mat.specularPower = 128;
mat.alpha = 0.85;
crystal.material = mat;
// Точечный свет для атмосферы
const light = new PointLight(`gd_diamond_light_${id}`, new Vector3(x, y, z), this.scene);
light.diffuse = new Color3(0.4, 0.8, 1.0);
light.specular = new Color3(0.5, 0.9, 1.0);
light.intensity = 0.5;
light.range = 4;
// Прячем исходный sphere (визуально — кристалл его заменяет)
try { orig.setEnabled(false); } catch (e) {}
this._items.push({
primId: id,
origMesh: orig,
mesh: crystal,
light,
baseY: y,
phase: id * 0.7,
collected: false,
});
// Если data.visible уже false (скрипт уровня загрузил флаг из save —
// алмаз был забран в прошлый заход), кристалл скрыть без эффекта.
if (data.visible === false) {
try { crystal.setEnabled(false); } catch (e) {}
try { light && light.setEnabled(false); } catch (e) {}
this._items[this._items.length - 1].collected = true;
}
n++;
}
console.log(`[GdDiamond] установлено ${n} кристаллов`);
}
_setupAnim() {
const pm = this._scene3d.primitiveManager;
this._onBeforeRender = () => {
this._t += 0.016;
for (const it of this._items) {
if (it.collected || !it.mesh) continue;
// Вращение по Y
it.mesh.rotation.y = this._t * 1.5 + it.phase;
// Плавание вверх-вниз
const bob = Math.sin(this._t * 2 + it.phase) * 0.25;
it.mesh.position.y = it.baseY + bob;
if (it.light) it.light.position.y = it.baseY + bob;
// Подбор определяем по data.visible — скрипт уровня вызывает
// game.scene.setVisible('primitive:14001', false) когда забрал.
// Это надёжно: setEnabled false мы сами установили в _replaceDiamonds.
const data = pm && pm.instances.get(it.primId);
if (data && data.visible === false) {
this._triggerCollect(it);
}
}
};
this.scene.onBeforeRenderObservable.add(this._onBeforeRender);
}
_triggerCollect(it) {
if (it.collected) return;
it.collected = true;
const pos = it.mesh.position.clone();
// Эффект частиц
try {
const ps = new ParticleSystem(`diamond_burst_${it.primId}`, 60, this.scene);
// Используем процедурную текстуру (точка) если нет ассета
ps.particleTexture = null; // Babylon делает квадратик по умолчанию
ps.emitter = pos;
ps.minEmitBox = new Vector3(-0.1, -0.1, -0.1);
ps.maxEmitBox = new Vector3(0.1, 0.1, 0.1);
ps.color1 = new Color4(0.5, 0.9, 1.0, 1);
ps.color2 = new Color4(0.9, 1.0, 1.0, 1);
ps.colorDead = new Color4(0.3, 0.5, 0.9, 0);
ps.minSize = 0.08; ps.maxSize = 0.20;
ps.minLifeTime = 0.4; ps.maxLifeTime = 1.0;
ps.emitRate = 0; // burst-режим
ps.manualEmitCount = 60;
ps.gravity = new Vector3(0, -3, 0);
ps.direction1 = new Vector3(-3, 4, -3);
ps.direction2 = new Vector3(3, 8, 3);
ps.minEmitPower = 1; ps.maxEmitPower = 4;
ps.updateSpeed = 0.02;
ps.disposeOnStop = true;
ps.start();
// Стоп через 200мс (один burst)
setTimeout(() => { try { ps.stop(); } catch (e) {} }, 200);
} catch (e) { console.warn('[GdDiamond] particles failed', e); }
// Кристалл — scale-up и fade за 400мс
const mesh = it.mesh;
const light = it.light;
const startT = performance.now();
const DUR = 400;
const fadeStep = () => {
const elapsed = performance.now() - startT;
if (elapsed > DUR) {
try { mesh.dispose(); } catch (e) {}
try { light && light.dispose(); } catch (e) {}
return;
}
const t = elapsed / DUR;
const s = 1 + t * 1.5;
mesh.scaling.set(s, s, s);
if (mesh.material) mesh.material.alpha = 0.85 * (1 - t);
if (light) light.intensity = 0.5 * (1 - t);
requestAnimationFrame(fadeStep);
};
requestAnimationFrame(fadeStep);
}
dispose() {
if (!this.scene) return;
try {
if (this._onBeforeRender) {
this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender);
this._onBeforeRender = null;
}
} catch (e) {}
for (const it of this._items) {
try { it.mesh && it.mesh.dispose(); } catch (e) {}
try { it.light && it.light.dispose(); } catch (e) {}
}
this._items = [];
this.scene = null;
this._scene3d = null;
}
}