Newer
Older
rpg / apps / game / src / agent / snapshot.ts
import type { SnapshotLayer, JsonValue } from '@rpg/engine';

/**
 * Контентный слой снапшота агентного моста. Чистые функции — тестируются
 * в Vitest без браузера; LocationScene/GameAgent подставляют живые данные.
 */

/** Герой. */
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;
    choices: string[];
    waitingForChoice: boolean;
}

/** Переход (дверь/выход/портал) — из данных локации. */
export interface TransitionSnapshot {
    tile: { x: number; y: number };
    to: string;
    label: string | null;
}

/** Интерактивный объект области (used — для once-объектов). */
export interface InteractableSnapshot {
    id: string;
    tile: { x: number; y: number };
    label: string | null;
    used: boolean;
}

/** Контентный слой сцены локации. */
export interface LocationSnapshot {
    scene: 'location';
    area: string;
    areaName: string;
    hero: HeroSnapshot;
    enemies: EnemySnapshot[];
    npcs: NpcSnapshot[];
    transitions: TransitionSnapshot[];
    interactables: InteractableSnapshot[];
    dialogue: DialogueSnapshot | null;
    cutscene: { active: boolean } | null;
    /** Последний тост (текст + тик) — единственный канал текста реакций. */
    lastToast: { text: string; tick: number } | null;
}

/** Полный снапшот игры: движковый слой + слои игры. */
export interface GameSnapshot {
    [k: string]: JsonValue | unknown;
    /** Флаги прохождения. */
    flags: string[];
    /** Переменные прохождения. */
    vars: Record<string, number | string | boolean>;
    /** Сумка. */
    inventory: { id: string; count: number }[];
}

/** Округление до 3 знаков — JSON читабельнее, точность в юнитах не теряется. */
function r(v: number): number {
    return Math.round(v * 1000) / 1000;
}

/** Слой героя. */
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 };
}

/** Слой уровня игры (вне сцен). */
export function gameLayer(o: {
    flags: string[];
    vars: Record<string, number | string | boolean>;
    inventory: { id: string; count: number }[];
}): GameSnapshot {
    return {
        flags: [...o.flags],
        vars: { ...o.vars },
        inventory: o.inventory.map((s) => ({ id: s.id, count: s.count }))
    };
}