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

165 lines
6.5 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.

/**
* GdPlayerTrail — частицы-шлейф за кубом игрока (этап G8).
*
* 3 типа trail соответствуют скинам из магазина (cubeSkinFactories.CUBE_TRAILS):
* - trail_white — белые точки, мягкое мерцание
* - trail_fire — оранжево-жёлтые искры с гравитацией
* - trail_ice — синие звёздочки, медленно падают
*
* Тип берётся из gd_progress.equipped_trail. При смене trail в магазине
* нужно пересоздать (мы пересоздаём при следующем enterPlayMode).
*/
import {
ParticleSystem, Color4, Color3, Vector3, MeshBuilder, StandardMaterial,
DynamicTexture, Texture,
} from '@babylonjs/core';
const PLAYER_PRIM_ID = 10001;
const STORYS_BASE = '/api-storys';
export class GdPlayerTrail {
constructor() {
this.scene = null;
this._scene3d = null;
this._cubeMesh = null;
this._emitter = null;
this._particles = null;
this._textureRefs = [];
}
/** projectId нужен чтобы прочитать gd_progress.equipped_trail. */
async attach(scene, scene3d, projectId, userId) {
if (!scene || !scene3d) return;
this.scene = scene;
this._scene3d = scene3d;
// Прочитать выбранный trail
let trailId = 'trail_white';
try {
const url = `${STORYS_BASE}/kubikon3d/savegame/${projectId}/${userId}/gd_progress`;
const r = await fetch(url, {
headers: { Authorization: localStorage.getItem('Authorization') || '' },
});
if (r.ok) {
const j = await r.json();
trailId = j?.data?.equipped_trail || 'trail_white';
}
} catch (e) { /* fallback на trail_white */ }
// Ждём куб игрока (он создаётся в primitiveManager при load)
let attempts = 0;
const tryAttach = () => {
attempts++;
const pm = scene3d.primitiveManager;
const data = pm?.instances?.get(PLAYER_PRIM_ID);
const mesh = data?.mesh;
if (!mesh) {
if (attempts < 10) setTimeout(tryAttach, 200);
return;
}
this._cubeMesh = mesh;
this._setupTrail(trailId);
console.log(`[GdPlayerTrail] trail=${trailId}, attached`);
};
tryAttach();
}
_setupTrail(trailId) {
const ps = new ParticleSystem('gd_trail', 200, this.scene);
ps.particleTexture = this._makeTexture(trailId);
ps.emitter = this._cubeMesh; // emitter — сам куб, частицы рождаются вокруг него
ps.minEmitBox = new Vector3(-0.3, -0.3, -0.3);
ps.maxEmitBox = new Vector3(0.3, 0.3, 0.3);
ps.minSize = 0.15;
ps.maxSize = 0.35;
ps.minLifeTime = 0.25;
ps.maxLifeTime = 0.6;
ps.emitRate = 60;
ps.minAngularSpeed = -2;
ps.maxAngularSpeed = 2;
ps.updateSpeed = 0.02;
if (trailId === 'trail_fire') {
ps.color1 = new Color4(1.0, 0.7, 0.1, 1);
ps.color2 = new Color4(1.0, 0.3, 0.0, 1);
ps.colorDead = new Color4(0.2, 0.0, 0.0, 0);
ps.minSize = 0.20; ps.maxSize = 0.45;
ps.minLifeTime = 0.30; ps.maxLifeTime = 0.7;
ps.gravity = new Vector3(0, -3, 0);
ps.direction1 = new Vector3(-1, -1, -1);
ps.direction2 = new Vector3(1, 0, 1);
ps.blendMode = ParticleSystem.BLENDMODE_ADD;
ps.emitRate = 80;
} else if (trailId === 'trail_ice') {
ps.color1 = new Color4(0.55, 0.85, 1.0, 1);
ps.color2 = new Color4(0.85, 0.95, 1.0, 1);
ps.colorDead = new Color4(0.7, 0.85, 1.0, 0);
ps.minSize = 0.18; ps.maxSize = 0.35;
ps.gravity = new Vector3(0, -2, 0);
ps.direction1 = new Vector3(-0.5, -0.5, -0.5);
ps.direction2 = new Vector3(0.5, 0.2, 0.5);
ps.blendMode = ParticleSystem.BLENDMODE_STANDARD;
} else {
// trail_white — мягкие белые точки
ps.color1 = new Color4(1, 1, 1, 0.95);
ps.color2 = new Color4(0.95, 0.95, 1.0, 0.85);
ps.colorDead = new Color4(1, 1, 1, 0);
ps.gravity = new Vector3(0, 0, 0);
ps.direction1 = new Vector3(-0.3, -0.3, -0.3);
ps.direction2 = new Vector3(0.3, 0.3, 0.3);
ps.blendMode = ParticleSystem.BLENDMODE_STANDARD;
}
ps.minEmitPower = 0.3;
ps.maxEmitPower = 1.0;
ps.start();
this._particles = ps;
}
/** Текстура частицы — круг с белым/цветным мягким градиентом. */
_makeTexture(trailId) {
const S = 64;
const dt = new DynamicTexture(`gd_trail_${trailId}_tex`, { width: S, height: S }, this.scene, true);
const ctx = dt.getContext();
ctx.clearRect(0, 0, S, S);
const cx = S / 2, cy = S / 2;
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, S / 2);
if (trailId === 'trail_ice') {
// Снежинка-звёздочка
ctx.strokeStyle = '#cce4ff';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a) * (S / 2 - 4), cy + Math.sin(a) * (S / 2 - 4));
ctx.stroke();
}
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
ctx.fill();
} else {
g.addColorStop(0, 'rgba(255,255,255,1)');
g.addColorStop(0.5, 'rgba(255,255,255,0.6)');
g.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, S / 2, 0, Math.PI * 2);
ctx.fill();
}
dt.hasAlpha = true;
dt.update();
this._textureRefs.push(dt);
return dt;
}
dispose() {
if (this._particles) try { this._particles.dispose(); } catch (e) {}
for (const t of this._textureRefs) try { t.dispose(); } catch (e) {}
this._textureRefs = [];
this._particles = null;
this._cubeMesh = null;
this._scene3d = null;
this.scene = null;
}
}