Files
bworld/client/gui/gui_screen.ts
T
2026-09-25 16:05:25 -03:00

176 lines
4.7 KiB
TypeScript

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<Texture>("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));
}
}