Newer
Older
rpg / apps / game / src / systems / Interactables.ts
import type { GameState } from '@rpg/engine';
import type { ItemId } from '../data/items';
import {
    resolveInteractable,
    type InteractableDef,
    type InteractCtx,
    type InteractPick,
    type InteractResponse
} from '../data/interactables';

/**
 * Рантайм интерактивных объектов области: поиск по тайлу, флаги used:<id>,
 * применение эффектов реакции через sink (сцена подставляет сумку/звук/тост).
 */

/** Эффекты реакции — мост между чистым резолвером и игрой. */
export interface InteractSink {
    hasItem(id: ItemId): boolean;
    give(id: ItemId, count: number): void;
    playSound(key: string): void;
    showToast(text: string): void;
}

export class Interactables {
    constructor(
        private defs: InteractableDef[],
        private state: GameState,
        private sink: InteractSink
    ) {}

    all(): InteractableDef[] {
        return this.defs;
    }

    /** Объект на тайле (или null). */
    defAt(x: number, y: number): InteractableDef | null {
        return this.defs.find((d) => d.tile.x === x && d.tile.y === y) ?? null;
    }

    /** Одноразовый объект уже использован (флаг used:<id> в GameState). */
    isUsed(id: string): boolean {
        return this.state.hasFlag(`used:${id}`);
    }

    /** Взаимодействие: выбор реакции + применение эффектов. */
    tryInteract(def: InteractableDef): InteractPick {
        const ctx: InteractCtx = {
            hasFlag: (f) => this.state.hasFlag(f),
            hasItem: (i) => this.sink.hasItem(i),
            getVar: (id) => this.state.getNumber(id),
            used: (id) => this.isUsed(id)
        };
        const pick = resolveInteractable(def, ctx);
        if (pick?.ok) this.apply(def, pick.response);
        return pick;
    }

    /** Применить эффекты реакции (порядок: флаги -> вар -> предмет -> звук -> тост). */
    private apply(def: InteractableDef, r: InteractResponse): void {
        for (const f of r.setFlags ?? []) this.state.setFlag(f);
        for (const f of r.clearFlags ?? []) this.state.clearFlag(f);
        if (r.setVar) this.state.setVar(r.setVar.id, r.setVar.value);
        if (r.addVar) this.state.setVar(r.addVar.id, this.state.getNumber(r.addVar.id) + r.addVar.by);
        if (r.gives) this.sink.give(r.gives, r.count ?? 1);
        if (def.once) this.state.setFlag(`used:${def.id}`);
        if (r.sound) this.sink.playSound(r.sound);
        this.sink.showToast(r.text);
    }
}