Compare commits

..

2 Commits

Author SHA1 Message Date
010b5a593e Update rooms.ts for numeric player IDs
- Changed Player.id from string to number
- Removed name field from Player interface
- Updated publicRoom to not expose name field
- Players are now identified by numeric IDs only
2026-04-08 18:48:07 -05:00
f5af850cf3 Remove player names, use numeric IDs
- Removed player name input from join page
- Changed player IDs to numeric (1-99999999)
- Auto-generate unique IDs on join
- Store only ID in localStorage (no name)
- Display #ID format in all player displays
- Updated server to validate numeric IDs
- Removed playerName from buzz events
- Greek team names preserved for auto-assignment
2026-04-08 18:47:43 -05:00
5 changed files with 108 additions and 246 deletions

31
.replit
View File

@@ -1,31 +0,0 @@
modules = ["bun-1.3"]
[agent]
expertMode = true
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Start application"
[[workflows.workflow]]
name = "Start application"
author = "agent"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "PORT=5000 bun --hot run src/server.ts"
waitForPort = 5000
[workflows.workflow.metadata]
outputType = "webview"
[[ports]]
localPort = 5000
externalPort = 80

View File

@@ -50,12 +50,16 @@
<div class="land-card">
<div class="land-card-icon"></div>
<h2>// JOIN A SESSION</h2>
<p>Enter a room code to join an existing session. You'll be assigned a numeric ID.</p>
<p>Enter a room code and your name to join an existing session.</p>
<div class="field">
<label>ROOM CODE</label>
<input id="ji-code" maxlength="8" placeholder="XXXXXX" style="letter-spacing:5px;text-transform:uppercase;font-size:20px;"
oninput="this.value=this.value.toUpperCase()" />
</div>
<div class="field">
<label>YOUR NAME</label>
<input id="ji-name" maxlength="24" placeholder="Enter name…" />
</div>
<button class="btn btn-g btn-full" onclick="joinRoom()">JOIN ROOM →</button>
</div>
</div>
@@ -177,21 +181,16 @@
onclick="this.select()" />
<span class="side-hint">SEC</span>
</div>
<div class="tog-row">
<div class="lbl">LINK TIMER</div>
<label class="tog">
<input type="checkbox" id="timer-tog" onchange="toggleTimerFromSwitch()" /><span class="tog-track"></span>
</label>
</div>
</div>
</div>
<!-- BUZZER CONTROLS -->
<div class="side-sec">
<div class="side-label">ROUND CONTROL</div>
<div class="side-label">BUZZER CONTROLS</div>
<div class="side-btn-group">
<button class="btn btn-g btn-full" id="mod-round-btn" onclick="toggleRound()">▶ OPEN ROUND</button>
<button class="btn btn-ghost btn-full" id="mod-resume-btn" style="display:none;" onclick="resumeRound()">▶ RESUME ROUND</button>
<button class="btn btn-g btn-full" onclick="ws_send({type:'open_round'})">▶ OPEN ROUND</button>
<button class="btn btn-ghost btn-full" onclick="ws_send({type:'close_round'})">■ CLOSE ROUND</button>
<button class="btn btn-yellow btn-full" onclick="ws_send({type:'reset_buzzer'})">↺ RESET BUZZER</button>
</div>
</div>
@@ -232,7 +231,7 @@
<div id="tab-buzzer">
<div class="panel">
<div class="panel-title">BUZZ ORDER</div>
<div class="empty" id="mod-bz-empty">No buzzes</div>
<div class="empty" id="mod-bz-empty">No buzzes — open a round to begin.</div>
<div class="buzz-list" id="mod-bz-list"></div>
</div>
</div>
@@ -332,8 +331,14 @@
</div>
<div class="buzz-wrap">
<div class="buzz-ripple" id="buzz-ripple" style="display:none;">
<div class="buzz-ring"></div>
<div class="buzz-ring"></div>
<div class="buzz-ring"></div>
</div>
<button id="buzz-btn" class="s-closed" disabled onclick="doBuzz()">BUZZ</button>
<div class="buzz-status" id="buzz-status">WAITING FOR ROUND</div>
<div class="spacebar-hint" id="spacebar-hint">SPACEBAR TO BUZZ</div>
</div>
<div class="p-panel">

View File

