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