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

/** Параметры пепла (масштаб мира v2: радиус в вокселях). */
const COUNT = 48; // хлопков в облаке (v1 ~40 частиц)
const RADIUS = 60; // половина окна вокруг камеры (кадр 80×45 вокс)
const TOP = 44; // высота верхнего слоя пепла над землёй
const SPEED = 0.35; // дрейф по ветру (вокс/сек)

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

/** Заворачивание значения в окно [min, min+size) — респавн с другой стороны. */
const wrap = (v: number, min: number, size: number): number => {
    let r = (v - min) % size;
    if (r < 0) r += size;
    return min + r;
};

export class AshParticles {
    private readonly cloud: VoxelCloud;

    constructor(renderer: VoxelRenderer) {
        this.cloud = renderer.addVoxelCloud([]);
    }

    /** Поза облака: цель камеры и время цикла (детерминизм гейтов). */
    update(t: number, target: readonly [number, number, number]): void {
        const voxels: { pos: [number, number, number]; color: string }[] = [];
        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,
                target[0] - RADIUS, RADIUS * 2,
            );
            const z = wrap(
                i * 47.3 + Math.cos(t * 0.5 + p) * 1.5,
                target[2] - RADIUS, RADIUS * 2,
            );
            // вертикаль: медленное оседание
            const fall = ((t * (0.25 + (i % 5) * 0.06) + (i * 3.7) % TOP) % TOP);
            voxels.push({ pos: [x, TOP - fall, z], color: ASH_COLOR });
        }
        this.cloud.update(voxels);
    }

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

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