Newer
Older
rpg / v2 / apps / game / src / ash.ts
/**
 * Атмосфера области (v1 Atmosphere, вариант 'ash'): медленные хлопья пепла
 * в воздухе. Отдельный слой частиц движка (спрайты-билборды, не воксели):
 * позы — детерминированные синусы от времени цикла (гейт-стабильно).
 * Хлопки живут в МИРОВЫХ координатах (не «приклеены» к камере): каждый
 * дрейфует по ветру от своего якоря, а окно видимости камеры их заворачивает.
 * Окно покрывает весь кадр с перекрытием (маржа за экраном), и в марже
 * альфа гаснет — ушедший хлопок появляется с противоположной стороны
 * уже за экраном, без «появления из воздуха».
 */
import type { ParticleLayer, VoxelRenderer } from '@rpg/engine';
import { edgeAlpha, particleWindow, wrap } from './particlesWindow';

/** Параметры пепла (окно — след кадра + маржа, в мировых юнитах). */
const COUNT = 128; // хлопков в облаке (окно выросло до всего кадра)
const TOP = 44; // высота верхнего слоя пепла над землёй
const SPEED = 1.2; // дрейф по ветру (юнит/сек; v1: 2..7 px/s при 32 px/юнит)
const MARGIN = 16; // перекрытие за краем кадра

/** Цвет пепла (палитра сцены: P3 — светлый пепел). */
const ASH_COLOR = '#3e3e48';

export class AshParticles {
    private readonly layer: ParticleLayer;

    constructor(private readonly renderer: VoxelRenderer) {
        this.layer = renderer.addParticles();
    }

    /** Поза облака: время цикла (окно — из цели камеры рендерера). */
    update(t: number): void {
        const win = particleWindow(this.renderer, 0, TOP, MARGIN);
        const points: { pos: [number, number, number]; color: string; alpha: number }[] = [];
        for (let i = 0; i < COUNT; i++) {
            // сид хлопка — от индекса; фазы кружения разнесены золотым сечением
            const p = i * 2.399;
            // мировой якорь + дрейф по ветру; окно камеры заворачивает
            const x = wrap(
                i * 61.7 + t * SPEED + Math.sin(t * 0.7 + p) * 1.5,
                win.minX, win.maxX - win.minX,
            );
            const z = wrap(
                i * 47.3 + Math.cos(t * 0.5 + p) * 1.5,
                win.minZ, win.maxZ - win.minZ,
            );
            // вертикаль: оседание 0.5..1.1 юнит/с (v1: 1..4 px/s)
            const fall = ((t * (0.5 + (i % 5) * 0.15) + (i * 3.7) % TOP) % TOP);
            const y = TOP - fall;
            points.push({ pos: [x, y, z], color: ASH_COLOR, alpha: edgeAlpha(win, MARGIN, x, z) });
        }
        this.layer.update(points);
    }

    /** Скрыть облако (область без атмосферы). */
    hide(): void {
        this.layer.update([]);
    }

    dispose(): void {
        this.layer.dispose();
    }
}