import { describe, expect, it } from 'vitest';
import {
AGENT_SNAPSHOT_KEYS,
collisionLayer,
dialogueLayer,
enemiesLayer,
gameLayer,
heroLayer,
lightingLayer,
npcsLayer,
type EnemySnapshot,
type GameSnapshot,
type DialogueSnapshot,
type LocationSnapshot,
type NpcSnapshot
} from '../snapshot';
import { predKeys } from '../GameAgent';
describe('снапшот агента — слои', () => {
it('герой: позиция округляется до 3 знаков, остальное копируется', () => {
const layer = heroLayer({
tile: { x: 12, y: 8 },
pos: { x: 1.23456, y: 5.0001 },
hp: 9,
maxHp: 12,
facing: 'side',
moving: false,
invuln: false,
inHazard: null
});
expect(layer.hero).toMatchObject({
tile: { x: 12, y: 8 },
pos: { x: 1.235, y: 5 },
hp: 9,
maxHp: 12,
facing: 'side'
});
});
it('враги: поля на месте, позиция округлена', () => {
const e: EnemySnapshot = {
kind: 'ash',
state: 'chase',
hp: 4,
pos: { x: 0.123456, y: 2.5 },
asleep: true,
dead: false
};
const layer = enemiesLayer([e]);
expect(layer.enemies).toEqual([
{ kind: 'ash', state: 'chase', hp: 4, pos: { x: 0.123, y: 2.5 }, asleep: true, dead: false }
]);
});
it('слой коллизий: blocked по тайлам, пропы отдельным списком', () => {
const data = {
width: 2,
height: 2,
tiles: [0, 1, 0, 0],
blocked: [1],
props: [{ id: 2, x: 0, y: 1, w: 2, h: 1 }]
};
const layer = collisionLayer(data);
expect(layer.collision).toEqual({
width: 2,
height: 2,
blocked: [0, 1, 1, 1], // стена (1,0) + весь footprint пропа
props: [{ x: 0, y: 1, w: 2, h: 1 }]
});
});
it('свет: ambient как есть, источники с округлением координат и интенсивности', () => {
const layer = lightingLayer({
ambient: 0x54586a,
sources: [
{ id: 'hearth', x: 123.4567, y: 67.8912, color: 0xf2b45a, intensity: 0.87654 },
{ id: 'lamp', x: 240, y: 135, color: 0xf2b45a, intensity: 0.5 }
]
});
expect(layer.lighting).toEqual({
ambient: 0x54586a,
sources: [
{ id: 'hearth', x: 123.457, y: 67.891, color: 0xf2b45a, intensity: 0.877 },
{ id: 'lamp', x: 240, y: 135, color: 0xf2b45a, intensity: 0.5 }
]
});
});
it('свет: пустой список источников допустим (дневная улица)', () => {
const layer = lightingLayer({ ambient: 0xffffff, sources: [] });
expect(layer.lighting).toEqual({ ambient: 0xffffff, sources: [] });
});
it('NPC: копия с met, без ссылок на источник', () => {
const n: NpcSnapshot = { id: 'elder', name: 'Ирвин', tile: { x: 20, y: 12 }, met: false };
const layer = npcsLayer([n]);
const out = (layer.npcs as unknown as NpcSnapshot[])[0]!;
expect(out).toEqual(n);
expect(out).not.toBe(n);
});
it('диалог: объект проходит, null остаётся null', () => {
const d: DialogueSnapshot = {
id: 'elder',
nodeId: 'n1',
speaker: 'Ирвин',
text: 'Привет',
mood: null,
tags: [],
choices: [],
path: ['n1'],
waitingForChoice: false
};
expect(dialogueLayer(d).dialogue).toEqual(d);
expect(dialogueLayer(null).dialogue).toBeNull();
});
it('слой игры: флаги/вары/сумка копируются защитно', () => {
const flags = ['metElder'];
const vars = { flowers: 2 };
const inv = [{ id: 'flower', count: 2 }];
const layer = gameLayer({ flags, vars, inventory: inv });
expect(layer).toEqual({ flags: ['metElder'], vars: { flowers: 2 }, inventory: [{ id: 'flower', count: 2 }] });
// Мутация источника не меняет слой.
flags.push('x');
vars.flowers = 99;
inv[0]!.count = 99;
expect(layer.flags).toEqual(['metElder']);
expect(layer.vars).toEqual({ flowers: 2 });
expect(layer.inventory[0]!.count).toBe(2);
});
});
describe('AGENT_SNAPSHOT_KEYS — анти-дрейф', () => {
const locationSample: LocationSnapshot = {
scene: 'location',
area: 'meadows',
areaName: 'Выжженные луга',
hero: {
tile: { x: 0, y: 0 },
pos: { x: 0, y: 0 },
hp: 10,
maxHp: 10,
facing: 'down',
moving: false,
invuln: false,
inHazard: null
},
enemies: [],
npcs: [],
transitions: [],
interactables: [],
collision: { width: 1, height: 1, blocked: [0], props: [] },
lighting: { ambient: 0xffffff, sources: [] },
dialogue: null,
cutscene: null,
lastToast: null
};
it('все top-level ключи сцены и слоя игры есть в списке', () => {
const gamePart: GameSnapshot = gameLayer({
flags: [],
vars: {},
inventory: []
}) as GameSnapshot;
const keys = new Set([...Object.keys(locationSample), ...Object.keys(gamePart)]);
expect([...keys].filter((k) => !AGENT_SNAPSHOT_KEYS.includes(k))).toEqual([]);
});
it('движковые ключи (слоя EngineAgent) не утеряны из списка', () => {
for (const k of ['tick', 'fps', 'fixedStep', 'scenes', 'transitioning', 'camera', 'pointer']) {
expect(AGENT_SNAPSHOT_KEYS).toContain(k);
}
});
it('в списке нет лишних (список = сцена + игра + движок)', () => {
const locationKeys = Object.keys(locationSample);
const gameKeys = ['flags', 'vars', 'inventory'];
const extra = AGENT_SNAPSHOT_KEYS.filter(
(k) => !locationKeys.includes(k) && !gameKeys.includes(k) && !['tick', 'fps', 'fixedStep', 'scenes', 'transitioning', 'camera', 'pointer'].includes(k)
);
expect(extra).toEqual([]);
});
});
describe('predKeys — top-level ключи предиката', () => {
it('одиночное обращение и вложенный путь', () => {
expect(predKeys('s.hero')).toEqual(['hero']);
expect(predKeys('s.hero && s.hero.tile.x === 1')).toEqual(['hero']);
});
it('несколько ключей и сложные выражения', () => {
expect(predKeys('!s.transitioning && s.area === "zvenets"')).toEqual(
expect.arrayContaining(['transitioning', 'area'])
);
expect(predKeys('s.vars.motes >= 1 || s.flags.includes("x")')).toEqual(
expect.arrayContaining(['vars', 'flags'])
);
expect(predKeys('(s.dialogue != null)')).toEqual(['dialogue']);
});
it('не ловит не-обращения (строки, другие объекты)', () => {
expect(predKeys('s.scene === "location"')).toEqual(['scene']);
expect(predKeys('s.x === 1')).toEqual(['x']); // pointer-слой — легальный ключ движка
expect(predKeys('foo.s.bar === 1')).toEqual([]); // не наш s
});
});