import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts"; import { AIR, FACE_OFFSETS, TICK_DELTA } from "$/common/constants.ts"; import type { Client } from "./client.ts"; import type { BlockHitResult } from "./level/client_level.ts"; // breaking and using blocks against a server, like minecraft's MultiPlayerGameMode. the client times breaking // and guesses the results so they feel instant, the server decides what really happens export class MultiPlayerGameMode { client: Client; // the block being broken, and how far along it is in seconds out of destroy_time destroy_pos: { x: number; y: number; z: number } | undefined; destroy_progress = 0; destroy_time = 0; constructor(client: Client) { this.client = client; } // the click that starts breaking, for mods' on_click start_destroy_block(hit: BlockHitResult) { this.client.connection.send({ type: "hit_block", x: hit.x, y: hit.y, z: hit.z }); } // called every tick the attack button is held on a block continue_destroy_block(hit: BlockHitResult) { const block_info = EverythingRegistry.get_by_id("blocks", hit.block)!; const tool_type = this.#held_item()?.tool_type; this.destroy_pos = { x: hit.x, y: hit.y, z: hit.z }; this.destroy_time = block_info.toughness ?? 9999; this.destroy_progress += TICK_DELTA * (tool_type === block_info.tool_to_break ? 2 : 1); if (this.destroy_progress >= this.destroy_time) { // show it right away, the server decides drops and corrects us if it disagrees this.client.level.break_block(hit.x, hit.y, hit.z); this.client.connection.send({ type: "break_block", x: hit.x, y: hit.y, z: hit.z }); this.destroy_progress = 0; this.destroy_time = 0; } } stop_destroy_block() { this.destroy_pos = undefined; this.destroy_progress = 0; this.destroy_time = 0; } // right click on a block use_item_on(hit: BlockHitResult) { const level = this.client.level; this.client.connection.send({ type: "use_block", x: hit.x, y: hit.y, z: hit.z, face: hit.face }); // guess that it places the held block, unless the block does something when used const block_info = EverythingRegistry.get_by_id("blocks", hit.block)!; const offset = FACE_OFFSETS[hit.face]; const target = { x: hit.x + offset.x, y: hit.y + offset.y, z: hit.z + offset.z }; const target_id = level.get_block(target.x, target.y, target.z); const replaceable = target_id === AIR || EverythingRegistry.get_by_id("blocks", target_id)?.replaceable; const held = this.#held_item(); // items with components might do something else on the server, like on_use const place_id = held?.components ? undefined : held?.block_id; if (!block_info.interactive && place_id && replaceable) { level.add_block({ ...target, id: place_id }); const slot = this.#held_slot(); slot.amount = slot.amount! - 1; } } // right click on nothing use_item() { this.client.connection.send({ type: "use_item" }); } #held_slot() { const inventories = this.client.player.inventories; return inventories.inventory.get_slot(inventories.hotbar_selected); } #held_item() { return EverythingRegistry.get("items", this.#held_slot().type_id ?? ""); } }