@@ -45,7 +45,7 @@ function schedReconn(){
const d=Math.min(8000,500*Math.pow(1.5,reconnAttempts++));
reconnTimer=setTimeout(()=>{
if(role==='mod'){const m=loadMod();if(m)connect(()=>ws_send({type:'mod_rejoin',roomId:m.id,modSecret:m.s}));}
else if(role==='player'&&myId){const p=loadPlay();if(p)connect(()=>ws_send({type:'join_room',roomId:p.rid,playerName:p.name,playerId:p.pid}));}
else if(role==='player'&&myId){const p=loadPlay();if(p)connect(()=>ws_send({type:'join_room',roomId:p.rid,playerId:p.pid}));}
else connect(null);
},d);
}
@@ -73,37 +73,29 @@ function handle(msg){
break;
case 'room_update':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayer();
if(role==='mod')renderMod();else renderPlayer();
break;
case 'settings_updated':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();renderModSettings();}else renderPlayer();
break;
case 'new_round':
toast('NEW ROUND STARTED','ok');
if(role==='mod'){renderMod();renderModSettings();}else renderPlayer();
break;
case 'round_open':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();}
else{renderPlayerBuzzer();startPlayerTimer();}
// If timer is enabled and this is a fresh open, load the timer
if(room.settings.timerSeconds>0 && modTimerRemaining===0 && !modTimerRunning){
modTimerRemaining=room.settings.timerSeconds;
renderModTimerDisplay();
}
if(role==='mod')renderMod();
else{renderPlayerBuzzer();startPlayerTimer();toast('ROUND OPEN','ok');}
break;
case 'round_closed':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();toast('ROUND CLOSED','warn');}
else{renderPlayerBuzzer();stopPlayerTimer();}
if(role==='mod')renderMod();
else{renderPlayerBuzzer();stopPlayerTimer();toast('ROUND CLOSED','warn');}
break;
case 'buzzer_reset':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
if(role==='mod')renderMod();else renderPlayerBuzzer();
break;
case 'buzz_event':
room=msg.room;
if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
if(role==='mod')renderModBuzz(msg);else{renderPlayerBuzzer();addFeed(msg);}
break;
case 'buzz_rejected':
toast(msg.reason,'warn');break;
@@ -180,7 +172,8 @@ function goSetup(){renderSetupTeamNames();showScr('s-setup');}
function joinRoom(){
const code=document.getElementById('ji-code').value.trim().toUpperCase();
if(!code){toast('Enter room code','err');return;}
connect(()=>ws_send({type:'join_room',roomId:code}));
const autoId=Math.floor(Math.random()*99999999)+1;
connect(()=>ws_send({type:'join_room',roomId:code,playerId:autoId}));
}
function openRejoin(){
const m=loadMod();if(!m)return;
@@ -280,6 +273,7 @@ function createRoom(){
// MOD TIMER
// ══════════════════════════════════════════════════════
function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);}
function modTimerLoad(){
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
modTimerRunning=false;
@@ -322,41 +316,6 @@ function modTimerToggle(){
}
}
function toggleTimer(){
modTimerToggle();
}
function modTimerToggle(){
if(modTimerRunning){
clearInterval(modTimerInterval);modTimerRunning=false;
document.getElementById('btn-timer-ss').textContent='START';
broadcastTimerToPlayers(modTimerRemaining,false);
} else {
if(modTimerRemaining<=0)modTimerLoad();
modTimerRunning=true;
document.getElementById('btn-timer-ss').textContent='PAUSE';
broadcastTimerToPlayers(modTimerRemaining,true);
modTimerInterval=setInterval(()=>{
modTimerRemaining--;
renderModTimerDisplay();
broadcastTimerToPlayers(modTimerRemaining,true);
if(modTimerRemaining<=0){
clearInterval(modTimerInterval);modTimerRunning=false;
document.getElementById('btn-timer-ss').textContent='START';
ws_send({type:'close_round'});
toast('TIME UP — round closed','warn');
if(typeof gsap!=='undefined'){
gsap.to('#mod-timer-disp',{scale:1.08,duration:0.1,yoyo:true,repeat:3,ease:'power2.inOut'});
}
}
},1000);
}
}
function toggleTimerFromSwitch(){
modTimerToggle();
}
function renderModTimerDisplay(){
const el=document.getElementById('mod-timer-disp');
const s=modTimerRemaining;
@@ -364,23 +323,6 @@ function renderModTimerDisplay(){
el.className='timer-digits'+(s<=5?' danger':s<=10?' warn':'');
}
function modTimerLoad(){
const sec=parseInt(document.getElementById('mod-timer-set').value)||30;
modTimerRemaining=sec;
modTimerRunning=false;
clearInterval(modTimerInterval);
document.getElementById('btn-timer-ss').textContent='START';
renderModTimerDisplay();
}
function modTimerReset(){
clearInterval(modTimerInterval);
modTimerRunning=false;
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
renderModTimerDisplay();
broadcastTimerToPlayers(modTimerRemaining,false);
}
function broadcastTimerToPlayers(sec,running){
try{
const bc=new BroadcastChannel('buzzer_timer');
@@ -389,78 +331,6 @@ function broadcastTimerToPlayers(sec,running){
}catch{}
}
// ══════════════════════════════════════════════════════
// MOD ROUND CONTROL
// ══════════════════════════════════════════════════════
function toggleRound(){
if(room.buzzerState.roundOpen){
ws_send({type:'close_round'});
// If linked (toggle ON) and timer running, pause it
if(document.getElementById('timer-tog').checked && modTimerRunning){
modTimerToggle();
}
} else {
ws_send({type:'open_round'});
// If linked (toggle ON) and timer stopped, reset and start it
if(document.getElementById('timer-tog').checked && !modTimerRunning){
modTimerLoad();
modTimerRunning=true;
document.getElementById('btn-timer-ss').textContent='PAUSE';
modTimerInterval=setInterval(()=>{
modTimerRemaining--;
renderModTimerDisplay();
broadcastTimerToPlayers(modTimerRemaining,true);
if(modTimerRemaining<=0){
clearInterval(modTimerInterval);modTimerRunning=false;
document.getElementById('btn-timer-ss').textContent='START';
ws_send({type:'close_round'});
toast('TIME UP — round closed','warn');
}
},1000);
}
}
}
function resumeRound(){
ws_send({type:'resume_round'});
// If linked (toggle ON) and timer stopped, resume it (preserve current value)
if(document.getElementById('timer-tog').checked && !modTimerRunning){
modTimerRunning=true;
document.getElementById('btn-timer-ss').textContent='PAUSE';
broadcastTimerToPlayers(modTimerRemaining,true);
modTimerInterval=setInterval(()=>{
modTimerRemaining--;
renderModTimerDisplay();
broadcastTimerToPlayers(modTimerRemaining,true);
if(modTimerRemaining<=0){
clearInterval(modTimerInterval);modTimerRunning=false;
document.getElementById('btn-timer-ss').textContent='START';
ws_send({type:'close_round'});
toast('TIME UP — round closed','warn');
}
},1000);
}
}
// ══════════════════════════════════════════════════════
// TIMER
// ══════════════════════════════════════════════════════
function renderRoundButtons(){
const btn=document.getElementById('mod-round-btn');
const resumeBtn=document.getElementById('mod-resume-btn');
if(room.buzzerState.roundOpen){
btn.innerHTML='■ PAUSE ROUND';
btn.className='btn btn-red btn-full';
resumeBtn.style.display='none';
} else {
btn.innerHTML='▶ OPEN ROUND';
btn.className='btn btn-g btn-full';
resumeBtn.style.display='block';
}
}
// ══════════════════════════════════════════════════════
// MOD RENDER
// ══════════════════════════════════════════════════════
@@ -482,7 +352,6 @@ function renderMod(){
renderModBuzz(null);
renderModPlayerList();
renderModTeams();
renderRoundButtons();
}
function renderModBuzz(evt){
@@ -504,7 +373,7 @@ function renderModBuzz(evt){
div.innerHTML=`
<div class="bz-rank ${idx===0?'first':''}">${idx+1}</div>
<div class="bz-info">
<div class="bz-name">${pid}</div>
<div class="bz-name">${esc(p.name)}</div>
${teamName?`<div class="bz-team" style="color:${color}">${esc(teamName)}</div>`:''}
${ms?`<div class="bz-ms">${ms} after first</div>`:''}
</div>
@@ -541,7 +410,7 @@ function renderModPlayerList(){
row.className='pl-row'+(p.isConnected?'':' offline');
row.innerHTML=`
<div class="pl-info" style="flex:1;min-width:0;">
<div class="pl-name">${p.id} ${p.isConnected?'':`<span class="tag tag-red" style="font-size:9px;padding:2px 6px;">OFFLINE</span>`}</div>
<div class="pl-name">${esc(p.name)} <span class="tag tag-y" style="font-size:12px;padding:3px 8px;background:rgba(249,226,175,0.15);color:var(--yellow);border:none;">#${p.playerId}</span> ${p.isConnected?'':`<span class="tag tag-red" style="font-size:9px;padding:2px 6px;">OFFLINE</span>`}</div>
${teamName?`<div class="pl-meta" style="color:${color}">${esc(teamName)}</div>`:'<div class="pl-meta">No team</div>'}
${teamSel}
</div>
@@ -567,7 +436,7 @@ function renderModTeams(){
card.innerHTML=`
<div class="tc-n" style="color:${color}">${esc(name)}</div>
<div class="tc-c" style="color:${color}">${members.length}</div>
<div class="tc-m">${members.map(p=>p.id).join('<br>')||'—'}</div>
<div class="tc-m">${members.map(p=>`#${p.playerId}`).join('<br>')||'—'}</div>
`;
grid.appendChild(card);
if(typeof gsap!=='undefined'){
@@ -585,8 +454,6 @@ function renderModSettings(){
document.getElementById('ls-numteams').value=s.numTeams;
segActivate('ls-seg-mode',s.mode);
renderLiveTeamNames();
renderRoundButtons();
renderModTimerDisplay();
}
function segActivate(groupId,val){
@@ -659,7 +526,7 @@ function renderPlayer(){
if(!room)return;
document.getElementById('p-code').textContent=room.id;
const me=room.players.find(p=>p.id===myId);
document.getElementById('p-namelbl').textContent=me?.id||'';
document.getElementById('p-namelbl').textContent=`#${me?.playerId||'?'}`;
renderTeamPicker();
renderPlayerBuzzer();
renderRoster();
@@ -682,20 +549,21 @@ function renderTeamPicker(){
btn.className='team-btn'+(isMine?' mine':'');
btn.style.borderColor=isMine?color:'var(--border2)';
btn.style.color=isMine?color:'var(--text)';
btn.style.padding='16px';
btn.style.fontSize='20px';
btn.style.fontWeight='600';
btn.style.cursor='pointer';
btn.innerHTML=`<div style="font-size:24px;">${esc(s.teamNames[i]??greekName(i))}</div><div style="font-size:14px;color:var(--dim);margin-top:4px;">${members.length} player${members.length!==1?'s':''}</div>`;
btn.innerHTML=`<div>${esc(s.teamNames[i]??greekName(i))}</div><div class="tb-count">${members.length} player${members.length!==1?'s':''}</div>`;
btn.onclick=()=>ws_send({type:'pick_team',teamIndex:i});
grid.appendChild(btn);
if(typeof gsap!=='undefined'){
gsap.fromTo(btn,{opacity:0,y:10},{opacity:1,y:0,duration:0.3,ease:'power2.out',delay:i*0.04});
}
}
}
let _lastBuzzState='';
function renderPlayerBuzzer(){
if(!room)return;
const btn=document.getElementById('buzz-btn');
const sts=document.getElementById('buzz-status');
const ripple=document.getElementById('buzz-ripple');
const bz=room.buzzerState;
const already=bz.buzzOrder.includes(myId);
const isFirst=bz.buzzOrder[0]===myId;
@@ -705,33 +573,49 @@ function renderPlayerBuzzer(){
if(!bz.roundOpen){
newState='closed';
btn.className='s-closed';btn.disabled=true;sts.textContent='WAITING FOR ROUND';
ripple.style.display='none';
if(hint)hint.classList.remove('visible');
} else if(isFirst){
newState='first';
btn.className='s-first';btn.disabled=false;sts.textContent='YOU BUZZED FIRST!';
ripple.style.display='flex';
if(hint)hint.classList.remove('visible');
} else if(already){
const pos=bz.buzzOrder.indexOf(myId)+1;
newState='buzzed';
btn.className='s-buzzed';btn.disabled=true;sts.textContent='BUZZED — #'+pos+' IN ORDER';
ripple.style.display='none';
if(hint)hint.classList.remove('visible');
} else if(room.settings.buzzerLockout&&bz.buzzOrder.length>0){
newState='locked';
btn.className='s-locked';btn.disabled=true;sts.textContent='BUZZER LOCKED OUT';
ripple.style.display='none';
if(hint)hint.classList.remove('visible');
} else {
newState='open';
btn.className='s-open';btn.disabled=false;sts.textContent='ROUND OPEN — BUZZ!';
ripple.style.display='flex';
if(hint)hint.classList.add('visible');
}
// Animate state transitions
if(newState!==_lastBuzzState && typeof gsap!=='undefined'){
if(newState==='first'){
gsap.fromTo(btn,{scale:0.88},{scale:1,duration:0.5,ease:'back.out(2.5)'});
gsap.fromTo(sts,{opacity:0,y:6},{opacity:1,y:0,duration:0.4,ease:'power2.out'});
} else if(newState==='open' && _lastBuzzState==='closed'){
gsap.fromTo(btn,{scale:0.95},{scale:1,duration:0.4,ease:'back.out(1.5)'});
} else if(newState==='closed'){
gsap.to(btn,{scale:0.97,duration:0.15,yoyo:true,repeat:1,ease:'power2.inOut'});
}
}
_lastBuzzState=newState;
}
function renderRoster(){
if(!room)return;
const el=document.getElementById('p-roster');
if(room.players.length===0){el.innerHTML='<div style="font-size:18px;color:var(--dim);padding:20px;">No players yet.</div>';return;}
if(room.players.length===0){el.innerHTML='<div style="font-size:13px;color:var(--dim);padding:12px;letter-spacing:1px;">No players yet.</div>';return;}
el.innerHTML='';
room.players.forEach(p=>{
const isMe=p.id===myId;
@@ -740,9 +624,9 @@ function renderRoster(){
const row=document.createElement('div');
row.className='roster-row'+(isMe?' roster-me':'');
row.innerHTML=`
<div class="roster-dot" style="width:16px;height:16px;border-radius:50%;background:${p.isConnected?(isMe?'var(--g)':color):'var(--border2)'}"></div>
<div style="font-size:20px;font-weight:700;">#${p.id}</div>
${teamName?`<div style="font-size:14px;color:${color};margin-top:2px;">${esc(teamName)}</div>`:''}
<div class="roster-dot" style="background:${p.isConnected?(isMe?'var(--g)':color):'var(--border2)'}"></div>
<div style="flex:1">${esc(p.name)} ${isMe?`<span style="font-size:11px;color:var(--dim);letter-spacing:1px;">(#${p.playerId}) (YOU)</span>`:''}</div>
${teamName?`<div style="font-size:12px;color:${color};letter-spacing:0.5px;">${esc(teamName)}</div>`:''}
`;
el.appendChild(row);
});
@@ -753,16 +637,30 @@ function addFeed(evt){
const isFirst=evt.buzzOrder?.[0]===evt.playerId;
const color=evt.teamIndex!==null?teamColor(evt.teamIndex):'var(--g)';
const teamStr=(room?.settings.mode==='teams'&&evt.teamIndex!==null)?` [${esc(room.settings.teamNames[evt.teamIndex]??'')}]`:'';
const playerStr=`#${evt.playerId}`;
const div=document.createElement('div');
div.className='feed-entry'+(isFirst?' first':'');
div.style.borderLeftColor=isFirst?'var(--yellow)':color;
div.innerHTML=`<strong>#${evt.playerId}</strong>${teamStr} buzzed${isFirst?' <span style="color:var(--yellow);font-weight:700;"> — FIRST!</span>':''}`;
div.innerHTML=`<strong>${playerStr}</strong>${teamStr} buzzed${isFirst?' <span style="color:var(--yellow);font-weight:700;"> — FIRST!</span>':''}`;
feed.prepend(div);
if(typeof gsap!=='undefined'){
gsap.fromTo(div,
{opacity:0,x:-20,borderLeftWidth:'3px'},
{opacity:1,x:0,duration:0.38,ease:'power3.out'}
);
}
while(feed.children.length>30)feed.removeChild(feed.lastChild);
}
function doBuzz(){
ws_send({type:'buzz'});
// Immediate haptic-like feedback
if(typeof gsap!=='undefined'){
const btn=document.getElementById('buzz-btn');
gsap.to(btn,{scale:0.91,duration:0.08,ease:'power2.in',onComplete:()=>{
gsap.to(btn,{scale:1,duration:0.25,ease:'back.out(2.5)'});
}});
}
}
// ══════════════════════════════════════════════════════

View File

@@ -11,8 +11,7 @@ export interface RoomSettings {
}
export interface Player {
id: string;
name: string;
id: number;
teamIndex: number | null;
ws: ServerWebSocket<unknown> | null;
isConnected: boolean;
@@ -81,7 +80,6 @@ export function publicRoom(room: Room) {
},
players: Array.from(room.players.values()).map(p => ({
id: p.id,
name: p.name,
teamIndex: p.teamIndex,
isConnected: p.isConnected,
})),

View File

@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
import {
rooms, wsToPlayer,
Room, Player,
genId, greekName, sanitize, freshBuzzer, publicRoom, broadcast, toMod,
sanitize, freshBuzzer, publicRoom, broadcast, toMod,
} from "./rooms";
type WS = ServerWebSocket<unknown>;
@@ -65,19 +65,20 @@ export function handleMessage(ws: WS, raw: string) {
if (!room) { er(ws, "Room not found"); return; }
if (room.locked) { er(ws, "Room is locked"); return; }
// Calculate numeric ID based on join order
const joinCount = room.players.size + 1;
const playerId = joinCount.toString();
const name = sanitize(msg.playerName ?? "", 24) || ""; // Keep name for compatibility but don't use it
const existingId = sanitize(msg.playerId ?? "", 12);
let player: Player | undefined;
if (playerId && room.players.has(playerId)) {
player = room.players.get(playerId)!;
player.ws = ws; player.isConnected = true; player.name = name;
if (existingId && existingId.match(/^\d+$/)) {
if (room.players.has(existingId)) {
player = room.players.get(existingId)!;
player.ws = ws; player.isConnected = true;
} else {
player = { id: playerId, name, teamIndex: null, ws, isConnected: true };
room.players.set(playerId, player);
er(ws, "Invalid player ID"); return;
}
} else {
const autoId = Math.floor(Math.random() * 99999999) + 1;
player = { id: autoId, teamIndex: null, ws, isConnected: true };
room.players.set(player.id, player);
}
wsToPlayer.set(ws, { roomId: room.id, playerId: player.id });
@@ -121,8 +122,8 @@ export function handleMessage(ws: WS, raw: string) {
const p = room.players.get(ctx.playerId)!;
const pubOrder = room.settings.showBuzzOrder ? bz.buzzOrder : [bz.buzzOrder[0]];
broadcast(room, { type: "buzz_event", playerId: ctx.playerId, playerName: p.name, teamIndex: p.teamIndex, buzzOrder: pubOrder, room: publicRoom(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) });
broadcast(room, { type: "buzz_event", playerId: ctx.playerId, teamIndex: p.teamIndex, buzzOrder: pubOrder, room: publicRoom(room) });
toMod(room, { type: "buzz_event", playerId: ctx.playerId, teamIndex: p.teamIndex, buzzOrder: bz.buzzOrder, buzzTimes: Object.fromEntries(bz.buzzTimes), room: publicRoom(room) });
return;
}
@@ -135,15 +136,6 @@ export function handleMessage(ws: WS, raw: string) {
room.buzzerState = freshBuzzer();
room.buzzerState.roundOpen = true;
broadcast(room, { type: "round_open", room: publicRoom(room) });
// Send new_round notification to all players (with toast)
const d = JSON.stringify({ type: "new_round" });
for (const p of room.players.values()) if (p.ws && p.isConnected) try { p.ws.send(d); } catch {}
if (room.modWs) try { room.modWs.send(d); } catch {}
break;
case "resume_round":
// Open round WITHOUT clearing existing buzzes (for accidental close recovery)
room.buzzerState.roundOpen = true;
broadcast(room, { type: "round_open", room: publicRoom(room) });
break;
case "close_round":
room.buzzerState.roundOpen = false;