Newer
Older
rpg / v2 / packages / engine / src / render / daynight.ts
/**
 * Чистая математика суток (перенос v1 lightSim): фактор ночи по часам,
 * трёхопорный ambient-микс (день → золотой час → ночь), лерп/затемнение
 * цвета и детерминированное мерцание источников. Без three и рендера —
 * тестируется в Vitest.
 */

/** Лерп цвета 0xRRGGBB по каналам: t=0 → a, t=1 → b. */
export function lerpColor(a: number, b: number, t: number): number {
    const ar = (a >> 16) & 0xff;
    const ag = (a >> 8) & 0xff;
    const ab = a & 0xff;
    const br = (b >> 16) & 0xff;
    const bg = (b >> 8) & 0xff;
    const bb = b & 0xff;
    const r = Math.round(ar + (br - ar) * t);
    const g = Math.round(ag + (bg - ag) * t);
    const bl = Math.round(ab + (bb - ab) * t);
    return (r << 16) | (g << 8) | bl;
}

/** Лерп ambient-цвета (алиас lerpColor — имя v1). */
export const lerpAmbient = lerpColor;

/** Яркость 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, закат dusk..night. */
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 };

/** Доля перехода день→ночь, на которой пик золотого часа. */
export const DUSK_PEAK = 0.45;

/** Дефолтный закатный тон (тёпло-золотой) — игра может переопределить. */
export const DEFAULT_DUSK_COLOR = 0xe8c090;

/**
 * Трёхопорный микс суток по фактору ночи k: день → золотой час → ночь
 * (рассвет проходит через тот же тёплый тон — это ожидаемо).
 */
export function dayNightAmbient(
    day: number,
    night: number,
    k: number,
    duskColor: number = DEFAULT_DUSK_COLOR,
): number {
    if (k <= 0) return day;
    if (k >= 1) return night;
    if (k < DUSK_PEAK) return lerpAmbient(day, duskColor, k / DUSK_PEAK);
    return lerpAmbient(duskColor, night, (k - DUSK_PEAK) / (1 - DUSK_PEAK));
}

/** Фактор ночи 0..1 по часам (mod 24): 0 — день, 1 — ночь, линейные переходы. */
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);
}

/**
 * Детерминированное мерцание: сумма двух синусов с фазой от 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);
}