import { SLOT_SIZE } from "$/common/constants.ts"; import { click_slot, Container } from "$/common/inventory.ts"; import { ClientMessage, ContainerKey, CRAFTING_RESULT_SLOT } from "$/common/protocol.ts"; import { point_inside_rec } from "../../common/utils.ts"; import { AssetManager } from "../assets.ts"; import { InputManager } from "../input_manager.ts"; import { ClientInventories } from "../inventory.ts"; import { canvas, Texture } from "../renderer/mod.ts"; import { draw_item, draw_nine_slice } from "../rendering/render_utils.ts"; export class Slot { container: ContainerKey; index: number; // relative to screen top left x: number; y: number; // can only be taken from, like the crafting or furnace result output: boolean; constructor(container: ContainerKey, index: number, x: number, y: number, output = false) { this.container = container; this.index = index; this.x = x; this.y = y; this.output = output; } } export abstract class GuiScreen { x: number = 0; y: number = 0; abstract on_tick(_delta: number): void; abstract on_render(): void; abstract on_close(): void; } // a screen of slots. the server owns the contents: clicks are sent to it and guessed locally // so they feel instant, then whatever the server syncs back wins export class GuiInventoryScreen extends GuiScreen { inventories: ClientInventories; send: (message: ClientMessage) => void; slots: Slot[] = []; hovering: Slot | undefined; inventory_width: number = 500; inventory_height: number = 500; constructor(inventories: ClientInventories, send: (message: ClientMessage) => void) { super(); this.inventories = inventories; this.send = send; } get_container(key: ContainerKey): Container | undefined { switch (key) { case "inventory": return this.inventories.inventory; case "crafting": return this.inventories.crafting; case "screen": return this.inventories.screen; } } on_tick(_delta: number): void { this.x = canvas.width / 2 - (this.inventory_width / 2); this.y = canvas.height / 2 - (this.inventory_height / 2); this.handle_interaction(); } on_render(): void { const ui = AssetManager.instance.get("bworld:ui"); for (const slot of this.slots) { const hovered = slot === this.hovering; draw_nine_slice( ui, hovered ? 19 * 16 : 160 + 32, hovered ? 16 : 0, 16, 16, 4, 4, 4, 4, this.x + slot.x, this.y + slot.y, SLOT_SIZE, SLOT_SIZE, ); } for (const slot of this.slots) { const item = this.get_container(slot.container)?.get_item(slot.index); if (item) { draw_item(item, this.x + slot.x, this.y + slot.y); } } if (this.inventories.cursor.item) { const mouse = InputManager.get_mouse_position(); draw_item(this.inventories.cursor.item, mouse.x, mouse.y); } } on_close(): void { this.send({ type: "close_screen" }); // the server puts these back in the inventory, show it right away const { inventory, crafting, cursor } = this.inventories; for (let i = 0; i < CRAFTING_RESULT_SLOT; i++) { const item = crafting.get_item(i); if (item) { inventory.add_item(item); crafting.set_item(i, undefined); } } crafting.set_item(CRAFTING_RESULT_SLOT, undefined); if (cursor.item) { inventory.add_item(cursor.item); cursor.item = undefined; } } handle_interaction() { this.hovering = undefined; const mouse = InputManager.get_mouse_position(); for (const slot of this.slots) { const hovering = point_inside_rec(mouse.x, mouse.y, this.x + slot.x, this.y + slot.y, SLOT_SIZE, SLOT_SIZE); if (!hovering) { continue; } this.hovering = slot; for (const button of [0, 2]) { if (InputManager.is_mouse_pressed(button)) { InputManager.consume_mouse(button); this.click(slot, button); return; } } } } click(slot: Slot, button: number) { this.send({ type: "click", container: slot.container, index: slot.index, button }); // output slots depend on server side things like recipes, just wait for the server const container = this.get_container(slot.container); if (container && !slot.output) { click_slot(container, slot.index, this.inventories.cursor, button); } } } // generic methods that is often needed export function add_player_inventory(handler: GuiInventoryScreen, offset_x: number, offset_y: number) { for (let i = 0; i < 3; ++i) { for (let l = 0; l < 9; ++l) { handler.slots.push( new Slot("inventory", l + i * 9 + 9, offset_x + l * SLOT_SIZE, offset_y + i * SLOT_SIZE), ); } } } export function add_player_hotbar(handler: GuiInventoryScreen, offset_x: number, offset_y: number) { for (let i = 0; i < 9; ++i) { handler.slots.push(new Slot("inventory", i, offset_x + i * SLOT_SIZE, offset_y)); } }