From 4ab187b991ec40993c4b030e1612d9bb41f18924 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 13:04:31 +0100 Subject: Defer char.ini loading and use direct img src for char icons Instead of eagerly fetching char_icon (with HEAD requests per extension) and char.ini for every character on join, set img.src directly to char_icon.png and defer char.ini loading until actually needed (character selection via handlePV, or first IC message via handleMS). This eliminates thousands of HTTP requests on join for large character lists. Co-Authored-By: Claude Opus 4.6 --- webAO/client/handleCharacterInfo.ts | 174 +++++++++++++++++++++--------------- webAO/packets/handlers/handleMS.ts | 5 +- webAO/packets/handlers/handlePU.ts | 6 +- webAO/packets/handlers/handlePV.ts | 3 +- webAO/packets/handlers/handleSC.ts | 7 +- webAO/packets/handlers/handleSI.ts | 1 + 6 files changed, 116 insertions(+), 80 deletions(-) (limited to 'webAO') diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts index 3764c84..0235f3b 100644 --- a/webAO/client/handleCharacterInfo.ts +++ b/webAO/client/handleCharacterInfo.ts @@ -2,50 +2,21 @@ import { client } from "../client"; import { safeTags } from "../encoding"; import iniParse from "../iniParse"; import request from "../services/request"; -import fileExists from "../utils/fileExists"; import { AO_HOST } from "./aoHost"; -export const getCharIcon = async (img: HTMLImageElement, charname: string) => { - img.alt = charname; - const charIconBaseUrl = `${AO_HOST}characters/${encodeURI( - charname.toLowerCase(), - )}/char_icon`; - for (let i = 0; i < client.charicon_extensions.length; i++) { - const fileUrl = charIconBaseUrl + client.charicon_extensions[i]; - const exists = await fileExists(fileUrl); - if (exists) { - img.alt = charname; - img.title = charname; - img.src = fileUrl; - return; - } - } -}; - /** - * Handles the incoming character information, and downloads the sprite + ini for it - * @param {Array} chargs packet arguments - * @param {Number} charid character ID + * Lightweight character setup that runs on join. Sets the icon src directly + * (letting the browser handle loading) and stores default character data. + * Does NOT fetch char.ini — that is deferred until needed via ensureCharIni. */ -export const handleCharacterInfo = async (chargs: string[], charid: number) => { +export const setupCharacterBasic = (chargs: string[], charid: number) => { const img = document.getElementById(`demo_${charid}`); if (chargs[0]) { - let cini: any = {}; - - getCharIcon(img, chargs[0]); - - // If the ini doesn't exist on the server this will throw an error - try { - const cinidata = await request( - `${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini`, - ); - cini = iniParse(cinidata); - } catch (err) { - cini = {}; - img.classList.add("noini"); - console.warn(`character ${chargs[0]} is missing from webAO`); - // If it does, give the user a visual indication that the character is unusable - } + img.alt = chargs[0]; + img.title = chargs[0]; + img.src = `${AO_HOST}characters/${encodeURI( + chargs[0].toLowerCase(), + )}/char_icon.png`; const mute_select = ( document.getElementById("mute_select") @@ -56,47 +27,108 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => { ); pair_select.add(new Option(safeTags(chargs[0]), String(charid))); - // sometimes ini files lack important settings - const default_options = { - name: chargs[0], - showname: chargs[0], - side: "def", - blips: "male", - chat: "", - category: "", - }; - cini.options = Object.assign(default_options, cini.options); - - // sometimes ini files lack important settings - const default_emotions = { - number: 0, - }; - cini.emotions = Object.assign(default_emotions, cini.emotions); - + // Store defaults — these get replaced with actual ini values by ensureCharIni client.chars[charid] = { name: safeTags(chargs[0]), - showname: safeTags(cini.options.showname), + showname: safeTags(chargs[0]), desc: safeTags(chargs[1]), - blips: safeTags(cini.options.blips).toLowerCase(), - gender: safeTags(cini.options.gender).toLowerCase(), - side: safeTags(cini.options.side).toLowerCase(), - chat: - cini.options.chat === "" - ? safeTags(cini.options.category).toLowerCase() - : safeTags(cini.options.chat).toLowerCase(), + blips: "male", + gender: "", + side: "def", + chat: "", evidence: chargs[3], - icon: img.src, - inifile: cini, + icon: "", muted: false, }; + } else { + console.warn(`missing charid ${charid}`); + img.style.display = "none"; + } +}; - if ( - client.chars[charid].blips === "male" && - client.chars[charid].gender !== "male" && - client.chars[charid].gender !== "" - ) { - client.chars[charid].blips = client.chars[charid].gender; +/** + * Fetches and parses char.ini for a character if not already loaded. + * Replaces default values in client.chars[charid] with actual ini values. + */ +export const ensureCharIni = async (charid: number): Promise => { + const char = client.chars[charid]; + if (!char) return {}; + if (char.inifile) return char.inifile; + + const img = document.getElementById(`demo_${charid}`); + let cini: any = {}; + + try { + const cinidata = await request( + `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char.ini`, + ); + cini = iniParse(cinidata); + } catch (err) { + cini = {}; + if (img) img.classList.add("noini"); + console.warn(`character ${char.name} is missing from webAO`); + } + + const default_options = { + name: char.name, + showname: char.name, + side: "def", + blips: "male", + chat: "", + category: "", + }; + cini.options = Object.assign(default_options, cini.options); + + const default_emotions = { + number: 0, + }; + cini.emotions = Object.assign(default_emotions, cini.emotions); + + // Replace defaults with actual ini values + char.showname = safeTags(cini.options.showname); + char.blips = safeTags(cini.options.blips).toLowerCase(); + char.gender = safeTags(cini.options.gender).toLowerCase(); + char.side = safeTags(cini.options.side).toLowerCase(); + char.chat = + cini.options.chat === "" + ? safeTags(cini.options.category).toLowerCase() + : safeTags(cini.options.chat).toLowerCase(); + char.icon = img ? img.src : ""; + char.inifile = cini; + + if ( + char.blips === "male" && + char.gender !== "male" && + char.gender !== "" + ) { + char.blips = char.gender; + } + + return cini; +}; + +/** + * Full character info load (used by iniEdit and handleMS ini-edit path). + * Fetches icon + ini for a single character, replacing any existing data. + */ +export const handleCharacterInfo = async (chargs: string[], charid: number) => { + const img = document.getElementById(`demo_${charid}`); + if (chargs[0]) { + img.alt = chargs[0]; + img.title = chargs[0]; + img.src = `${AO_HOST}characters/${encodeURI( + chargs[0].toLowerCase(), + )}/char_icon.png`; + + // Reset inifile so ensureCharIni will re-fetch + if (client.chars[charid]) { + client.chars[charid].name = safeTags(chargs[0]); + client.chars[charid].inifile = null; + } else { + setupCharacterBasic(chargs, charid); } + + await ensureCharIni(charid); } else { console.warn(`missing charid ${charid}`); img.style.display = "none"; diff --git a/webAO/packets/handlers/handleMS.ts b/webAO/packets/handlers/handleMS.ts index 2622fe6..9b46bc0 100644 --- a/webAO/packets/handlers/handleMS.ts +++ b/webAO/packets/handlers/handleMS.ts @@ -1,7 +1,7 @@ /* eslint indent: ["error", 2, { "SwitchCase": 1 }] */ import { client, extrafeatures, UPDATE_INTERVAL } from "../../client"; -import { handleCharacterInfo } from "../../client/handleCharacterInfo"; +import { handleCharacterInfo, ensureCharIni } from "../../client/handleCharacterInfo"; import { resetICParams } from "../../client/resetICParams"; import { prepChat, safeTags } from "../../encoding"; import { handle_ic_speaking } from "../../viewport/utils/handleICSpeaking"; @@ -27,6 +27,9 @@ export const handleMS = (args: string[]) => { ); const chargs = (`${char_name}&` + "iniediter").split("&"); handleCharacterInfo(chargs, char_id); + } else if (!client.chars[char_id].inifile) { + // Lazily load char.ini in background so future messages have proper data + ensureCharIni(char_id); } } diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 508bb51..18e508e 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,6 +1,6 @@ import { client } from "../../client"; -import { getCharIcon } from "../../client/handleCharacterInfo"; import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; +import { AO_HOST } from "../../client/aoHost"; /** * Handles a playerlist update @@ -19,7 +19,9 @@ export const handlePU = (args: string[]) => { break; case 1: const playerImg = playerRow.childNodes[0].firstChild; - getCharIcon(playerImg, data); + playerImg.alt = data; + playerImg.title = data; + playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon.png`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; break; diff --git a/webAO/packets/handlers/handlePV.ts b/webAO/packets/handlers/handlePV.ts index 4ac747f..149d90e 100644 --- a/webAO/packets/handlers/handlePV.ts +++ b/webAO/packets/handlers/handlePV.ts @@ -3,6 +3,7 @@ import fileExists from "../../utils/fileExists"; import { updateActionCommands } from "../../dom/updateActionCommands"; import { pickEmotion } from "../../dom/pickEmotion"; import { AO_HOST } from "../../client/aoHost"; +import { ensureCharIni } from "../../client/handleCharacterInfo"; function addEmoteButton(i: number, imgurl: string, desc: string) { const emotesList = document.getElementById("client_emo"); @@ -34,7 +35,7 @@ export const handlePV = async (args: string[]) => { const emotesList = document.getElementById("client_emo"); emotesList.style.display = ""; emotesList.innerHTML = ""; // Clear emote box - const ini = me.inifile; + const ini = await ensureCharIni(client.charID); me.side = ini.options.side; updateActionCommands(me.side); if (ini.emotions.number === 0) { diff --git a/webAO/packets/handlers/handleSC.ts b/webAO/packets/handlers/handleSC.ts index 271a164..f7b789f 100644 --- a/webAO/packets/handlers/handleSC.ts +++ b/webAO/packets/handlers/handleSC.ts @@ -1,7 +1,7 @@ import queryParser from "../../utils/queryParser"; import { client } from "../../client"; -import { handleCharacterInfo } from "../../client/handleCharacterInfo"; +import { setupCharacterBasic } from "../../client/handleCharacterInfo"; const { mode } = queryParser(); /** @@ -17,13 +17,10 @@ export const handleSC = async (args: string[]) => { document.getElementById("client_charselect")!.style.display = "block"; } - document.getElementById("client_loadingtext")!.innerHTML = - "Loading Characters"; for (let i = 1; i < args.length; i++) { const chargs = args[i].split("&"); const charid = i - 1; - - setTimeout(() => handleCharacterInfo(chargs, charid), charid * 6); + setupCharacterBasic(chargs, charid); } // We're done with the characters, request the music client.sender.sendServer("RM#%"); diff --git a/webAO/packets/handlers/handleSI.ts b/webAO/packets/handlers/handleSI.ts index f20f4b2..eac84e0 100644 --- a/webAO/packets/handlers/handleSI.ts +++ b/webAO/packets/handlers/handleSI.ts @@ -20,6 +20,7 @@ export const handleSI = (args: string[]) => { const demothing = document.createElement("img"); demothing.className = "demothing"; + demothing.loading = "lazy"; demothing.id = `demo_${i}`; const demoonclick = document.createAttribute("onclick"); demoonclick.value = `pickChar(${i})`; -- cgit From 0c76b200cc68c59772df930acd34a58bd6272c7f Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 13:10:27 +0100 Subject: Catch rejected play() promises from browser autoplay policy Browsers reject .play() with a DOMException when the user hasn't interacted with the document yet. Add .catch(() => {}) to all 9 play() call sites to suppress the uncaught promise rejection. Co-Authored-By: Claude Opus 4.6 --- webAO/client/checkCallword.ts | 2 +- webAO/packets/handlers/handleMC.ts | 2 +- webAO/packets/handlers/handleRMC.ts | 2 +- webAO/packets/handlers/handleZZ.ts | 2 +- webAO/viewport/utils/handleICSpeaking.ts | 2 +- webAO/viewport/utils/initTestimonyUpdater.ts | 2 +- webAO/viewport/viewport.ts | 6 +++--- 7 files changed, 9 insertions(+), 9 deletions(-) (limited to 'webAO') diff --git a/webAO/client/checkCallword.ts b/webAO/client/checkCallword.ts index b7413f7..795eac9 100644 --- a/webAO/client/checkCallword.ts +++ b/webAO/client/checkCallword.ts @@ -11,7 +11,7 @@ export function checkCallword(message: string, sfxAudio: HTMLAudioElement) { if (item !== "" && message.toLowerCase().includes(item.toLowerCase())) { sfxAudio.pause(); sfxAudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - sfxAudio.play(); + sfxAudio.play().catch(() => {}); } } } diff --git a/webAO/packets/handlers/handleMC.ts b/webAO/packets/handlers/handleMC.ts index 8a1c0a2..44237ea 100644 --- a/webAO/packets/handlers/handleMC.ts +++ b/webAO/packets/handlers/handleMC.ts @@ -24,7 +24,7 @@ export const handleMC = (args: string[]) => { music.src = `${AO_HOST}sounds/music/${encodeURI(track.toLowerCase())}`; } music.loop = looping; - music.play(); + music.play().catch(() => {}); try { musicname = client.chars[charID].name; diff --git a/webAO/packets/handlers/handleRMC.ts b/webAO/packets/handlers/handleRMC.ts index 7758844..0cc6aa7 100644 --- a/webAO/packets/handlers/handleRMC.ts +++ b/webAO/packets/handlers/handleRMC.ts @@ -17,7 +17,7 @@ export const handleRMC = (args: string[]) => { music.currentTime += parseFloat( music.totime + (new Date().getTime() / 1000 - music.offset), ).toFixed(3); - music.play(); + music.play().catch(() => {}); }, false, ); diff --git a/webAO/packets/handlers/handleZZ.ts b/webAO/packets/handlers/handleZZ.ts index 31ceef5..0496d42 100644 --- a/webAO/packets/handlers/handleZZ.ts +++ b/webAO/packets/handlers/handleZZ.ts @@ -18,6 +18,6 @@ export const handleZZ = (args: string[]) => { const oldvolume = client.viewport.getSfxAudio().volume; client.viewport.getSfxAudio().volume = 1; client.viewport.getSfxAudio().src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - client.viewport.getSfxAudio().play(); + client.viewport.getSfxAudio().play().catch(() => {}); client.viewport.getSfxAudio().volume = oldvolume; }; diff --git a/webAO/viewport/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts index 6cf665c..ec791c5 100644 --- a/webAO/viewport/utils/handleICSpeaking.ts +++ b/webAO/viewport/utils/handleICSpeaking.ts @@ -169,7 +169,7 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => { )}/${shout}.opus`; const exists = await fileExists(perCharPath); client.viewport.shoutaudio.src = exists ? perCharPath : client.resources[shout].sfx; - client.viewport.shoutaudio.play(); + client.viewport.shoutaudio.play().catch(() => {}); client.viewport.setShoutTimer(client.resources[shout].duration); } else { client.viewport.setShoutTimer(0); diff --git a/webAO/viewport/utils/initTestimonyUpdater.ts b/webAO/viewport/utils/initTestimonyUpdater.ts index c1e4d93..a545c97 100644 --- a/webAO/viewport/utils/initTestimonyUpdater.ts +++ b/webAO/viewport/utils/initTestimonyUpdater.ts @@ -18,7 +18,7 @@ export const initTestimonyUpdater = () => { } client.viewport.testimonyAudio.src = client.resources[testimony].sfx; - client.viewport.testimonyAudio.play(); + client.viewport.testimonyAudio.play().catch(() => {}); const testimonyOverlay = ( document.getElementById("client_testimony") diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts index c03d750..a7363a9 100644 --- a/webAO/viewport/viewport.ts +++ b/webAO/viewport/viewport.ts @@ -104,7 +104,7 @@ const viewport = (): Viewport => { sfxAudio.pause(); sfxAudio.loop = looping; sfxAudio.src = sfxname; - sfxAudio.play(); + sfxAudio.play().catch(() => {}); }; /** * Updates the testimony overaly @@ -150,7 +150,7 @@ const viewport = (): Viewport => { const charEmote = chatmsg.sprite.toLowerCase(); if (chatmsg.content.charAt(textnow.length) !== " ") { - blipChannels[currentBlipChannel].play(); + blipChannels[currentBlipChannel].play().catch(() => {}); currentBlipChannel++; currentBlipChannel %= blipChannels.length; } @@ -356,7 +356,7 @@ const viewport = (): Viewport => { eviBox.style.opacity = "1"; testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`; - testimonyAudio.play(); + testimonyAudio.play().catch(() => {}); if (chatmsg.side === "def") { // Only def show evidence on right -- cgit From 5eeb7ac9d90137c3b5ce9578c47bcc2ccff21c7e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 21:10:00 +0100 Subject: Use charicon_extensions from extensions.json for char icon URLs Instead of hardcoding .png, read the preferred extension from client.charicon_extensions[0] (populated via extensions.json), falling back to .png if unavailable. Co-Authored-By: Claude Opus 4.6 --- webAO/client/handleCharacterInfo.ts | 6 ++++-- webAO/packets/handlers/handlePU.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'webAO') diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts index 0235f3b..28992e9 100644 --- a/webAO/client/handleCharacterInfo.ts +++ b/webAO/client/handleCharacterInfo.ts @@ -14,9 +14,10 @@ export const setupCharacterBasic = (chargs: string[], charid: number) => { if (chargs[0]) { img.alt = chargs[0]; img.title = chargs[0]; + const iconExt = client.charicon_extensions[0] || ".png"; img.src = `${AO_HOST}characters/${encodeURI( chargs[0].toLowerCase(), - )}/char_icon.png`; + )}/char_icon${iconExt}`; const mute_select = ( document.getElementById("mute_select") @@ -116,9 +117,10 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => { if (chargs[0]) { img.alt = chargs[0]; img.title = chargs[0]; + const iconExt = client.charicon_extensions[0] || ".png"; img.src = `${AO_HOST}characters/${encodeURI( chargs[0].toLowerCase(), - )}/char_icon.png`; + )}/char_icon${iconExt}`; // Reset inifile so ensureCharIni will re-fetch if (client.chars[charid]) { diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 18e508e..0f51029 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -21,7 +21,8 @@ export const handlePU = (args: string[]) => { const playerImg = playerRow.childNodes[0].firstChild; playerImg.alt = data; playerImg.title = data; - playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon.png`; + const iconExt = client.charicon_extensions[0] || ".png"; + playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; break; -- cgit From 95e4124b0c02b4e5be383c41ed4241566f40e6e6 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 21:53:59 +0100 Subject: Prefetch char.ini for characters present in area via playerlist Store player data (charId, area) in an in-memory Map on the client, updated by PR/PU packet handlers. Use this to eagerly load char.ini when a player's character appears in our area or when switching areas, eliminating the lazy-load delay on first IC message. Co-Authored-By: Claude Opus 4.6 --- webAO/client.ts | 2 ++ webAO/dom/areaClick.ts | 8 ++++++++ webAO/packets/handlers/handlePR.ts | 9 +++++++-- webAO/packets/handlers/handlePU.ts | 20 ++++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) (limited to 'webAO') diff --git a/webAO/client.ts b/webAO/client.ts index b7a15a4..95df67a 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -150,6 +150,7 @@ class Client extends EventEmitter { connect: () => void; loadResources: () => void; isLowMemory: () => void; + players: Map; charicon_extensions: string[]; emote_extensions: string[]; emotions_extensions: string[]; @@ -211,6 +212,7 @@ class Client extends EventEmitter { this.temp_packet = ""; loadResources; isLowMemory; + this.players = new Map(); this.charicon_extensions = [".png", ".webp"]; this.emote_extensions = [".gif", ".png", ".apng", ".webp", ".webp.static"]; this.emotions_extensions = [".png", ".webp"]; diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts index 1e41f4b..19953a5 100644 --- a/webAO/dom/areaClick.ts +++ b/webAO/dom/areaClick.ts @@ -1,5 +1,6 @@ import { client } from "../client"; import { updatePlayerAreas } from "./updatePlayerAreas"; +import { ensureCharIni } from "../client/handleCharacterInfo"; /** * Triggered when an item on the area list is clicked. * @param {HTMLElement} el @@ -14,5 +15,12 @@ export function area_click(el: HTMLElement) { document.getElementById("client_log")!.appendChild(areaHr); client.area = Number(el.id.substring(4)); updatePlayerAreas(client.area); + + // Prefetch char.ini for all characters present in the new area + for (const player of client.players.values()) { + if (player.area === client.area && player.charId >= 0) { + ensureCharIni(player.charId); + } + } } window.area_click = area_click; diff --git a/webAO/packets/handlers/handlePR.ts b/webAO/packets/handlers/handlePR.ts index aeb3969..e39103d 100644 --- a/webAO/packets/handlers/handlePR.ts +++ b/webAO/packets/handlers/handlePR.ts @@ -53,6 +53,11 @@ function removePlayer(playerID: number) { */ export const handlePR = (args: string[]) => { const playerID = Number(args[1]); - if (Number(args[2]) === 0) addPlayer(playerID); - else if (Number(args[2]) === 1) removePlayer(playerID); + if (Number(args[2]) === 0) { + addPlayer(playerID); + client.players.set(playerID, { charId: -1, area: 0 }); + } else if (Number(args[2]) === 1) { + removePlayer(playerID); + client.players.delete(playerID); + } }; diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 0f51029..d8d9b44 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,6 +1,7 @@ import { client } from "../../client"; import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; import { AO_HOST } from "../../client/aoHost"; +import { ensureCharIni } from "../../client/handleCharacterInfo"; /** * Handles a playerlist update @@ -25,6 +26,18 @@ export const handlePU = (args: string[]) => { playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; + const charId = client.chars.findIndex( + (c: any) => c && c.name.toLowerCase() === data.toLowerCase() + ); + if (charId >= 0) { + const player = client.players.get(Number(args[1])); + if (player) { + player.charId = charId; + if (player.area === client.area) { + ensureCharIni(charId); + } + } + } break; case 2: const showName = playerRow.childNodes[2]; @@ -33,6 +46,13 @@ export const handlePU = (args: string[]) => { case 3: playerRow.className = `area${data}`; updatePlayerAreas(client.area); + const puPlayer = client.players.get(Number(args[1])); + if (puPlayer) { + puPlayer.area = Number(data); + if (puPlayer.area === client.area && puPlayer.charId >= 0) { + ensureCharIni(puPlayer.charId); + } + } default: break; } -- cgit From f26d35429e66ddcdd02c10f57b315b0f02b4add2 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 22:21:42 +0100 Subject: Fix playerlist icons using wrong asset URL PR/PU packets arrive before the ASS packet, so playerlist icon srcs were set with the default AO_HOST. Now handleASS re-applies the correct asset URL to existing playerlist images after AO_HOST updates. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleASS.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'webAO') diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index a46d68e..1ecfd04 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,4 +1,5 @@ -import { setAOhost } from "../../client/aoHost"; +import { setAOhost, AO_HOST } from "../../client/aoHost"; +import { client } from "../../client"; /** * new asset url!! @@ -6,4 +7,20 @@ import { setAOhost } from "../../client/aoHost"; */ export const handleASS = (args: string[]) => { if (args[1] !== "None") setAOhost(args[1]); + + // Re-apply playerlist icon srcs that were set before AO_HOST was known + const iconExt = client.charicon_extensions[0] || ".png"; + for (const [playerID, player] of client.players) { + if (player.charId >= 0) { + const char = client.chars[player.charId]; + if (char) { + const img = document.querySelector( + `#client_playerlist_entry${playerID} img` + ); + if (img) { + img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; + } + } + } + } }; -- cgit From 9c68a1afcf178a86063f094b96471fa73531bd9a Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 23:01:25 +0100 Subject: Use setAOhost return value instead of stale AO_HOST import setAOhost now returns the current AO_HOST so handleASS can use the freshly set value rather than the import captured before the update. Co-Authored-By: Claude Opus 4.6 --- webAO/client/aoHost.ts | 3 ++- webAO/packets/handlers/handleASS.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'webAO') diff --git a/webAO/client/aoHost.ts b/webAO/client/aoHost.ts index 003e237..0d7cb0f 100644 --- a/webAO/client/aoHost.ts +++ b/webAO/client/aoHost.ts @@ -2,7 +2,7 @@ import queryParser from "../utils/queryParser"; const { asset } = queryParser(); export let AO_HOST = asset; -export const setAOhost = (val: string) => { +export const setAOhost = (val: string): string => { const currentProtocol = window.location.protocol; const assetProtocol = val.split(":")[0] + ":"; @@ -19,4 +19,5 @@ export const setAOhost = (val: string) => { AO_HOST = val; } console.log("Asset URL ist now " + AO_HOST); + return AO_HOST; }; diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index 1ecfd04..092e4f9 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,4 +1,4 @@ -import { setAOhost, AO_HOST } from "../../client/aoHost"; +import { setAOhost } from "../../client/aoHost"; import { client } from "../../client"; /** @@ -6,7 +6,7 @@ import { client } from "../../client"; * @param {Array} args packet arguments */ export const handleASS = (args: string[]) => { - if (args[1] !== "None") setAOhost(args[1]); + const host = args[1] !== "None" ? setAOhost(args[1]) : args[1]; // Re-apply playerlist icon srcs that were set before AO_HOST was known const iconExt = client.charicon_extensions[0] || ".png"; @@ -18,7 +18,7 @@ export const handleASS = (args: string[]) => { `#client_playerlist_entry${playerID} img` ); if (img) { - img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; + img.src = `${host}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; } } } -- cgit From 020dfcda00ca06b9a06e7076eaf8a0164ae1327e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Tue, 10 Feb 2026 23:38:17 +0100 Subject: Refactor playerlist to state-driven rendering with renderPlayerList handlePR and handlePU now only update client.playerlist state, and renderPlayerList handles all DOM rendering from that state. Co-Authored-By: Claude Opus 4.6 --- webAO/client.ts | 5 ++-- webAO/dom/areaClick.ts | 10 ------- webAO/dom/renderPlayerList.ts | 50 +++++++++++++++++++++++++++++++++ webAO/dom/updatePlayerAreas.ts | 24 ---------------- webAO/packets/handlers/handleASS.ts | 21 ++------------ webAO/packets/handlers/handlePR.ts | 55 +++---------------------------------- webAO/packets/handlers/handlePU.ts | 47 ++++++++++--------------------- 7 files changed, 75 insertions(+), 137 deletions(-) create mode 100644 webAO/dom/renderPlayerList.ts delete mode 100644 webAO/dom/updatePlayerAreas.ts (limited to 'webAO') diff --git a/webAO/client.ts b/webAO/client.ts index 95df67a..05a40c9 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -150,7 +150,8 @@ class Client extends EventEmitter { connect: () => void; loadResources: () => void; isLowMemory: () => void; - players: Map; + /** Maps player ID to player data */ + playerlist: Map; charicon_extensions: string[]; emote_extensions: string[]; emotions_extensions: string[]; @@ -212,7 +213,7 @@ class Client extends EventEmitter { this.temp_packet = ""; loadResources; isLowMemory; - this.players = new Map(); + this.playerlist = new Map(); this.charicon_extensions = [".png", ".webp"]; this.emote_extensions = [".gif", ".png", ".apng", ".webp", ".webp.static"]; this.emotions_extensions = [".png", ".webp"]; diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts index 19953a5..f7b177e 100644 --- a/webAO/dom/areaClick.ts +++ b/webAO/dom/areaClick.ts @@ -1,6 +1,4 @@ import { client } from "../client"; -import { updatePlayerAreas } from "./updatePlayerAreas"; -import { ensureCharIni } from "../client/handleCharacterInfo"; /** * Triggered when an item on the area list is clicked. * @param {HTMLElement} el @@ -14,13 +12,5 @@ export function area_click(el: HTMLElement) { areaHr.textContent = `switched to ${el.textContent}`; document.getElementById("client_log")!.appendChild(areaHr); client.area = Number(el.id.substring(4)); - updatePlayerAreas(client.area); - - // Prefetch char.ini for all characters present in the new area - for (const player of client.players.values()) { - if (player.area === client.area && player.charId >= 0) { - ensureCharIni(player.charId); - } - } } window.area_click = area_click; diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts new file mode 100644 index 0000000..43dab64 --- /dev/null +++ b/webAO/dom/renderPlayerList.ts @@ -0,0 +1,50 @@ +import { client } from "../client"; +import { AO_HOST } from "../client/aoHost"; + +export function renderPlayerList() { + const list = document.getElementById("client_playerlist") as HTMLTableElement; + list.innerHTML = ""; + + for (const [playerID, player] of client.playerlist) { + const playerRow = list.insertRow(); + playerRow.id = `client_playerlist_entry${playerID}`; + + const imgCell = playerRow.insertCell(0); + imgCell.style.width = "64px"; + const img = document.createElement("img"); + if (player.charId >= 0) { + const char = client.chars[player.charId]; + if (char) { + const iconExt = client.charicon_extensions[0] || ".png"; + img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; + img.alt = char.name; + img.title = char.name; + } + } + imgCell.appendChild(img); + + const charNameCell = playerRow.insertCell(1); + charNameCell.textContent = + player.charId >= 0 ? `[${playerID}] ${player.charName}` : ""; + + const showNameCell = playerRow.insertCell(2); + showNameCell.textContent = player.showName; + + const oocNameCell = playerRow.insertCell(3); + oocNameCell.textContent = player.name; + + const kickCell = playerRow.insertCell(4); + kickCell.style.width = "64px"; + const kick = document.createElement("button"); + kick.innerText = "Kick"; + kick.onclick = () => window.kickPlayer(playerID); + kickCell.appendChild(kick); + + const banCell = playerRow.insertCell(5); + banCell.style.width = "64px"; + const ban = document.createElement("button"); + ban.innerText = "Ban"; + ban.onclick = () => window.banPlayer(playerID); + banCell.appendChild(ban); + } +} diff --git a/webAO/dom/updatePlayerAreas.ts b/webAO/dom/updatePlayerAreas.ts deleted file mode 100644 index 99eccf1..0000000 --- a/webAO/dom/updatePlayerAreas.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { client } from "../client"; -import { area_click } from "./areaClick"; -/** - * Triggered when someone switches areas - * @param {Number} ownarea - */ -export function updatePlayerAreas(ownarea: number) { - for (let i = 0; i < client.areas.length; i++) { - if (i === ownarea) - for (let classelement of Array.from( - document.getElementsByClassName( - `area${i}`, - ) as HTMLCollectionOf, - )) - classelement.style.display = ""; - else - for (let classelement of Array.from( - document.getElementsByClassName( - `area${i}`, - ) as HTMLCollectionOf, - )) - classelement.style.display = "none"; - } -} diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index 092e4f9..c50443a 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,26 +1,11 @@ import { setAOhost } from "../../client/aoHost"; -import { client } from "../../client"; +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * new asset url!! * @param {Array} args packet arguments */ export const handleASS = (args: string[]) => { - const host = args[1] !== "None" ? setAOhost(args[1]) : args[1]; - - // Re-apply playerlist icon srcs that were set before AO_HOST was known - const iconExt = client.charicon_extensions[0] || ".png"; - for (const [playerID, player] of client.players) { - if (player.charId >= 0) { - const char = client.chars[player.charId]; - if (char) { - const img = document.querySelector( - `#client_playerlist_entry${playerID} img` - ); - if (img) { - img.src = `${host}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; - } - } - } - } + if (args[1] !== "None") setAOhost(args[1]); + renderPlayerList(); }; diff --git a/webAO/packets/handlers/handlePR.ts b/webAO/packets/handlers/handlePR.ts index e39103d..f81d8dc 100644 --- a/webAO/packets/handlers/handlePR.ts +++ b/webAO/packets/handlers/handlePR.ts @@ -1,51 +1,5 @@ import { client } from "../../client"; -import { kickPlayer, banPlayer } from "../../dom/banPlayer"; - -function addPlayer(playerID: number) { - const list = document.getElementById("client_playerlist"); - const playerRow = list.insertRow(); - playerRow.id = `client_playerlist_entry${playerID}`; - playerRow.className = `area0`; - - const imgCell = playerRow.insertCell(0); - imgCell.style.width = "64px"; - const img = document.createElement("img"); - imgCell.appendChild(img); - - const name = document.createTextNode("No Data"); - - const charNameCell = playerRow.insertCell(1); - charNameCell.appendChild(name); - const showNameCell = playerRow.insertCell(2); - showNameCell.appendChild(name); - const oocNameCell = playerRow.insertCell(3); - oocNameCell.appendChild(name); - - const kickCell = playerRow.insertCell(4); - kickCell.style.width = "64px"; - const kick = document.createElement("button"); - kick.innerText = "Kick"; - kick.onclick = () => { - window.kickPlayer(playerID); - }; - kickCell.appendChild(kick); - - const banCell = playerRow.insertCell(5); - banCell.style.width = "64px"; - const ban = document.createElement("button"); - ban.innerText = "Ban"; - ban.onclick = () => { - window.banPlayer(playerID); - }; - banCell.appendChild(ban); -} - -function removePlayer(playerID: number) { - const playerRow = ( - document.getElementById(`client_playerlist_entry${playerID}`) - ); - playerRow.remove(); -} +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * Handles a player joining or leaving @@ -54,10 +8,9 @@ function removePlayer(playerID: number) { export const handlePR = (args: string[]) => { const playerID = Number(args[1]); if (Number(args[2]) === 0) { - addPlayer(playerID); - client.players.set(playerID, { charId: -1, area: 0 }); + client.playerlist.set(playerID, { charId: -1, charName: "", showName: "", name: "", area: 0 }); } else if (Number(args[2]) === 1) { - removePlayer(playerID); - client.players.delete(playerID); + client.playerlist.delete(playerID); } + renderPlayerList(); }; diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index d8d9b44..3b70ad3 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,59 +1,42 @@ import { client } from "../../client"; -import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; -import { AO_HOST } from "../../client/aoHost"; import { ensureCharIni } from "../../client/handleCharacterInfo"; +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * Handles a playerlist update * @param {Array} args packet arguments */ export const handlePU = (args: string[]) => { - const playerRow = ( - document.getElementById(`client_playerlist_entry${Number(args[1])}`) - ); + const playerID = Number(args[1]); + const player = client.playerlist.get(playerID); + if (!player) return; + const type = Number(args[2]); const data = args[3]; + switch (type) { case 0: - const oocName = playerRow.childNodes[3]; - oocName.innerText = data; + player.name = data; break; case 1: - const playerImg = playerRow.childNodes[0].firstChild; - playerImg.alt = data; - playerImg.title = data; - const iconExt = client.charicon_extensions[0] || ".png"; - playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; - const charName = playerRow.childNodes[1]; - charName.innerText = `[${args[1]}] ${data}`; + player.charName = data; const charId = client.chars.findIndex( (c: any) => c && c.name.toLowerCase() === data.toLowerCase() ); if (charId >= 0) { - const player = client.players.get(Number(args[1])); - if (player) { - player.charId = charId; - if (player.area === client.area) { - ensureCharIni(charId); - } - } + player.charId = charId; + ensureCharIni(charId); } break; case 2: - const showName = playerRow.childNodes[2]; - showName.innerText = data; + player.showName = data; break; case 3: - playerRow.className = `area${data}`; - updatePlayerAreas(client.area); - const puPlayer = client.players.get(Number(args[1])); - if (puPlayer) { - puPlayer.area = Number(data); - if (puPlayer.area === client.area && puPlayer.charId >= 0) { - ensureCharIni(puPlayer.charId); - } - } + player.area = Number(data); + break; default: break; } + + renderPlayerList(); }; -- cgit From 9993c378613b20b6f6f74b324c22c3bfda4c71fc Mon Sep 17 00:00:00 2001 From: David Skoland Date: Tue, 10 Feb 2026 23:59:48 +0100 Subject: Use charName directly for playerlist rendering and add table styling Render char icons and names from the character name string (PU type 1) instead of gating on charId lookup. Add header row and row separators to the playerlist table. Co-Authored-By: Claude Opus 4.6 --- webAO/dom/renderPlayerList.ts | 25 +++++++++++++++---------- webAO/styles/client.css | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) (limited to 'webAO') diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts index 43dab64..d0f08c6 100644 --- a/webAO/dom/renderPlayerList.ts +++ b/webAO/dom/renderPlayerList.ts @@ -5,27 +5,32 @@ export function renderPlayerList() { const list = document.getElementById("client_playerlist") as HTMLTableElement; list.innerHTML = ""; + const header = list.createTHead().insertRow(); + for (const label of ["Icon", "Character", "Showname", "OOC Name"]) { + const th = document.createElement("th"); + th.textContent = label; + header.appendChild(th); + } + + const body = list.createTBody(); for (const [playerID, player] of client.playerlist) { - const playerRow = list.insertRow(); + const playerRow = body.insertRow(); playerRow.id = `client_playerlist_entry${playerID}`; const imgCell = playerRow.insertCell(0); imgCell.style.width = "64px"; const img = document.createElement("img"); - if (player.charId >= 0) { - const char = client.chars[player.charId]; - if (char) { - const iconExt = client.charicon_extensions[0] || ".png"; - img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; - img.alt = char.name; - img.title = char.name; - } + if (player.charName) { + const iconExt = client.charicon_extensions[0] || ".png"; + img.src = `${AO_HOST}characters/${encodeURI(player.charName.toLowerCase())}/char_icon${iconExt}`; + img.alt = player.charName; + img.title = player.charName; } imgCell.appendChild(img); const charNameCell = playerRow.insertCell(1); charNameCell.textContent = - player.charId >= 0 ? `[${playerID}] ${player.charName}` : ""; + player.charName ? `[${playerID}] ${player.charName}` : ""; const showNameCell = playerRow.insertCell(2); showNameCell.textContent = player.showName; diff --git a/webAO/styles/client.css b/webAO/styles/client.css index 88eca59..e3ee820 100644 --- a/webAO/styles/client.css +++ b/webAO/styles/client.css @@ -714,6 +714,23 @@ opacity: 0.5; } +#client_playerlist { + width: 100%; + border-collapse: collapse; +} + +#client_playerlist th, +#client_playerlist td { + border-bottom: 1px solid #555; + padding: 4px 6px; + text-align: left; +} + +#client_playerlist th { + font-weight: bold; + border-bottom: 2px solid #888; +} + .hrtext { overflow: hidden; text-align: center; -- cgit From 6314a7e61ad85aaf9313ed2947853e8e1d2aea33 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Wed, 11 Feb 2026 00:05:04 +0100 Subject: Add area column to playerlist Co-Authored-By: Claude Opus 4.6 --- webAO/dom/renderPlayerList.ts | 9 ++++++--- webAO/styles/mod.css | 4 ++-- webAO/styles/nomod.css | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) (limited to 'webAO') diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts index d0f08c6..a78851b 100644 --- a/webAO/dom/renderPlayerList.ts +++ b/webAO/dom/renderPlayerList.ts @@ -6,7 +6,7 @@ export function renderPlayerList() { list.innerHTML = ""; const header = list.createTHead().insertRow(); - for (const label of ["Icon", "Character", "Showname", "OOC Name"]) { + for (const label of ["Icon", "Character", "Showname", "OOC Name", "Area"]) { const th = document.createElement("th"); th.textContent = label; header.appendChild(th); @@ -38,14 +38,17 @@ export function renderPlayerList() { const oocNameCell = playerRow.insertCell(3); oocNameCell.textContent = player.name; - const kickCell = playerRow.insertCell(4); + const areaCell = playerRow.insertCell(4); + areaCell.textContent = String(player.area); + + const kickCell = playerRow.insertCell(5); kickCell.style.width = "64px"; const kick = document.createElement("button"); kick.innerText = "Kick"; kick.onclick = () => window.kickPlayer(playerID); kickCell.appendChild(kick); - const banCell = playerRow.insertCell(5); + const banCell = playerRow.insertCell(6); banCell.style.width = "64px"; const ban = document.createElement("button"); ban.innerText = "Ban"; diff --git a/webAO/styles/mod.css b/webAO/styles/mod.css index 1e077b3..05f8c87 100644 --- a/webAO/styles/mod.css +++ b/webAO/styles/mod.css @@ -1,4 +1,4 @@ -table#client_playerlist td:nth-child(5), -td:nth-child(6) { +table#client_playerlist td:nth-child(6), +td:nth-child(7) { display: inherit; } diff --git a/webAO/styles/nomod.css b/webAO/styles/nomod.css index 1c03547..132e40e 100644 --- a/webAO/styles/nomod.css +++ b/webAO/styles/nomod.css @@ -1,4 +1,4 @@ -table#client_playerlist td:nth-child(5), -td:nth-child(6) { +table#client_playerlist td:nth-child(6), +td:nth-child(7) { display: none; } -- cgit From c380112d5f29b68bfa301527405fdf372835900e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Wed, 11 Feb 2026 00:10:05 +0100 Subject: Filter playerlist by area and remove Area column Hide players not in the client's current area. Re-render playerlist on area switch. Remove the now-redundant Area column. Co-Authored-By: Claude Opus 4.6 --- webAO/dom/areaClick.ts | 2 ++ webAO/dom/renderPlayerList.ts | 10 ++++------ webAO/styles/mod.css | 4 ++-- webAO/styles/nomod.css | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) (limited to 'webAO') diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts index f7b177e..27682c7 100644 --- a/webAO/dom/areaClick.ts +++ b/webAO/dom/areaClick.ts @@ -1,4 +1,5 @@ import { client } from "../client"; +import { renderPlayerList } from "./renderPlayerList"; /** * Triggered when an item on the area list is clicked. * @param {HTMLElement} el @@ -12,5 +13,6 @@ export function area_click(el: HTMLElement) { areaHr.textContent = `switched to ${el.textContent}`; document.getElementById("client_log")!.appendChild(areaHr); client.area = Number(el.id.substring(4)); + renderPlayerList(); } window.area_click = area_click; diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts index a78851b..a382194 100644 --- a/webAO/dom/renderPlayerList.ts +++ b/webAO/dom/renderPlayerList.ts @@ -6,7 +6,7 @@ export function renderPlayerList() { list.innerHTML = ""; const header = list.createTHead().insertRow(); - for (const label of ["Icon", "Character", "Showname", "OOC Name", "Area"]) { + for (const label of ["Icon", "Character", "Showname", "OOC Name"]) { const th = document.createElement("th"); th.textContent = label; header.appendChild(th); @@ -16,6 +16,7 @@ export function renderPlayerList() { for (const [playerID, player] of client.playerlist) { const playerRow = body.insertRow(); playerRow.id = `client_playerlist_entry${playerID}`; + playerRow.style.display = player.area === client.area ? "" : "none"; const imgCell = playerRow.insertCell(0); imgCell.style.width = "64px"; @@ -38,17 +39,14 @@ export function renderPlayerList() { const oocNameCell = playerRow.insertCell(3); oocNameCell.textContent = player.name; - const areaCell = playerRow.insertCell(4); - areaCell.textContent = String(player.area); - - const kickCell = playerRow.insertCell(5); + const kickCell = playerRow.insertCell(4); kickCell.style.width = "64px"; const kick = document.createElement("button"); kick.innerText = "Kick"; kick.onclick = () => window.kickPlayer(playerID); kickCell.appendChild(kick); - const banCell = playerRow.insertCell(6); + const banCell = playerRow.insertCell(5); banCell.style.width = "64px"; const ban = document.createElement("button"); ban.innerText = "Ban"; diff --git a/webAO/styles/mod.css b/webAO/styles/mod.css index 05f8c87..1e077b3 100644 --- a/webAO/styles/mod.css +++ b/webAO/styles/mod.css @@ -1,4 +1,4 @@ -table#client_playerlist td:nth-child(6), -td:nth-child(7) { +table#client_playerlist td:nth-child(5), +td:nth-child(6) { display: inherit; } diff --git a/webAO/styles/nomod.css b/webAO/styles/nomod.css index 132e40e..1c03547 100644 --- a/webAO/styles/nomod.css +++ b/webAO/styles/nomod.css @@ -1,4 +1,4 @@ -table#client_playerlist td:nth-child(6), -td:nth-child(7) { +table#client_playerlist td:nth-child(5), +td:nth-child(6) { display: none; } -- cgit From 5bb35a981e2f35df7fbf126faa8655ee3b3ca142 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Wed, 11 Feb 2026 00:12:51 +0100 Subject: Clamp playerlist char icons to 60x60 pixels Co-Authored-By: Claude Opus 4.6 --- webAO/dom/renderPlayerList.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'webAO') diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts index a382194..48354ae 100644 --- a/webAO/dom/renderPlayerList.ts +++ b/webAO/dom/renderPlayerList.ts @@ -21,6 +21,8 @@ export function renderPlayerList() { const imgCell = playerRow.insertCell(0); imgCell.style.width = "64px"; const img = document.createElement("img"); + img.style.maxWidth = "60px"; + img.style.maxHeight = "60px"; if (player.charName) { const iconExt = client.charicon_extensions[0] || ".png"; img.src = `${AO_HOST}characters/${encodeURI(player.charName.toLowerCase())}/char_icon${iconExt}`; -- cgit