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

178 lines
5.5 KiB
TypeScript

import { ClientLevel } from "./level/client_level.ts";
import type { BlockHitResult } from "./level/client_level.ts";
import { LocalPlayer } from "./entity/local_player.ts";
import { RemotePlayer } from "./entity/remote_player.ts";
import { Camera } from "./camera.ts";
import { Options } from "./options.ts";
import { MultiPlayerGameMode } from "./game_mode.ts";
import { ClientPacketListener } from "./packet_listener.ts";
import { GameRenderer } from "./rendering/game_renderer.ts";
import { ChatComponent } from "./gui/chat_component.ts";
import { GuiInventoryScreen, GuiScreen } from "./gui/gui_screen.ts";
import { GuiPlayerInventory } from "./gui/gui_player_inventory.ts";
import { GuiChat } from "./gui/gui_chat.ts";
import { InputManager } from "./input_manager.ts";
import { Connection } from "./network.ts";
import { canvas, resize_canvas } from "./renderer/mod.ts";
import { show_fatal_error } from "./fatal.ts";
// the game client, like minecraft's Minecraft class: owns the level, the player, the screens and the renderer,
// and runs one step of the game every frame
export class Client {
connection: Connection;
options = new Options();
level: ClientLevel;
player: LocalPlayer;
camera = new Camera();
game_mode: MultiPlayerGameMode;
packet_listener: ClientPacketListener;
game_renderer = new GameRenderer();
chat = new ChatComponent();
// open screens, the last one is on top and gets the input
screens: GuiScreen[] = [];
// what the player is looking at, updated every frame
hit_result: BlockHitResult | undefined;
debugging = false;
constructor(connection: Connection) {
this.connection = connection;
self.addEventListener("resize", resize_canvas);
resize_canvas();
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
this.level = new ClientLevel(connection.seed);
for (const [x, y, z, id, state] of connection.initial_changes) {
this.level.record_change(x, y, z, id, state);
}
const spawn = connection.spawn;
this.player = new LocalPlayer(this, connection.id, connection.name);
this.player.set_position(spawn.x, spawn.y, spawn.z);
this.player.yaw = spawn.yaw;
this.player.pitch = spawn.pitch;
this.player.inventories.hotbar_selected = connection.selected_slot;
this.level.add_entity(this.player);
for (const info of connection.initial_players) {
this.level.add_entity(new RemotePlayer(this.level, info));
}
this.game_mode = new MultiPlayerGameMode(this);
this.packet_listener = new ClientPacketListener(this);
}
get screen(): GuiScreen | undefined {
return this.screens.at(-1);
}
push_screen(screen: GuiScreen) {
this.screens.push(screen);
}
pop_screen() {
this.screens.pop()?.on_close();
}
// one frame: network, input, the level and its entities, then drawing
run_frame(delta: number) {
this.packet_listener.handle_packets();
if (this.connection.closed) {
show_fatal_error("Lost connection to the server");
return;
}
this.screen?.on_tick(delta);
this.#handle_keybinds(delta);
this.level.update_loaded_chunks(this.player.x, this.player.z, this.options.render_distance);
this.level.tick(delta);
this.game_renderer.render(this);
}
#handle_keybinds(delta: number) {
const options = this.options;
const player = this.player;
if (InputManager.is_key_pressed(options.key_inventory)) {
if (!this.screen) {
this.push_screen(new GuiPlayerInventory(player.inventories, (m) => this.connection.send(m)));
} else if (this.screen instanceof GuiInventoryScreen) {
this.pop_screen();
}
}
if (InputManager.is_key_pressed(options.key_chat) && !this.screen) {
this.push_screen(new GuiChat(this));
}
if (InputManager.is_key_pressed("Escape")) {
this.pop_screen();
}
if (InputManager.is_key_pressed(options.key_debug)) {
this.debugging = !this.debugging;
}
if (InputManager.is_key_pressed(options.key_fullscreen)) {
InputManager.toggle_fullscreen();
}
if (InputManager.is_mouse_grabbed()) {
const mouse = InputManager.get_mouse_delta();
player.turn(mouse.x, mouse.y);
}
InputManager.set_mouse_grabbed(!this.screen);
this.hit_result = this.level.pick(player.x, player.y + player.eye_height, player.z, player.yaw, player.pitch);
this.#handle_block_interaction(delta);
if (!this.screen) {
this.#handle_hotbar();
}
}
#handle_block_interaction(delta: number) {
const hit = this.hit_result;
const attacking = !this.screen && InputManager.is_mouse_down(0);
if (!attacking || !hit) {
this.game_mode.stop_destroy_block();
}
if (this.screen) {
return;
}
if (hit) {
if (InputManager.is_mouse_pressed(0)) {
this.game_mode.start_destroy_block(hit);
}
if (attacking) {
this.game_mode.continue_destroy_block(hit, delta);
} else if (InputManager.is_mouse_pressed(2)) {
this.game_mode.use_item_on(hit);
}
} else if (InputManager.is_mouse_pressed(2)) {
this.game_mode.use_item();
}
}
#handle_hotbar() {
const inventories = this.player.inventories;
const previous = inventories.hotbar_selected;
const scroll = InputManager.get_wheel_delta();
if (scroll > 0) {
inventories.hotbar_selected = Math.min(8, inventories.hotbar_selected + 1);
} else if (scroll < 0) {
inventories.hotbar_selected = Math.max(0, inventories.hotbar_selected - 1);
}
const pressed = this.options.key_hotbar.findIndex((key) => InputManager.is_key_pressed(key));
if (pressed !== -1) {
inventories.hotbar_selected = pressed;
}
if (inventories.hotbar_selected !== previous) {
this.connection.send({ type: "select_slot", slot: inventories.hotbar_selected });
}
}
}