Newer
Older
rpg / packages / engine / src / render / particleSim.ts
/**
 * Чистая симуляция частицы — без Pixi, тестируется в Vitest.
 * ParticleEmitter использует эти функции для интеграции и спавна.
 */

import type { Rng } from '../math/rng';
import type { EmitterOptions } from './Particles';

/** Состояние частицы без привязки к отображению. */
export interface ParticleState {
    x: number;
    y: number;
    vx: number;
    vy: number;
    age: number;
    /** Секунд до смерти. */
    lifetime: number;
}

/** Один шаг интеграции (полу-неявный Эйлер). Возвращает alpha с учётом fadeOut. */
export function stepParticle(
    p: ParticleState,
    dt: number,
    acceleration: { x?: number; y?: number } | undefined,
    fadeOut: boolean
): number {
    p.age += dt;
    p.vx += (acceleration?.x ?? 0) * dt;
    p.vy += (acceleration?.y ?? 0) * dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    return fadeOut ? Math.max(0, 1 - p.age / p.lifetime) : 1;
}

/** Случайная начальная частица по опциям эмиттера (позиция относительно центра). */
export function sampleSpawn(options: EmitterOptions, rng: Rng): ParticleState {
    const area = options.spawnArea;
    const v = options.velocity;
    return {
        x: area ? rng.int(0, area.width) - area.width / 2 : 0,
        y: area ? rng.int(0, area.height) - area.height / 2 : 0,
        vx: v.x[0] + rng.next() * (v.x[1] - v.x[0]),
        vy: v.y[0] + rng.next() * (v.y[1] - v.y[0]),
        age: 0,
        lifetime:
            options.lifetime[0] + rng.next() * (options.lifetime[1] - options.lifetime[0])
    };
}

/**
 * Случайный «взрыв»: частицы разлетаются из точки по кругу.
 * speed — (min..max) пикселей/сек; возвращает состояния для эмиттера.
 */
export function sampleBurst(
    count: number,
    speed: [number, number],
    lifetime: [number, number],
    rng: Rng
): ParticleState[] {
    const out: ParticleState[] = [];
    for (let i = 0; i < count; i++) {
        const angle = rng.next() * Math.PI * 2;
        const v = speed[0] + rng.next() * (speed[1] - speed[0]);
        out.push({
            x: 0,
            y: 0,
            vx: Math.cos(angle) * v,
            vy: Math.sin(angle) * v,
            age: 0,
            lifetime: lifetime[0] + rng.next() * (lifetime[1] - lifetime[0])
        });
    }
    return out;
}