import { gridOf, ENGINE_SNAPSHOT_KEYS, type SnapshotLayer, type JsonValue, type TileMapData } from '@rpg/engine';
/**
* Контентный слой снапшота агентного моста. Чистые функции — тестируются
* в Vitest без браузера; LocationScene/GameAgent подставляют живые данные.
*/
/** Top-level ключи полного снапшота игры: движковый слой + сцена + слой игры. */
export const AGENT_SNAPSHOT_KEYS: readonly string[] = [
...ENGINE_SNAPSHOT_KEYS,
// слой сцены локации (LocationSnapshot)
'scene',
'area',
'areaName',
'hero',
'enemies',
'npcs',
'transitions',
'interactables',
'collision',
'lighting',
'dialogue',
'cutscene',
'lastToast',
// слой игры (gameLayer)
'flags',
'vars',
'inventory',
'time'
];
/** Герой. */
export interface HeroSnapshot {
tile: { x: number; y: number };
pos: { x: number; y: number };
hp: number;
maxHp: number;
facing: string;
moving: boolean;
invuln: boolean;
inHazard: string | null;
}
/** Враг. */
export interface EnemySnapshot {
kind: string;
state: string | null;
hp: number;
pos: { x: number; y: number };
asleep: boolean;
dead: boolean;
}
/** NPC. */
export interface NpcSnapshot {
id: string;
name: string;
tile: { x: number; y: number };
met: boolean;
}
/** Активный диалог (или null). */
export interface DialogueSnapshot {
id: string | null;
nodeId: string | null;
speaker: string | null;
text: string | null;
/** Настроение реплики (презентационное; на геймплей не влияет). */
mood: string | null;
/** Свободные метки узла — для фильтров проверок. */
tags: string[];
choices: string[];
/** Показанные узлы по порядку — как диалог был пройден. */
path: string[];
waitingForChoice: boolean;
}
/** Переход (дверь/выход/портал) — из данных локации. */
export interface TransitionSnapshot {
tile: { x: number; y: number };
to: string;
/** Как срабатывает: шагом на тайл или кликом. */
trigger: 'step' | 'click';
label: string | null;
}
/** Интерактивный объект области (used — для once-объектов). */
export interface InteractableSnapshot {
id: string;
tile: { x: number; y: number };
label: string | null;
used: boolean;
}
/** Источник света (пост-мерцание). */
export interface LightSourceSnapshot {
id: string;
x: number;
y: number;
color: number;
intensity: number;
}
/** Освещение сцены: ambient (multiply-цвет) + виньетка + активные источники. */
export interface LightingSnapshot {
ambient: number;
/** Целевая интенсивность виньетки 0..1. */
vignette: number;
sources: LightSourceSnapshot[];
}
/** Карта коллизий для агента: стены/вода по тайлам + крупные пропы. */
export interface CollisionSnapshot {
width: number;
height: number;
/** Проходимость по тайлам, построчно: 0 — свободен, 1 — блок (включая footprint пропов). */
blocked: number[];
/** Крупные объекты с footprint'ом (дома и т.п.). */
props: { x: number; y: number; w: number; h: number }[];
}
/** Контентный слой сцены локации. */
export interface LocationSnapshot {
scene: 'location';
area: string;
areaName: string;
hero: HeroSnapshot;
enemies: EnemySnapshot[];
npcs: NpcSnapshot[];
transitions: TransitionSnapshot[];
interactables: InteractableSnapshot[];
collision: CollisionSnapshot;
/** Освещение области (ambient + активные источники). */
lighting: LightingSnapshot;
dialogue: DialogueSnapshot | null;
cutscene: { active: boolean } | null;
/** Последний тост (текст + тик) — единственный канал текста реакций. */
lastToast: { text: string; tick: number } | null;
}
/** Время суток (ход игры — непрерывный, 1 с реального = 1 игровая минута). */
export interface TimeSnapshot {
/** Целые минуты суток 0..1439. */
minutes: number;
/** Строка «8:24» (как на часах предмета). */
label: string;
/** Фактор ночи 0..1 (dayNightFactor по часам). */
night: number;
}
/** Полный снапшот игры: движковый слой + слои игры. */
export interface GameSnapshot {
[k: string]: JsonValue | unknown;
/** Флаги прохождения. */
flags: string[];
/** Переменные прохождения. */
vars: Record<string, number | string | boolean>;
/** Сумка. */
inventory: { id: string; count: number }[];
/** Время суток. */
time: TimeSnapshot;
}
/** Округление до 3 знаков — JSON читабельнее, точность в юнитах не теряется. */
function r(v: number): number {
return Math.round(v * 1000) / 1000;
}
/** Слой карты коллизий: blocked по тайлам (gridOf учитывает footprint пропов). */
export function collisionLayer(data: TileMapData): SnapshotLayer {
const grid = gridOf(data);
const blocked: number[] = [];
for (let y = 0; y < data.height; y++) {
for (let x = 0; x < data.width; x++) {
blocked.push(grid.isWalkable(x, y) ? 0 : 1);
}
}
const collision: CollisionSnapshot = {
width: data.width,
height: data.height,
blocked,
props: (data.props ?? []).map((p) => ({ x: p.x, y: p.y, w: p.w ?? 1, h: p.h ?? 1 }))
};
return { collision: collision as unknown as JsonValue };
}
/** Слой героя. */
export function heroLayer(o: HeroSnapshot): SnapshotLayer {
return {
hero: {
tile: { x: o.tile.x, y: o.tile.y },
pos: { x: r(o.pos.x), y: r(o.pos.y) },
hp: o.hp,
maxHp: o.maxHp,
facing: o.facing,
moving: o.moving,
invuln: o.invuln,
inHazard: o.inHazard
}
};
}
/** Слой врагов. */
export function enemiesLayer(enemies: EnemySnapshot[]): SnapshotLayer {
return {
enemies: enemies.map((e) => ({
kind: e.kind,
state: e.state,
hp: e.hp,
pos: { x: r(e.pos.x), y: r(e.pos.y) },
asleep: e.asleep,
dead: e.dead
})) as unknown as JsonValue[]
};
}
/** Слой NPC. */
export function npcsLayer(npcs: NpcSnapshot[]): SnapshotLayer {
return {
npcs: npcs.map((n) => ({
id: n.id,
name: n.name,
tile: { x: n.tile.x, y: n.tile.y },
met: n.met
})) as unknown as JsonValue[]
};
}
/** Слой диалога. */
export function dialogueLayer(d: DialogueSnapshot | null): SnapshotLayer {
return { dialogue: d ? (d as unknown as JsonValue) : null };
}
/** Слой освещения: ambient + виньетка + активные источники (координаты экранные px). */
export function lightingLayer(o: LightingSnapshot): SnapshotLayer {
return {
lighting: {
ambient: o.ambient,
vignette: r(o.vignette),
sources: o.sources.map((s) => ({
id: s.id,
x: r(s.x),
y: r(s.y),
color: s.color,
intensity: r(s.intensity)
}))
} as unknown as JsonValue
};
}
/** Слой уровня игры (вне сцен). */
export function gameLayer(o: {
flags: string[];
vars: Record<string, number | string | boolean>;
inventory: { id: string; count: number }[];
time: TimeSnapshot;
}): GameSnapshot {
return {
flags: [...o.flags],
vars: { ...o.vars },
inventory: o.inventory.map((s) => ({ id: s.id, count: s.count })),
time: { ...o.time }
};
}