Newer
Older
rpg / packages / engine / src / map / minimap.ts
import type { TileMapData } from './IsometricTileMap';

/**
 * Миникарта: чистый рендер TileMapData в RGBA-картинку (без Pixi — движок
 * не знает про текстуры; в пиксельную текстуру картинку превращает игра).
 * Схема «вид сверху»: клетка сетки = тайл, цвет — из палитры вызывающей
 * стороны (Record<id тайла, 0xRRGGBB>). Незаданные тайлы — fallbackColor.
 */

export interface MinimapMarker {
    /** Тайловые координаты точки интереса. */
    x: number;
    y: number;
    /** Цвет маркера (0xRRGGBB). */
    color: number;
}

export interface MinimapOptions {
    /** Цвет клетки по id тайла (0xRRGGBB). */
    colors: Record<number, number>;
    /** Цвет тайла без записи в colors (по умолчанию тёмный). */
    fallbackColor?: number;
    /** Размер клетки в пикселях (2–3 читаются лучше всего). */
    cell?: number;
    /** Точки интереса поверх карты (герой, переходы, NPC). */
    markers?: MinimapMarker[];
}

/** Результат рендера: RGBA-байты, 4 на пиксель. */
export interface MinimapImage {
    width: number;
    height: number;
    rgba: Uint8Array;
}

/** 0xRRGGBB → [r, g, b]. */
function rgbOf(color: number): [number, number, number] {
    return [(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
}

/**
 * Отрисовать карту в пиксельную картинку: сетка width×height клеток
 * (cell px каждая), маркеры закрашивают свою клетку поверх.
 */
export function renderMinimap(data: TileMapData, opts: MinimapOptions): MinimapImage {
    const cell = opts.cell ?? 3;
    const width = data.width * cell;
    const height = data.height * cell;
    const rgba = new Uint8Array(width * height * 4);
    const put = (x: number, y: number, [r, g, b]: [number, number, number]): void => {
        const i = (y * width + x) * 4;
        rgba[i] = r;
        rgba[i + 1] = g;
        rgba[i + 2] = b;
        rgba[i + 3] = 255;
    };
    for (let ty = 0; ty < data.height; ty++) {
        for (let tx = 0; tx < data.width; tx++) {
            const id = data.tiles[ty * data.width + tx]!;
            const color = rgbOf(opts.colors[id] ?? opts.fallbackColor ?? 0x1a1a20);
            for (let dy = 0; dy < cell; dy++) {
                for (let dx = 0; dx < cell; dx++) put(tx * cell + dx, ty * cell + dy, color);
            }
        }
    }
    // Маркеры: закрашенная клетка с рамкой цвета фона — читается на любом поле.
    for (const m of opts.markers ?? []) {
        if (m.x < 0 || m.y < 0 || m.x >= data.width || m.y >= data.height) continue;
        const [r, g, b] = rgbOf(m.color);
        for (let dy = 0; dy < cell; dy++) {
            for (let dx = 0; dx < cell; dx++) {
                const border = dx === 0 || dy === 0 || dx === cell - 1 || dy === cell - 1;
                put(m.x * cell + dx, m.y * cell + dy, border ? [20, 20, 24] : [r, g, b]);
            }
        }
    }
    return { width, height, rgba };
}