Newer
Older
rpg / docs / engine / inventory.md

Инвентарь (модель)

Модуль packages/engine/src/inventory/Inventory.ts — жанронезависимый контейнер предметов. Движок знает только модель: стаки, лимиты, события. Реестр предметов (имена, описания) и UI-сцены — на стороне игры (в этой игре: apps/game/src/data/items.ts, InventoryScene).

Модель

Содержимое — Map<itemId, count>: стаки неявные (количество), порядок слотов — порядок вставки.

export interface InventoryOptions {
    /** Максимум разных слотов (id). undefined/0 — без лимита. */
    maxSlots?: number;
    /** Максимум штук в одном слоте. undefined/0 — без лимита. */
    maxPerStack?: number;
}

const inv = new Inventory({ maxSlots: 12, maxPerStack: 99 });

Лимиты не бросают исключенийadd обрезает добавление и возвращает, сколько реально влезло (все вызовы в игре игнорируют возврат — совместимо):

inv.add('cloth');            // → 1 (сколько влезло)
inv.add('cloth', 5);         // частичное добавление под лимит
inv.spaceFor('cloth');       // сколько ещё влезет (стак + свободные слоты)
inv.remove('cloth', 2);      // → убрано; пустой слот исчезает
inv.count('cloth'); inv.has('cloth');
inv.all;                     // [{ id, count }] — порядок вставки
inv.size;                    // занято слотов
inv.slotLimit; inv.stackLimit; // Infinity, если не заданы
inv.empty; inv.clear();
inv.serialize(); inv.load(data); // { items: Record<string, number> } — формат сейвов, не менять

load игнорирует мусор и count <= 0, клампит под лимиты (берёт первые N слотов), эмитит batch-событие. inventoryFromData(data, options?) — фабрика.

События

onChange(fn) возвращает отписку. Аргумент — деталь изменения:

export interface InventoryChange {
    kind: 'add' | 'remove' | 'clear' | 'load';
    /** Затронутый id; для batch-событий 'clear'/'load' — ''. */
    id: string;
    /** Знаковая дельта (+добавлено / -убрано); для 'clear'/'load' — 0. */
    count: number;
    /** Итоговый счётчик предмета после изменения. */
    after: number;
}

const off = inv.onChange((ch) => redraw(ch.kind === 'load' ? 'всё' : ch.id));

add/remove эмитят по одному событию на предмет; clear/load — одно batch-событие (view просто перерисовывается целиком). События не эмитятся, когда ничего не изменилось (add 0 влезло, clear на пустом).

Типизация id

Класс генеричен: Inventory<TId extends string = string>. Игра может дать Inventory<ItemId> — опечатка в id ловится компилятором; дефолт — строки, чтобы движок не знал предметов.

Граница движок/игра

  • Движок: контейнер (этот модуль). Никаких имён предметов, цен, весов.
  • Игра: реестр ItemDef, форматирование строк (inventoryItems), UI-сцена, лимиты сумки как контент (new Inventory(...) в Game.ts).

Юза́бельные предметы (игра)

«Применение» предмета — чистый хелпер игры useItemLine(id, {label}) → string | null (apps/game/src/data/items.ts): юза́бельные предметы дают строку-результат (часы → «Время: 8:24»), остальные — null (сумка молчит). InventoryScene показывает сумку списком с курсором (MenuList), Enter вызывает хелпер и кладёт строку в PixelText внизу панели. Новые юза́бельные предметы расширяют useItemLine, а не сцену.

Тесты

packages/engine/src/inventory/__tests__/Inventory.test.ts — add/count, remove, лимиты (частичное добавление, отказ на новый слот, load-кламп), детали всех 4 kind события, round-trip serialize/load, clear, генерик.