Newer
Older
rpg / v2 / packages / engine / src / render / __tests__ / particleLayers.test.ts
import { describe, expect, it } from 'vitest';
import * as THREE from 'three';
import {
    createQuadLayer, createSpriteLayer, makeParticleLightUniforms, makeParticlePerspUniforms,
    type ParticleLayer, type ParticlePoint,
} from '../particles';

const pt = (i: number): ParticlePoint => ({ pos: [i, 1, 2], color: '#a8bacb' });
const lights = makeParticleLightUniforms();
const perspU = makeParticlePerspUniforms();
perspU.uPxScale!.value = 3; // прежний per-layer масштаб — теперь в общем блоке

/** Регрессия #218: держатель слоя (WeatherSystem) держит методы, снятые со
 *  слоя в момент создания (и вызывает их каждый тик). Раньше рост ёмкости
 *  перезаписывал методы слоя методами свежей реализации — но снятая ранее
 *  ссылка продолжала указывать в мёртвое замыкание: каждый тик сверх
 *  ёмкости создавал новый слой с однократной записью, и замороженные
 *  снимки дождя копились в сцене (пелена на экране). Теперь фасад
 *  стабильн: сколько бы ни было ростов, в сцене ровно один меш. */
describe('слои частиц: рост ёмкости не плодит меши (#218)', () => {
    const factories: [string, (scene: THREE.Scene, cap: number) => ParticleLayer][] = [
        ['спрайты', (s, cap) => createSpriteLayer(s, cap, perspU, lights, false)],
        ['квады', (s, cap) => createQuadLayer(s, cap, lights, false)],
    ];
    for (const [name, make] of factories) {
        it(`${name}: превышение ёмкости подменяет меш, а не плодит слои`, () => {
            const scene = new THREE.Scene();
            const layer = make(scene, 2);
            const update = layer.update; // как WeatherSystem: метод снят заранее
            const meshes = () => scene.children.length;
            expect(meshes()).toBe(1);
            update([pt(0), pt(1)]);
            expect(meshes()).toBe(1);
            update([pt(0), pt(1), pt(2), pt(3)]); // рост 2 → 4
            expect(meshes()).toBe(1);
            update([pt(0), pt(1), pt(2)]); // выше старой ёмкости, ниже новой
            expect(meshes()).toBe(1);
            update([pt(0), pt(1), pt(2), pt(3)]); // повторное превышение
            expect(meshes()).toBe(1);
            layer.setTint(0x40465c); // тинт после роста идёт в живой меш
            layer.dispose();
            expect(meshes()).toBe(0);
            layer.dispose(); // повторный dispose безопасен
            expect(meshes()).toBe(0);
        });
    }
});

/** Перспективное затухание спрайтов: общий блок юниформ шарится IUniform'ами —
 *  рост ёмкости (новый impl/меш) не отрывается от блока рендера. */
describe('слои частиц: общий persp-блок переживает рост ёмкости', () => {
    it('новый меш ссылается на те же IUniform', () => {
        const scene = new THREE.Scene();
        const layer = createSpriteLayer(scene, 2, perspU, lights, false);
        layer.update([pt(0), pt(1), pt(2), pt(3)]); // рост 2 → 4
        const mesh = scene.children[0] as THREE.Points;
        const u = (mesh.material as THREE.ShaderMaterial).uniforms;
        expect(u.uPxScale).toBe(perspU.uPxScale);
        expect(u.uPersp).toBe(perspU.uPersp);
        expect(u.uRefDist).toBe(perspU.uRefDist);
        layer.dispose();
    });
});

/** Регрессия ревью: рост ёмкости пересоздаёт impl с дефолтным белым uTint —
 *  слой должен помнить свой tint и переапплаить его в свежий меш. */
describe('слои частиц: tint переживает рост ёмкости', () => {
    const factories: [string, (scene: THREE.Scene, cap: number) => ParticleLayer][] = [
        ['спрайты', (s, cap) => createSpriteLayer(s, cap, perspU, lights, false)],
        ['квады', (s, cap) => createQuadLayer(s, cap, lights, false)],
    ];
    for (const [name, make] of factories) {
        it(`${name}: tint до роста остаётся в новом меше`, () => {
            const scene = new THREE.Scene();
            const layer = make(scene, 2);
            layer.setTint(0x40465c);
            layer.update([pt(0), pt(1), pt(2), pt(3)]); // рост 2 → 4
            const mesh = scene.children[0] as THREE.Points;
            const t = (mesh.material as THREE.ShaderMaterial).uniforms.uTint.value as THREE.Vector3;
            expect(t.x).toBeCloseTo(0x40 / 255);
            expect(t.y).toBeCloseTo(0x46 / 255);
            expect(t.z).toBeCloseTo(0x5c / 255);
            layer.dispose();
        });
    }
});