Newer
Older
rpg / apps / game / src / systems / __tests__ / Interactables.test.ts
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('addVar: подбор мотов копит вар, а не затирает', () => {
        const { world, st, calls } = make([
            def({
                id: 'mote',
                kind: 'pickup',
                once: true,
                responses: [{ text: 'мот', gives: 'mote', addVar: { id: 'motes', by: 1 } }]
            })
        ]);
        world.tryInteract(world.defAt(3, 3)!);
        expect(st.getNumber('motes')).toBe(1);
        // Второй такой же объект (другой тайл): вар растёт от прежнего значения.
        const { world: w2, st: st2 } = make([
            def({
                id: 'mote_2',
                kind: 'pickup',
                once: true,
                responses: [{ text: 'мот', gives: 'mote', addVar: { id: 'motes', by: 1 } }]
            })
        ]);
        st2.setVar('motes', 2);
        w2.tryInteract(w2.defAt(3, 3)!);
        expect(st2.getNumber('motes')).toBe(3);
        expect(calls.toast).toEqual(['мот']);
    });

    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('после');
    });
});