/**
* Воздушные частицы области сверх атмосферы v1: падающие листья рощи
* (профиль 'leaves' — медленный листопад с покачиванием, редкий тёплый
* лист) и мелкая пыль разъезда ('dust' — лёгкая взвесь, еле ползёт).
* Отдельный слой частиц движка (спрайты-билборды, не воксели): позы —
* детерминированные синусы от времени цикла (гейт-стабильно); окно
* видимости камеры с перекрытием, в марже за экраном альфа гаснет —
* лист/пылинка не «появляется из воздуха».
*/
import type { ParticleLayer, VoxelRenderer } from '@rpg/engine';
import { edgeAlpha, particleWindow, wrap } from './particlesWindow';
/** Профиль воздушных частиц области. */
export type AirFxKind = 'leaves' | 'dust';
/** Параметры профилей: всё в юнитах мира. */
const COUNT = 26; // листьев в кронах рощи
const LEAVES_H = 26; // слой листопада (кроны ~17-23 + запас)
const LEAVES_FALL = 1.1; // скорость падения листа (юнит/сек)
const LEAVES_COLORS = ['#7d8662', '#5e6b4a', '#c9a04a'] as const; // крона + тёплый лист
const DUST_COUNT = 40;
const DUST_H = 18; // слой пыли над разъездом
const DUST_COLOR = '#94949e'; // P6 — пыль в свете
const DUST_ALPHA = 0.32;
const MARGIN = 16; // перекрытие окна за краем кадра
export class AirFx {
private readonly layer: ParticleLayer;
constructor(
private readonly renderer: VoxelRenderer,
private readonly kind: AirFxKind,
) {
this.layer = renderer.addParticles();
}
/** Снапшот для моста: профиль и число частиц. */
get info(): { kind: AirFxKind; count: number } {
return { kind: this.kind, count: this.kind === 'leaves' ? COUNT : DUST_COUNT };
}
/** Поза слоя: время цикла (окно — из цели камеры рендерера). */
update(t: number): void {
const points: { pos: [number, number, number]; color: string; alpha?: number; size?: number }[] = [];
if (this.kind === 'leaves') {
const win = particleWindow(this.renderer, 0, LEAVES_H, MARGIN);
for (let i = 0; i < COUNT; i++) {
const p = i * 2.399; // золотое сечение: разброс фаз
// мировой якорь (окно камеры заворачивает) + покачивание падения
const ax = wrap(i * 67.3 + Math.sin(t * 0.21 + p) * 2.5, win.minX, win.maxX - win.minX);
const az = wrap(i * 43.9 + Math.cos(t * 0.17 + p * 1.7) * 2.5, win.minZ, win.maxZ - win.minZ);
// падение: лист опускается и рождается снова в кронах
const y = LEAVES_H - ((t * (LEAVES_FALL * (0.7 + (i % 4) * 0.2)) + (i * 4.1) % LEAVES_H) % LEAVES_H);
points.push({
pos: [ax + Math.sin(t * 1.1 + p) * 0.8, y, az + Math.cos(t * 0.9 + p) * 0.8],
color: LEAVES_COLORS[i % 7 === 6 ? 2 : i % 2]!,
alpha: edgeAlpha(win, MARGIN, ax, az),
size: 1.4,
});
}
} else {
const win = particleWindow(this.renderer, 0, DUST_H, MARGIN);
for (let i = 0; i < DUST_COUNT; i++) {
const p = i * 2.399;
// пыль почти стоит на месте: медленный дрейф-взвесь
const ax = wrap(i * 71.9 + Math.sin(t * 0.07 + p) * 3, win.minX, win.maxX - win.minX);
const az = wrap(i * 97.3 + Math.cos(t * 0.06 + p * 1.3) * 3, win.minZ, win.maxZ - win.minZ);
const y = 2 + Math.sin(t * 0.11 + p * 2.3) * (2 + (i % 3) * 3);
points.push({
pos: [ax, y, az],
color: DUST_COLOR,
alpha: DUST_ALPHA * edgeAlpha(win, MARGIN, ax, az),
size: 0.7,
});
}
}
this.layer.update(points);
}
/** Скрыть слой (мост setAsh / область без профиля). */
hide(): void {
this.layer.update([]);
}
dispose(): void {
this.layer.dispose();
}
}