import { Container, Sprite, Texture } from 'pixi.js';
import { createRng, type Rng } from '../math/rng';
import { stepParticle, sampleSpawn, sampleBurst, type ParticleState } from './particleSim';
/**
* Простая система частиц: эмиттер спавнит точки (Texture.WHITE, подкрашенные)
* с заданной скоростью, временем жизни и затуханием.
* Используется для атмосферы (пепел, мотыльки, пыль) и эффектов (взрывы — burst/oneShot).
*/
export interface EmitterOptions {
/** Цвет частиц (0xRRGGBB). */
color: number;
/** Частиц в секунду. */
rate: number;
/** Время жизни частицы, секунды (min..max). */
lifetime: [number, number];
/** Начальная скорость, виртуальных пикселей/сек: (minX..maxX, minY..maxY). */
velocity: { x: [number, number]; y: [number, number] };
/** Ускорение (гравитация/ветер), пикселей/сек². */
acceleration?: { x?: number; y?: number };
/** Размер частицы в пикселях (1..4). */
size?: number;
/** Спавн в прямоугольнике вокруг позиции эмиттера (по умолчанию точка). */
spawnArea?: { width: number; height: number };
/** Затухание альфы к концу жизни (по умолчанию true). */
fadeOut?: boolean;
/** Seed для повторяемого движения. */
seed?: number;
/**
* Радиальный разброс вместо velocity: скорость частиц (min..max) по кругу
* (для burst-взрывов). Если задан — velocity игнорируется.
*/
radialSpeed?: [number, number];
}
interface Particle {
state: ParticleState;
sprite: Sprite;
}
export class ParticleEmitter extends Container {
private particles: Particle[] = [];
private spawnTimer = 0;
private rng: Rng;
private selfDestruct = false;
constructor(private options: EmitterOptions) {
super();
this.rng = createRng(options.seed ?? 0x9e3779b9);
}
update(dt: number): void {
// Спавн по rate (для burst-эмиттеров rate = 0)
this.spawnTimer += dt * this.options.rate;
while (this.spawnTimer >= 1) {
this.spawnTimer -= 1;
this.spawn();
}
// Обновление через чистую симуляцию
for (const p of this.particles) {
const alpha = stepParticle(p.state, dt, this.options.acceleration, this.options.fadeOut ?? true);
p.sprite.x = p.state.x;
p.sprite.y = p.state.y;
p.sprite.alpha = alpha;
}
// Сбор «умерших»
const dead = this.particles.filter((p) => p.state.age >= p.state.lifetime);
for (const p of dead) {
p.sprite.destroy();
}
if (dead.length > 0) {
this.particles = this.particles.filter((p) => p.state.age < p.state.lifetime);
}
// oneShot: все частицы умерли и новые не рождаются — уничтожиться
if (this.selfDestruct && this.particles.length === 0 && this.options.rate <= 0) {
this.destroy();
}
}
/** Убрать все частицы (например, при выходе со сцены). */
clear(): void {
for (const p of this.particles) p.sprite.destroy();
this.particles = [];
}
get count(): number {
return this.particles.length;
}
/**
* Мгновенно выпустить count частиц (взрыв/удар): игнорирует rate.
* Опции можно переопределить (цвет, radialSpeed, lifetime...).
*/
burst(count: number, opts?: Partial<EmitterOptions>): void {
if (opts) this.options = { ...this.options, ...opts };
if (this.options.radialSpeed) {
for (const s of sampleBurst(count, this.options.radialSpeed, this.options.lifetime, this.rng)) {
this.addParticle(s);
}
} else {
for (let i = 0; i < count; i++) this.spawn();
}
}
/**
* Одноразовый эмиттер-взрыв: spawнит count частиц сразу и уничтожает себя,
* когда все умерли. Добавить на сцену и обновлять как обычный эмиттер.
*/
static oneShot(count: number, opts: EmitterOptions): ParticleEmitter {
const e = new ParticleEmitter({ ...opts, rate: 0 });
e.selfDestruct = true;
e.burst(count);
return e;
}
private addParticle(state: ParticleState): void {
const size = this.options.size ?? 2;
const sprite = new Sprite(Texture.WHITE);
sprite.width = size;
sprite.height = size;
sprite.tint = this.options.color;
sprite.alpha = 0; // появится в следующем кадре без «вспышки»
this.particles.push({ state, sprite });
this.addChild(sprite);
}
private spawn(): void {
this.addParticle(sampleSpawn(this.options, this.rng));
}
}