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)
165 lines
6.7 KiB
JavaScript
165 lines
6.7 KiB
JavaScript
/**
|
||
* GdPlayerCube — визуальные эффекты на куб игрока (этап G7).
|
||
*
|
||
* Находит primitive с id=10001 (REF_BODY в скрипте уровня) и навешивает:
|
||
* 1. Чёрный outline толщиной 0.04 (renderOutline).
|
||
* 2. Бледный glow в primary-цвете скина (через GlowLayer.referenceMeshToUseItsOwnMaterial).
|
||
* 3. Squash & stretch: при увеличении скорости падения сжимается по Y,
|
||
* при отскоке от земли — растягивается.
|
||
*
|
||
* Использование:
|
||
* const pc = new GdPlayerCube();
|
||
* pc.attach(scene, scene3d);
|
||
* pc.dispose();
|
||
*/
|
||
import { Color3, GlowLayer } from '@babylonjs/core';
|
||
|
||
const PLAYER_PRIM_ID = 10001;
|
||
|
||
export class GdPlayerCube {
|
||
constructor() {
|
||
this.scene = null;
|
||
this._scene3d = null;
|
||
this._cubeMesh = null;
|
||
this._glow = null;
|
||
this._onBeforeRender = null;
|
||
this._prevPlayerY = null;
|
||
this._prevVy = 0;
|
||
this._baseScaleY = 1;
|
||
}
|
||
|
||
attach(scene, scene3d) {
|
||
if (!scene || !scene3d) return;
|
||
this.scene = scene;
|
||
this._scene3d = scene3d;
|
||
// Откладываем — primitive может быть не создан в момент enterPlayMode
|
||
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);
|
||
else console.warn('[GdPlayerCube] куб игрока не найден');
|
||
return;
|
||
}
|
||
this._cubeMesh = mesh;
|
||
this._baseScaleY = mesh.scaling?.y || 1;
|
||
this._setupGlow();
|
||
this._setupSquash();
|
||
console.log('[GdPlayerCube] прикреплён, mesh=', mesh.name);
|
||
};
|
||
tryAttach();
|
||
}
|
||
|
||
/** Чёрная обводка по силуэту куба. */
|
||
_setupOutline() {
|
||
const m = this._cubeMesh;
|
||
try {
|
||
m.renderOutline = true;
|
||
m.outlineWidth = 0.04;
|
||
m.outlineColor = new Color3(0, 0, 0);
|
||
} catch (e) { console.warn('[GdPlayerCube] outline failed', e); }
|
||
}
|
||
|
||
/** GlowLayer — куб светится своим primary цветом скина.
|
||
* GlowLayer создаётся один раз на сцену; если уже есть — используем его. */
|
||
_setupGlow() {
|
||
try {
|
||
// Не создаём GlowLayer если он уже есть на сцене (другой код мог)
|
||
let glow = this.scene.effectLayers?.find?.((l) => l.name === 'gd_glow');
|
||
if (!glow) {
|
||
glow = new GlowLayer('gd_glow', this.scene, { mainTextureFixedSize: 256, blurKernelSize: 24 });
|
||
glow.intensity = 0.6;
|
||
this._glow = glow;
|
||
} else {
|
||
this._glow = null; // не наш, не disposить
|
||
}
|
||
glow.addIncludedOnlyMesh(this._cubeMesh);
|
||
} catch (e) { console.warn('[GdPlayerCube] glow failed', e); }
|
||
}
|
||
|
||
/** Squash при падении / stretch при взлёте.
|
||
* Считаем vy = (y_now - y_prev) / dt. При большой |vy| меняем scaling.
|
||
* При приземлении (vy резко 0) — короткий «бум» сжатия по Y. */
|
||
_setupSquash() {
|
||
this._prevPlayerY = null;
|
||
this._prevVy = 0;
|
||
this._landSquashLeft = 0; // секунд осталось эффекта squash после удара
|
||
|
||
this._onBeforeRender = () => {
|
||
const m = this._cubeMesh;
|
||
const pp = this._scene3d?.player?._pos;
|
||
if (!m || !pp || !m.scaling) return;
|
||
const dt = this.scene.getEngine().getDeltaTime() / 1000;
|
||
if (dt <= 0 || dt > 0.2) return;
|
||
const y = pp.y;
|
||
if (this._prevPlayerY == null) {
|
||
this._prevPlayerY = y;
|
||
return;
|
||
}
|
||
const vy = (y - this._prevPlayerY) / dt;
|
||
// Детект приземления: была отрицательная vy, стала ≈ 0
|
||
if (this._prevVy < -8 && Math.abs(vy) < 2) {
|
||
this._landSquashLeft = 0.18;
|
||
}
|
||
this._prevPlayerY = y;
|
||
this._prevVy = vy;
|
||
|
||
// Целевой масштаб
|
||
let sy = this._baseScaleY;
|
||
let sxz = 1;
|
||
if (this._landSquashLeft > 0) {
|
||
// Сжатие после удара: y 0.7, xz 1.2 → линейный возврат
|
||
const t = this._landSquashLeft / 0.18;
|
||
const squashAmt = Math.sin(t * Math.PI) * 0.3;
|
||
sy = this._baseScaleY * (1 - squashAmt);
|
||
sxz = 1 + squashAmt * 0.5;
|
||
this._landSquashLeft -= dt;
|
||
if (this._landSquashLeft < 0) this._landSquashLeft = 0;
|
||
} else if (vy > 5) {
|
||
// Взлёт — растягивание по y
|
||
const k = Math.min(1, vy / 15);
|
||
sy = this._baseScaleY * (1 + 0.18 * k);
|
||
sxz = 1 - 0.08 * k;
|
||
} else if (vy < -5) {
|
||
// Падение — лёгкое сжатие по y (минимальное, чтобы не дёргалось)
|
||
const k = Math.min(1, -vy / 15);
|
||
sy = this._baseScaleY * (1 - 0.10 * k);
|
||
sxz = 1 + 0.05 * k;
|
||
}
|
||
// Плавно интерполируем к целевому
|
||
const lerp = (a, b, t) => a + (b - a) * t;
|
||
const speed = 12;
|
||
m.scaling.y = lerp(m.scaling.y, sy, Math.min(1, dt * speed));
|
||
m.scaling.x = lerp(m.scaling.x, sxz, Math.min(1, dt * speed));
|
||
m.scaling.z = lerp(m.scaling.z, sxz, Math.min(1, dt * speed));
|
||
};
|
||
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) {}
|
||
if (this._cubeMesh) {
|
||
try {
|
||
this._cubeMesh.renderOutline = false;
|
||
this._cubeMesh.scaling.set(1, 1, 1);
|
||
} catch (e) {}
|
||
}
|
||
if (this._glow) {
|
||
try { this._glow.dispose(); } catch (e) {}
|
||
this._glow = null;
|
||
}
|
||
this._cubeMesh = null;
|
||
this._scene3d = null;
|
||
this.scene = null;
|
||
}
|
||
}
|