aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
authorDavid Skoland <davidskoland@gmail.com>2026-03-24 12:23:45 +0100
committerDavid Skoland <davidskoland@gmail.com>2026-03-24 12:23:45 +0100
commit1a1ed4e1d0568a1610d5f5da3d541a59afe2b863 (patch)
tree6df185dcb2994767619d2dc32e45e27e3496aff3 /webAO
parent4715e7ccde04a77ff04f1ac839c151eaebc4ad44 (diff)
Add reconnect UI, disconnect button, and visual cleanup
- Redesign disconnect overlay as a full-screen modal with dark backdrop - Add working Reconnect button that properly re-establishes WebSocket connection - Add Disconnect button in Settings for testing - Separate disconnect and ban/kick codepaths (no reconnect on ban) - Log disconnect notice in IC log using hrtext style - Refactor area list rendering from client state (renderAreaList) - Extract appendICNotice for reusable IC log notices - Clean up charselect: hide during loading, simplify toolbar layout - Freshen loading screen and charselect styling - Remove loading progress text updates (just show "Loading...") - Guard against undefined client.chars and client.serv Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'webAO')
-rw-r--r--webAO/client.ts28
-rw-r--r--webAO/client/appendICNotice.ts10
-rw-r--r--webAO/client/createArea.ts25
-rw-r--r--webAO/client/fixLastArea.ts4
-rw-r--r--webAO/client/handleBans.ts2
-rw-r--r--webAO/client/sender/sendCharacter.ts2
-rw-r--r--webAO/dom/areaClick.ts7
-rw-r--r--webAO/dom/disconnectButton.ts12
-rw-r--r--webAO/dom/reconnectButton.ts22
-rw-r--r--webAO/dom/renderAreaList.ts24
-rw-r--r--webAO/dom/window.ts1
-rw-r--r--webAO/packets/handlers/handleARUP.ts15
-rw-r--r--webAO/packets/handlers/handleCI.ts2
-rw-r--r--webAO/packets/handlers/handleEI.ts2
-rw-r--r--webAO/packets/handlers/handleEM.ts1
-rw-r--r--webAO/packets/handlers/handleSM.ts3
-rw-r--r--webAO/styles/client.css94
17 files changed, 176 insertions, 78 deletions
diff --git a/webAO/client.ts b/webAO/client.ts
index 05a40c9..e00afe6 100644
--- a/webAO/client.ts
+++ b/webAO/client.ts
@@ -13,6 +13,8 @@ import { Viewport } from "./viewport/interfaces/Viewport";
import { EventEmitter } from "events";
import { onReplayGo } from "./dom/onReplayGo";
import { packetHandler } from "./packets/packetHandler";
+import { appendICNotice } from "./client/appendICNotice";
+import { renderAreaList } from "./dom/renderAreaList";
import { loadResources } from "./client/loadResources";
import { AO_HOST } from "./client/aoHost";
import {
@@ -111,6 +113,8 @@ export enum clientState {
Connected,
// Should be set once the client has joined the server (after handshake)
Joined,
+ // Set when a reconnect attempt is in progress
+ Reconnecting,
}
export let lastICMessageTime = new Date(0);
@@ -274,6 +278,10 @@ class Client extends EventEmitter {
*/
onOpen(_e: Event) {
client.state = clientState.Connected;
+ document.getElementById("client_error_overlay").style.display = "none";
+ document.getElementById("client_waiting").style.display = "block";
+ document.getElementById("client_loading").style.display = "block";
+ document.getElementById("client_charselect").style.display = "none";
client.joinServer();
}
@@ -282,19 +290,21 @@ class Client extends EventEmitter {
* @param {CloseEvent} e
*/
onClose(e: CloseEvent) {
- client.state = clientState.NotConnected;
console.error(`The connection was closed: ${e.reason} (${e.code})`);
+ if (this.state === clientState.Reconnecting) return;
+ client.state = clientState.NotConnected;
if (this.banned === false) {
if (this.areas.length > 0) {
document.getElementById("client_errortext").textContent =
"You were disconnected from the server.";
+ appendICNotice("Disconnected from the server.");
} else {
document.getElementById("client_errortext").textContent =
"Could not connect to the server.";
}
+ (<HTMLElement>document.getElementById("client_reconnect")).style.display = "";
}
- document.getElementById("client_waiting").style.display = "block";
- document.getElementById("client_error").style.display = "flex";
+ document.getElementById("client_error_overlay").style.display = "flex";
document.getElementById("client_loading").style.display = "none";
document.getElementById("error_id").textContent = String(e.code);
this.cleanup();
@@ -364,10 +374,14 @@ class Client extends EventEmitter {
* @param {ErrorEvent} e
*/
onError(e: ErrorEvent) {
- client.state = clientState.NotConnected;
console.error(`A network error occurred`);
console.error(e);
- document.getElementById("client_error").style.display = "flex";
+ if (this.state === clientState.Reconnecting) return;
+ client.state = clientState.NotConnected;
+ document.getElementById("client_errortext").textContent =
+ "Could not connect to the server.";
+ (<HTMLElement>document.getElementById("client_reconnect")).style.display = "";
+ document.getElementById("client_error_overlay").style.display = "flex";
this.cleanup();
}
@@ -376,7 +390,7 @@ class Client extends EventEmitter {
*/
cleanup() {
clearInterval(this.checkUpdater);
- this.serv.close();
+ if (this.serv) this.serv.close();
}
/**
@@ -412,7 +426,7 @@ class Client extends EventEmitter {
resetAreaList() {
this.areas = [];
- document.getElementById("areas").innerHTML = "";
+ renderAreaList();
fetchBackgroundList();
fetchEvidenceList();
fetchCharacterList();
diff --git a/webAO/client/appendICNotice.ts b/webAO/client/appendICNotice.ts
new file mode 100644
index 0000000..29065db
--- /dev/null
+++ b/webAO/client/appendICNotice.ts
@@ -0,0 +1,10 @@
+/**
+ * Appends a notice (hrtext divider) to the IC log.
+ * @param {string} msg the notice text
+ */
+export function appendICNotice(msg: string) {
+ const el = document.createElement("div");
+ el.className = "hrtext";
+ el.textContent = msg;
+ document.getElementById("client_log")!.appendChild(el);
+}
diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts
index 9a40bef..dfc57e8 100644
--- a/webAO/client/createArea.ts
+++ b/webAO/client/createArea.ts
@@ -1,32 +1,15 @@
import { client } from "../client";
-import { area_click } from "../dom/areaClick";
+import { renderAreaList } from "../dom/renderAreaList";
import { safeTags } from "../encoding";
export const createArea = (id: number, aname: string) => {
const name = safeTags(aname);
- const thisarea = {
+ client.areas.push({
name,
players: 0,
status: "IDLE",
cm: "",
locked: "FREE",
- };
-
- client.areas.push(thisarea);
-
- // Create area button
- const newarea = document.createElement("SPAN");
- newarea.className = "area-button area-default";
- newarea.id = `area${id}`;
- newarea.innerText = thisarea.name;
- newarea.title =
- `Players: ${thisarea.players}\n` +
- `Status: ${thisarea.status}\n` +
- `CM: ${thisarea.cm}\n` +
- `Area lock: ${thisarea.locked}`;
- newarea.onclick = function () {
- area_click(newarea);
- };
-
- document.getElementById("areas")!.appendChild(newarea);
+ });
+ renderAreaList();
};
diff --git a/webAO/client/fixLastArea.ts b/webAO/client/fixLastArea.ts
index a9979da..839b14c 100644
--- a/webAO/client/fixLastArea.ts
+++ b/webAO/client/fixLastArea.ts
@@ -1,4 +1,5 @@
import { client } from "../client";
+import { renderAreaList } from "../dom/renderAreaList";
import { addTrack } from "./addTrack";
/**
@@ -7,8 +8,7 @@ import { addTrack } from "./addTrack";
export const fix_last_area = () => {
if (client.areas.length > 0) {
const malplaced = client.areas.pop().name;
- const areas = document.getElementById("areas")!;
- areas.removeChild(areas.lastChild);
+ renderAreaList();
addTrack(malplaced);
}
};
diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts
index 9eec9be..cf7f881 100644
--- a/webAO/client/handleBans.ts
+++ b/webAO/client/handleBans.ts
@@ -6,7 +6,7 @@ import { safeTags } from "../encoding";
* @param {string} reason why
*/
export const handleBans = (type: string, reason: string) => {
- document.getElementById("client_error")!.style.display = "flex";
+ document.getElementById("client_error_overlay")!.style.display = "flex";
document.getElementById("client_errortext")!.innerHTML =
`${type}:<br>${safeTags(reason).replace(/\n/g, "<br />")}`;
(<HTMLElement>document.getElementById("client_reconnect")).style.display =
diff --git a/webAO/client/sender/sendCharacter.ts b/webAO/client/sender/sendCharacter.ts
index 2db4dcd..eed6a99 100644
--- a/webAO/client/sender/sendCharacter.ts
+++ b/webAO/client/sender/sendCharacter.ts
@@ -5,7 +5,7 @@ import { client } from "../../client";
* @param {number} character the character ID
*/
export const sendCharacter = (character: number) => {
- if (character === -1 || client.chars[character].name) {
+ if (character === -1 || (client.chars[character] && client.chars[character].name)) {
client.sender.sendServer(`CC#${client.playerID}#${character}#web#%`);
}
};
diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts
index 27682c7..120ef39 100644
--- a/webAO/dom/areaClick.ts
+++ b/webAO/dom/areaClick.ts
@@ -1,4 +1,5 @@
import { client } from "../client";
+import { appendICNotice } from "../client/appendICNotice";
import { renderPlayerList } from "./renderPlayerList";
/**
* Triggered when an item on the area list is clicked.
@@ -7,11 +8,7 @@ import { renderPlayerList } from "./renderPlayerList";
export function area_click(el: HTMLElement) {
const area = client.areas[el.id.substring(4)].name;
client.sender.sendMusicChange(area);
-
- const areaHr = document.createElement("div");
- areaHr.className = "hrtext";
- areaHr.textContent = `switched to ${el.textContent}`;
- document.getElementById("client_log")!.appendChild(areaHr);
+ appendICNotice(`switched to ${el.textContent}`);
client.area = Number(el.id.substring(4));
renderPlayerList();
}
diff --git a/webAO/dom/disconnectButton.ts b/webAO/dom/disconnectButton.ts
new file mode 100644
index 0000000..35daf16
--- /dev/null
+++ b/webAO/dom/disconnectButton.ts
@@ -0,0 +1,12 @@
+import { client } from "../client";
+
+/**
+ * Triggered when the disconnect button in settings is pushed.
+ * Forces a disconnection for testing purposes.
+ */
+export function DisconnectButton() {
+ if (client.serv && client.serv.readyState === WebSocket.OPEN) {
+ client.serv.close();
+ }
+}
+window.DisconnectButton = DisconnectButton;
diff --git a/webAO/dom/reconnectButton.ts b/webAO/dom/reconnectButton.ts
index 079e7fc..ae492fb 100644
--- a/webAO/dom/reconnectButton.ts
+++ b/webAO/dom/reconnectButton.ts
@@ -1,16 +1,26 @@
-import Client, { client, setClient } from "../client";
+import Client, { client, clientState, setClient } from "../client";
import queryParser from "../utils/queryParser";
-const { ip: serverIP } = queryParser();
+const { ip: serverIP, connect } = queryParser();
/**
* Triggered when the reconnect button is pushed.
*/
export function ReconnectButton() {
- client.cleanup();
- setClient(new Client(serverIP));
+ document.getElementById("client_errortext")!.textContent = "Reconnecting...";
- if (client) {
- document.getElementById("client_error")!.style.display = "none";
+ // Build the connection string the same way the initial connection does
+ let connectionString = connect;
+ if (!connectionString && serverIP) {
+ connectionString = `ws://${serverIP}`;
}
+
+ const hdid = client.hdid;
+ client.state = clientState.Reconnecting;
+ client.cleanup();
+
+ const newClient = new Client(connectionString);
+ setClient(newClient);
+ newClient.hdid = hdid;
+ newClient.connect();
}
window.ReconnectButton = ReconnectButton;
diff --git a/webAO/dom/renderAreaList.ts b/webAO/dom/renderAreaList.ts
new file mode 100644
index 0000000..e622765
--- /dev/null
+++ b/webAO/dom/renderAreaList.ts
@@ -0,0 +1,24 @@
+import { client } from "../client";
+import { area_click } from "./areaClick";
+
+export function renderAreaList() {
+ const container = document.getElementById("areas")!;
+ container.innerHTML = "";
+
+ for (let i = 0; i < client.areas.length; i++) {
+ const area = client.areas[i];
+ const el = document.createElement("SPAN");
+ el.className = `area-button area-${area.status.toLowerCase()}`;
+ el.id = `area${i}`;
+ el.innerText = `${area.name} (${area.players}) [${area.status}]`;
+ el.title =
+ `Players: ${area.players}\n` +
+ `Status: ${area.status}\n` +
+ `CM: ${area.cm}\n` +
+ `Area lock: ${area.locked}`;
+ el.onclick = function () {
+ area_click(el);
+ };
+ container.appendChild(el);
+ }
+}
diff --git a/webAO/dom/window.ts b/webAO/dom/window.ts
index ee1b121..48d0714 100644
--- a/webAO/dom/window.ts
+++ b/webAO/dom/window.ts
@@ -30,6 +30,7 @@ declare global {
pickChar: (ccharacter: any) => void;
chartable_filter: (_event: any) => void;
ReconnectButton: (_event: any) => void;
+ DisconnectButton: () => void;
opusCheck: (channel: HTMLAudioElement) => OnErrorEventHandlerNonNull;
imgError: (image: any) => void;
charError: (image: any) => void;
diff --git a/webAO/packets/handlers/handleARUP.ts b/webAO/packets/handlers/handleARUP.ts
index 7f72b15..5a7aebb 100644
--- a/webAO/packets/handlers/handleARUP.ts
+++ b/webAO/packets/handlers/handleARUP.ts
@@ -1,4 +1,5 @@
import { client } from "../../client";
+import { renderAreaList } from "../../dom/renderAreaList";
import { safeTags } from "../../encoding";
/**
@@ -10,7 +11,6 @@ export const handleARUP = (args: string[]) => {
for (let i = 0; i < args.length - 1; i++) {
if (client.areas[i]) {
// the server sends us ARUP before we even get the area list
- const thisarea = document.getElementById(`area${i}`)!;
switch (Number(args[0])) {
case 0: // playercount
client.areas[i].players = Number(args[i + 1]);
@@ -25,18 +25,7 @@ export const handleARUP = (args: string[]) => {
client.areas[i].locked = safeTags(args[i + 1]);
break;
}
-
- thisarea.className = `area-button area-${client.areas[
- i
- ].status.toLowerCase()}`;
-
- thisarea.innerText = `${client.areas[i].name} (${client.areas[i].players}) [${client.areas[i].status}]`;
-
- thisarea.title =
- `Players: ${client.areas[i].players}\n` +
- `Status: ${client.areas[i].status}\n` +
- `CM: ${client.areas[i].cm}\n` +
- `Area lock: ${client.areas[i].locked}`;
}
}
+ renderAreaList();
};
diff --git a/webAO/packets/handlers/handleCI.ts b/webAO/packets/handlers/handleCI.ts
index c0cbd84..58a6dad 100644
--- a/webAO/packets/handlers/handleCI.ts
+++ b/webAO/packets/handlers/handleCI.ts
@@ -8,8 +8,6 @@ import { handleCharacterInfo } from "../../client/handleCharacterInfo";
*/
export const handleCI = (args: string[]) => {
// Loop through the 10 characters that were sent
- document.getElementById("client_loadingtext")!.innerHTML =
- `Loading Character ${args[1]}/${client.char_list_length}`;
for (let i = 2; i <= args.length - 2; i++) {
if (i % 2 === 0) {
const chargs = args[i].split("&");
diff --git a/webAO/packets/handlers/handleEI.ts b/webAO/packets/handlers/handleEI.ts
index 3d15766..6602214 100644
--- a/webAO/packets/handlers/handleEI.ts
+++ b/webAO/packets/handlers/handleEI.ts
@@ -11,8 +11,6 @@ import { prepChat } from "../../encoding";
* @param {Array} args packet arguments
*/
export const handleEI = (args: string[]) => {
- document.getElementById("client_loadingtext")!.innerHTML =
- `Loading Evidence ${args[1]}/${client.evidence_list_length}`;
const evidenceID = Number(args[1]);
const arg = args[2].split("&");
client.evidences[evidenceID] = {
diff --git a/webAO/packets/handlers/handleEM.ts b/webAO/packets/handlers/handleEM.ts
index b3947dd..94d224c 100644
--- a/webAO/packets/handlers/handleEM.ts
+++ b/webAO/packets/handlers/handleEM.ts
@@ -10,7 +10,6 @@ import { isAudio } from "../../client/isAudio";
* @param {Array} args packet arguments
*/
export const handleEM = (args: string[]) => {
- document.getElementById("client_loadingtext")!.innerHTML = "Loading Music";
if (args[1] === "0") {
client.resetMusicList();
client.resetAreaList();
diff --git a/webAO/packets/handlers/handleSM.ts b/webAO/packets/handlers/handleSM.ts
index 1c3fd5e..17453c1 100644
--- a/webAO/packets/handlers/handleSM.ts
+++ b/webAO/packets/handlers/handleSM.ts
@@ -8,14 +8,11 @@ import { createArea } from "../../client/createArea";
* @param {Array} args packet arguments
*/
export const handleSM = (args: string[]) => {
- document.getElementById("client_loadingtext")!.innerHTML = "Loading Music ";
client.resetMusicList();
client.resetAreaList();
client.musics_time = false;
- document.getElementById("client_loadingtext")!.innerHTML = `Loading Music`;
-
for (let i = 1; i < args.length - 1; i++) {
// Check when found the song for the first time
const trackname = args[i];
diff --git a/webAO/styles/client.css b/webAO/styles/client.css
index e3ee820..773e9aa 100644
--- a/webAO/styles/client.css
+++ b/webAO/styles/client.css
@@ -58,25 +58,75 @@
}
}
+#client_error_overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 200;
+}
+
#client_error {
- position: absolute;
display: flex;
flex-direction: column;
- padding: 10px;
- top: 50%;
- left: 50%;
- margin-right: -50%;
- transform: translate(-50%, -50%);
+ padding: 24px 36px;
justify-content: center;
align-items: center;
- background: #a00;
+ background: rgba(0, 0, 0, 0.9);
+ border: 2px solid #c00;
+ border-radius: 8px;
color: #fff;
font-size: large;
- z-index: 100;
+ min-width: 280px;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
+}
+
+#client_error_icon {
+ font-size: 48px;
+ color: #f44;
+ margin-bottom: 4px;
}
#client_errortext {
- animation: error_blink 3s ease-in-out infinite;
+ margin: 4px 0 8px;
+ text-align: center;
+}
+
+#client_error_code {
+ color: #999;
+ font-size: 12px;
+ margin: 0 0 16px;
+}
+
+#client_reconnect {
+ background: #c00;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ padding: 10px 32px;
+ font-size: 16px;
+ cursor: pointer;
+ margin-bottom: 12px;
+ transition: background 0.2s;
+}
+
+#client_reconnect:hover {
+ background: #e22;
+}
+
+#client_error_help {
+ color: #aaa;
+ font-size: 12px;
+ margin: 0;
+}
+
+#client_error_help a {
+ color: #6af;
}
#client_secondfactor {
@@ -102,10 +152,12 @@
justify-content: center;
text-align: center;
align-items: center;
+ box-sizing: border-box;
font-size: large;
- overflow-y: scroll;
+ overflow-y: auto;
z-index: 100;
- background: #555;
+ background: #333;
+ border: 1px solid #000;
}
#client_loading {
@@ -114,8 +166,7 @@
justify-content: center;
text-align: center;
align-items: center;
- background: black;
- color: lightgreen;
+ color: #fff;
font-size: large;
}
@@ -142,7 +193,22 @@
display: block;
text-align: center;
margin: 0 auto;
- background: #444;
+ padding: 10px;
+ background: #333;
+ color: #eee;
+}
+
+#client_charselect_toolbar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 6px;
+ margin-top: 5px;
+ margin-bottom: 10px;
+}
+
+#client_charactersearch {
+ width: 150px;
}
#client_icwrapper {