/** * 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; } }