Server authority
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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<T = unknown | undefined> {
|
||||
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<ItemRegistry>("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;
|
||||
}
|
||||
Reference in New Issue
Block a user