Files
Battle-for-Tismo/client/js/net.js
T
2026-09-16 19:59:18 +02:00

187 lines
5.0 KiB
JavaScript

// Browser network role: talks to the authoritative Node server over a WebSocket
// and exposes high-level events so the screens do not care about the transport.
export const DEFAULT_PORT = 27015;
export const DEFAULT_PATH = "/ws";
export class Network {
constructor() {
this.socket = null;
this.isAdmin = false;
this.localPlayerId = 0;
this.players = {};
this._handlers = {};
this._loginFailure = "";
this._connectResolve = null;
this._connectReject = null;
}
on(event, handler) {
if (!this._handlers[event]) this._handlers[event] = [];
this._handlers[event].push(handler);
}
_emit(event, ...args) {
for (const handler of this._handlers[event] || []) handler(...args);
}
isClientConnected() {
return this.localPlayerId !== 0;
}
defaultServerUrl() {
const location = window.location;
if (location.protocol === "http:" || location.protocol === "https:") {
const scheme = location.protocol === "https:" ? "wss" : "ws";
return `${scheme}://${location.host}${DEFAULT_PATH}`;
}
return `ws://127.0.0.1:${DEFAULT_PORT}${DEFAULT_PATH}`;
}
// Accepts a bare hostname or a full ws(s):// or http(s):// URL.
normalizeUrl(host, port = DEFAULT_PORT) {
let url = String(host || "").trim().replace(/\/+$/, "");
if (url.startsWith("https://")) url = "wss://" + url.slice("https://".length);
else if (url.startsWith("http://")) url = "ws://" + url.slice("http://".length);
else if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
url = `ws://${url}:${port}`;
}
const afterScheme = url.slice(url.indexOf("://") + 3);
if (!afterScheme.includes("/")) url += DEFAULT_PATH;
return url;
}
connect(url) {
this.close();
this._loginFailure = "";
this.localPlayerId = 0;
this.isAdmin = false;
return new Promise((resolve, reject) => {
let socket;
try {
socket = new WebSocket(url);
} catch (error) {
reject(error);
return;
}
this.socket = socket;
this._connectResolve = resolve;
this._connectReject = reject;
socket.onopen = () => {
if (this._connectResolve) {
this._connectResolve();
this._connectResolve = null;
this._connectReject = null;
}
};
socket.onerror = () => {
if (this._connectReject) {
this._connectReject(new Error("connection failed"));
this._connectResolve = null;
this._connectReject = null;
}
};
socket.onclose = () => {
const failure = this._loginFailure;
this._loginFailure = "";
this.localPlayerId = 0;
this.isAdmin = false;
if (this._connectReject) {
this._connectReject(new Error("connection closed"));
this._connectResolve = null;
this._connectReject = null;
return;
}
if (failure) this._emit("session_failed", failure);
else this._emit("server_disconnected");
};
socket.onmessage = (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch {
return;
}
this._handleMessage(message);
};
});
}
requestGameList(url) {
return this.connect(url).then(() => {
this._send({ t: "request_games" });
});
}
createGame(name, password) {
return this.connect(this.defaultServerUrl()).then(() => {
this._send({ t: "login", name, password });
});
}
joinGame(url, name, password) {
return this.connect(url).then(() => {
this._send({ t: "login", name, password });
});
}
sendGameSetup(setup) {
this._send({ t: "configure_game", setup });
}
sendOrder(order) {
this._send({ t: "order", order });
}
close() {
if (!this.socket) return;
const socket = this.socket;
this.socket = null;
socket.onopen = null;
socket.onerror = null;
socket.onclose = null;
socket.onmessage = null;
try {
socket.close();
} catch {
// ignore
}
}
_send(message) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
}
}
_handleMessage(message) {
switch (message.t) {
case "assigned":
this.localPlayerId = message.playerId;
this.isAdmin = !!message.admin;
this._emit("session_started", message);
break;
case "players":
this.players = message.players || {};
this._emit("players_updated", this.players);
break;
case "game_list":
this._emit("game_list_received", message.games || []);
break;
case "login_rejected":
this._loginFailure = message.reason || "Login failed.";
this._emit("session_failed", this._loginFailure);
this.close();
break;
case "game_configured":
this._emit("game_configured");
break;
case "game_state":
this._emit("game_state_received", message.state);
break;
default:
break;
}
}
}