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)
132 lines
5.4 KiB
JavaScript
132 lines
5.4 KiB
JavaScript
/**
|
||
* GdSpikes — заменяем cone-примитивы на выбранный шип из spikeFactories.
|
||
*
|
||
* Размеры подгоняем под габариты оригинального cone (PrimitiveTypes.js
|
||
* defaultScale 1.3×1.6×1.3). Низ шипа = верх пола (y=1).
|
||
*
|
||
* Hitbox/коллизия не трогается — она в скрипте уровня по SPIKES[] (по x).
|
||
*/
|
||
import { Vector3 } from '@babylonjs/core';
|
||
import { SPIKE_CATALOG } from '../AdminPreview/gdSpikes/spikeFactories';
|
||
|
||
// Выбор юзера через /admin-preview/gd-spikes. Эпоха → spike-id.
|
||
// Меняется когда юзер сохраняет новый выбор в gd_spike_choices.
|
||
const DEFAULT_SPIKE_BY_EPOCH = {
|
||
1: 'e1_v5', // Терновник
|
||
2: 'e2_v1', // Кристалл синий
|
||
3: 'e3_v4',
|
||
4: 'e4_v4',
|
||
5: 'e5_v4',
|
||
6: 'e6_v6',
|
||
7: 'e7_v8',
|
||
8: 'e8_v7',
|
||
9: 'e9_v7',
|
||
10: 'e10_v7',
|
||
};
|
||
|
||
export class GdSpikes {
|
||
constructor() {
|
||
this.scene = null;
|
||
this._scene3d = null;
|
||
this._spikeId = null;
|
||
this._spikeRoots = [];
|
||
this._spikeHandles = [];
|
||
this._hiddenOriginals = [];
|
||
this._onBeforeRender = null;
|
||
this._t = 0;
|
||
}
|
||
|
||
attach(scene, scene3d, epoch = 1, spikeId = null) {
|
||
if (!scene) return;
|
||
this.scene = scene;
|
||
this._scene3d = scene3d;
|
||
this._spikeId = spikeId || DEFAULT_SPIKE_BY_EPOCH[epoch] || 'e1_v5';
|
||
this._replaceCones();
|
||
this._setupSpin();
|
||
}
|
||
|
||
_replaceCones() {
|
||
const pm = this._scene3d?.primitiveManager;
|
||
if (!pm) return;
|
||
const factory = SPIKE_CATALOG.find(s => s.id === this._spikeId);
|
||
if (!factory) {
|
||
console.warn('[GdSpikes] фабрика не найдена:', this._spikeId);
|
||
return;
|
||
}
|
||
// Подгонка: шип должен быть как cone, но реально hitbox = радиус 0.7
|
||
// вокруг данной точки. Визуальный диаметр должен быть ~0.8м, высота ~1.4м.
|
||
const TARGET_H = 1.4;
|
||
const TARGET_W = 0.8;
|
||
// Наши фабрики строят шип высотой ~1.4, диаметром ~0.9
|
||
const FACTORY_H = 1.4;
|
||
const FACTORY_W = 0.9;
|
||
const scaleY = TARGET_H / FACTORY_H;
|
||
const scaleXZ = TARGET_W / FACTORY_W;
|
||
const FLOOR_TOP = 1.0;
|
||
let n = 0;
|
||
for (const data of pm.instances.values()) {
|
||
if (String(data.type) !== 'cone') continue;
|
||
if (typeof data.x !== 'number') continue;
|
||
const handle = factory.make(this.scene, `gd_spike_inst_${data.id}`);
|
||
if (!handle || !handle.root) continue;
|
||
const root = handle.root;
|
||
// Если шип на полу (y близко к 0.8 = центр стандартного cone у пола) —
|
||
// ставим на FLOOR_TOP=1. Иначе сохраняем оригинальную y (потолочные,
|
||
// на платформах, в воздухе).
|
||
const origY = typeof data.y === 'number' ? data.y : FLOOR_TOP;
|
||
const isFloorSpike = Math.abs(origY - 0.8) < 0.3;
|
||
const targetY = isFloorSpike ? FLOOR_TOP : origY;
|
||
root.position = new Vector3(data.x, targetY, data.z || 0);
|
||
root.scaling = new Vector3(scaleXZ, scaleY, scaleXZ);
|
||
// Если оригинал был перевёрнут (rotationX≈π) — переворачиваем и копию.
|
||
const origRotX = data.rotationX || 0;
|
||
if (Math.abs(origRotX - Math.PI) < 0.1) {
|
||
root.rotation.x = Math.PI;
|
||
}
|
||
if (data.mesh) {
|
||
data.mesh.setEnabled(false);
|
||
this._hiddenOriginals.push(data.mesh);
|
||
}
|
||
this._spikeRoots.push(root);
|
||
this._spikeHandles.push(handle);
|
||
n++;
|
||
}
|
||
console.log(`[GdSpikes] заменено ${n} конусов на '${this._spikeId}', scale=(${scaleXZ.toFixed(2)}, ${scaleY.toFixed(2)})`);
|
||
}
|
||
|
||
_setupSpin() {
|
||
this._onBeforeRender = () => {
|
||
this._t += 0.01;
|
||
for (let i = 0; i < this._spikeRoots.length; i++) {
|
||
const root = this._spikeRoots[i];
|
||
if (!root) continue;
|
||
// Лёгкое смещение фазы между шипами — чтобы не все одновременно
|
||
root.rotation.y = this._t * 1.3 + i * 0.5;
|
||
}
|
||
};
|
||
this.scene.onBeforeRenderObservable.add(this._onBeforeRender);
|
||
}
|
||
|
||
dispose() {
|
||
if (!this.scene) return;
|
||
try {
|
||
if (this._onBeforeRender) {
|
||
this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender);
|
||
this._onBeforeRender = null;
|
||
}
|
||
} catch (e) {}
|
||
for (const m of this._hiddenOriginals) {
|
||
try { m.setEnabled(true); } catch (e) {}
|
||
}
|
||
// dispose handles — фабрика умеет освобождать всё, что создала
|
||
for (const h of this._spikeHandles) {
|
||
try { h.dispose && h.dispose(); } catch (e) {}
|
||
}
|
||
this._spikeHandles = [];
|
||
this._spikeRoots = [];
|
||
this._hiddenOriginals = [];
|
||
this._scene3d = null;
|
||
this.scene = null;
|
||
}
|
||
}
|