/**
* Чистая математика освещения — без Pixi, тестируется в Vitest.
* Lighting использует эти функции для кадров источников и лерпа ambient.
*/
import { lerpColor } from '../anim/flash';
/** Базовый радиус текстуры свечения в px — на него нормируется scale спрайта. */
export const GLOW_BASE_PX = 32;
/**
* Источник света в экранных пикселях (позицию считает сцена через Camera.toScreen —
* движок ничего не знает об изометрии).
*/
export interface LightDef {
id: string;
x: number;
y: number;
/** Цвет свечения 0xRRGGBB. */
color: number;
/** Яркость: 0..~1.5 (по умолчанию 1). */
intensity?: number;
/** Радиус гало в px (по умолчанию 2 * GLOW_BASE_PX). */
radius?: number;
/** Амплитуда мерцания 0..1 (0 — ровный свет). */
flicker?: number;
/** Период мерцания, сек (по умолчанию 0.9). */
flickerPeriod?: number;
/** Фаза мерцания — детерминизм между запусками. */
seed?: number;
/** Выключенный источник не рисуется (кадр с alpha 0). */
enabled?: boolean;
}
/** Видимое состояние источника на шаге. */
export interface LightFrame {
id: string;
x: number;
y: number;
tint: number;
alpha: number;
scale: number;
}
/**
* Детерминированное мерцание: сумма двух синусов с фазой от seed.
* Одинаковые (time, seed) — одинаковый результат; диапазон [1-amount, 1].
*/
export function flickerFactor(time: number, seed: number, amount: number, period: number): number {
if (amount <= 0) return 1;
const p = period > 0 ? period : 0.9;
const f = time / p + seed * Math.PI * 2;
const wave = (Math.sin(f) + Math.sin(f * 2.37 + seed * 5)) * 0.5; // -1..1
return 1 - amount * (wave * 0.5 + 0.5);
}
/** Кадр источника: чистая функция, без Pixi и без мутаций. */
export function lightFrame(def: LightDef, time: number): LightFrame {
const intensity = def.intensity ?? 1;
const radius = def.radius ?? 2 * GLOW_BASE_PX;
const enabled = def.enabled ?? true;
const alpha = enabled
? intensity * flickerFactor(time, def.seed ?? 0, def.flicker ?? 0, def.flickerPeriod ?? 0.9)
: 0;
return { id: def.id, x: def.x, y: def.y, tint: def.color, alpha, scale: radius / GLOW_BASE_PX };
}
/**
* Огибающая импульса (вспышки): трапеция «рост → плато → спад».
* attack/decay — сек; hold — плато на пике (по умолчанию 0).
*/
export interface PulseSpec {
/** Рост 0→1, сек (0 — мгновенный пик). */
attack: number;
/** Плато на пике, сек (по умолчанию 0). */
hold?: number;
/** Спад 1→0, сек. */
decay: number;
}
/** Полная длительность импульса, сек. */
export function pulseTotal(spec: PulseSpec): number {
return Math.max(0, spec.attack) + Math.max(0, spec.hold ?? 0) + Math.max(0, spec.decay);
}
/** Огибающая импульса в момент t от старта: 0..1, вне [0, total] — 0. */
export function pulseEnvelope(t: number, spec: PulseSpec): number {
const attack = Math.max(0, spec.attack);
const hold = Math.max(0, spec.hold ?? 0);
const decay = Math.max(0, spec.decay);
if (t <= 0 || t >= attack + hold + decay) return 0;
if (t < attack) return t / attack;
if (t < attack + hold) return 1;
return 1 - (t - attack - hold) / decay;
}
/** Лерп ambient-цвета по каналам: k=0 → from, k=1 → to. */
export function lerpAmbient(from: number, to: number, k: number): number {
return lerpColor(from, to, k);
}
/** Яркость ambient-цвета: dimColor(c, 1) === c, dimColor(c, 0) === 0x000000. */
export function dimColor(color: number, brightness: number): number {
const k = Math.max(0, Math.min(1, brightness));
const r = Math.round(((color >> 16) & 0xff) * k);
const g = Math.round(((color >> 8) & 0xff) * k);
const b = Math.round((color & 0xff) * k);
return (r << 16) | (g << 8) | b;
}
/**
* Границы суток в часах: рассвет dawn..day (ночь → день),
* день day..dusk, закат dusk..night (день → ночь), ночь night..dawn.
*/
export interface DayNightSpec {
dawn: number;
day: number;
dusk: number;
night: number;
}
/** Дефолтная кривая игры: рассвет 06–08, закат 18–20, ночь 20–06. */
export const DEFAULT_DAY_NIGHT: DayNightSpec = { dawn: 6, day: 8, dusk: 18, night: 20 };
/**
* Фактор ночи 0..1 по времени суток (часы, mod 24): 0 — день, 1 — ночь,
* линейный переход на рассвете и закате. Непрерывен на стыке 24/0.
*/
export function dayNightFactor(tHours: number, spec: DayNightSpec = DEFAULT_DAY_NIGHT): number {
const t = ((tHours % 24) + 24) % 24;
if (t >= spec.night || t < spec.dawn) return 1;
if (t < spec.day) return 1 - (t - spec.dawn) / (spec.day - spec.dawn);
if (t < spec.dusk) return 0;
return (t - spec.dusk) / (spec.night - spec.dusk);
}