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