Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026). Содержит изменения которые делались в процессе подготовки прод-окружения: Фиксы импортов после выноса из minecraftia: - Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/) - Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/ - API.js скопирован из минки целиком (было 8 экспортов, стало 312) - Добавлены PLAYER_URL, MyButton_1, недостающие компоненты - Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require) Структура ассетов: - public/kubikon-templates/ → public/assets/kubikon-templates/ - public/kubikon-learn/ → public/assets/kubikon-learn/ - (код искал в /assets/, файлы лежали без /assets/) Навигация роутов внутри студии: - /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced) - /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X UI: - Новый компонент StudioHeader (61px, как в минке) + копия favicon - WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера - SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке) - Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык) Документация: - docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR - docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API - API_USAGE.md — список эндпоинтов backend - README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/ .gitignore: - public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 '../../admin-preview/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;
|
||
}
|
||
}
|