import { describe, it, expect } from 'vitest';
import { Container, Sprite, Texture } from 'pixi.js';
import { FxLayer } from '../FxLayer';
import { FxRing } from '../FxRing';
const TICK = 1 / 60;
describe('FxLayer.ring', () => {
it('кольцо доигрывает и удаляется из слоя', () => {
const layer = new FxLayer();
const ring = layer.ring({ color: 0xd99a32, from: 8, to: 96, duration: 0.4 }, { x: 10, y: 20 });
expect(layer.children).toContain(ring);
for (let i = 0; i < 24; i++) layer.update(TICK);
expect(ring.destroyed).toBe(true);
expect(layer.children).not.toContain(ring);
});
it('зацикленное кольцо не самоуничтожается, cancel() гасит', () => {
const layer = new FxLayer();
const ring = layer.ring(
{ color: 0xff0000, from: 8, to: 96, duration: 0.3, repeat: Infinity },
{ x: 0, y: 0 }
);
for (let i = 0; i < 120; i++) layer.update(TICK);
expect(ring.destroyed).toBe(false);
ring.cancel();
expect(ring.destroyed).toBe(true);
});
it('родитель-опция: вьюха уходит в переданный контейнер', () => {
const layer = new FxLayer();
const depth = new Container();
const ring = layer.ring({ color: 0xffffff, from: 0, to: 10, duration: 0.1 }, { x: 0, y: 0 }, { parent: depth });
expect(ring.parent).toBe(depth);
});
});
describe('FxLayer.floatText', () => {
it('текст создаётся, поднимается и уничтожается по завершении', () => {
const layer = new FxLayer();
const fx = layer.floatText('+1', { x: 100, y: 100 }, { duration: 1, rise: 8 });
expect(layer.children).toContain(fx);
layer.update(TICK);
expect(fx.alpha).toBeGreaterThan(0);
expect(fx.y).toBeLessThanOrEqual(100);
for (let i = 0; i < 60; i++) layer.update(TICK);
expect(fx.destroyed).toBe(true);
expect(layer.children).not.toContain(fx);
});
});
describe('FxLayer.fade', () => {
it('вьюха растворяется и уничтожается', () => {
const layer = new FxLayer();
const parent = new Container();
const view = new Container();
view.alpha = 1;
parent.addChild(view);
layer.fade(view, { duration: 0.5 });
for (let i = 0; i < 30; i++) layer.update(TICK);
expect(view.destroyed).toBe(true);
expect(view.parent).toBeNull();
});
it('keep: true — вьюха жива с целевой альфой', () => {
const layer = new FxLayer();
const parent = new Container();
const view = new Container();
parent.addChild(view);
layer.fade(view, { duration: 0.5, to: 0.4, keep: true });
for (let i = 0; i < 30; i++) layer.update(TICK);
expect(view.destroyed).toBe(false);
expect(view.alpha).toBeCloseTo(0.4, 5);
expect(view.parent).toBe(parent);
});
it('вьюха, разрушенная извне во время эффекта, не роняет update', () => {
const layer = new FxLayer();
const view = new Container();
layer.fade(view, { duration: 1 });
layer.update(TICK);
view.destroy({ children: true });
expect(() => layer.update(TICK)).not.toThrow();
});
});
describe('FxLayer.pop', () => {
it('масштаб возвращается к базовому, вьюха жива', () => {
const layer = new FxLayer();
const view = new Container();
view.scale.set(2);
layer.pop(view, { duration: 0.25, fromScale: 0.5 });
layer.update(TICK);
expect(view.scale.x).toBeGreaterThan(0.5 * 2); // разгоны от базового
for (let i = 0; i < 15; i++) layer.update(TICK);
expect(view.scale.x).toBeCloseTo(2, 5);
expect(view.destroyed).toBe(false);
});
});
describe('FxLayer.flash', () => {
it('повторный флэш по тому же спрайту не дублируется, destroy спрайта чистится', () => {
const layer = new FxLayer();
const sprite = new Sprite(Texture.WHITE);
layer.flash(sprite, 0xff0000);
layer.flash(sprite, 0xff0000); // перезапуск, а не вторая запись
layer.update(TICK);
expect(sprite.tint).not.toBe(0xffffff); // вспышка применилась
for (let i = 0; i < 20; i++) layer.update(TICK); // 0.15 с — флэш доиграл
expect(sprite.tint).toBe(0xffffff);
sprite.destroy();
expect(() => layer.update(TICK)).not.toThrow();
});
});
describe('FxLayer.burst', () => {
it('детерминирован по seed: одинаковые позиции частиц', () => {
const a = new FxLayer({ seed: 42 });
const b = new FxLayer({ seed: 42 });
const opts = { rate: 0, lifetime: [0.5, 0.5] as [number, number], radialSpeed: [50, 100] as [number, number] };
const ea = a.burst(5, { x: 0, y: 0 }, opts);
const eb = b.burst(5, { x: 0, y: 0 }, opts);
a.update(TICK);
b.update(TICK);
expect(ea.children.map((c) => `${c.x},${c.y}`)).toEqual(eb.children.map((c) => `${c.x},${c.y}`));
});
it('эмиттер самоуничтожается, когда частицы умерли', () => {
const layer = new FxLayer({ seed: 1 });
const fx = layer.burst(2, { x: 0, y: 0 }, { rate: 0, lifetime: [0.2, 0.2] });
for (let i = 0; i < 20; i++) layer.update(TICK);
expect(fx.destroyed).toBe(true);
});
});
describe('FxLayer.trail', () => {
it('тянет эмиттер за якорем; stop() гасит хвост и завершает эффект', () => {
const layer = new FxLayer();
const anchor = new Container();
anchor.position.set(10, 10);
const trail = layer.trail(anchor, { rate: 30, lifetime: [0.1, 0.1], seed: 7 });
anchor.position.set(50, 30);
layer.update(TICK);
expect(trail.view.x).toBe(50);
trail.stop();
for (let i = 0; i < 20; i++) layer.update(TICK);
expect(trail.destroyed).toBe(true);
});
});
describe('FxLayer.clear', () => {
it('гасит все активные эффекты и флэш-записи', () => {
const layer = new FxLayer();
const ring = layer.ring({ color: 0xffffff, from: 0, to: 10, duration: 5 }, { x: 0, y: 0 });
const view = new Container();
layer.pop(view, { duration: 5 });
const sprite = new Sprite(Texture.WHITE);
layer.flash(sprite, 0xff0000);
layer.clear();
expect(ring.destroyed).toBe(true);
expect(view.scale.x).toBe(1); // pop вернул базовый масштаб
layer.update(TICK); // не падает
});
});
describe('FxRing', () => {
it('радиус растёт по спеку, альфа гаснет к концу', () => {
const ring = new FxRing({ color: 0xd99a32, from: 8, to: 96, duration: 0.4 });
ring.update(0.2);
ring.update(0.2);
expect(ring.done).toBe(true);
expect(ring.destroyed).toBe(true);
});
});