import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts"; // how item stacks are saved and sent over the network export interface ItemData { id: string; count: number; data?: unknown; } export class ItemStack { type_id: string; amount: number; max_amount: number; data?: T; constructor(type_id: string | string, amount: number = 1, max_amount: number = 64) { this.type_id = type_id; this.amount = amount; this.max_amount = max_amount; this.data = undefined; const item_info = EverythingRegistry.get("items", type_id); if (item_info?.on_create) { item_info.on_create(this); } } clone(): ItemStack { const item = new ItemStack(this.type_id, this.amount, this.max_amount); item.data = structuredClone(this.data); return item; } to_data(): ItemData { return this.data === undefined ? { id: this.type_id, count: this.amount } : { id: this.type_id, count: this.amount, data: this.data }; } static from_data(data: ItemData): ItemStack { const item = new ItemStack(data.id, data.count); if (data.data !== undefined) { item.data = data.data; } return item; } } export class ContainerSlot { #item_stack: ItemStack | undefined; has_item() { return this.#item_stack !== undefined; } set_item(item_stack: ItemStack | undefined) { if ((item_stack?.amount ?? 0) <= 0) { item_stack = undefined; } this.#item_stack = item_stack; } get_item() { return this.#item_stack; } get type_id() { return this.#item_stack?.type_id; } set amount(new_amount: number) { if (this.#item_stack) { this.#item_stack.amount = new_amount; if (this.#item_stack.amount <= 0) { this.#item_stack = undefined; } } } get amount(): number | undefined { return this.#item_stack?.amount; } get max_amount() { return this.#item_stack?.max_amount; } } export class Container { #slots: ContainerSlot[] = []; readonly size: number; constructor(size: number) { this.size = size; for (let i = 0; i < size; i += 1) { this.#slots.push(new ContainerSlot()); } } // returns how many items didn't fit add_item(item_stack: ItemStack): number { for (const slot of this.#slots.filter((slot) => slot.has_item())) { const slot_item = slot.get_item()!; if (slot_item.type_id === item_stack.type_id) { const missing = slot_item.max_amount - slot_item.amount; const adding = Math.min(missing, item_stack.amount); slot_item.amount += adding; item_stack.amount -= adding; if (item_stack.amount === 0) { return 0; } } } for (const slot of this.#slots) { if (!slot.has_item()) { slot.set_item(item_stack); return 0; } } // TODO: drop item on ground return item_stack.amount; } get_item(slot: number): ItemStack | undefined { return this.#slots[slot]?.get_item(); } get_slot(slot: number): ContainerSlot { return this.#slots[slot]; } set_item(slot: number, item: ItemStack | undefined) { this.#slots[slot].set_item(item); } to_data(): (ItemData | null)[] { return this.#slots.map((slot) => slot.get_item()?.to_data() ?? null); } load(data: (ItemData | null)[]) { for (let i = 0; i < this.size; i += 1) { const item = data[i]; this.#slots[i].set_item(item ? ItemStack.from_data(item) : undefined); } } clear() { for (const slot of this.#slots) { slot.set_item(undefined); } } } // the item a player is carrying around with the mouse in an inventory screen export interface Cursor { item: ItemStack | undefined; } export const LEFT_CLICK = 0; export const RIGHT_CLICK = 2; // what clicking a normal slot does, the server runs this for real and clients run it to predict export function click_slot(container: Container, index: number, cursor: Cursor, button: number) { if (button === LEFT_CLICK) { left_click(container, index, cursor); } else if (button === RIGHT_CLICK) { right_click(container, index, cursor); } if (cursor.item && cursor.item.amount <= 0) { cursor.item = undefined; } } function swap_with_cursor(container: Container, index: number, cursor: Cursor) { const slot = container.get_slot(index); const original = cursor.item; cursor.item = slot.get_item(); slot.set_item(original); } function left_click(container: Container, index: number, cursor: Cursor) { const holding = cursor.item; const slot = container.get_slot(index); const slot_item = slot.get_item(); // if you aren't holding anything // "swap" with nothing on your hand (pick it up) if (!holding) { swap_with_cursor(container, index, cursor); return; } // if you are holding something and slot type equals holding type // try to add to stack if (slot_item && slot.type_id === holding.type_id) { const space_left = slot.max_amount! - slot_item.amount; const amount_to_add = Math.min(space_left, holding.amount); slot_item.amount += amount_to_add; holding.amount -= amount_to_add; return; } // if something on hand but not the same // swap swap_with_cursor(container, index, cursor); } function right_click(container: Container, index: number, cursor: Cursor) { const holding = cursor.item; const slot = container.get_slot(index); const slot_item = slot.get_item(); // if holding something if (holding) { // and slot type equals holding type // add 1 to matching stack if (slot_item && slot.type_id === holding.type_id) { if (slot_item.amount < slot_item.max_amount) { slot_item.amount += 1; holding.amount -= 1; } return; } // place 1 into empty slot if (!slot_item) { const new_item = holding.clone(); new_item.amount = 1; slot.set_item(new_item); holding.amount -= 1; return; } // if something on hand but not the same // swap swap_with_cursor(container, index, cursor); return; } // if player is holding nothing and clicks nothing, nothing happens if (!slot_item) { return; } // pick up half of the stack const original_amount = slot_item.amount; const half = Math.floor(original_amount / 2); const picked_up = slot_item.clone(); picked_up.amount = original_amount - half; cursor.item = picked_up; slot.amount = half; } // output slots (furnace result, crafting result) can only be taken from, all at once // returns whether it was taken export function take_output(item: ItemStack, cursor: Cursor): boolean { const holding = cursor.item; if (!holding) { cursor.item = item.clone(); return true; } if (holding.type_id === item.type_id && holding.max_amount - holding.amount >= item.amount) { holding.amount += item.amount; return true; } return false; }