Newer
Older
rpg / packages / engine / src / render / shadowSim.ts
/**
 * Чистая математика теней — без Pixi, тестируется в Vitest.
 * Солнце ходит по дуге в пределах дня из DayNightSpec (dawn..night): тень
 * утром «на запад экрана», вечером «на восток», в полдень короткая. Мир
 * пепельный — свет рассеянный, тени мягкие: альфа скромная, края ступенчатые
 * (рисует makeShadowTexture), геометрия — вытянутый эллипс от базы объекта.
 */

import { type DayNightSpec, DEFAULT_DAY_NIGHT } from './lightSim';

/** Цвет тени — нейтральный тёмно-серый (пепельная гамма, не чёрный). */
export const SHADOW_COLOR = 0x2f2f38;

/** Гладкая ступень 0..1 (нулевой наклон на краях — без рывков альфы). */
function smooth(t: number): number {
    const x = Math.max(0, Math.min(1, t));
    return x * x * (3 - 2 * x);
}

/** Состояние солнца для теней: направление тени (экран), длина, альфа. */
export interface SunShadow {
    /** Единичный вектор тени на экране (dx вправо, dy вниз). */
    dirX: number;
    dirY: number;
    /** Длина тени на единицу высоты объекта (короткая в полдень). */
    len: number;
    /** Сила тени 0..1 (пепельный мир — солнце рассеянное). */
    alpha: number;
}

/**
 * Солнце по часам суток: null — ночи (тьма рисует darkness-слой, тени от
 * солнца не видны). Границы и разгон альфы — из DayNightSpec: подъём
 * dawn..day, спад dusk..night; направление качается от «на запад экрана»
 * утром до «на восток» вечером.
 */
export function sunShadow(hours: number, spec: DayNightSpec = DEFAULT_DAY_NIGHT): SunShadow | null {
    const t = ((hours % 24) + 24) % 24;
    const rise = spec.dawn;
    const set = spec.night;
    if (t < rise || t >= set) return null;
    const up = smooth((t - rise) / Math.max(0.001, spec.day - rise));
    const down = 1 - smooth((t - spec.dusk) / Math.max(0.001, set - spec.dusk));
    const k = (t - rise) / (set - rise); // 0..1 — путь солнца за день
    const dx = -1 + 2 * k;
    const dy = 0.6; // свет всегда «сверху» экрана — тень чуть вниз
    const n = Math.hypot(dx, dy);
    const len = 0.55 + 1.05 * Math.pow(Math.abs(k - 0.5) * 2, 1.4);
    return { dirX: dx / n, dirY: dy / n, len, alpha: 0.34 * Math.min(up, down) };
}

/** Размещение блоба-тени относительно базы объекта (в px виртуального экрана). */
export interface BlobPlacement {
    /** Центр эллипса относительно точки базы (ноги/основания). */
    x: number;
    y: number;
    /** Поворот эллипса — вдоль направления тени. */
    rotation: number;
    /** Полуоси эллипса, px. */
    scaleX: number;
    scaleY: number;
    /** Прозрачность — сила солнца. */
    alpha: number;
}

/**
 * Эллипс тени для объекта высотой heightPx с «толщиной» radiusPx: тень
 * начинается у базы и тянется вдоль направления на heightPx·len, ширина
 * поперёк почти не меняется от длины.
 */
export function blobPlacement(radiusPx: number, heightPx: number, sun: SunShadow): BlobPlacement {
    const reach = heightPx * sun.len;
    const tip = reach + radiusPx * 0.8; // дальний край тени от базы
    const a = tip / 2;
    const b = radiusPx * (0.7 + 0.3 * sun.len);
    return {
        x: sun.dirX * a,
        y: sun.dirY * a,
        rotation: Math.atan2(sun.dirY, sun.dirX),
        scaleX: a,
        scaleY: b,
        alpha: sun.alpha
    };
}