/**
* Атмосферные частицы (v1 Atmosphere): тёплые мотыльки над лугами
* (светятся цветом, дрейф по синусу) и дымка прудов (серые клочья,
* якорятся на клетки воды и медленно бродят на месте). Voxel-облако,
* позы — детерминированные синусы от времени цикла, гейт-стабильно.
*/
import type { VoxelCloud, VoxelRenderer } from '@rpg/engine';
import type { WaterCell } from './waterShimmer';
/** Профиль частиц области. */
export type AmbientKind = 'moths' | 'fog';
const MOTH_COUNT = 10;
const MOTH_COLORS = ['#f0d878', '#d8b050']; // B-тёплые: только «жизнь»
const FOG_COUNT = 22;
const FOG_COLOR = '#a7b3bd'; // светлее воды — клочья читаются туманом
const RADIUS = 22; // половина ширины облака мотыльков вокруг камеры (как пепел)
/** Клок тумана: квадрат 2×2 вокселя ЛЕЖИТ на воде (как полупрозрачные
* пятна v1 — непрозрачный куб в воздухе читался бы «ящиком»). */
const FOG_TILE: readonly (readonly [number, number])[] = [[0, 0], [1, 0], [0, 1], [1, 1]];
/** Сдвиг клочьев из плоскости куба воды — против z-fighting копланарных
* граней на кромках (как у бликов шиммера, EPS 0.03). */
const FOG_EPS = 0.03;
export class AmbientParticles {
private readonly cloud: VoxelCloud;
constructor(
renderer: VoxelRenderer,
private readonly kind: AmbientKind,
/** Клетки поверхности воды — якоря клочьев дымки (только для fog). */
private readonly cells: readonly WaterCell[] = [],
) {
this.cloud = renderer.addVoxelCloud([]);
this.cloud.mesh.castShadow = false; // частицы не бросают теней
}
/** Снапшот для моста: профиль и число частиц (у мотыльков всегда видимы). */
get info(): { kind: AmbientKind; count: number } {
return {
kind: this.kind,
count: this.kind === 'moths' ? MOTH_COUNT : this.cells.length ? FOG_COUNT * FOG_TILE.length : 0,
};
}
/** Поза облака: цель камеры и время цикла (детерминизм гейтов). */
update(t: number, target: readonly [number, number, number]): void {
const voxels: { pos: [number, number, number]; color: string }[] = [];
if (this.kind === 'moths') {
for (let i = 0; i < MOTH_COUNT; i++) {
const p = i * 2.399; // золотое сечение: разброс фаз
// кружение вокруг камеры: медленный орбитальный дрейф
const x = target[0] + Math.sin(t * 0.13 + p) * RADIUS * 0.8;
const z = target[2] + Math.cos(t * 0.11 + p * 1.7) * RADIUS * 0.8;
// полёт: плавная волна над травой + трепет
const y = 4.5 + Math.sin(t * 0.4 + p * 2) * 2.2;
voxels.push({
pos: [x + Math.sin(t * 1.7 + p) * 0.6, y, z + Math.cos(t * 1.5 + p) * 0.6],
color: MOTH_COLORS[i % MOTH_COLORS.length]!,
});
}
} else {
// клочья дымят НА воде: якорь — клетка поверхности, медленное
// блуждание на месте (без якорей — клочьев нет: дымка только прудам)
const n = this.cells.length;
for (let i = 0; i < FOG_COUNT && n > 0; i++) {
const p = i * 2.399;
const a = this.cells[(i * 131) % n]!; // 131 взаимно прост с длиной — разброс
const x = a.x + Math.sin(t * 0.05 + p) * 2.5;
const z = a.z + Math.cos(t * 0.04 + p * 1.3) * 2.5;
for (const [dx, dz] of FOG_TILE) {
voxels.push({ pos: [x + dx + FOG_EPS, 2.56, z + dz + FOG_EPS], color: FOG_COLOR });
}
}
}
this.cloud.update(voxels);
this.cloud.mesh.castShadow = false; // пересоздание облака возвращает тень
}
/** Скрыть облако (мост setAsh / смена профиля). */
hide(): void {
this.cloud.update([]);
}
dispose(): void {
this.cloud.dispose();
}
}