Newer
Older
rpg / apps / game / src / systems / combat / los.ts
import type { Vec2 } from '@rpg/engine';

/**
 * Линия видимости по сетке (чистая математика, без Pixi).
 * Отрезок дискретизируется шагом 0.25 юнита; тайл с непрозрачным объектом
 * (стена, дом, башня, дерево) перекрывает обзор. Не в движке: правила
 * прозрачности зависят от игры, второй пользователь не виден.
 */

/** Шаг дискретизации отрезка, юнитов. */
const STEP = 0.25;

/** Виден ли `to` из `from` с учётом непрозрачных тайлов. */
export function hasLineOfSight(
    from: Vec2,
    to: Vec2,
    isOpaque: (tx: number, ty: number) => boolean
): boolean {
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const len = Math.hypot(dx, dy);
    if (len < 1e-6) return true;
    const n = Math.ceil(len / STEP);
    const ux = dx / n;
    const uy = dy / n;
    // Концевые точки не проверяем: сами враг/герой стоят в своих тайлах.
    for (let i = 1; i < n; i++) {
        const x = from.x + ux * i;
        const y = from.y + uy * i;
        if (isOpaque(Math.floor(x), Math.floor(y))) return false;
    }
    return true;
}

/**
 * Непрозрачен ли тайл карты: высокий объект (TallSpec) перекрывает взгляд.
 * Вода и ровный пепел прозрачны — сгустки видят через низину.
 */
export function tileOpaque(map: {
    data: { width: number; height: number; tiles: number[]; tall?: Record<number, unknown> };
}): (tx: number, ty: number) => boolean {
    return (tx: number, ty: number): boolean => {
        const { width, height, tiles, tall } = map.data;
        if (tx < 0 || ty < 0 || tx >= width || ty >= height) return true;
        return tall?.[tiles[ty * width + tx]] !== undefined;
    };
}