/**
* Инвентарь — жанронезависимый контейнер предметов.
* Хранит счётчики по id: `Map<itemId, count>`. Стеки неявные — количество.
* Смена состава оповещает подписчиков (view-агностично): подписка — как у EventBus,
* но события эмитит сам контейнер, чтобы работать без внешней шины.
*/
export interface InventoryState {
/** Предмет -> количество (только > 0). */
items: Record<string, number>;
}
/** Создать инвентарь из сериализованного состояния. */
export function inventoryFromData(data: InventoryData | undefined | null): Inventory {
const inv = new Inventory();
if (data) inv.load(data);
return inv;
}
/** Сериализованное состояние инвентаря (для сейвов). */
export interface InventoryData {
items: Record<string, number>;
}
export class Inventory {
private counts = new Map<string, number>();
private listeners = new Set<() => void>();
/** Подписка на любое изменение содержимого. Возвращает отписку. */
onChange(fn: () => void): () => void {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
/** Добавить n штук предмета (n >= 1). */
add(itemId: string, n = 1): void {
if (n <= 0) return;
this.counts.set(itemId, (this.counts.get(itemId) ?? 0) + n);
this.notify();
}
/**
* Убрать n штук предмета. Если после вычитания стало <= 0 — слот исчезает.
* Возвращает, сколько реально убрано (нельзя убрать больше, чем есть).
*/
remove(itemId: string, n = 1): number {
const have = this.counts.get(itemId) ?? 0;
if (have <= 0 || n <= 0) return 0;
const left = Math.max(0, have - n);
if (left === 0) this.counts.delete(itemId);
else this.counts.set(itemId, left);
this.notify();
return Math.min(n, have);
}
/** Сколько штук предмета лежит (0, если нет). */
count(itemId: string): number {
return this.counts.get(itemId) ?? 0;
}
/** Есть ли предмет (в количестве >= 1). */
has(itemId: string): boolean {
return this.count(itemId) > 0;
}
/** Все слоты (id -> количество); порядок вставки. */
get all(): Array<{ id: string; count: number }> {
return [...this.counts].map(([id, count]) => ({ id, count }));
}
get empty(): boolean {
return this.counts.size === 0;
}
/** Полностью очистить (новая игра). */
clear(): void {
if (this.counts.size === 0) return;
this.counts.clear();
this.notify();
}
serialize(): InventoryData {
return { items: Object.fromEntries(this.counts) };
}
load(data: InventoryData): void {
this.counts.clear();
for (const [id, count] of Object.entries(data.items ?? {})) {
if (typeof count === 'number' && count > 0) this.counts.set(id, count);
}
this.notify();
}
private notify(): void {
for (const fn of this.listeners) fn();
}
}