Compare commits
5 Commits
main
...
da28e052bb
| Author | SHA1 | Date | |
|---|---|---|---|
| da28e052bb | |||
|
|
ea24923c1d | ||
|
|
02762acc59 | ||
|
|
402311e6ec | ||
|
|
2d94c769a4 |
31
.replit
31
.replit
@@ -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
|
|
||||||
@@ -50,12 +50,16 @@
|
|||||||
<div class="land-card">
|
<div class="land-card">
|
||||||
<div class="land-card-icon">⬜</div>
|
<div class="land-card-icon">⬜</div>
|
||||||
<h2>// JOIN A SESSION</h2>
|
<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">
|
<div class="field">
|
||||||
<label>ROOM CODE</label>
|
<label>ROOM CODE</label>
|
||||||
<input id="ji-code" maxlength="8" placeholder="XXXXXX" style="letter-spacing:5px;text-transform:uppercase;font-size:20px;"
|
<input id="ji-code" maxlength="8" placeholder="XXXXXX" style="letter-spacing:5px;text-transform:uppercase;font-size:20px;"
|
||||||
oninput="this.value=this.value.toUpperCase()" />
|
oninput="this.value=this.value.toUpperCase()" />
|
||||||
</div>
|
</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>
|
<button class="btn btn-g btn-full" onclick="joinRoom()">JOIN ROOM →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,21 +181,16 @@
|
|||||||
onclick="this.select()" />
|
onclick="this.select()" />
|
||||||
<span class="side-hint">SEC</span>
|
<span class="side-hint">SEC</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- BUZZER CONTROLS -->
|
<!-- BUZZER CONTROLS -->
|
||||||
<div class="side-sec">
|
<div class="side-sec">
|
||||||
<div class="side-label">ROUND CONTROL</div>
|
<div class="side-label">BUZZER CONTROLS</div>
|
||||||
<div class="side-btn-group">
|
<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-g btn-full" onclick="ws_send({type:'open_round'})">▶ 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-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -232,7 +231,7 @@
|
|||||||
<div id="tab-buzzer">
|
<div id="tab-buzzer">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">BUZZ ORDER</div>
|
<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 class="buzz-list" id="mod-bz-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,8 +331,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buzz-wrap">
|
<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>
|
<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="buzz-status" id="buzz-status">WAITING FOR ROUND</div>
|
||||||
|
<div class="spacebar-hint" id="spacebar-hint">SPACEBAR TO BUZZ</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-panel">
|
<div class="p-panel">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ let playerTimerRemaining=0,playerTimerInterval=null;
|
|||||||
const saveMod=(id,s)=>localStorage.setItem('mod',JSON.stringify({id,s}));
|
const saveMod=(id,s)=>localStorage.setItem('mod',JSON.stringify({id,s}));
|
||||||
const loadMod=()=>{try{return JSON.parse(localStorage.getItem('mod')||'null');}catch{return null;}};
|
const loadMod=()=>{try{return JSON.parse(localStorage.getItem('mod')||'null');}catch{return null;}};
|
||||||
const clearMod=()=>{localStorage.removeItem('mod');document.getElementById('rejoin-bar').style.display='none';};
|
const clearMod=()=>{localStorage.removeItem('mod');document.getElementById('rejoin-bar').style.display='none';};
|
||||||
const savePlay=(rid,pid)=>localStorage.setItem('play',JSON.stringify({rid,pid}));
|
const savePlay=(rid,pid,name)=>localStorage.setItem('play',JSON.stringify({rid,pid,name}));
|
||||||
const loadPlay=()=>{try{return JSON.parse(localStorage.getItem('play')||'null');}catch{return null;}};
|
const loadPlay=()=>{try{return JSON.parse(localStorage.getItem('play')||'null');}catch{return null;}};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
@@ -68,42 +68,34 @@ function handle(msg){
|
|||||||
break;
|
break;
|
||||||
case 'joined':
|
case 'joined':
|
||||||
myId=msg.playerId;room=msg.room;role='player';
|
myId=msg.playerId;room=msg.room;role='player';
|
||||||
savePlay(room.id,myId);
|
savePlay(room.id,myId,document.getElementById('ji-name').value||loadPlay()?.name||'');
|
||||||
showScr('s-player');renderPlayer();
|
showScr('s-player');renderPlayer();
|
||||||
break;
|
break;
|
||||||
case 'room_update':
|
case 'room_update':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayer();
|
if(role==='mod')renderMod();else renderPlayer();
|
||||||
break;
|
break;
|
||||||
case 'settings_updated':
|
case 'settings_updated':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();renderModSettings();}else renderPlayer();
|
if(role==='mod'){renderMod();renderModSettings();}else renderPlayer();
|
||||||
break;
|
|
||||||
case 'new_round':
|
|
||||||
toast('NEW ROUND STARTED','ok');
|
|
||||||
break;
|
break;
|
||||||
case 'round_open':
|
case 'round_open':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}
|
if(role==='mod')renderMod();
|
||||||
else{renderPlayerBuzzer();startPlayerTimer();}
|
else{renderPlayerBuzzer();startPlayerTimer();toast('ROUND OPEN','ok');}
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'round_closed':
|
case 'round_closed':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();toast('ROUND CLOSED','warn');}
|
if(role==='mod')renderMod();
|
||||||
else{renderPlayerBuzzer();stopPlayerTimer();}
|
else{renderPlayerBuzzer();stopPlayerTimer();toast('ROUND CLOSED','warn');}
|
||||||
break;
|
break;
|
||||||
case 'buzzer_reset':
|
case 'buzzer_reset':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
|
if(role==='mod')renderMod();else renderPlayerBuzzer();
|
||||||
break;
|
break;
|
||||||
case 'buzz_event':
|
case 'buzz_event':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
|
if(role==='mod')renderModBuzz(msg);else{renderPlayerBuzzer();addFeed(msg);}
|
||||||
break;
|
break;
|
||||||
case 'buzz_rejected':
|
case 'buzz_rejected':
|
||||||
toast(msg.reason,'warn');break;
|
toast(msg.reason,'warn');break;
|
||||||
@@ -179,8 +171,10 @@ function setConn(on){
|
|||||||
function goSetup(){renderSetupTeamNames();showScr('s-setup');}
|
function goSetup(){renderSetupTeamNames();showScr('s-setup');}
|
||||||
function joinRoom(){
|
function joinRoom(){
|
||||||
const code=document.getElementById('ji-code').value.trim().toUpperCase();
|
const code=document.getElementById('ji-code').value.trim().toUpperCase();
|
||||||
|
const name=document.getElementById('ji-name').value.trim();
|
||||||
if(!code){toast('Enter room code','err');return;}
|
if(!code){toast('Enter room code','err');return;}
|
||||||
connect(()=>ws_send({type:'join_room',roomId:code}));
|
if(!name){toast('Enter your name','err');return;}
|
||||||
|
connect(()=>ws_send({type:'join_room',roomId:code,playerName:name}));
|
||||||
}
|
}
|
||||||
function openRejoin(){
|
function openRejoin(){
|
||||||
const m=loadMod();if(!m)return;
|
const m=loadMod();if(!m)return;
|
||||||
@@ -280,6 +274,7 @@ function createRoom(){
|
|||||||
// MOD TIMER
|
// MOD TIMER
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);}
|
function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);}
|
||||||
|
|
||||||
function modTimerLoad(){
|
function modTimerLoad(){
|
||||||
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
|
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
|
||||||
modTimerRunning=false;
|
modTimerRunning=false;
|
||||||
@@ -322,41 +317,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(){
|
function renderModTimerDisplay(){
|
||||||
const el=document.getElementById('mod-timer-disp');
|
const el=document.getElementById('mod-timer-disp');
|
||||||
const s=modTimerRemaining;
|
const s=modTimerRemaining;
|
||||||
@@ -364,23 +324,6 @@ function renderModTimerDisplay(){
|
|||||||
el.className='timer-digits'+(s<=5?' danger':s<=10?' warn':'');
|
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){
|
function broadcastTimerToPlayers(sec,running){
|
||||||
try{
|
try{
|
||||||
const bc=new BroadcastChannel('buzzer_timer');
|
const bc=new BroadcastChannel('buzzer_timer');
|
||||||
@@ -389,78 +332,6 @@ function broadcastTimerToPlayers(sec,running){
|
|||||||
}catch{}
|
}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
|
// MOD RENDER
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
@@ -482,7 +353,6 @@ function renderMod(){
|
|||||||
renderModBuzz(null);
|
renderModBuzz(null);
|
||||||
renderModPlayerList();
|
renderModPlayerList();
|
||||||
renderModTeams();
|
renderModTeams();
|
||||||
renderRoundButtons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModBuzz(evt){
|
function renderModBuzz(evt){
|
||||||
@@ -504,7 +374,7 @@ function renderModBuzz(evt){
|
|||||||
div.innerHTML=`
|
div.innerHTML=`
|
||||||
<div class="bz-rank ${idx===0?'first':''}">${idx+1}</div>
|
<div class="bz-rank ${idx===0?'first':''}">${idx+1}</div>
|
||||||
<div class="bz-info">
|
<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>`:''}
|
${teamName?`<div class="bz-team" style="color:${color}">${esc(teamName)}</div>`:''}
|
||||||
${ms?`<div class="bz-ms">${ms} after first</div>`:''}
|
${ms?`<div class="bz-ms">${ms} after first</div>`:''}
|
||||||
</div>
|
</div>
|
||||||
@@ -541,7 +411,7 @@ function renderModPlayerList(){
|
|||||||
row.className='pl-row'+(p.isConnected?'':' offline');
|
row.className='pl-row'+(p.isConnected?'':' offline');
|
||||||
row.innerHTML=`
|
row.innerHTML=`
|
||||||
<div class="pl-info" style="flex:1;min-width:0;">
|
<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)} ${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>'}
|
${teamName?`<div class="pl-meta" style="color:${color}">${esc(teamName)}</div>`:'<div class="pl-meta">No team</div>'}
|
||||||
${teamSel}
|
${teamSel}
|
||||||
</div>
|
</div>
|
||||||
@@ -567,7 +437,7 @@ function renderModTeams(){
|
|||||||
card.innerHTML=`
|
card.innerHTML=`
|
||||||
<div class="tc-n" style="color:${color}">${esc(name)}</div>
|
<div class="tc-n" style="color:${color}">${esc(name)}</div>
|
||||||
<div class="tc-c" style="color:${color}">${members.length}</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=>esc(p.name)).join('<br>')||'—'}</div>
|
||||||
`;
|
`;
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
if(typeof gsap!=='undefined'){
|
if(typeof gsap!=='undefined'){
|
||||||
@@ -585,8 +455,6 @@ function renderModSettings(){
|
|||||||
document.getElementById('ls-numteams').value=s.numTeams;
|
document.getElementById('ls-numteams').value=s.numTeams;
|
||||||
segActivate('ls-seg-mode',s.mode);
|
segActivate('ls-seg-mode',s.mode);
|
||||||
renderLiveTeamNames();
|
renderLiveTeamNames();
|
||||||
renderRoundButtons();
|
|
||||||
renderModTimerDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function segActivate(groupId,val){
|
function segActivate(groupId,val){
|
||||||
@@ -659,7 +527,7 @@ function renderPlayer(){
|
|||||||
if(!room)return;
|
if(!room)return;
|
||||||
document.getElementById('p-code').textContent=room.id;
|
document.getElementById('p-code').textContent=room.id;
|
||||||
const me=room.players.find(p=>p.id===myId);
|
const me=room.players.find(p=>p.id===myId);
|
||||||
document.getElementById('p-namelbl').textContent=me?.id||'';
|
document.getElementById('p-namelbl').textContent=me?.name??'';
|
||||||
renderTeamPicker();
|
renderTeamPicker();
|
||||||
renderPlayerBuzzer();
|
renderPlayerBuzzer();
|
||||||
renderRoster();
|
renderRoster();
|
||||||
@@ -682,20 +550,21 @@ function renderTeamPicker(){
|
|||||||
btn.className='team-btn'+(isMine?' mine':'');
|
btn.className='team-btn'+(isMine?' mine':'');
|
||||||
btn.style.borderColor=isMine?color:'var(--border2)';
|
btn.style.borderColor=isMine?color:'var(--border2)';
|
||||||
btn.style.color=isMine?color:'var(--text)';
|
btn.style.color=isMine?color:'var(--text)';
|
||||||
btn.style.padding='16px';
|
btn.innerHTML=`<div>${esc(s.teamNames[i]??greekName(i))}</div><div class="tb-count">${members.length} player${members.length!==1?'s':''}</div>`;
|
||||||
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.onclick=()=>ws_send({type:'pick_team',teamIndex:i});
|
btn.onclick=()=>ws_send({type:'pick_team',teamIndex:i});
|
||||||
grid.appendChild(btn);
|
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(){
|
function renderPlayerBuzzer(){
|
||||||
if(!room)return;
|
if(!room)return;
|
||||||
const btn=document.getElementById('buzz-btn');
|
const btn=document.getElementById('buzz-btn');
|
||||||
const sts=document.getElementById('buzz-status');
|
const sts=document.getElementById('buzz-status');
|
||||||
|
const ripple=document.getElementById('buzz-ripple');
|
||||||
const bz=room.buzzerState;
|
const bz=room.buzzerState;
|
||||||
const already=bz.buzzOrder.includes(myId);
|
const already=bz.buzzOrder.includes(myId);
|
||||||
const isFirst=bz.buzzOrder[0]===myId;
|
const isFirst=bz.buzzOrder[0]===myId;
|
||||||
@@ -705,33 +574,49 @@ function renderPlayerBuzzer(){
|
|||||||
if(!bz.roundOpen){
|
if(!bz.roundOpen){
|
||||||
newState='closed';
|
newState='closed';
|
||||||
btn.className='s-closed';btn.disabled=true;sts.textContent='WAITING FOR ROUND';
|
btn.className='s-closed';btn.disabled=true;sts.textContent='WAITING FOR ROUND';
|
||||||
|
ripple.style.display='none';
|
||||||
if(hint)hint.classList.remove('visible');
|
if(hint)hint.classList.remove('visible');
|
||||||
} else if(isFirst){
|
} else if(isFirst){
|
||||||
newState='first';
|
newState='first';
|
||||||
btn.className='s-first';btn.disabled=false;sts.textContent='YOU BUZZED FIRST!';
|
btn.className='s-first';btn.disabled=false;sts.textContent='YOU BUZZED FIRST!';
|
||||||
|
ripple.style.display='flex';
|
||||||
if(hint)hint.classList.remove('visible');
|
if(hint)hint.classList.remove('visible');
|
||||||
} else if(already){
|
} else if(already){
|
||||||
const pos=bz.buzzOrder.indexOf(myId)+1;
|
const pos=bz.buzzOrder.indexOf(myId)+1;
|
||||||
newState='buzzed';
|
newState='buzzed';
|
||||||
btn.className='s-buzzed';btn.disabled=true;sts.textContent='BUZZED — #'+pos+' IN ORDER';
|
btn.className='s-buzzed';btn.disabled=true;sts.textContent='BUZZED — #'+pos+' IN ORDER';
|
||||||
|
ripple.style.display='none';
|
||||||
if(hint)hint.classList.remove('visible');
|
if(hint)hint.classList.remove('visible');
|
||||||
} else if(room.settings.buzzerLockout&&bz.buzzOrder.length>0){
|
} else if(room.settings.buzzerLockout&&bz.buzzOrder.length>0){
|
||||||
newState='locked';
|
newState='locked';
|
||||||
btn.className='s-locked';btn.disabled=true;sts.textContent='BUZZER LOCKED OUT';
|
btn.className='s-locked';btn.disabled=true;sts.textContent='BUZZER LOCKED OUT';
|
||||||
|
ripple.style.display='none';
|
||||||
if(hint)hint.classList.remove('visible');
|
if(hint)hint.classList.remove('visible');
|
||||||
} else {
|
} else {
|
||||||
newState='open';
|
newState='open';
|
||||||
btn.className='s-open';btn.disabled=false;sts.textContent='ROUND OPEN — BUZZ!';
|
btn.className='s-open';btn.disabled=false;sts.textContent='ROUND OPEN — BUZZ!';
|
||||||
|
ripple.style.display='flex';
|
||||||
if(hint)hint.classList.add('visible');
|
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;
|
_lastBuzzState=newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRoster(){
|
function renderRoster(){
|
||||||
if(!room)return;
|
if(!room)return;
|
||||||
const el=document.getElementById('p-roster');
|
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='';
|
el.innerHTML='';
|
||||||
room.players.forEach(p=>{
|
room.players.forEach(p=>{
|
||||||
const isMe=p.id===myId;
|
const isMe=p.id===myId;
|
||||||
@@ -740,9 +625,9 @@ function renderRoster(){
|
|||||||
const row=document.createElement('div');
|
const row=document.createElement('div');
|
||||||
row.className='roster-row'+(isMe?' roster-me':'');
|
row.className='roster-row'+(isMe?' roster-me':'');
|
||||||
row.innerHTML=`
|
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 class="roster-dot" style="background:${p.isConnected?(isMe?'var(--g)':color):'var(--border2)'}"></div>
|
||||||
<div style="font-size:20px;font-weight:700;">#${p.id}</div>
|
<div style="flex:1">${esc(p.name)}${isMe?' <span style="font-size:11px;color:var(--dim);letter-spacing:1px;">(YOU)</span>':''}</div>
|
||||||
${teamName?`<div style="font-size:14px;color:${color};margin-top:2px;">${esc(teamName)}</div>`:''}
|
${teamName?`<div style="font-size:12px;color:${color};letter-spacing:0.5px;">${esc(teamName)}</div>`:''}
|
||||||
`;
|
`;
|
||||||
el.appendChild(row);
|
el.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -756,13 +641,26 @@ function addFeed(evt){
|
|||||||
const div=document.createElement('div');
|
const div=document.createElement('div');
|
||||||
div.className='feed-entry'+(isFirst?' first':'');
|
div.className='feed-entry'+(isFirst?' first':'');
|
||||||
div.style.borderLeftColor=isFirst?'var(--yellow)':color;
|
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>${esc(evt.playerName)}</strong>${teamStr} buzzed${isFirst?' <span style="color:var(--yellow);font-weight:700;"> — FIRST!</span>':''}`;
|
||||||
feed.prepend(div);
|
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);
|
while(feed.children.length>30)feed.removeChild(feed.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
function doBuzz(){
|
function doBuzz(){
|
||||||
ws_send({type:'buzz'});
|
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)'});
|
||||||
|
}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 22px;
|
font-size: 18px;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,17 +108,17 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 32px;
|
font-size: 22px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 8px;
|
letter-spacing: 6px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-sub {
|
.logo-sub {
|
||||||
font-size: 16px;
|
font-size: 11px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hdr-r {
|
.hdr-r {
|
||||||
@@ -129,13 +129,13 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-chip {
|
.room-chip {
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
background: rgba(180,190,254,0.1);
|
background: rgba(180,190,254,0.1);
|
||||||
border: 1px solid rgba(180,190,254,0.3);
|
border: 1px solid rgba(180,190,254,0.3);
|
||||||
padding: 10px 20px;
|
padding: 6px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
display: none;
|
display: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -143,14 +143,14 @@ header {
|
|||||||
.conn-pill {
|
.conn-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
letter-spacing: 1.5px;
|
letter-spacing: 1px;
|
||||||
background: var(--s0);
|
background: var(--s0);
|
||||||
border: 1px solid var(--s1);
|
border: 1px solid var(--s1);
|
||||||
padding: 10px 20px;
|
padding: 7px 14px;
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conn-dot {
|
.conn-dot {
|
||||||
@@ -201,55 +201,35 @@ header {
|
|||||||
.hero-decoration { display: none; }
|
.hero-decoration { display: none; }
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: clamp(72px, 16vw, 130px);
|
font-size: clamp(60px, 14vw, 108px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 20px;
|
letter-spacing: 16px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 0 0 40px rgba(180,190,254,0.4), 0 0 80px rgba(180,190,254,0.15);
|
text-shadow: 0 0 40px rgba(180,190,254,0.4), 0 0 80px rgba(180,190,254,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero p {
|
.hero p {
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
letter-spacing: 5px;
|
letter-spacing: 4px;
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-badge {
|
.hero-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-top: 20px;
|
margin-top: 16px;
|
||||||
padding: 10px 20px;
|
padding: 7px 16px;
|
||||||
border: 1px solid var(--s2);
|
border: 1px solid var(--s2);
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
font-size: 16px;
|
font-size: 12px;
|
||||||
letter-spacing: 2.5px;
|
letter-spacing: 2px;
|
||||||
color: var(--ov2);
|
color: var(--ov2);
|
||||||
background: var(--s0);
|
background: var(--s0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rejoin-bar {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--ov1);
|
|
||||||
margin-top: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rejoin-bar a {
|
|
||||||
color: var(--blue);
|
|
||||||
text-decoration: none;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 14px 24px;
|
|
||||||
border: 1px solid var(--s1);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all .2s;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-badge-dot {
|
.hero-badge-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -303,17 +283,17 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.land-card h2 {
|
.land-card h2 {
|
||||||
font-size: 20px;
|
font-size: 15px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
margin-bottom: -6px;
|
margin-bottom: -4px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.land-card p {
|
.land-card p {
|
||||||
font-size: 17px;
|
font-size: 15px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
line-height: 2;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rejoin-bar {
|
.rejoin-bar {
|
||||||
@@ -364,17 +344,17 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setup-title {
|
.setup-title {
|
||||||
font-size: 22px;
|
font-size: 17px;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-sub {
|
.setup-sub {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.5px;
|
||||||
line-height: 1.8;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── FORM ATOMS ────────────────────────────────────── */
|
/* ── FORM ATOMS ────────────────────────────────────── */
|
||||||
@@ -386,8 +366,8 @@ header {
|
|||||||
|
|
||||||
label,
|
label,
|
||||||
.lbl {
|
.lbl {
|
||||||
font-size: 17px;
|
font-size: 14px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 1.5px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -472,22 +452,22 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tog-row .lbl {
|
.tog-row .lbl {
|
||||||
font-size: 17px;
|
font-size: 15px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.3px;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lbl-sub {
|
.lbl-sub {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--ov2);
|
color: var(--ov2);
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tog {
|
.tog {
|
||||||
@@ -635,29 +615,18 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-size: 17px;
|
font-size: 14px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
margin-bottom: 26px;
|
margin-bottom: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-size: 15px;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title::after {
|
.panel-title::after {
|
||||||
content: '';
|
content: '';
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -735,24 +704,24 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-label {
|
.side-label {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
margin-bottom: 18px;
|
margin-bottom: 14px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-hint {
|
.side-hint {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-room-code {
|
.side-room-code {
|
||||||
font-size: 42px;
|
font-size: 34px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 10px;
|
letter-spacing: 8px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 0 0 20px rgba(180,190,254,0.3);
|
text-shadow: 0 0 20px rgba(180,190,254,0.3);
|
||||||
@@ -765,10 +734,10 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-status {
|
.side-status {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
line-height: 2.2;
|
line-height: 2;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── TIMER BLOCK ───────────────────────────────────── */
|
/* ── TIMER BLOCK ───────────────────────────────────── */
|
||||||
@@ -845,10 +814,10 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 18px 26px;
|
padding: 16px 22px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 17px;
|
font-size: 14px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 1.5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
@@ -891,15 +860,15 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bz-rank {
|
.bz-rank {
|
||||||
width: 52px;
|
width: 42px;
|
||||||
height: 52px;
|
height: 42px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--s1);
|
background: var(--s1);
|
||||||
border: 3px solid var(--s2);
|
border: 2px solid var(--s2);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 22px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -909,12 +878,17 @@ select option {
|
|||||||
background: rgba(249,226,175,0.15);
|
background: rgba(249,226,175,0.15);
|
||||||
border-color: var(--yellow);
|
border-color: var(--yellow);
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
box-shadow: 0 0 16px rgba(249,226,175,0.25);
|
box-shadow: 0 0 16px rgba(249,226,175,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bz-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.bz-name {
|
.bz-name {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -922,16 +896,16 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bz-team {
|
.bz-team {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bz-ms {
|
.bz-ms {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
margin-top: 5px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── PLAYERS ───────────────────────────────────────── */
|
/* ── PLAYERS ───────────────────────────────────────── */
|
||||||
@@ -943,31 +917,12 @@ select option {
|
|||||||
.pl-row {
|
.pl-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
padding: 18px 0;
|
padding: 16px 0;
|
||||||
border-bottom: 1px solid var(--s0);
|
border-bottom: 1px solid var(--s0);
|
||||||
transition: opacity .2s;
|
transition: opacity .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-name {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-meta {
|
|
||||||
font-size: 15px;
|
|
||||||
color: var(--ov2);
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-row:last-child {
|
.pl-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
@@ -1022,24 +977,24 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.team-card .tc-n {
|
.team-card .tc-n {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 1.5px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-card .tc-c {
|
.team-card .tc-c {
|
||||||
font-size: 40px;
|
font-size: 34px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-card .tc-m {
|
.team-card .tc-m {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
margin-top: 12px;
|
margin-top: 10px;
|
||||||
line-height: 2;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── SETTINGS ──────────────────────────────────────── */
|
/* ── SETTINGS ──────────────────────────────────────── */
|
||||||
@@ -1057,16 +1012,16 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sl {
|
.sl {
|
||||||
font-size: 17px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.3px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sd {
|
.sd {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--ov2);
|
color: var(--ov2);
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1085,35 +1040,35 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.p-room-lbl {
|
.p-room-lbl {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--ov1);
|
color: var(--ov1);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-room-code {
|
.p-room-code {
|
||||||
font-size: 52px;
|
font-size: 44px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 14px;
|
letter-spacing: 12px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
text-shadow: 0 0 24px rgba(180,190,254,0.35);
|
text-shadow: 0 0 24px rgba(180,190,254,0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-name-lbl {
|
.p-name-lbl {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
margin-top: 14px;
|
margin-top: 12px;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.5px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* player timer */
|
/* player timer */
|
||||||
.p-timer {
|
.p-timer {
|
||||||
font-size: 84px;
|
font-size: 72px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--teal);
|
color: var(--teal);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -1121,31 +1076,6 @@ select option {
|
|||||||
transition: color .3s, text-shadow .3s;
|
transition: color .3s, text-shadow .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-picker-title {
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: 4px;
|
|
||||||
color: var(--sub0);
|
|
||||||
margin-bottom: 22px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-btn {
|
|
||||||
padding: 26px 20px;
|
|
||||||
border: 2px solid var(--s1);
|
|
||||||
border-radius: var(--r);
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 17px;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
transition: all .2s;
|
|
||||||
box-shadow: var(--shadow-panel);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-timer.warn {
|
.p-timer.warn {
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
text-shadow: 0 0 20px rgba(249,226,175,0.5);
|
text-shadow: 0 0 20px rgba(249,226,175,0.5);
|
||||||
@@ -1343,25 +1273,25 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.buzz-status {
|
.buzz-status {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-height: 32px;
|
min-height: 26px;
|
||||||
transition: color .3s;
|
transition: color .3s;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacebar-hint {
|
.spacebar-hint {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
color: var(--ov0);
|
color: var(--ov0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .3s;
|
transition: opacity .3s;
|
||||||
margin-top: -16px;
|
margin-top: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacebar-hint.visible {
|
.spacebar-hint.visible {
|
||||||
@@ -1375,34 +1305,14 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.p-panel-title {
|
.p-panel-title {
|
||||||
font-size: 17px;
|
font-size: 14px;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 3px;
|
||||||
color: var(--sub0);
|
color: var(--sub0);
|
||||||
margin-bottom: 18px;
|
margin-bottom: 16px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed-entry {
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: var(--s0);
|
|
||||||
border: 1px solid var(--s1);
|
|
||||||
border-left: 4px solid var(--blue);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 17px;
|
|
||||||
border-radius: 0 var(--r) var(--r) 0;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roster-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 14px 0;
|
|
||||||
border-bottom: 1px solid var(--s0);
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-entry {
|
.feed-entry {
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
background: var(--s0);
|
background: var(--s0);
|
||||||
@@ -1462,28 +1372,22 @@ select option {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal p {
|
|
||||||
font-size: 17px;
|
|
||||||
color: var(--ov1);
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border: 2px solid var(--s2);
|
border: 2px solid var(--s2);
|
||||||
border-radius: var(--r2);
|
border-radius: var(--r2);
|
||||||
padding: 52px 48px;
|
padding: 44px 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 28px;
|
gap: 24px;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
|
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal h2 {
|
.modal h2 {
|
||||||
font-size: 22px;
|
font-size: 18px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 2px;
|
||||||
color: var(--lav);
|
color: var(--lav);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -1508,11 +1412,11 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
padding: 16px 24px;
|
padding: 14px 22px;
|
||||||
background: #24243e;
|
background: #24243e;
|
||||||
border: 2px solid var(--s2);
|
border: 2px solid var(--s2);
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
letter-spacing: 1.5px;
|
letter-spacing: 1px;
|
||||||
border-radius: var(--r);
|
border-radius: var(--r);
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
@@ -1520,15 +1424,6 @@ select option {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--ov1);
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.ok {
|
.toast.ok {
|
||||||
border-color: rgba(166,227,161,0.5);
|
border-color: rgba(166,227,161,0.5);
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
@@ -1577,7 +1472,7 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timer-digits {
|
.timer-digits {
|
||||||
font-size: 52px;
|
font-size: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.land-card {
|
.land-card {
|
||||||
@@ -1599,8 +1494,8 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
letter-spacing: 5px;
|
letter-spacing: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#s-mod {
|
#s-mod {
|
||||||
|
|||||||
@@ -65,19 +65,16 @@ export function handleMessage(ws: WS, raw: string) {
|
|||||||
if (!room) { er(ws, "Room not found"); return; }
|
if (!room) { er(ws, "Room not found"); return; }
|
||||||
if (room.locked) { er(ws, "Room is locked"); return; }
|
if (room.locked) { er(ws, "Room is locked"); return; }
|
||||||
|
|
||||||
// Calculate numeric ID based on join order
|
const name = sanitize(msg.playerName ?? "Player", 24) || "Player";
|
||||||
const joinCount = room.players.size + 1;
|
const existingId = sanitize(msg.playerId ?? "", 12);
|
||||||
const playerId = joinCount.toString();
|
|
||||||
const name = sanitize(msg.playerName ?? "", 24) || ""; // Keep name for compatibility but don't use it
|
|
||||||
|
|
||||||
let player: Player | undefined;
|
let player: Player | undefined;
|
||||||
|
|
||||||
if (playerId && room.players.has(playerId)) {
|
if (existingId && room.players.has(existingId)) {
|
||||||
player = room.players.get(playerId)!;
|
player = room.players.get(existingId)!;
|
||||||
player.ws = ws; player.isConnected = true; player.name = name;
|
player.ws = ws; player.isConnected = true; player.name = name;
|
||||||
} else {
|
} else {
|
||||||
player = { id: playerId, name, teamIndex: null, ws, isConnected: true };
|
player = { id: genId(), name, teamIndex: null, ws, isConnected: true };
|
||||||
room.players.set(playerId, player);
|
room.players.set(player.id, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
wsToPlayer.set(ws, { roomId: room.id, playerId: player.id });
|
wsToPlayer.set(ws, { roomId: room.id, playerId: player.id });
|
||||||
@@ -135,15 +132,6 @@ export function handleMessage(ws: WS, raw: string) {
|
|||||||
room.buzzerState = freshBuzzer();
|
room.buzzerState = freshBuzzer();
|
||||||
room.buzzerState.roundOpen = true;
|
room.buzzerState.roundOpen = true;
|
||||||
broadcast(room, { type: "round_open", room: publicRoom(room) });
|
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;
|
break;
|
||||||
case "close_round":
|
case "close_round":
|
||||||
room.buzzerState.roundOpen = false;
|
room.buzzerState.roundOpen = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user