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.HOUSE_V3]: 0x5a4a3c,
[TILES.HOUSE_V4]: 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();
}
}