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

170 lines
7.1 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.

/**
* GdFinish — финишные ворота + конфетти при пересечении (этап G5).
*
* Ставит выбранную фабрику ворот на FINISH_X (из cylinder-примитива или из
* самого правого ненулевого x уровня). Поворачивает поперёк трассы (Y=90°).
*
* При вызове .celebrate() запускает конфетти-партикль на 1.5с.
*
* Финиш-событие триггерится в скрипте уровня (по FINISH_X) — мы лишь рисуем
* ворота. Подключим celebrate из BabylonScene при gdFinish сообщении.
*/
import {
Vector3, TransformNode, ParticleSystem, Texture, DynamicTexture,
Color3, Color4, MeshBuilder, StandardMaterial,
} from '@babylonjs/core';
import { FINISH_CATALOG } from '../AdminPreview/gdFinishes/finishFactories';
const DEFAULT_FINISH_BY_EPOCH = {
1: 'f1_v4', // Рустик (выбор юзера)
2: 'f2_v5', // Бронзовая
3: 'f3_v3',
4: 'f4_v4',
5: 'f5_v2',
6: 'f6_v4',
7: 'f7_v1',
8: 'f8_v5',
9: 'f9_v4',
10: 'f10_v5',
};
export class GdFinish {
constructor() {
this.scene = null;
this._scene3d = null;
this._handle = null;
this._root = null;
this._particles = null;
this._particleEmitter = null;
this._celebrated = false;
}
attach(scene, scene3d, epoch = 1, finishId = null) {
if (!scene || !scene3d) return;
this.scene = scene;
this._scene3d = scene3d;
const id = finishId || DEFAULT_FINISH_BY_EPOCH[epoch] || 'f1_v1';
const factory = FINISH_CATALOG.find(f => f.id === id);
if (!factory) {
console.warn('[GdFinish] фабрика не найдена:', id);
return;
}
// Определяем FINISH_X — ищем cylinder-примитив (это стандартный gd_finish)
let finishX = null;
const pm = scene3d.primitiveManager;
if (pm) {
for (const data of pm.instances.values()) {
if (String(data.type) === 'cylinder' && typeof data.x === 'number' && data.x > 50) {
// самый правый cylinder = финиш
if (finishX == null || data.x > finishX) finishX = data.x;
}
}
}
if (finishX == null) {
console.warn('[GdFinish] не нашли cylinder-финиш, ставлю на x=1000');
finishX = 1000;
}
// Скрываем оригинальный cylinder если он рядом с финишем
if (pm) {
for (const data of pm.instances.values()) {
if (String(data.type) === 'cylinder' && Math.abs(data.x - finishX) < 2 && data.mesh) {
try { data.mesh.setEnabled(false); } catch (e) {}
this._hiddenFinishCylinder = data.mesh;
break;
}
}
}
const h = factory.make(scene, `gd_finish_inst`);
if (!h || !h.root) return;
this._handle = h;
this._root = h.root;
this._root.position = new Vector3(finishX, 1, 0);
this._root.rotation.y = Math.PI / 2; // поперёк трассы
this._finishX = finishX;
this._setupConfetti(finishX);
// Автоматически срабатывает когда куб игрока пересекает FINISH_X.
// Скрипт уровня сам показывает win-screen, мы только конфетти добавим.
this._onBeforeRender = () => {
if (this._celebrated) return;
const pp = this._scene3d?.player?._pos;
if (!pp) return;
if (pp.x >= this._finishX - 1.5) {
this.celebrate();
}
};
this.scene.onBeforeRenderObservable.add(this._onBeforeRender);
}
/** Создать ParticleSystem конфетти (старт по celebrate). */
_setupConfetti(finishX) {
// Маленький emitter-mesh — невидимая точка над финишем
const emitter = MeshBuilder.CreateBox('gd_confetti_emitter', { size: 0.1 }, this.scene);
emitter.position.set(finishX, 4, 0);
emitter.isVisible = false;
this._particleEmitter = emitter;
// Текстура частицы — квадратик canvas (просто белый круг)
const T = 32;
const dt = new DynamicTexture('gd_confetti_tex', { width: T, height: T }, this.scene, true);
const ctx = dt.getContext();
ctx.clearRect(0, 0, T, T);
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(T / 2, T / 2, T / 2 - 2, 0, Math.PI * 2);
ctx.fill();
dt.hasAlpha = true;
dt.update();
const ps = new ParticleSystem('gd_confetti', 400, this.scene);
ps.particleTexture = dt;
ps.emitter = emitter;
ps.minEmitBox = new Vector3(-1.5, 0, -1.5);
ps.maxEmitBox = new Vector3(1.5, 0, 1.5);
// Конфетти разных цветов
ps.color1 = new Color4(1, 0.3, 0.4, 1);
ps.color2 = new Color4(0.3, 0.7, 1, 1);
ps.colorDead = new Color4(1, 1, 1, 0);
ps.minSize = 0.15; ps.maxSize = 0.35;
ps.minLifeTime = 1.0; ps.maxLifeTime = 2.5;
ps.emitRate = 0; // пока выключен
ps.gravity = new Vector3(0, -8, 0);
ps.direction1 = new Vector3(-3, 5, -3);
ps.direction2 = new Vector3(3, 8, 3);
ps.minAngularSpeed = -3; ps.maxAngularSpeed = 3;
ps.minEmitPower = 1; ps.maxEmitPower = 3;
ps.updateSpeed = 0.02;
ps.start(); // активен но без эмиссии
this._particles = ps;
}
/** Включить конфетти на ~1.5 секунды. Вызывает уровень при пересечении финиша. */
celebrate() {
if (this._celebrated || !this._particles) return;
this._celebrated = true;
this._particles.emitRate = 250;
setTimeout(() => {
try { this._particles.emitRate = 0; } catch (e) {}
}, 1500);
}
dispose() {
try {
if (this._onBeforeRender && this.scene) {
this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender);
this._onBeforeRender = null;
}
} catch (e) {}
if (this._handle && this._handle.dispose) try { this._handle.dispose(); } catch (e) {}
if (this._particles) try { this._particles.dispose(); } catch (e) {}
if (this._particleEmitter) try { this._particleEmitter.dispose(); } catch (e) {}
if (this._hiddenFinishCylinder) try { this._hiddenFinishCylinder.setEnabled(true); } catch (e) {}
this._handle = null;
this._root = null;
this._particles = null;
this._particleEmitter = null;
this._hiddenFinishCylinder = null;
this.scene = null;
this._scene3d = null;
}
}