Lobby standee picker for online rooms

Waiting rooms now show the six wizard standees: tap to claim yours,
claimed ones gray out with the claimant named for screen readers, the
roster shows each player's chosen standee (or "choosing..."), and
conflicts are refused first-come. Choices sync live to everyone in
the room, resolve to first-free defaults for the undecided when the
host flips the boards, persist in the start line of the room log (so
restored games keep their colors), and flow through the same engine
config field the hotseat picker uses. Also fixed: the static-serving
realpath guard crashed the dev server when no client build exists —
it now 404s static requests instead. Verified end to end: claim,
conflict refusal, second pick, start, and in-game colorIndex 5/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 01:19:42 -04:00
co-authored by Claude Fable 5
parent 77ae65b263
commit 4cbf5a013e
5 changed files with 91 additions and 6 deletions
+17 -1
View File
@@ -19,6 +19,7 @@ import type { Command, PlayerId } from "@wizwar/engine";
import {
claimTransferCode,
createRoom,
pickColor,
getRoom,
joinRoom,
loadPersistedRooms,
@@ -42,9 +43,15 @@ const MIME: Record<string, string> = {
".png": "image/png", ".svg": "image/svg+xml", ".ico": "image/x-icon",
".woff2": "font/woff2", ".json": "application/json",
};
const staticRoot = realpathSync(normalize(STATIC_DIR));
// The client build may be absent in development (vite serves it instead);
// serve a 404 for static requests in that case rather than dying on boot.
const staticRoot = existsSync(STATIC_DIR) ? realpathSync(normalize(STATIC_DIR)) : null;
const httpServer = createServer((req, res) => {
try {
if (!staticRoot) {
res.writeHead(404).end("client not built");
return;
}
let url: string;
try {
url = decodeURIComponent((req.url ?? "/").split("?")[0]!);
@@ -100,6 +107,7 @@ function roomInfo(room: Room) {
players: room.players,
hostId: room.hostId,
started: room.state !== null,
colors: Object.fromEntries(room.colorChoices),
};
}
@@ -205,6 +213,14 @@ wss.on("connection", (socket) => {
send(socket, { type: "transferClaimed", seat: result });
break;
}
case "pickColor": {
const room = session.roomId ? getRoom(session.roomId) : undefined;
if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" });
const problem = pickColor(room, session.playerId, Number(msg.color));
if (problem) return send(socket, { type: "error", message: problem });
broadcastRoomState(room);
break;
}
case "myGames": {
// {seats: [{roomId, name, token}]} -> summaries for valid seats.
const seats = Array.isArray(msg.seats) ? msg.seats : [];