version 2
This commit is contained in:
21
bun.lock
21
bun.lock
@@ -5,30 +5,11 @@
|
||||
"": {
|
||||
"name": "buzzer",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"hono": "^4.12.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"hono": "^4.7.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
115
src/rooms.ts
115
src/rooms.ts
@@ -1,28 +1,25 @@
|
||||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
export interface RoomSettings {
|
||||
teamMode: boolean;
|
||||
mode: "individual" | "teams";
|
||||
numTeams: number;
|
||||
teamNames: string[];
|
||||
allowRebuzz: boolean;
|
||||
buzzerLockout: boolean;
|
||||
showOrder: boolean;
|
||||
pointsCorrect: number;
|
||||
pointsIncorrect: number;
|
||||
pointsNeg: boolean;
|
||||
playerPickTeam: boolean; // players can self-assign teams
|
||||
showBuzzOrder: boolean; // show full order vs first only
|
||||
buzzerLockout: boolean; // lock after first buzz
|
||||
timerSeconds: number; // 0 = no timer
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
id: string;
|
||||
name: string;
|
||||
teamIndex: number | null;
|
||||
score: number;
|
||||
ws: ServerWebSocket<unknown> | null;
|
||||
isConnected: boolean;
|
||||
joinedAt: number;
|
||||
}
|
||||
|
||||
export interface BuzzerState {
|
||||
roundOpen: boolean;
|
||||
active: boolean;
|
||||
buzzOrder: string[];
|
||||
buzzTimes: Map<string, number>;
|
||||
}
|
||||
@@ -34,85 +31,55 @@ export interface Room {
|
||||
settings: RoomSettings;
|
||||
players: Map<string, Player>;
|
||||
buzzerState: BuzzerState;
|
||||
locked: boolean;
|
||||
createdAt: number;
|
||||
locked: boolean; // room locked to new joins
|
||||
teamLocked: boolean; // team selection locked
|
||||
}
|
||||
|
||||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
export const rooms = new Map<string, Room>();
|
||||
export const rooms = new Map<string, Room>();
|
||||
export const wsToPlayer = new Map<ServerWebSocket<unknown>, { roomId: string; playerId: string }>();
|
||||
|
||||
export function generateId(len = 8): string {
|
||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
let out = "";
|
||||
for (let i = 0; i < len; i++) out += chars[Math.floor(Math.random() * chars.length)];
|
||||
return out;
|
||||
export function genId(len = 8): string {
|
||||
const c = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
let s = "";
|
||||
for (let i = 0; i < len; i++) s += c[Math.floor(Math.random() * c.length)];
|
||||
return s;
|
||||
}
|
||||
|
||||
export function defaultSettings(): RoomSettings {
|
||||
export function sanitize(v: unknown, max = 32): string {
|
||||
return String(v ?? "").trim().replace(/[<>"'`]/g, "").slice(0, max);
|
||||
}
|
||||
|
||||
export function freshBuzzer(): BuzzerState {
|
||||
return { roundOpen: false, buzzOrder: [], buzzTimes: new Map() };
|
||||
}
|
||||
|
||||
export function publicRoom(room: Room) {
|
||||
return {
|
||||
teamMode: false,
|
||||
numTeams: 2,
|
||||
teamNames: ["ALPHA", "BETA", "GAMMA", "DELTA", "EPSILON", "ZETA", "ETA", "THETA"],
|
||||
allowRebuzz: false,
|
||||
buzzerLockout: true,
|
||||
showOrder: true,
|
||||
pointsCorrect: 100,
|
||||
pointsIncorrect: 0,
|
||||
pointsNeg: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function freshBuzzerState(): BuzzerState {
|
||||
return { roundOpen: false, active: false, buzzOrder: [], buzzTimes: new Map() };
|
||||
}
|
||||
|
||||
export function sanitize(str: unknown, maxLen = 32): string {
|
||||
return String(str ?? "")
|
||||
.trim()
|
||||
.replace(/[<>"'`]/g, "")
|
||||
.slice(0, maxLen);
|
||||
}
|
||||
|
||||
export function getPublicRoom(room: Room) {
|
||||
return {
|
||||
id: room.id,
|
||||
settings: room.settings,
|
||||
locked: room.locked,
|
||||
modOnline: room.modWs !== null,
|
||||
id: room.id,
|
||||
settings: room.settings,
|
||||
locked: room.locked,
|
||||
teamLocked: room.teamLocked,
|
||||
modOnline: room.modWs !== null,
|
||||
buzzerState: {
|
||||
roundOpen: room.buzzerState.roundOpen,
|
||||
active: room.buzzerState.active,
|
||||
buzzOrder: room.buzzerState.buzzOrder,
|
||||
},
|
||||
players: Array.from(room.players.values()).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
score: p.score,
|
||||
players: Array.from(room.players.values()).map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
isConnected: p.isConnected,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function broadcastToRoom(
|
||||
room: Room,
|
||||
msg: object,
|
||||
exclude?: ServerWebSocket<unknown>
|
||||
) {
|
||||
const data = JSON.stringify(msg);
|
||||
for (const p of room.players.values()) {
|
||||
if (p.ws && p.isConnected && p.ws !== exclude) {
|
||||
try { p.ws.send(data); } catch {}
|
||||
}
|
||||
}
|
||||
if (room.modWs && room.modWs !== exclude) {
|
||||
try { room.modWs.send(data); } catch {}
|
||||
}
|
||||
export function broadcast(room: Room, msg: object, exclude?: ServerWebSocket<unknown>) {
|
||||
const d = JSON.stringify(msg);
|
||||
for (const p of room.players.values())
|
||||
if (p.ws && p.isConnected && p.ws !== exclude) try { p.ws.send(d); } catch {}
|
||||
if (room.modWs && room.modWs !== exclude) try { room.modWs.send(d); } catch {}
|
||||
}
|
||||
|
||||
export function sendToMod(room: Room, msg: object) {
|
||||
if (!room.modWs) return;
|
||||
try { room.modWs.send(JSON.stringify(msg)); } catch {}
|
||||
}
|
||||
export function toMod(room: Room, msg: object) {
|
||||
if (room.modWs) try { room.modWs.send(JSON.stringify(msg)); } catch {}
|
||||
}
|
||||
@@ -6,38 +6,19 @@ const HTML = readFileSync("./src/public/index.html", "utf-8");
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
fetch(req, server) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/ws") {
|
||||
const ok = server.upgrade(req);
|
||||
if (!ok) return new Response("WebSocket upgrade failed", { status: 400 });
|
||||
if (new URL(req.url).pathname === "/ws") {
|
||||
if (!server.upgrade(req)) return new Response("WS upgrade failed", { status: 400 });
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
return new Response(HTML, {
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
return new Response(HTML, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
||||
},
|
||||
websocket: {
|
||||
perMessageDeflate: true,
|
||||
maxPayloadLength: 64 * 1024,
|
||||
message(ws, msg) {
|
||||
if (typeof msg !== "string") return;
|
||||
handleMessage(ws, msg);
|
||||
},
|
||||
close(ws) {
|
||||
handleClose(ws);
|
||||
},
|
||||
maxPayloadLength: 32 * 1024,
|
||||
message(ws, msg) { if (typeof msg === "string") handleMessage(ws, msg); },
|
||||
close(ws) { handleClose(ws); },
|
||||
open(_ws) {},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`\x1b[32m ██████╗ ██╗ ██╗███████╗███████╗███████╗██████╗ \x1b[0m`);
|
||||
console.log(`\x1b[32m ██╔══██╗██║ ██║╚══███╔╝╚══███╔╝██╔════╝██╔══██╗\x1b[0m`);
|
||||
console.log(`\x1b[32m ██████╔╝██║ ██║ ███╔╝ ███╔╝ █████╗ ██████╔╝\x1b[0m`);
|
||||
console.log(`\x1b[32m ██╔══██╗██║ ██║ ███╔╝ ███╔╝ ██╔══╝ ██╔══██╗\x1b[0m`);
|
||||
console.log(`\x1b[32m ██████╔╝╚██████╔╝███████╗███████╗███████╗██║ ██║\x1b[0m`);
|
||||
console.log(`\x1b[32m ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝\x1b[0m`);
|
||||
console.log(`\x1b[2m ─────────────────────────────────────────────────\x1b[0m`);
|
||||
console.log(`\x1b[32m ONLINE\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`);
|
||||
console.log(`\x1b[2m ─────────────────────────────────────────────────\x1b[0m\n`);
|
||||
console.log(`\x1b[32m[BUZZER]\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`);
|
||||
@@ -2,275 +2,239 @@ import type { ServerWebSocket } from "bun";
|
||||
import {
|
||||
rooms, wsToPlayer,
|
||||
Room, Player,
|
||||
generateId, defaultSettings, freshBuzzerState, sanitize,
|
||||
getPublicRoom, broadcastToRoom, sendToMod,
|
||||
genId, sanitize, freshBuzzer, publicRoom, broadcast, toMod,
|
||||
} from "./rooms";
|
||||
|
||||
type WS = ServerWebSocket<unknown>;
|
||||
|
||||
function send(ws: WS, msg: object) {
|
||||
try { ws.send(JSON.stringify(msg)); } catch {}
|
||||
}
|
||||
|
||||
function err(ws: WS, message: string) {
|
||||
send(ws, { type: "error", message });
|
||||
}
|
||||
const tx = (ws: WS, msg: object) => { try { ws.send(JSON.stringify(msg)); } catch {} };
|
||||
const er = (ws: WS, m: string) => tx(ws, { type: "error", message: m });
|
||||
const modOf = (ws: WS): Room | null => { for (const r of rooms.values()) if (r.modWs === ws) return r; return null; };
|
||||
|
||||
export function handleMessage(ws: WS, raw: string) {
|
||||
let msg: any;
|
||||
try { msg = JSON.parse(raw); } catch { return; }
|
||||
|
||||
const type: string = msg?.type ?? "";
|
||||
|
||||
// ── CREATE ROOM ──────────────────────────────────────────────────────────
|
||||
/* ── CREATE ROOM ── */
|
||||
if (type === "create_room") {
|
||||
const roomId = generateId(6);
|
||||
const modSecret = generateId(24);
|
||||
const s = msg.settings ?? {};
|
||||
const numTeams = Math.min(8, Math.max(2, (s.numTeams ?? 2) | 0));
|
||||
const defaultNames = ["ALPHA","BETA","GAMMA","DELTA","EPSILON","ZETA","ETA","THETA"];
|
||||
const teamNames: string[] = [];
|
||||
for (let i = 0; i < numTeams; i++)
|
||||
teamNames.push(sanitize(s.teamNames?.[i] ?? defaultNames[i], 32));
|
||||
|
||||
const room: Room = {
|
||||
id: roomId,
|
||||
moderatorSecret: modSecret,
|
||||
id: genId(6),
|
||||
moderatorSecret: genId(24),
|
||||
modWs: ws,
|
||||
settings: defaultSettings(),
|
||||
players: new Map(),
|
||||
buzzerState: freshBuzzerState(),
|
||||
locked: false,
|
||||
createdAt: Date.now(),
|
||||
settings: {
|
||||
mode: s.mode === "teams" ? "teams" : "individual",
|
||||
numTeams,
|
||||
teamNames,
|
||||
playerPickTeam: s.playerPickTeam === true,
|
||||
showBuzzOrder: s.showBuzzOrder !== false,
|
||||
buzzerLockout: s.buzzerLockout !== false,
|
||||
timerSeconds: Math.min(300, Math.max(0, (s.timerSeconds ?? 0) | 0)),
|
||||
},
|
||||
players: new Map(),
|
||||
buzzerState: freshBuzzer(),
|
||||
locked: false,
|
||||
teamLocked: false,
|
||||
};
|
||||
rooms.set(roomId, room);
|
||||
send(ws, { type: "room_created", roomId, modSecret, room: getPublicRoom(room) });
|
||||
rooms.set(room.id, room);
|
||||
tx(ws, { type: "room_created", roomId: room.id, modSecret: room.moderatorSecret, room: publicRoom(room) });
|
||||
return;
|
||||
}
|
||||
|
||||
// ── MOD REJOIN ────────────────────────────────────────────────────────────
|
||||
/* ── MOD REJOIN ── */
|
||||
if (type === "mod_rejoin") {
|
||||
const { roomId, modSecret } = msg;
|
||||
const room = rooms.get(sanitize(roomId, 12));
|
||||
if (!room || room.moderatorSecret !== sanitize(modSecret, 32)) {
|
||||
err(ws, "Invalid moderator credentials"); return;
|
||||
const room = rooms.get(sanitize(msg.roomId, 10));
|
||||
if (!room || room.moderatorSecret !== sanitize(msg.modSecret, 32)) {
|
||||
er(ws, "Invalid credentials"); return;
|
||||
}
|
||||
room.modWs = ws;
|
||||
send(ws, { type: "mod_joined", room: getPublicRoom(room) });
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) }, ws);
|
||||
tx(ws, { type: "mod_joined", room: publicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) }, ws);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── JOIN ROOM (player) ────────────────────────────────────────────────────
|
||||
/* ── JOIN ROOM ── */
|
||||
if (type === "join_room") {
|
||||
const roomId = sanitize(msg.roomId ?? "", 12);
|
||||
const room = rooms.get(roomId);
|
||||
if (!room) { err(ws, "Room not found"); return; }
|
||||
if (room.locked) { err(ws, "Room is locked"); return; }
|
||||
if (msg.modSecret) { er(ws, "Unauthorized"); return; }
|
||||
const room = rooms.get(sanitize(msg.roomId ?? "", 10));
|
||||
if (!room) { er(ws, "Room not found"); return; }
|
||||
if (room.locked) { er(ws, "Room is locked"); return; }
|
||||
|
||||
const name = sanitize(msg.playerName ?? "Player", 24) || "Player";
|
||||
const existingId = sanitize(msg.playerId ?? "", 12);
|
||||
|
||||
// Block anyone trying to present the mod secret as a player
|
||||
if (msg.modSecret) { err(ws, "Unauthorized"); return; }
|
||||
|
||||
let player: Player | undefined;
|
||||
|
||||
if (existingId && room.players.has(existingId)) {
|
||||
player = room.players.get(existingId)!;
|
||||
player.ws = ws;
|
||||
player.isConnected = true;
|
||||
player.name = name;
|
||||
player.ws = ws; player.isConnected = true; player.name = name;
|
||||
} else {
|
||||
const playerId = generateId();
|
||||
player = {
|
||||
id: playerId, name,
|
||||
teamIndex: null, score: 0,
|
||||
ws, isConnected: true,
|
||||
joinedAt: Date.now(),
|
||||
};
|
||||
room.players.set(playerId, player);
|
||||
player = { id: genId(), name, teamIndex: null, ws, isConnected: true };
|
||||
room.players.set(player.id, player);
|
||||
}
|
||||
|
||||
wsToPlayer.set(ws, { roomId, playerId: player.id });
|
||||
send(ws, { type: "joined", playerId: player.id, room: getPublicRoom(room) });
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) }, ws);
|
||||
wsToPlayer.set(ws, { roomId: room.id, playerId: player.id });
|
||||
tx(ws, { type: "joined", playerId: player.id, room: publicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) }, ws);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── BUZZ ──────────────────────────────────────────────────────────────────
|
||||
/* ── PICK TEAM (player) ── */
|
||||
if (type === "pick_team") {
|
||||
const ctx = wsToPlayer.get(ws);
|
||||
if (!ctx) return;
|
||||
const room = rooms.get(ctx.roomId);
|
||||
if (!room) return;
|
||||
if (room.teamLocked || !room.settings.playerPickTeam || room.settings.mode !== "teams") {
|
||||
er(ws, "Team selection not allowed"); return;
|
||||
}
|
||||
const p = room.players.get(ctx.playerId);
|
||||
if (!p) return;
|
||||
const ti = typeof msg.teamIndex === "number" ? msg.teamIndex : null;
|
||||
p.teamIndex = ti !== null ? Math.max(0, Math.min(room.settings.numTeams - 1, ti | 0)) : null;
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── BUZZ ── */
|
||||
if (type === "buzz") {
|
||||
const ctx = wsToPlayer.get(ws);
|
||||
if (!ctx) { err(ws, "Not a player"); return; }
|
||||
if (!ctx) return;
|
||||
const room = rooms.get(ctx.roomId);
|
||||
if (!room) return;
|
||||
const bz = room.buzzerState;
|
||||
|
||||
if (!bz.roundOpen) { send(ws, { type: "buzz_rejected", reason: "Round not open" }); return; }
|
||||
if (bz.buzzOrder.includes(ctx.playerId) && !room.settings.allowRebuzz) {
|
||||
send(ws, { type: "buzz_rejected", reason: "Already buzzed" }); return;
|
||||
if (!bz.roundOpen) {
|
||||
tx(ws, { type: "buzz_rejected", reason: "Round not open" }); return;
|
||||
}
|
||||
if (bz.buzzOrder.includes(ctx.playerId)) {
|
||||
tx(ws, { type: "buzz_rejected", reason: "Already buzzed" }); return;
|
||||
}
|
||||
if (room.settings.buzzerLockout && bz.buzzOrder.length > 0) {
|
||||
send(ws, { type: "buzz_rejected", reason: "Buzzer locked out" }); return;
|
||||
tx(ws, { type: "buzz_rejected", reason: "Locked out" }); return;
|
||||
}
|
||||
|
||||
bz.buzzOrder.push(ctx.playerId);
|
||||
bz.buzzTimes.set(ctx.playerId, Date.now());
|
||||
bz.active = true;
|
||||
|
||||
const player = room.players.get(ctx.playerId)!;
|
||||
const publicOrder = room.settings.showOrder ? bz.buzzOrder : [bz.buzzOrder[0]];
|
||||
const p = room.players.get(ctx.playerId)!;
|
||||
const pubOrder = room.settings.showBuzzOrder ? bz.buzzOrder : [bz.buzzOrder[0]];
|
||||
|
||||
broadcastToRoom(room, {
|
||||
broadcast(room, {
|
||||
type: "buzz_event",
|
||||
playerId: ctx.playerId,
|
||||
playerName: player.name,
|
||||
teamIndex: player.teamIndex,
|
||||
buzzOrder: publicOrder,
|
||||
room: getPublicRoom(room),
|
||||
playerId: ctx.playerId,
|
||||
playerName: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
buzzOrder: pubOrder,
|
||||
room: publicRoom(room),
|
||||
});
|
||||
// mod gets full order + timestamps always
|
||||
sendToMod(room, {
|
||||
type: "buzz_event",
|
||||
playerId: ctx.playerId,
|
||||
playerName: player.name,
|
||||
teamIndex: player.teamIndex,
|
||||
buzzOrder: bz.buzzOrder,
|
||||
buzzTimes: Object.fromEntries(bz.buzzTimes),
|
||||
room: getPublicRoom(room),
|
||||
toMod(room, {
|
||||
type: "buzz_event",
|
||||
playerId: ctx.playerId,
|
||||
playerName: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
buzzOrder: bz.buzzOrder,
|
||||
buzzTimes: Object.fromEntries(bz.buzzTimes),
|
||||
room: publicRoom(room),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── MOD-ONLY FROM HERE ────────────────────────────────────────────────────
|
||||
// find room where this ws is the mod
|
||||
let room: Room | undefined;
|
||||
for (const r of rooms.values()) {
|
||||
if (r.modWs === ws) { room = r; break; }
|
||||
}
|
||||
if (!room) { err(ws, "Not authorized"); return; }
|
||||
/* ── MOD ONLY ── */
|
||||
const room = modOf(ws);
|
||||
if (!room) { er(ws, "Not authorized"); return; }
|
||||
|
||||
switch (type) {
|
||||
case "open_round": {
|
||||
room.buzzerState = freshBuzzerState();
|
||||
case "open_round":
|
||||
room.buzzerState = freshBuzzer();
|
||||
room.buzzerState.roundOpen = true;
|
||||
broadcastToRoom(room, { type: "round_open", room: getPublicRoom(room) });
|
||||
send(ws, { type: "round_open", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "round_open", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
case "close_round": {
|
||||
|
||||
case "close_round":
|
||||
room.buzzerState.roundOpen = false;
|
||||
broadcastToRoom(room, { type: "round_closed", room: getPublicRoom(room) });
|
||||
send(ws, { type: "round_closed", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "round_closed", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
case "reset_buzzer": {
|
||||
room.buzzerState = freshBuzzerState();
|
||||
broadcastToRoom(room, { type: "buzzer_reset", room: getPublicRoom(room) });
|
||||
send(ws, { type: "buzzer_reset", room: getPublicRoom(room) });
|
||||
|
||||
case "reset_buzzer":
|
||||
room.buzzerState = freshBuzzer();
|
||||
broadcast(room, { type: "buzzer_reset", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
|
||||
case "update_settings": {
|
||||
const s = msg.settings ?? {};
|
||||
const st = room.settings;
|
||||
if (typeof s.teamMode === "boolean") st.teamMode = s.teamMode;
|
||||
if (typeof s.numTeams === "number") st.numTeams = Math.min(8, Math.max(2, s.numTeams | 0));
|
||||
if (Array.isArray(s.teamNames)) st.teamNames = s.teamNames.slice(0, 8).map((n: unknown) => sanitize(n, 32));
|
||||
if (typeof s.allowRebuzz === "boolean") st.allowRebuzz = s.allowRebuzz;
|
||||
if (typeof s.buzzerLockout === "boolean") st.buzzerLockout = s.buzzerLockout;
|
||||
if (typeof s.showOrder === "boolean") st.showOrder = s.showOrder;
|
||||
if (typeof s.pointsCorrect === "number") st.pointsCorrect = Math.min(10000, Math.max(0, s.pointsCorrect | 0));
|
||||
if (typeof s.pointsIncorrect === "number") st.pointsIncorrect = Math.min(10000, Math.max(0, s.pointsIncorrect | 0));
|
||||
if (typeof s.pointsNeg === "boolean") st.pointsNeg = s.pointsNeg;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "settings_updated", room: getPublicRoom(room) });
|
||||
if (s.mode === "individual" || s.mode === "teams") st.mode = s.mode;
|
||||
if (typeof s.numTeams === "number") st.numTeams = Math.min(8, Math.max(2, s.numTeams | 0));
|
||||
if (Array.isArray(s.teamNames)) st.teamNames = s.teamNames.slice(0, 8).map((n: unknown) => sanitize(n, 32));
|
||||
if (typeof s.playerPickTeam === "boolean") st.playerPickTeam = s.playerPickTeam;
|
||||
if (typeof s.showBuzzOrder === "boolean") st.showBuzzOrder = s.showBuzzOrder;
|
||||
if (typeof s.buzzerLockout === "boolean") st.buzzerLockout = s.buzzerLockout;
|
||||
if (typeof s.timerSeconds === "number") st.timerSeconds = Math.min(300, Math.max(0, s.timerSeconds | 0));
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
tx(ws, { type: "settings_updated", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
|
||||
case "assign_team": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p) {
|
||||
p.teamIndex = typeof msg.teamIndex === "number" ? Math.max(0, Math.min(7, msg.teamIndex | 0)) : null;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "adjust_score": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p && typeof msg.delta === "number") {
|
||||
p.score = Math.max(-99999, Math.min(99999, p.score + (msg.delta | 0)));
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "set_score": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p && typeof msg.score === "number") {
|
||||
p.score = Math.max(-99999, Math.min(99999, msg.score | 0));
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mark_correct": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p) {
|
||||
p.score += room.settings.pointsCorrect;
|
||||
room.buzzerState = freshBuzzerState();
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mark_incorrect": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p && room.settings.pointsNeg) {
|
||||
p.score -= room.settings.pointsIncorrect;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
p.teamIndex = typeof msg.teamIndex === "number"
|
||||
? Math.max(0, Math.min(7, msg.teamIndex | 0))
|
||||
: null;
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "kick_player": {
|
||||
const p = room.players.get(sanitize(msg.playerId, 12));
|
||||
if (p) {
|
||||
if (p.ws) { try { p.ws.send(JSON.stringify({ type: "kicked" })); } catch {} }
|
||||
if (p.ws) {
|
||||
try { p.ws.send(JSON.stringify({ type: "kicked" })); } catch {}
|
||||
wsToPlayer.delete(p.ws);
|
||||
}
|
||||
room.players.delete(p.id);
|
||||
wsToPlayer.delete(p.ws!);
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "lock_room": {
|
||||
|
||||
case "lock_room":
|
||||
room.locked = msg.locked === true;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
case "reset_scores": {
|
||||
for (const p of room.players.values()) p.score = 0;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
|
||||
case "lock_teams":
|
||||
room.teamLocked = msg.locked === true;
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
case "reset_teams": {
|
||||
|
||||
case "reset_teams":
|
||||
for (const p of room.players.values()) p.teamIndex = null;
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
send(ws, { type: "room_update", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
break;
|
||||
}
|
||||
case "end_room": {
|
||||
broadcastToRoom(room, { type: "room_ended" });
|
||||
for (const p of room.players.values()) {
|
||||
if (p.ws) wsToPlayer.delete(p.ws);
|
||||
}
|
||||
|
||||
case "end_room":
|
||||
broadcast(room, { type: "room_ended" });
|
||||
for (const p of room.players.values()) if (p.ws) wsToPlayer.delete(p.ws);
|
||||
room.modWs = null;
|
||||
rooms.delete(room.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleClose(ws: WS) {
|
||||
// mod disconnect?
|
||||
// mod disconnect
|
||||
for (const room of rooms.values()) {
|
||||
if (room.modWs === ws) {
|
||||
room.modWs = null;
|
||||
broadcastToRoom(room, { type: "mod_disconnected" });
|
||||
broadcast(room, { type: "mod_disconnected" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -282,8 +246,7 @@ export function handleClose(ws: WS) {
|
||||
if (room) {
|
||||
const p = room.players.get(ctx.playerId);
|
||||
if (p) { p.isConnected = false; p.ws = null; }
|
||||
broadcastToRoom(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
sendToMod(room, { type: "room_update", room: getPublicRoom(room) });
|
||||
broadcast(room, { type: "room_update", room: publicRoom(room) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user