135 lines
3.4 KiB
TypeScript
135 lines
3.4 KiB
TypeScript
import { BlockChange, ClientMessage, PlayerInfo, ServerMessage } from "$/common/protocol.ts";
|
|
import type { ModListing } from "$/common/mod_loader.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;
|
|
name: string;
|
|
seed: string;
|
|
mods: ModListing[];
|
|
initial_changes: BlockChange[];
|
|
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
|
selected_slot: number;
|
|
players = new Map<string, RemotePlayer>();
|
|
|
|
// 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<ServerMessage, { type: "welcome" }>) {
|
|
this.socket = socket;
|
|
this.id = welcome.id;
|
|
this.name = welcome.name;
|
|
this.seed = welcome.seed;
|
|
this.mods = welcome.mods;
|
|
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<Connection | undefined> {
|
|
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") ?? "";
|
|
}
|