Newer
Older
rpg / packages / engine / src / render / lightSim.ts
/**
 * Чистая математика освещения — без 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 {
    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 { x: def.x, y: def.y, tint: def.color, alpha, scale: radius / GLOW_BASE_PX };
}

/** Лерп 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;
}