import { Container, Sprite } from 'pixi.js';
import { Updatables, type Updatable } from '../../core/Updatables';
import type { Vec2 } from '../../math/Vec2';
import type { PixelTextOptions } from '../../ui/PixelText';
import { ParticleEmitter, type EmitterOptions } from '../Particles';
import { SpriteFlash } from '../spriteFx';
import { FxRing } from './FxRing';
import { FxFloatText } from './FxFloatText';
import { FxFade, type FxFadeOptions } from './FxFade';
import { FxPop } from './FxPop';
import { FxTrail } from './FxTrail';
import type { FxRingSpec, FxFloatSpec, FxPopSpec } from './fxSim';
/** Опции слоя. */
export interface FxLayerOptions {
/** Seed по умолчанию для разовых частиц (burst), если opts не передал свой. */
seed?: number;
}
/** Опции floatText: математика + стиль текста + родитель (для depth-слоёв). */
export interface FxLayerFloatOptions extends Partial<FxFloatSpec> {
style?: Partial<PixelTextOptions>;
parent?: Container;
}
/** Родитель вьюхи эффекта вместо самого слоя (для IsoDepthLayer.addFx). */
export interface FxParentOptions {
parent?: Container;
}
/**
* Слой визуальных эффектов: сам тикается (реализует Updatable — добавить один
* раз в engine.fx), спавнит one-shot пресеты и сам вычищает закончившиеся.
* Позиции — локальные px контейнера, куда добавлен слой (worldRoot — мировые
* эффекты через проекцию сцены, uiRoot — тосты/HUD). Изометрию слой не знает.
*/
export class FxLayer extends Container implements Updatable {
private readonly items = new Updatables();
private readonly flashes = new Map<Sprite, SpriteFlash>();
private readonly defaultSeed: number;
private seedCounter = 0;
constructor(opts?: FxLayerOptions) {
super();
this.defaultSeed = opts?.seed ?? 0x5eed;
}
/** Ручное добавление своего эффекта (эмиттер, SpriteMotion, FxTrail…). */
add<T extends Updatable>(fx: T): T {
this.items.add(fx);
return fx;
}
/** Погасить и уничтожить все активные эффекты (exit сцены, смена тоста). */
clear(): void {
this.items.forEach((u) => {
const cancellable = u as { cancel?: () => void };
if (cancellable.cancel) cancellable.cancel();
else if (!u.destroyed && u instanceof Container) {
u.destroy({ children: true });
}
});
this.items.clear();
this.flashes.clear();
}
update(dt: number): void {
this.items.update(dt);
for (const [sprite, flash] of this.flashes) {
if (sprite.destroyed) {
this.flashes.delete(sprite);
continue;
}
flash.update(dt);
}
}
/** Расходящееся/сходящееся кольцо; repeat > 1 — телеграф зоны. */
ring(spec: FxRingSpec, at: Vec2, opts?: FxParentOptions): FxRing {
const fx = new FxRing(spec);
fx.position.set(at.x, at.y);
return this.mount(fx, opts);
}
/** Всплывающий текст (числа урона, тосты, подборы). */
floatText(text: string, at: Vec2, opts?: FxLayerFloatOptions): FxFloatText {
const { style, parent, ...spec } = opts ?? {};
const fx = new FxFloatText(text, at, { duration: 1, ...spec }, style);
return this.mount(fx, { parent });
}
/** Растворение (to < 1) или проявление произвольной вьюхи + авто-destroy. */
fade(view: Container, spec: FxFadeOptions): FxFade {
return this.items.add(new FxFade(view, spec)) as FxFade;
}
/** Появление с овершутом масштаба. */
pop(view: Container, spec?: FxPopSpec): FxPop {
return this.items.add(new FxPop(view, spec)) as FxPop;
}
/** Хит-флэш: один SpriteFlash на спрайт, повторный вызов перезапускает. */
flash(sprite: Sprite, color: number, seconds?: number): void {
let flash = this.flashes.get(sprite);
if (!flash) {
flash = new SpriteFlash(sprite, color, seconds);
this.flashes.set(sprite, flash);
}
flash.start();
}
/** Разовые частицы в точке (ParticleEmitter.oneShot) с самоуничтожением. */
burst(count: number, at: Vec2, opts: EmitterOptions, parent?: Container): ParticleEmitter {
const seed = opts.seed ?? (this.defaultSeed + this.seedCounter++);
const fx = ParticleEmitter.oneShot(count, { ...opts, seed });
fx.position.set(at.x, at.y);
(parent ?? this).addChild(fx);
return this.items.add(fx) as ParticleEmitter;
}
/** Шлейф за движущейся вьюхой (снаряды, герой). */
trail(view: Container, opts: EmitterOptions, parent?: Container): FxTrail {
const fx = new FxTrail(view, opts);
(parent ?? this).addChild(fx.view);
return this.items.add(fx) as FxTrail;
}
/** Повесить вьюху эффекта на слой или переданного родителя. */
private mount<T extends Container & Updatable>(fx: T, opts?: FxParentOptions): T {
(opts?.parent ?? this).addChild(fx);
return this.items.add(fx) as T;
}
/** Разрушение слоя гасит все эффекты (exit сцены — одна строка). */
override destroy(options?: Parameters<Container['destroy']>[0]): void {
this.clear();
super.destroy(options);
}
}