Newer
Older
rpg / apps / game / src / scenes / MapScene.ts
import {
    Graphics,
    MenuSceneBase,
    Panel,
    PixelText,
    Sprite,
    Texture,
    renderMinimap,
    type MinimapMarker
} from '@rpg/engine';
import { Game } from '../Game';
import { TILES } from '../data/map';
import { areaOf } from '../data/locations';
import { playSfx } from '../data/sfxSpecs';

/**
 * Палитра миникарты: id тайла → цвет (0xRRGGBB, из арт-палитры). Варианты
 * тайлов красятся как базовый. Незаданный — тёмный fallback движка.
 */
const MAP_COLORS: Record<number, number> = {
    [TILES.GRASS]: 0x4a5340, // G1 — трава
    [TILES.GRASS_V1]: 0x4a5340,
    [TILES.GRASS_V2]: 0x4a5340,
    [TILES.PATH]: 0x77634f, // T1 — протоптанная земля
    [TILES.PATH_V1]: 0x77634f,
    [TILES.WATER]: 0x3a4356, // C0 — вода
    [TILES.TREE]: 0x343b2c, // G0 — тёмная крона
    [TILES.TREE_V1]: 0x343b2c,
    [TILES.TREE_V2]: 0x343b2c,
    [TILES.ASH]: 0x3e3e48, // P3 — пепел
    [TILES.ASH_V1]: 0x3e3e48,
    [TILES.BELLFLOWER]: 0xf2b45a, // F1 — цветок (тёплый)
    [TILES.TOWER]: 0x26262e, // M0 — высокая башня
    [TILES.HOUSE]: 0x5a4a3c, // T0 — крыши
    [TILES.HOUSE_V1]: 0x5a4a3c,
    [TILES.HOUSE_V2]: 0x5a4a3c,
    [TILES.FLOOR]: 0x44454e, // M1 — полы интерьеров
    [TILES.WALL]: 0x26262e, // M0 — стены
    [TILES.WELL]: 0x8a6a2e, // B0 — латунный колодец
    [TILES.WIRE]: 0x5c3b2e, // R1 — бухты кабеля
    [TILES.RAIL]: 0x767681, // P5 — металл рельсов
    [TILES.STAIRS]: 0x77634f,
    [TILES.MACHINE]: 0x26262e
};

/** Цвета маркеров: герой (мигает), переходы, NPC. */
const HERO_COLOR = 0xf2d49a;
const TRANSITION_COLOR = 0xb2b2bb;
const NPC_COLOR = 0xf2b45a;

/** RGBA-картинка движка → пиксельная текстура (nearest, без размытия). */
function textureFromMinimap(img: { width: number; height: number; rgba: Uint8Array }): Texture {
    const canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext('2d')!;
    ctx.putImageData(new ImageData(new Uint8ClampedArray(img.rgba), img.width, img.height), 0, 0);
    const tex = Texture.from(canvas);
    tex.source.scaleMode = 'nearest';
    return tex;
}

/**
 * Карта окрестностей (предмет map_scroll): схема текущей области поверх
 * локации, push/pop как у сумки. Тайлы — клетки палитры; переходы и NPC —
 * маркеры; герой — мигающая точка. Esc/E/клавиша сумки — назад.
 */
export class MapScene extends MenuSceneBase {
    /** Мигающий маркер героя. */
    private hero: Graphics | null = null;
    private blinkT = 0;

    constructor(
        private game: Game,
        private onBack: () => void
    ) {
        super(
            { input: game.engine.input, inputBlocked: () => game.scenes.transitioning },
            { up: 'up', down: 'down', confirm: 'advance', cancel: 'menu', extra: ['inventory'] }
        );
    }

    protected build(): void {
        const location = this.game.activeLocation;
        const areaId = location?.areaId;
        const map = areaId ? this.game.mapFiles.get(areaId) : undefined;
        if (!location || !map) return; // карта открыта вне локации — не бывает

        const area = areaOf(areaId);
        const markers: MinimapMarker[] = [];
        // Переходы — светлые клетки (в т.ч. двери/назад).
        for (const t of area.transitions) {
            markers.push({ x: t.tile.x, y: t.tile.y, color: TRANSITION_COLOR });
        }
        // NPC — тёплые клетки (тайл привязки вычисляется, считаем из pos).
        for (const npc of area.npcs) {
            markers.push({ x: Math.floor(npc.pos.x), y: Math.floor(npc.pos.y), color: NPC_COLOR });
        }

        const panel = new Panel({ width: 300, height: 222 });
        panel.position.set((Game.VIRTUAL_W - 300) / 2, (Game.VIRTUAL_H - 222) / 2);

        const title = new PixelText({ text: area.name, size: 14, color: 0xd8c79a });
        title.anchor.set(0.5);
        title.position.set(150, 14);
        panel.addChild(title);

        const img = renderMinimap(map, { colors: MAP_COLORS, markers, cell: 3 });
        const sprite = new Sprite(textureFromMinimap(img));
        // Целочисленный масштаб: картинка целиком в панели.
        const scale = Math.max(1, Math.floor(Math.min(270 / img.width, 168 / img.height)));
        sprite.scale.set(scale);
        sprite.position.set(150 - (img.width * scale) / 2, 40 + (168 - img.height * scale) / 2);
        panel.addChild(sprite);

        // Герой — клетка-маркер поверх (мигает в update).
        const heroTile = location.heroTile();
        this.hero = new Graphics();
        this.hero.rect(0, 0, 3 * scale, 3 * scale).fill(HERO_COLOR);
        this.hero.position.set(
            sprite.position.x + heroTile.x * 3 * scale,
            sprite.position.y + heroTile.y * 3 * scale
        );
        panel.addChild(this.hero);

        const hint = new PixelText({ text: 'Esc — назад', size: 9, color: 0xf2d49a });
        hint.position.set(16, 204);
        panel.addChild(hint);

        this.view.addChild(panel);
        this.game.renderer.uiRoot.addChild(this.view);
    }

    update(dt: number): void {
        super.update(dt);
        // Мигание маркера героя (локация не тикается — мерцает сама карта).
        if (this.hero) {
            this.blinkT += dt;
            this.hero.visible = Math.floor(this.blinkT / 0.4) % 2 === 0;
        }
    }

    /** Esc или клавиша сумки — назад (в сумку, если карта открыта из неё). */
    protected onCancel(): void {
        this.close();
    }

    protected onAction(): void {
        this.close();
    }

    private close(): void {
        playSfx(this.game.audio, 'sfx/ui_click');
        this.onBack();
    }
}