import { AssetManager } from "./assets.ts"; import { Client } from "./client.ts"; import { InputManager } from "./input_manager.ts"; import { Connection } from "./network.ts"; import { connect, HandshakeError, is_trusted, join, load_atlas, remember_trust, ServerAddress } from "./handshake.ts"; import { confirm_mods } from "./confirm_mods.ts"; import { ModLoadError } from "$/common/mod_loader.ts"; import { begin_drawing, canvas, clear_background, end_drawing, init_font, init_window, load_texture, resize_canvas, } from "./renderer/mod.ts"; import { is_stopped, show_fatal_error } from "./fatal.ts"; import { load_client_mods, set_mods_client } from "./mods.ts"; import type { GuiScreen } from "./gui/gui_screen.ts"; import { TitleScreen } from "./gui/title_screen.ts"; import { DisconnectedScreen } from "./gui/disconnected_screen.ts"; // runs whatever is showing every frame: a menu screen before joining (and after leaving), or the game export class ClientLoop { running = false; last_time = 0; client: Client | undefined; screen: GuiScreen | undefined; frame_count = 0; last_fps_time = 0; start() { document.addEventListener("visibilitychange", () => { if (!document.hidden) { this.last_time = performance.now(); } }); this.running = true; const now = performance.now(); this.last_time = now; this.frame_count = 0; this.last_fps_time = this.last_time; requestAnimationFrame((time) => this.loop(time)); } stop() { this.running = false; } show_screen(screen: GuiScreen) { this.screen?.on_close(); this.client = undefined; this.screen = screen; InputManager.set_mouse_grabbed(false); } play(connection: Connection) { const client = new Client(connection, (message) => this.show_screen(new DisconnectedScreen(message))); set_mods_client(client); client.chat.add("Connected to the server"); this.screen?.on_close(); this.screen = undefined; this.client = client; console.log("Game started"); } loop(time: number) { if (!this.running || is_stopped()) { return; } const delta = (time - this.last_time) / 1000; begin_drawing(); clear_background(0.69, 0.8, 1, 1.0); if (this.client) { this.client.run_frame(delta); } else if (this.screen) { this.screen.on_tick(delta); this.screen.on_render(); } end_drawing(); this.frame_count += 1; const now = performance.now(); if (now - this.last_fps_time >= 1000) { const fps = (this.frame_count * 1000) / (now - this.last_fps_time); console.log(`FPS: ${fps.toFixed(2)}`); this.frame_count = 0; this.last_fps_time = now; } InputManager.update(); this.last_time = time; requestAnimationFrame((time) => this.loop(time)); } } // a server's blocks, items and scripts can't be taken back out once loaded, so a failure after that point // can't go back to the title screen to try again, the page has to start over class FailedAfterLoadingMods extends Error {} // the game only runs against a server, it owns the world and everything in it. see "Delivery to clients" in MODS.md async function join_server(address: ServerAddress, name: string, status: (text: string) => void): Promise { const { socket, welcome } = await connect(address, name); console.log(`Connected to ${address.ws_url}`); let loaded_mods = false; try { if (address.cross_origin && !is_trusted(address, welcome)) { status("Waiting for you to accept the server's mods..."); if (!await confirm_mods(address, welcome)) { throw new HandshakeError(`You didn't join ${address.base.host}`); } remember_trust(address, welcome); } // textures, blocks and items all come from the server's mods, so this happens before anything else status("Downloading the server's mods..."); const atlas = await load_atlas(address, welcome.atlas); loaded_mods = true; AssetManager.instance.assets["bworld:textures"] = load_texture(atlas.image); AssetManager.instance.assets["bworld:textures_info"] = atlas.regions; await load_client_mods(welcome.mods, address.base); console.log(`Mods: ${welcome.mods.map((mod) => `${mod.id} ${mod.version}`).join(", ") || "none"}`); status("Joining..."); const joined = await join(socket); return new Connection(socket, welcome, joined); } catch (e) { socket.close(); const message = error_message(e); throw loaded_mods ? new FailedAfterLoadingMods(message) : new HandshakeError(message); } } function error_message(e: unknown) { if (e instanceof HandshakeError) { return e.message; } if (e instanceof ModLoadError) { return `Couldn't load the server's mods: ${e.message}`; } return `Something went wrong joining the server: ${e instanceof Error ? e.message : e}`; } const game_canvas = document.getElementById("game") as HTMLCanvasElement; if (!game_canvas) { throw Error("Canvas was not found"); } try { await init_window(game_canvas); } catch (e) { show_fatal_error(e instanceof Error ? e.message : String(e)); throw e; } InputManager.initialize(game_canvas); self.addEventListener("resize", resize_canvas); resize_canvas(); canvas.addEventListener("contextmenu", (event) => event.preventDefault()); AssetManager.instance.load("bworld:assets_text", "/assets/ASSETS.md"); AssetManager.instance.load("bworld:player", "/assets/sprites/player.png"); AssetManager.instance.load("bworld:roguelike", "/assets/sprites/roguelike.png"); AssetManager.instance.load("bworld:ui", "/assets/sprites/ui.png"); AssetManager.instance.load("bworld:m6x11", "/assets/fonts/m6x11.png"); AssetManager.instance.load("bworld:m6x11_fnt", "/assets/fonts/m6x11.fnt"); await AssetManager.instance.load_all(); init_font(); const loop = new ClientLoop(); loop.show_screen( new TitleScreen(async (address, name, status) => { let connection: Connection; try { connection = await join_server(address, name, status); } catch (e) { if (e instanceof FailedAfterLoadingMods) { loop.show_screen(new DisconnectedScreen(e.message)); return; } throw e; } loop.play(connection); }), ); loop.start();