import { Container, Graphics, ParticleEmitter, type Updatable } from '@rpg/engine';
import { Game } from '../Game';
import type { AreaDef, HazardDef } from '../data/locations';
import type { PlayerController } from './PlayerController';
/**
* Атмосфера локации: пепел/мотыли (частицы мира), дымка зон наката (UI-слой),
* механика низин (замедление без маски, тост при входе) и виньетка опасности.
*/
/** Виньетка: цель при низком hp / в накате без маски и фейд перехода. */
const VIGNETTE_LOW_HP = 0.3;
const VIGNETTE_HAZARD = 0.35;
const VIGNETTE_FADE = 0.5;
export interface AtmosphereDeps {
game: Game;
area: AreaDef;
/** Слои: частицы — в мир, дымка — под UI (index 0 uiRoot). */
worldRoot: Container;
uiRoot: Container;
/** Реестр тикающихся (engine.fx). */
addFx(u: Updatable): void;
player: PlayerController;
/** Текущий hp героя (виньетка при низком). */
heroHp: () => number;
/** Виньетка движкового света: уровень + цель с фейдом. */
vignette: { level(): number; set(intensity: number, fadeSec: number): void };
showToast(text: string): void;
}
export class Atmosphere {
/** Пепел на лугах/деревне (в интерьерах null). */
readonly ash: ParticleEmitter | null;
/** Тёплые мотыльки над лугами (светящиеся, дрейф по синусу). */
readonly moths: ParticleEmitter | null;
/** Оверлей дымки (зоны наката). Виден только в низинах. */
readonly fog: Graphics;
/** Зона наката, в которой герой был в прошлом кадре (тост при входе). */
inHazard: HazardDef | null = null;
/** Текущая зона наката перекрыта полотном (для виньетки). */
hazardMasked = false;
constructor(private deps: AtmosphereDeps) {
const area = deps.area;
// Пепел на лугах, туман над прудами, в интерьере — ничего.
this.ash =
area.atmosphere === 'none'
? null
: area.atmosphere === 'fog'
? new ParticleEmitter({
color: 0x7a7a88,
rate: 6,
lifetime: [5, 10],
velocity: { x: [-3, 3], y: [-1, 1] },
size: 2,
spawnArea: { width: 560, height: 340 },
seed: 20260906
})
: new ParticleEmitter({
color: 0x666677,
rate: 5,
lifetime: [4, 9],
velocity: { x: [-9, -3], y: [-2, 2] },
size: 1,
spawnArea: { width: 520, height: 300 },
seed: 20260905
});
if (this.ash) {
this.ash.position.set(240, 120);
deps.worldRoot.addChild(this.ash);
// Пепел тикается реестром эффектов (engine.fx) — без ручного update.
deps.addFx(this.ash);
}
// Тёплые мотыльки над лугами: светятся (add), дышат по синусу, в интерьере их нет.
this.moths =
area.atmosphere === 'ash'
? new ParticleEmitter({
colors: [0xf0d878, 0xd8b050],
blend: 'add',
rate: 2,
lifetime: [3, 6],
velocity: { x: [-4, 4], y: [-2, 2] },
size: 1,
wobble: 8,
fadeIn: 0.6,
spawnArea: { width: 520, height: 300 },
seed: 20260907
})
: null;
if (this.moths) {
this.moths.position.set(240, 120);
deps.worldRoot.addChild(this.moths);
deps.addFx(this.moths);
}
// Дымка наката: полупрозрачная пелена на весь экран (в низинах без маски — гуще).
this.fog = new Graphics().rect(0, 0, Game.VIRTUAL_W, Game.VIRTUAL_H).fill(0x5a6a78);
this.fog.alpha = 0;
deps.uiRoot.addChildAt(this.fog, 0);
}
/** Зона наката под ногами: без маски герой еле идёт (тост при входе). */
updateHazard(tile: { x: number; y: number }): void {
const { game, area, player } = this.deps;
const hazard =
(area.hazards ?? []).find((h) =>
h.tiles.some((t) => t.x === tile.x && t.y === tile.y)
) ?? null;
const masked = hazard !== null && game.inventory.has(hazard.requiresItem);
player.speedMul = hazard !== null && !masked ? 0.5 : 1;
// Вход в низину: предупреждение один раз за вход.
if (hazard !== null && this.inHazard === null) {
this.deps.showToast(
masked ? `${hazard.name}: полотно держит — но пепел у самых губ.` : `${hazard.name}! Дышать поверх — потерять голос.`
);
void game.audio.play('sfx/ash_hiss', 0.5);
}
this.inHazard = hazard;
this.hazardMasked = masked;
// Дымка: плотная без маски, лёгкая с ней, вне низин её нет.
this.fog.alpha = hazard === null ? 0 : masked ? 0.12 : 0.28;
}
/** Виньетка: низкий hp или накат без маски — края экрана темнеют. */
updateVignette(): void {
const target = Math.max(
this.deps.heroHp() <= 2 ? VIGNETTE_LOW_HP : 0,
this.inHazard !== null && !this.hazardMasked ? VIGNETTE_HAZARD : 0
);
if (this.deps.vignette.level() !== target) this.deps.vignette.set(target, VIGNETTE_FADE);
}
/** Погасить/уничтожить свои вьюхи (exit сцены; engine.fx дочистит сам). */
destroy(): void {
this.ash?.clear();
this.moths?.clear();
this.ash?.destroy();
this.moths?.destroy();
this.fog.destroy();
}
}