import { BlockChange, ClientMessage, PlayerInfo, ServerMessage } from "$/common/protocol.ts"; const CONNECT_TIMEOUT_MS = 3000; export interface RemotePlayer extends PlayerInfo { // where we draw them, eased towards x/y/z so movement isnt choppy display_x: number; display_y: number; display_z: number; color: [number, number, number]; } export class Connection { socket: WebSocket; id: string; seed: string; initial_changes: BlockChange[]; spawn: { x: number; y: number; z: number; yaw: number; pitch: number }; selected_slot: number; players = new Map(); // handled by the network system inside the game loop, not whenever the socket feels like it incoming: ServerMessage[] = []; closed = false; constructor(socket: WebSocket, welcome: Extract) { this.socket = socket; this.id = welcome.id; this.seed = welcome.seed; this.initial_changes = welcome.changes; this.spawn = welcome.spawn; this.selected_slot = welcome.selected_slot; for (const player of welcome.players) { this.add_player(player); } socket.addEventListener("message", (event) => { this.incoming.push(JSON.parse(event.data)); }); socket.addEventListener("close", () => { this.closed = true; }); } send(message: ClientMessage) { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } } add_player(player: PlayerInfo) { this.players.set(player.id, { ...player, display_x: player.x, display_y: player.y, display_z: player.z, color: color_from_name(player.name), }); } static open(url: string, name: string): Promise { return new Promise((resolve) => { let socket: WebSocket; try { socket = new WebSocket(url); } catch { resolve(undefined); return; } const timeout = setTimeout(() => { socket.close(); resolve(undefined); }, CONNECT_TIMEOUT_MS); socket.addEventListener("open", () => { socket.send(JSON.stringify({ type: "hello", name } satisfies ClientMessage)); }); socket.addEventListener("message", (event) => { const message: ServerMessage = JSON.parse(event.data); if (message.type === "welcome") { clearTimeout(timeout); resolve(new Connection(socket, message)); } }, { once: true }); socket.addEventListener("error", () => { clearTimeout(timeout); resolve(undefined); }); }); } } function color_from_name(name: string): [number, number, number] { let hash = 0; for (const ch of name) { hash = (hash * 31 + ch.charCodeAt(0)) | 0; } const hue = ((hash % 360) + 360) % 360; // hsl with s=0.6 l=0.6 to rgb const c = 0.48; const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); const m = 0.36; const [r, g, b] = hue < 60 ? [c, x, 0] : hue < 120 ? [x, c, 0] : hue < 180 ? [0, c, x] : hue < 240 ? [0, x, c] : hue < 300 ? [x, 0, c] : [c, 0, x]; return [r + m, g + m, b + m]; } export function get_server_url(): string { const params = new URLSearchParams(location.search); const server = params.get("server"); const protocol = location.protocol === "https:" ? "wss:" : "ws:"; return `${protocol}//${server ?? location.host}/ws`; } export function get_player_name(): string { return new URLSearchParams(location.search).get("name") ?? ""; }