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

206 lines
7.8 KiB
JavaScript

/**
* BeamManager — лучи (Beam) и следы (Trail) как объекты сцены (Фаза 5.2).
*
* Beam — светящаяся линия между двумя точками. Точки могут быть
* фиксированными координатами или ref объектов — тогда луч
* следует за объектами каждый кадр (лазеры, мосты света,
* соединения, цепи).
* Trail — шлейф, тянущийся за движущимся объектом (Babylon TrailMesh).
*
* Живут только в Play-режиме. Управляются скриптом через game.fx.* —
* каждый вызов возвращает прокси-объект.
*/
import {
MeshBuilder, StandardMaterial, Color3, Vector3,
} from '@babylonjs/core';
import { TrailMesh } from '@babylonjs/core/Meshes/trailMesh';
let _fxIdSeq = 1;
export class BeamManager {
constructor(scene3d) {
this.scene3d = scene3d;
this.scene = scene3d.scene;
/** @type {Map<number, object>} id → fx state (beam | trail) */
this.items = new Map();
this._renderHook = null;
}
start() {
if (this._renderHook) return;
this._renderHook = () => this._tick();
this.scene.registerBeforeRender(this._renderHook);
}
stop() {
if (this._renderHook) {
try { this.scene.unregisterBeforeRender(this._renderHook); } catch (e) {}
this._renderHook = null;
}
for (const it of this.items.values()) this._disposeItem(it);
this.items.clear();
}
_disposeItem(it) {
try {
if (it.mesh) it.mesh.dispose();
if (it.mat) it.mat.dispose();
} catch (e) { /* ignore */ }
}
/**
* Создать луч между двумя точками.
* opts: { from, to — {x,y,z} или ref-строка объекта;
* color: '#hex', width: толщина (м) }.
* Возвращает id.
*/
addBeam(opts = {}) {
const id = _fxIdSeq++;
const width = Number.isFinite(opts.width) ? opts.width : 0.15;
const mat = new StandardMaterial('beamMat_' + id, this.scene);
const col = Color3.FromHexString(opts.color || '#66ccff');
mat.diffuseColor = col;
mat.emissiveColor = col;
mat.disableLighting = true;
// Цилиндр-заготовка единичной высоты — масштабируем под длину луча.
const mesh = MeshBuilder.CreateCylinder('beam_' + id,
{ height: 1, diameter: width, tessellation: 8 }, this.scene);
mesh.material = mat;
mesh.isPickable = false;
mesh.renderingGroupId = 1;
const it = {
id, type: 'beam', mesh, mat,
from: opts.from, to: opts.to,
};
this.items.set(id, it);
this._updateBeam(it); // сразу позиционируем
return id;
}
/** Сменить цвет луча. */
setBeamColor(id, color) {
const it = this.items.get(Number(id));
if (!it || it.type !== 'beam' || !it.mat) return;
const col = Color3.FromHexString(color || '#66ccff');
it.mat.diffuseColor = col;
it.mat.emissiveColor = col;
}
/** Сменить концы луча (координаты или ref). */
setBeamEndpoints(id, from, to) {
const it = this.items.get(Number(id));
if (!it || it.type !== 'beam') return;
if (from !== undefined) it.from = from;
if (to !== undefined) it.to = to;
}
/**
* Создать шлейф за объектом.
* ref — ref-строка объекта. opts: { color, width, lifetime (сек) }.
* Возвращает id.
*/
addTrail(ref, opts = {}) {
const data = this._resolve(ref);
const mesh = data && (data.mesh || data.rootMesh || data.rootNode);
if (!mesh) return null;
const id = _fxIdSeq++;
const width = Number.isFinite(opts.width) ? opts.width : 0.4;
const lifetime = Number.isFinite(opts.lifetime) ? opts.lifetime : 1.5;
// TrailMesh(name, generator, scene, diameter, length, autoStart).
// length — сколько сегментов хранить; считаем из lifetime (≈60 fps).
const segments = Math.max(10, Math.round(lifetime * 60));
const trail = new TrailMesh('trail_' + id, mesh, this.scene,
width, segments, true);
const mat = new StandardMaterial('trailMat_' + id, this.scene);
const col = Color3.FromHexString(opts.color || '#ffcc44');
mat.diffuseColor = col;
mat.emissiveColor = col;
mat.disableLighting = true;
mat.alpha = 0.7;
trail.material = mat;
trail.isPickable = false;
trail.renderingGroupId = 1;
const it = { id, type: 'trail', mesh: trail, mat };
this.items.set(id, it);
return id;
}
/** Убрать луч/след по id. */
remove(id) {
const it = this.items.get(Number(id));
if (!it) return;
this._disposeItem(it);
this.items.delete(it.id);
}
// ===== внутреннее =====
_tick() {
if (this.items.size === 0) return;
for (const it of this.items.values()) {
// Trail обновляется самим Babylon (autoStart). Beam — мы.
if (it.type === 'beam') this._updateBeam(it);
}
}
_updateBeam(it) {
const a = this._point(it.from);
const b = this._point(it.to);
if (!a || !b || !it.mesh) return;
const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
const len = Math.hypot(dx, dy, dz);
if (len < 0.001) { it.mesh.setEnabled(false); return; }
it.mesh.setEnabled(true);
// Цилиндр единичной высоты вдоль локальной оси Y. Растягиваем по длине.
it.mesh.scaling.y = len;
// Центр луча.
it.mesh.position.set((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2);
// Ориентируем ось Y цилиндра вдоль вектора a→b.
const dir = new Vector3(dx, dy, dz).normalize();
// yaw + pitch так, чтобы +Y смотрел вдоль dir.
const yaw = Math.atan2(dir.x, dir.z);
const pitch = Math.acos(Math.max(-1, Math.min(1, dir.y)));
it.mesh.rotation.set(pitch, yaw, 0);
}
/** Точка из {x,y,z} или ref-строки объекта. */
_point(p) {
if (!p) return null;
if (typeof p === 'object' && Number.isFinite(p.x)) {
return { x: p.x, y: p.y, z: p.z };
}
if (typeof p === 'string') {
const d = this._resolve(p);
if (d) return { x: d.x, y: d.y, z: d.z };
}
return null;
}
/** Резолв ref в data-объект менеджера. */
_resolve(ref) {
if (typeof ref !== 'string') return null;
const rt = this.scene3d.gameRuntime;
let r = ref;
if (rt && rt._localToReal && rt._localToReal.has(r)) {
r = rt._localToReal.get(r);
}
const colon = r.indexOf(':');
if (colon < 0) return null;
const kind = r.slice(0, colon);
const rest = r.slice(colon + 1);
const getFrom = (mgr) => {
if (!mgr || !mgr.instances) return null;
let d = mgr.instances.get(rest);
if (!d) {
const n = Number(rest);
if (Number.isFinite(n)) d = mgr.instances.get(n);
}
return d || null;
};
if (kind === 'primitive') return getFrom(this.scene3d.primitiveManager);
if (kind === 'model') return getFrom(this.scene3d.modelManager);
return null;
}
}