/** * GdDiamond — визуал алмазов и эффект подбора для GD-уровней. * * 1. Находит все primitive id ∈ [14001, 14002] и заменяет sphere * на красивый **октаэдр-кристалл** с emissive свечением и точечным светом. * 2. Idle-анимация: вращение + плавание вверх-вниз. * 3. При исчезновении (setEnabled(false) или setVisible(false)) — взрыв ParticleSystem * на 1 секунду на месте алмаза. * * Подключается в BabylonScene.enterPlayMode. */ import { Vector3, Color3, Color4, StandardMaterial, MeshBuilder, PointLight, ParticleSystem, Texture, } from '@babylonjs/core'; const DIAMOND_IDS = [14001, 14002]; export class GdDiamond { constructor() { this.scene = null; this._scene3d = null; this._items = []; // [{ primId, root, mesh, light, baseY, phase }] this._onBeforeRender = null; this._t = 0; } attach(scene, scene3d) { if (!scene || !scene3d) return; this.scene = scene; this._scene3d = scene3d; // Отложенно — primitives могут не быть загружены let attempts = 0; const tryAttach = () => { attempts++; const pm = scene3d.primitiveManager; if (!pm || !pm.instances) { if (attempts < 10) setTimeout(tryAttach, 200); return; } this._replaceDiamonds(); this._setupAnim(); }; tryAttach(); } _replaceDiamonds() { const pm = this._scene3d.primitiveManager; let n = 0; for (const id of DIAMOND_IDS) { const data = pm.instances.get(id); if (!data || !data.mesh) continue; const orig = data.mesh; const x = data.x, y = data.y, z = data.z || 0; // Сделать красивый октаэдр-кристалл вместо sphere const crystal = MeshBuilder.CreatePolyhedron(`gd_diamond_${id}`, { type: 1, size: 0.55 }, this.scene); // type=1 = octahedron crystal.position.set(x, y, z); // Материал — голубой стеклянный с свечением const mat = new StandardMaterial(`gd_diamond_mat_${id}`, this.scene); mat.diffuseColor = new Color3(0.5, 0.85, 1.0); mat.emissiveColor = new Color3(0.3, 0.7, 1.0); mat.specularColor = new Color3(1, 1, 1); mat.specularPower = 128; mat.alpha = 0.85; crystal.material = mat; // Точечный свет для атмосферы const light = new PointLight(`gd_diamond_light_${id}`, new Vector3(x, y, z), this.scene); light.diffuse = new Color3(0.4, 0.8, 1.0); light.specular = new Color3(0.5, 0.9, 1.0); light.intensity = 0.5; light.range = 4; // Прячем исходный sphere (визуально — кристалл его заменяет) try { orig.setEnabled(false); } catch (e) {} this._items.push({ primId: id, origMesh: orig, mesh: crystal, light, baseY: y, phase: id * 0.7, collected: false, }); // Если data.visible уже false (скрипт уровня загрузил флаг из save — // алмаз был забран в прошлый заход), кристалл скрыть без эффекта. if (data.visible === false) { try { crystal.setEnabled(false); } catch (e) {} try { light && light.setEnabled(false); } catch (e) {} this._items[this._items.length - 1].collected = true; } n++; } console.log(`[GdDiamond] установлено ${n} кристаллов`); } _setupAnim() { const pm = this._scene3d.primitiveManager; this._onBeforeRender = () => { this._t += 0.016; for (const it of this._items) { if (it.collected || !it.mesh) continue; // Вращение по Y it.mesh.rotation.y = this._t * 1.5 + it.phase; // Плавание вверх-вниз const bob = Math.sin(this._t * 2 + it.phase) * 0.25; it.mesh.position.y = it.baseY + bob; if (it.light) it.light.position.y = it.baseY + bob; // Подбор определяем по data.visible — скрипт уровня вызывает // game.scene.setVisible('primitive:14001', false) когда забрал. // Это надёжно: setEnabled false мы сами установили в _replaceDiamonds. const data = pm && pm.instances.get(it.primId); if (data && data.visible === false) { this._triggerCollect(it); } } }; this.scene.onBeforeRenderObservable.add(this._onBeforeRender); } _triggerCollect(it) { if (it.collected) return; it.collected = true; const pos = it.mesh.position.clone(); // Эффект частиц try { const ps = new ParticleSystem(`diamond_burst_${it.primId}`, 60, this.scene); // Используем процедурную текстуру (точка) если нет ассета ps.particleTexture = null; // Babylon делает квадратик по умолчанию ps.emitter = pos; ps.minEmitBox = new Vector3(-0.1, -0.1, -0.1); ps.maxEmitBox = new Vector3(0.1, 0.1, 0.1); ps.color1 = new Color4(0.5, 0.9, 1.0, 1); ps.color2 = new Color4(0.9, 1.0, 1.0, 1); ps.colorDead = new Color4(0.3, 0.5, 0.9, 0); ps.minSize = 0.08; ps.maxSize = 0.20; ps.minLifeTime = 0.4; ps.maxLifeTime = 1.0; ps.emitRate = 0; // burst-режим ps.manualEmitCount = 60; ps.gravity = new Vector3(0, -3, 0); ps.direction1 = new Vector3(-3, 4, -3); ps.direction2 = new Vector3(3, 8, 3); ps.minEmitPower = 1; ps.maxEmitPower = 4; ps.updateSpeed = 0.02; ps.disposeOnStop = true; ps.start(); // Стоп через 200мс (один burst) setTimeout(() => { try { ps.stop(); } catch (e) {} }, 200); } catch (e) { console.warn('[GdDiamond] particles failed', e); } // Кристалл — scale-up и fade за 400мс const mesh = it.mesh; const light = it.light; const startT = performance.now(); const DUR = 400; const fadeStep = () => { const elapsed = performance.now() - startT; if (elapsed > DUR) { try { mesh.dispose(); } catch (e) {} try { light && light.dispose(); } catch (e) {} return; } const t = elapsed / DUR; const s = 1 + t * 1.5; mesh.scaling.set(s, s, s); if (mesh.material) mesh.material.alpha = 0.85 * (1 - t); if (light) light.intensity = 0.5 * (1 - t); requestAnimationFrame(fadeStep); }; requestAnimationFrame(fadeStep); } dispose() { if (!this.scene) return; try { if (this._onBeforeRender) { this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender); this._onBeforeRender = null; } } catch (e) {} for (const it of this._items) { try { it.mesh && it.mesh.dispose(); } catch (e) {} try { it.light && it.light.dispose(); } catch (e) {} } this._items = []; this.scene = null; this._scene3d = null; } }