import { describe, expect, it } from 'vitest';
import { Interactables, type InteractSink } from '../Interactables';
import type { GameState } from '@rpg/engine';
import type { InteractableDef } from '../../data/interactables';
/** Фейковый GameState: флаги и вар-числа в памяти. */
function fakeState(): GameState {
const flags = new Set<string>();
const vars = new Map<string, number>();
return {
setFlag: (f: string) => void flags.add(f),
hasFlag: (f: string) => flags.has(f),
clearFlag: (f: string) => void flags.delete(f),
setVar: (k: string, v: number) => void vars.set(k, Number(v)),
getNumber: (k: string) => vars.get(k) ?? 0
} as unknown as GameState;
}
const def = (over: Partial<InteractableDef> = {}): InteractableDef => ({
id: 'chest',
tile: { x: 3, y: 3 },
kind: 'container',
once: true,
responses: [{ text: 'в сундуке', gives: 'cloth' }],
...over
});
function make(defs: InteractableDef[], sinkOver: Partial<InteractSink> = {}) {
const calls: { toast: string[]; gave: [string, number][]; sound: string[] } = {
toast: [],
gave: [],
sound: []
};
const sink: InteractSink = {
hasItem: () => false,
give: (id: string, n: number) => calls.gave.push([id, n]),
playSound: (k: string) => calls.sound.push(k),
showToast: (t: string) => calls.toast.push(t),
...sinkOver
};
const st = fakeState();
return { world: new Interactables(defs, st, sink), calls, st };
}
describe('Interactables', () => {
it('defAt — поиск по тайлу', () => {
const { world } = make([def()]);
expect(world.defAt(3, 3)?.id).toBe('chest');
expect(world.defAt(0, 0)).toBeNull();
});
it('once: реакция + used:<id>; повтор — пусто', () => {
const { world, calls } = make([def()]);
expect(world.isUsed('chest')).toBe(false);
const pick = world.tryInteract(world.defAt(3, 3)!);
expect(pick?.ok).toBe(true);
expect(calls.toast).toEqual(['в сундуке']);
expect(calls.gave).toEqual([['cloth', 1]]);
expect(world.isUsed('chest')).toBe(true);
// Повтор: реакции нет, тост не дублируется.
expect(world.tryInteract(world.defAt(3, 3)!)).toBeNull();
expect(calls.toast).toHaveLength(1);
});
it('не-once объект реагирует повторно', () => {
const { world, calls } = make([
def({ id: 'hearth', once: false, responses: [{ text: 'тепло' }] })
]);
world.tryInteract(world.defAt(3, 3)!);
world.tryInteract(world.defAt(3, 3)!);
expect(calls.toast).toEqual(['тепло', 'тепло']);
});
it('эффекты: флаги/вар/count/звук', () => {
const { world, calls, st } = make([
def({
once: false,
responses: [
{
text: 'рычаг',
setFlags: ['a'],
clearFlags: ['b'],
setVar: { id: 'motes', value: 7 },
gives: 'salt',
count: 2,
sound: 'sfx/bell_hit'
}
]
})
]);
st.setFlag('b');
world.tryInteract(world.defAt(3, 3)!);
expect(st.hasFlag('a')).toBe(true);
expect(st.hasFlag('b')).toBe(false);
expect(st.getNumber('motes')).toBe(7);
expect(calls.gave).toEqual([['salt', 2]]);
expect(calls.sound).toEqual(['sfx/bell_hit']);
});
it('when от GameState: реакция меняется с флагом', () => {
const { world, calls, st } = make([
def({
once: false,
responses: [
{ when: { flag: 'quest_bells_done' }, text: 'после' },
{ text: 'до' }
]
})
]);
world.tryInteract(world.defAt(3, 3)!);
expect(calls.toast[0]).toBe('до');
st.setFlag('quest_bells_done');
world.tryInteract(world.defAt(3, 3)!);
expect(calls.toast[1]).toBe('после');
});
});