feat: increase max players to 16, add dept leaderboard and scoring constants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+172
-74
@@ -10,17 +10,17 @@
|
||||
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
:root{
|
||||
--bg:#070b14;
|
||||
--surface:#0d1220;
|
||||
--surface2:#131928;
|
||||
--accent:#00ffe0;
|
||||
--accent2:#ff3e6c;
|
||||
--warn:#ffb800;
|
||||
--slow:#ff6b00;
|
||||
--boost:#00ff88;
|
||||
--bg:#ffffff;
|
||||
--surface:#f0f4f8;
|
||||
--surface2:#e2e8f0;
|
||||
--accent:#0077cc;
|
||||
--accent2:#cc0033;
|
||||
--warn:#b35c00;
|
||||
--slow:#cc4400;
|
||||
--boost:#00994d;
|
||||
--blocked:#ff1744;
|
||||
--text:#ddeeff;
|
||||
--muted:#3a4a66;
|
||||
--text:#0a0f1e;
|
||||
--muted:#4a5568;
|
||||
--mono:'Share Tech Mono',monospace;
|
||||
--sans:'Exo 2',sans-serif;
|
||||
}
|
||||
@@ -31,11 +31,52 @@ html,body{width:100%;height:100vh;background:var(--bg);color:var(--text);
|
||||
body::before{
|
||||
content:'';position:fixed;inset:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0,255,224,0.025) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,255,224,0.025) 1px,transparent 1px);
|
||||
linear-gradient(rgba(0,119,204,0.08) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,119,204,0.08) 1px,transparent 1px);
|
||||
background-size:40px 40px;pointer-events:none;
|
||||
}
|
||||
|
||||
/* ── PASSWORD GATE ── */
|
||||
#auth-gate{
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
background:var(--bg);
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:1.5rem;
|
||||
}
|
||||
#auth-gate .logo{
|
||||
font-family:var(--sans);font-weight:900;font-size:3rem;letter-spacing:-0.02em;
|
||||
color:var(--accent);text-shadow:0 0 30px rgba(0,119,204,0.4);line-height:1;
|
||||
}
|
||||
#auth-gate .logo span{color:var(--accent2);}
|
||||
#auth-gate .gate-label{
|
||||
font-family:var(--mono);font-size:.7rem;letter-spacing:.15em;color:var(--muted);
|
||||
}
|
||||
#auth-gate .gate-field{
|
||||
display:flex;flex-direction:column;align-items:center;gap:.75rem;width:100%;max-width:300px;
|
||||
}
|
||||
#auth-pass{
|
||||
width:100%;padding:.9rem 1rem;text-align:center;
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.4);border-radius:3px;
|
||||
color:var(--text);font-family:var(--mono);font-size:1.1rem;letter-spacing:.15em;
|
||||
outline:none;transition:border-color .2s;
|
||||
}
|
||||
#auth-pass:focus{border-color:var(--accent);}
|
||||
#auth-pass.shake{animation:shake .3s ease both;}
|
||||
#auth-btn{
|
||||
width:100%;padding:.85rem;
|
||||
background:var(--accent);border:none;border-radius:3px;
|
||||
color:#fff;font-family:var(--mono);font-size:.75rem;letter-spacing:.15em;
|
||||
cursor:pointer;transition:opacity .2s;
|
||||
}
|
||||
#auth-btn:hover{opacity:.9;}
|
||||
#auth-btn:active{transform:scale(.97);}
|
||||
#auth-err{
|
||||
font-family:var(--mono);font-size:.65rem;color:var(--accent2);
|
||||
letter-spacing:.1em;opacity:0;transition:opacity .2s;
|
||||
}
|
||||
#auth-err.visible{opacity:1;}
|
||||
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
|
||||
|
||||
.screen{display:none;height:100vh;}
|
||||
.screen.active{display:flex;}
|
||||
|
||||
@@ -43,14 +84,14 @@ body::before{
|
||||
#lobby-screen{flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center;}
|
||||
|
||||
.logo{font-family:var(--sans);font-weight:900;font-size:4rem;letter-spacing:-0.02em;line-height:1;
|
||||
color:var(--accent);text-shadow:0 0 40px rgba(0,255,224,.4);margin-bottom:.25rem;}
|
||||
color:var(--accent);text-shadow:0 0 40px rgba(0,119,204,0.6);margin-bottom:.25rem;}
|
||||
.logo span{color:var(--accent2);}
|
||||
.tagline{font-family:var(--mono);font-size:.85rem;color:var(--text);letter-spacing:.15em;margin-bottom:2.5rem;}
|
||||
|
||||
.lobby-row{display:flex;gap:2rem;align-items:flex-start;justify-content:center;width:100%;max-width:800px;margin-bottom:2rem;}
|
||||
|
||||
/* QR */
|
||||
.qr-box{background:var(--surface);border:1px solid rgba(0,255,224,.1);border-radius:4px;
|
||||
.qr-box{background:var(--surface);border:1px solid rgba(0,119,204,0.25);border-radius:4px;
|
||||
padding:1.25rem;text-align:center;min-width:200px;}
|
||||
.box-label{font-family:var(--mono);font-size:.75rem;color:var(--text);letter-spacing:.15em;margin-bottom:.75rem;}
|
||||
#qrcode{display:flex;justify-content:center;margin-bottom:.75rem;}
|
||||
@@ -58,21 +99,21 @@ body::before{
|
||||
.qr-url{font-family:var(--mono);font-size:.85rem;color:var(--accent);}
|
||||
|
||||
/* Players box */
|
||||
.players-box{flex:1;background:var(--surface);border:1px solid rgba(0,255,224,.1);
|
||||
.players-box{flex:1;background:var(--surface);border:1px solid rgba(0,119,204,0.25);
|
||||
border-radius:4px;padding:1.25rem;}
|
||||
.player-count{font-family:var(--sans);font-weight:900;font-size:4.5rem;
|
||||
letter-spacing:-0.04em;color:var(--accent);line-height:1;margin-bottom:.15rem;}
|
||||
.player-count-sub{font-family:var(--mono);font-size:.8rem;color:var(--text);margin-bottom:1rem;}
|
||||
.p-chips{display:flex;flex-wrap:wrap;gap:.4rem;max-height:100px;overflow-y:auto;}
|
||||
.p-chip{padding:.4rem .9rem;border-radius:2px;
|
||||
background:var(--surface2);border:1px solid rgba(0,255,224,.25);
|
||||
background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
|
||||
font-family:var(--mono);font-size:.85rem;color:var(--accent);}
|
||||
|
||||
/* rounds config */
|
||||
.rounds-cfg{display:flex;align-items:center;gap:.75rem;margin-top:1.5rem;
|
||||
font-family:var(--mono);font-size:.8rem;color:var(--text);}
|
||||
.rounds-cfg select{
|
||||
background:var(--surface2);border:1px solid rgba(0,255,224,.25);
|
||||
background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
|
||||
color:var(--accent);font-family:var(--mono);font-size:.8rem;
|
||||
padding:.4rem .7rem;border-radius:2px;outline:none;cursor:pointer;
|
||||
}
|
||||
@@ -93,7 +134,7 @@ body::before{
|
||||
|
||||
.game-left{
|
||||
flex:1;display:flex;flex-direction:column;
|
||||
padding:1.25rem;border-right:1px solid rgba(0,255,224,.08);
|
||||
padding:1.25rem;border-right:1px solid rgba(0,119,204,0.15);
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
@@ -107,19 +148,17 @@ body::before{
|
||||
.objective-bar{
|
||||
display:flex;align-items:center;gap:.75rem;
|
||||
padding:.75rem 1.25rem;
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.2);border-radius:3px;
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.35);border-radius:3px;
|
||||
margin-bottom:1rem;font-family:var(--mono);font-size:.85rem;color:var(--text);
|
||||
}
|
||||
.obj-node{display:inline-flex;align-items:center;justify-content:center;
|
||||
width:40px;height:40px;border-radius:50%;font-weight:800;font-size:1.1rem;}
|
||||
.obj-start{background:rgba(0,255,224,.15);border:2px solid var(--accent);color:var(--accent);}
|
||||
.obj-start{background:rgba(0,119,204,0.3);border:2px solid var(--accent);color:var(--accent);}
|
||||
.obj-end{background:rgba(255,62,108,.15);border:2px solid var(--accent2);color:var(--accent2);}
|
||||
|
||||
/* host grid */
|
||||
.grid-wrap{flex:1;display:flex;align-items:center;justify-content:center;}
|
||||
#host-canvas{display:block;border-radius:4px;}
|
||||
|
||||
/* legend */
|
||||
.legend{display:flex;gap:1.25rem;margin-top:.75rem;flex-wrap:wrap;}
|
||||
.legend-item{display:flex;align-items:center;gap:.4rem;
|
||||
font-family:var(--mono);font-size:.75rem;color:var(--text);}
|
||||
@@ -130,8 +169,7 @@ body::before{
|
||||
padding:1.25rem;gap:1rem;overflow-y:auto;
|
||||
}
|
||||
|
||||
/* submission panel */
|
||||
.panel{background:var(--surface);border:1px solid rgba(0,255,224,.15);
|
||||
.panel{background:var(--surface);border:1px solid rgba(0,119,204,0.3);
|
||||
border-radius:4px;padding:1rem;}
|
||||
.panel-title{font-family:var(--mono);font-size:.7rem;color:var(--text);
|
||||
letter-spacing:.12em;margin-bottom:.75rem;}
|
||||
@@ -141,21 +179,19 @@ body::before{
|
||||
.ans-bar-wrap{height:4px;background:var(--surface2);border-radius:2px;}
|
||||
.ans-bar{height:100%;background:var(--accent);border-radius:2px;transition:width .3s;}
|
||||
|
||||
/* submission list */
|
||||
.sub-list{display:flex;flex-direction:column;gap:.4rem;margin-top:.75rem;}
|
||||
.sub-item{display:flex;align-items:center;gap:.5rem;
|
||||
padding:.6rem .75rem;border-radius:2px;
|
||||
background:var(--surface2);border:1px solid rgba(0,255,224,.1);
|
||||
background:var(--surface2);border:1px solid rgba(0,119,204,0.25);
|
||||
font-family:var(--mono);font-size:.8rem;}
|
||||
.sub-name{flex:1;color:var(--text);}
|
||||
.sub-pts{color:var(--accent);font-size:.8rem;}
|
||||
.sub-pts.fail{color:var(--accent2);}
|
||||
.sub-time{color:var(--muted);font-size:.7rem;}
|
||||
|
||||
/* leaderboard */
|
||||
.lb-item{display:flex;align-items:center;gap:.5rem;
|
||||
padding:.55rem .75rem;margin-bottom:.35rem;border-radius:2px;
|
||||
background:var(--surface2);border:1px solid rgba(0,255,224,.1);}
|
||||
background:var(--surface2);border:1px solid rgba(0,119,204,0.25);}
|
||||
.lb-rank{font-family:var(--mono);font-size:.7rem;color:var(--text);min-width:22px;}
|
||||
.lb-name{flex:1;font-size:1rem;font-weight:700;}
|
||||
.lb-score{font-family:var(--mono);font-size:.7rem;color:var(--accent);}
|
||||
@@ -167,7 +203,7 @@ body::before{
|
||||
|
||||
.results-top{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
padding:1rem 1.5rem;border-bottom:1px solid rgba(0,255,224,.08);
|
||||
padding:1rem 1.5rem;border-bottom:1px solid rgba(0,119,204,0.15);
|
||||
}
|
||||
.results-title{font-family:var(--sans);font-weight:900;font-size:2rem;
|
||||
letter-spacing:-0.02em;color:var(--accent);}
|
||||
@@ -175,19 +211,17 @@ body::before{
|
||||
|
||||
.results-body{display:flex;flex:1;gap:0;overflow:hidden;}
|
||||
|
||||
/* results grid */
|
||||
.results-left{flex:1;display:flex;flex-direction:column;
|
||||
padding:1.25rem;border-right:1px solid rgba(0,255,224,.08);}
|
||||
padding:1.25rem;border-right:1px solid rgba(0,119,204,0.15);}
|
||||
.results-grid-wrap{flex:1;display:flex;align-items:center;justify-content:center;}
|
||||
#results-canvas{display:block;border-radius:4px;}
|
||||
|
||||
/* results right */
|
||||
.results-right{width:300px;padding:1.25rem;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;}
|
||||
|
||||
.path-cards{display:flex;flex-direction:column;gap:.5rem;}
|
||||
.path-card{
|
||||
padding:.7rem .9rem;border-radius:3px;
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.08);
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.15);
|
||||
}
|
||||
.path-card-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem;}
|
||||
.path-player{font-weight:700;font-size:1rem;}
|
||||
@@ -199,7 +233,7 @@ body::before{
|
||||
.path-node{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
width:20px;height:20px;border-radius:50%;
|
||||
background:var(--surface2);border:1px solid rgba(0,255,224,.15);
|
||||
background:var(--surface2);border:1px solid rgba(0,119,204,0.3);
|
||||
font-family:var(--mono);font-size:.5rem;color:var(--accent);
|
||||
}
|
||||
.path-arrow{font-size:.5rem;color:var(--muted);align-self:center;}
|
||||
@@ -229,7 +263,7 @@ body::before{
|
||||
.rest-list{display:flex;flex-direction:column;gap:.4rem;max-width:420px;width:100%;}
|
||||
.rest-item{display:flex;align-items:center;gap:.75rem;
|
||||
padding:.65rem 1rem;border-radius:3px;
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.12);font-size:1rem;}
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.25);font-size:1rem;}
|
||||
.rest-rank{font-family:var(--mono);font-size:.7rem;color:var(--text);min-width:24px;}
|
||||
.rest-name{flex:1;font-weight:700;}
|
||||
.rest-score{font-family:var(--mono);font-size:.7rem;color:var(--accent);}
|
||||
@@ -247,8 +281,20 @@ body::before{
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── AUTH GATE ── -->
|
||||
<div id="auth-gate">
|
||||
<div class="logo">ROUTE<span>RUSH</span></div>
|
||||
<div class="gate-label">// HOST ACCESS REQUIRED</div>
|
||||
<div class="gate-field">
|
||||
<input type="password" id="auth-pass" placeholder="PASSWORD" autocomplete="off" autocorrect="off"
|
||||
onkeydown="if(event.key==='Enter')checkAuth()">
|
||||
<button id="auth-btn" onclick="checkAuth()">▶ ENTER HOST PANEL</button>
|
||||
<div id="auth-err">INCORRECT PASSWORD</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LOBBY -->
|
||||
<div class="screen active" id="lobby-screen">
|
||||
<div class="screen" id="lobby-screen">
|
||||
<div class="logo">ROUTE<span>RUSH</span></div>
|
||||
<div class="tagline">// HOST CONTROL PANEL</div>
|
||||
<div class="lobby-row">
|
||||
@@ -260,7 +306,7 @@ body::before{
|
||||
<div class="players-box">
|
||||
<div class="box-label">PLAYERS CONNECTED</div>
|
||||
<div class="player-count" id="player-count">0</div>
|
||||
<div class="player-count-sub">/ 4 MAX</div>
|
||||
<div class="player-count-sub">/ 16 MAX</div>
|
||||
<div class="p-chips" id="p-chips"></div>
|
||||
<div class="rounds-cfg">
|
||||
<span>ROUNDS:</span>
|
||||
@@ -275,11 +321,18 @@ body::before{
|
||||
<option value="45">45s</option>
|
||||
<option value="60">60s</option>
|
||||
</select>
|
||||
<span>PRIZE AVG:</span>
|
||||
<input id="prize-threshold" type="number" value="3000" min="500" max="4500" step="100"
|
||||
style="width:72px;background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
|
||||
color:var(--accent);font-family:var(--mono);font-size:.8rem;
|
||||
padding:.4rem .5rem;border-radius:2px;outline:none;text-align:center;">
|
||||
<span style="font-size:.65rem;color:var(--muted)">= 25 PTS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="hbtn hbtn-primary" id="start-btn" disabled onclick="startRound()">▶ START GAME</button>
|
||||
<button class="hbtn hbtn-danger" onclick="clearLobby()">✕ CLEAR LOBBY</button>
|
||||
<button class="hbtn hbtn-danger" onclick="clearScores()">🗑 CLEAR SCORES</button>
|
||||
<button class="hbtn hbtn-secondary" onclick="location.href='scores'">📊 HISTORY</button>
|
||||
</div>
|
||||
@@ -382,24 +435,50 @@ body::before{
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── AUTH GATE ─────────────────────────────────────────────────────────────────
|
||||
const HOST_PASS = 'ni2026';
|
||||
|
||||
function checkAuth() {
|
||||
const val = document.getElementById('auth-pass').value;
|
||||
if (val === HOST_PASS) {
|
||||
document.getElementById('auth-gate').style.display = 'none';
|
||||
initHost();
|
||||
} else {
|
||||
const inp = document.getElementById('auth-pass');
|
||||
const err = document.getElementById('auth-err');
|
||||
inp.classList.remove('shake');
|
||||
void inp.offsetWidth; // reflow to restart animation
|
||||
inp.classList.add('shake');
|
||||
err.classList.add('visible');
|
||||
inp.value = '';
|
||||
inp.focus();
|
||||
setTimeout(() => err.classList.remove('visible'), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── STATE ────────────────────────────────────────────────────────────────────
|
||||
let ws, timerInterval;
|
||||
let currentGrid = null, currentRound = 0, currentTotal = 0, totalPlayers = 0;
|
||||
let lastSubmissions = [], lastLeaderboard = [];
|
||||
|
||||
// ── QR ────────────────────────────────────────────────────────────────────────
|
||||
const baseUrl = `${location.protocol}//${location.hostname}${location.port?':'+location.port:''}${location.pathname.replace(/\/host\/?$/,'')}`;
|
||||
document.getElementById('join-url').textContent = baseUrl;
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: baseUrl,
|
||||
width: 200, height: 200,
|
||||
colorDark: '#00ffe0', colorLight: '#0d1220',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
// ── INIT (called after auth) ──────────────────────────────────────────────────
|
||||
function initHost() {
|
||||
// QR Code
|
||||
const baseUrl = `${location.protocol}//${location.hostname}${location.port?':'+location.port:''}${location.pathname.replace(/\/host\/?$/,'')}`;
|
||||
document.getElementById('join-url').textContent = baseUrl;
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: baseUrl,
|
||||
width: 200, height: 200,
|
||||
colorDark: '#0077cc', colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
connectWS();
|
||||
show('lobby-screen');
|
||||
}
|
||||
|
||||
// ── CANVAS ────────────────────────────────────────────────────────────────────
|
||||
const GRID = 4;
|
||||
const MOD_COLOR = {normal:'#00ffe0', slow:'#ff6b00', boost:'#00ff88', blocked:'#ff1744'};
|
||||
const MOD_COLOR = {normal:'#0077cc', slow:'#ff6b00', boost:'#00ff88', blocked:'#ff1744'};
|
||||
|
||||
function setupCanvas(canvasEl, wrapEl) {
|
||||
const size = Math.min(wrapEl.clientWidth, wrapEl.clientHeight, 480);
|
||||
@@ -413,7 +492,7 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
|
||||
const size = canvasEl.width;
|
||||
const ctx = canvasEl.getContext('2d');
|
||||
const CELL = size / GRID;
|
||||
const NODE_R = CELL * 0.2;
|
||||
const NODE_R = CELL * 0.14;
|
||||
const OX = CELL / 2, OY = CELL / 2;
|
||||
|
||||
function nodePos(id) {
|
||||
@@ -422,41 +501,61 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
// edges
|
||||
grid.edges.forEach(e => {
|
||||
const a = nodePos(e.a), b = nodePos(e.b);
|
||||
const color = MOD_COLOR[e.modifier] || '#00ffe0';
|
||||
const color = MOD_COLOR[e.modifier] || '#0077cc';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
if (e.modifier === 'blocked') {
|
||||
ctx.setLineDash([4,4]);
|
||||
ctx.strokeStyle = 'rgba(255,23,68,0.25)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([6,5]);
|
||||
ctx.strokeStyle = 'rgba(255,23,68,0.55)';
|
||||
ctx.lineWidth = 2.5;
|
||||
} else if (e.modifier !== 'normal') {
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = `${color}bb`;
|
||||
ctx.lineWidth = 3;
|
||||
} else {
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = `${color}2a`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeStyle = `${color}66`;
|
||||
ctx.lineWidth = 2;
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// modifier label
|
||||
if (e.modifier !== 'normal') {
|
||||
const mx = (a.x+b.x)/2, my = (a.y+b.y)/2;
|
||||
const label = e.modifier==='slow'?'2×':e.modifier==='boost'?'½×':'✕';
|
||||
ctx.fillStyle='rgba(7,11,20,0.9)';
|
||||
ctx.fillRect(mx-12,my-9,24,18);
|
||||
ctx.fillStyle=color;
|
||||
ctx.font=`bold ${Math.max(12,CELL*.16)}px 'Share Tech Mono',monospace`;
|
||||
ctx.textAlign='center';ctx.textBaseline='middle';
|
||||
ctx.fillText(label,mx,my);
|
||||
const label = e.modifier==='slow'?'x2':e.modifier==='boost'?'x½':'✕';
|
||||
const fontSize = Math.max(14, CELL * 0.22);
|
||||
ctx.font = `bold ${fontSize}px 'Share Tech Mono',monospace`;
|
||||
const tw = ctx.measureText(label).width;
|
||||
const padX = 8, padY = 5;
|
||||
const bw = tw + padX*2, bh = fontSize + padY*2;
|
||||
const bx = mx - bw/2, by = my - bh/2;
|
||||
const r = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx+r,by); ctx.lineTo(bx+bw-r,by);
|
||||
ctx.quadraticCurveTo(bx+bw,by,bx+bw,by+r);
|
||||
ctx.lineTo(bx+bw,by+bh-r);
|
||||
ctx.quadraticCurveTo(bx+bw,by+bh,bx+bw-r,by+bh);
|
||||
ctx.lineTo(bx+r,by+bh);
|
||||
ctx.quadraticCurveTo(bx,by+bh,bx,by+bh-r);
|
||||
ctx.lineTo(bx,by+r);
|
||||
ctx.quadraticCurveTo(bx,by,bx+r,by);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign='center'; ctx.textBaseline='middle';
|
||||
ctx.fillText(label, mx, my);
|
||||
}
|
||||
});
|
||||
|
||||
// highlighted paths (player routes)
|
||||
const pathColors = ['#00ffe0','#ff3e6c','#ffb800','#cc44ff'];
|
||||
const pathColors = ['#0077cc','#cc0033','#ffb800','#cc44ff'];
|
||||
highlightPaths.forEach((p, pi) => {
|
||||
if (!p || p.length < 2) return;
|
||||
const color = pathColors[pi % pathColors.length];
|
||||
@@ -478,7 +577,6 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
|
||||
ctx.lineJoin = 'miter';
|
||||
});
|
||||
|
||||
// nodes
|
||||
grid.nodes.forEach(n => {
|
||||
const {x,y} = nodePos(n.id);
|
||||
const isStart = n.id === grid.start;
|
||||
@@ -597,7 +695,8 @@ function startTimer(secs) {
|
||||
function startRound() {
|
||||
const rounds = parseInt(document.getElementById('rounds-select').value);
|
||||
const timeLimit = parseInt(document.getElementById('time-select').value);
|
||||
ws.send(JSON.stringify({type: 'start_round', total_rounds: rounds, time_limit: timeLimit}));
|
||||
const prizeThreshold = parseInt(document.getElementById('prize-threshold').value) || 3000;
|
||||
ws.send(JSON.stringify({type: 'start_round', total_rounds: rounds, time_limit: timeLimit, prize_threshold: prizeThreshold}));
|
||||
}
|
||||
|
||||
function endRound() {
|
||||
@@ -613,6 +712,10 @@ function nextRound() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearLobby() {
|
||||
ws.send(JSON.stringify({type: "reset"}));
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
clearInterval(timerInterval);
|
||||
ws.send(JSON.stringify({type: 'reset'}));
|
||||
@@ -631,15 +734,13 @@ function showResults(msg) {
|
||||
document.getElementById('res-round-label').textContent = `ROUND ${msg.round} / ${currentTotal}`;
|
||||
document.getElementById('next-btn').textContent = msg.is_final ? 'PODIUM →' : 'NEXT ROUND →';
|
||||
|
||||
// Draw grid with all paths
|
||||
const rc = document.getElementById('results-canvas');
|
||||
const wrap = document.querySelector('.results-grid-wrap');
|
||||
setupCanvas(rc, wrap);
|
||||
const paths = msg.submissions.map(s => s.path);
|
||||
drawGridOnCanvas(rc, msg.grid, paths);
|
||||
|
||||
// Path cards
|
||||
const pathColors = ['#00ffe0','#ff3e6c','#ffb800','#cc44ff'];
|
||||
const pathColors = ['#0077cc','#cc0033','#ffb800','#cc44ff'];
|
||||
document.getElementById('path-cards').innerHTML = msg.submissions.map((s, i) => {
|
||||
const color = pathColors[i % pathColors.length];
|
||||
const nodes = (s.path||[]).map(n =>
|
||||
@@ -655,7 +756,6 @@ function showResults(msg) {
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Leaderboard
|
||||
const medals = ['🥇','🥈','🥉'];
|
||||
document.getElementById('res-lb').innerHTML = msg.leaderboard.map((p,i) =>
|
||||
`<div class="lb-item">
|
||||
@@ -693,7 +793,7 @@ function showPodium(lb) {
|
||||
}
|
||||
|
||||
function confetti() {
|
||||
const colors = ['#00ffe0','#ff3e6c','#ffb800','#00ff88','#cc44ff'];
|
||||
const colors = ['#0077cc','#cc0033','#ffb800','#00ff88','#cc44ff'];
|
||||
for (let i = 0; i < 70; i++) {
|
||||
setTimeout(() => {
|
||||
const el = document.createElement('div');
|
||||
@@ -704,8 +804,6 @@ function confetti() {
|
||||
}, i * 45);
|
||||
}
|
||||
}
|
||||
|
||||
connectWS();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+762
-342
File diff suppressed because it is too large
Load Diff
+253
-74
@@ -9,15 +9,15 @@
|
||||
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
:root{
|
||||
--bg:#070b14;
|
||||
--surface:#0d1220;
|
||||
--surface2:#131928;
|
||||
--accent:#00ffe0;
|
||||
--accent2:#ff3e6c;
|
||||
--bg:#ffffff;
|
||||
--surface:#f0f4f8;
|
||||
--surface2:#e2e8f0;
|
||||
--accent:#0077cc;
|
||||
--accent2:#cc0033;
|
||||
--warn:#ffb800;
|
||||
--boost:#00ff88;
|
||||
--text:#ddeeff;
|
||||
--muted:#3a4a66;
|
||||
--text:#0a0f1e;
|
||||
--muted:#4a5568;
|
||||
--mono:'Share Tech Mono',monospace;
|
||||
--sans:'Exo 2',sans-serif;
|
||||
}
|
||||
@@ -27,20 +27,20 @@ html,body{min-height:100vh;background:var(--bg);color:var(--text);font-family:va
|
||||
body::before{
|
||||
content:'';position:fixed;inset:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0,255,224,0.025) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,255,224,0.025) 1px,transparent 1px);
|
||||
linear-gradient(rgba(0,119,204,0.06) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,119,204,0.06) 1px,transparent 1px);
|
||||
background-size:40px 40px;pointer-events:none;z-index:0;
|
||||
}
|
||||
|
||||
header{
|
||||
position:relative;z-index:1;
|
||||
padding:1.75rem 2rem 1.25rem;
|
||||
border-bottom:1px solid rgba(0,255,224,0.08);
|
||||
border-bottom:1px solid rgba(0,119,204,0.12);
|
||||
display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:1rem;
|
||||
}
|
||||
.header-left h1{
|
||||
font-family:var(--sans);font-weight:900;font-size:2rem;letter-spacing:-0.03em;
|
||||
color:var(--accent);text-shadow:0 0 20px rgba(0,255,224,.3);line-height:1;
|
||||
color:var(--accent);text-shadow:0 0 20px rgba(0,119,204,0.4);line-height:1;
|
||||
}
|
||||
.header-left h1 span{color:var(--accent2);}
|
||||
.header-left p{font-family:var(--mono);font-size:.55rem;color:var(--muted);letter-spacing:.12em;margin-top:.3rem;}
|
||||
@@ -48,60 +48,108 @@ header{
|
||||
.nav-links a{
|
||||
font-family:var(--mono);font-size:.6rem;color:var(--muted);
|
||||
text-decoration:none;padding:.35rem .8rem;
|
||||
border:1px solid rgba(0,255,224,.1);border-radius:2px;
|
||||
border:1px solid rgba(0,119,204,0.15);border-radius:2px;
|
||||
transition:all .15s;letter-spacing:.08em;
|
||||
}
|
||||
.nav-links a:hover{color:var(--accent);border-color:rgba(0,255,224,.3);}
|
||||
.nav-links a:hover{color:var(--accent);border-color:rgba(0,119,204,0.4);}
|
||||
|
||||
main{position:relative;z-index:1;max-width:960px;margin:0 auto;padding:2rem;}
|
||||
|
||||
/* loading / empty */
|
||||
#loading{text-align:center;color:var(--muted);padding:4rem;font-family:var(--mono);font-size:.75rem;
|
||||
letter-spacing:.1em;}
|
||||
#loading{text-align:center;color:var(--muted);padding:4rem;font-family:var(--mono);font-size:.75rem;letter-spacing:.1em;}
|
||||
#loading::after{content:'...';animation:dots 1.2s step-end infinite;}
|
||||
@keyframes dots{0%{content:''}33%{content:'.'}66%{content:'..'}100%{content:'...'}}
|
||||
#empty{display:none;text-align:center;padding:4rem;font-family:var(--mono);font-size:.7rem;color:var(--muted);}
|
||||
#empty .icon{font-size:3rem;margin-bottom:1rem;display:block;}
|
||||
|
||||
#content{display:none;}
|
||||
|
||||
/* section */
|
||||
.section{margin-bottom:2.5rem;}
|
||||
.section-title{
|
||||
display:flex;align-items:center;gap:.75rem;margin-bottom:1.25rem;
|
||||
}
|
||||
.section-title h2{
|
||||
font-family:var(--sans);font-weight:900;font-size:1.1rem;letter-spacing:-0.01em;
|
||||
}
|
||||
.section-title{display:flex;align-items:center;gap:.75rem;margin-bottom:1.25rem;}
|
||||
.section-title h2{font-family:var(--sans);font-weight:900;font-size:1.1rem;letter-spacing:-0.01em;}
|
||||
.section-title .pill{
|
||||
font-family:var(--mono);font-size:.45rem;letter-spacing:.15em;
|
||||
padding:.2rem .5rem;border-radius:2px;
|
||||
background:rgba(0,255,224,.06);border:1px solid rgba(0,255,224,.15);color:var(--accent);
|
||||
background:rgba(0,119,204,0.1);border:1px solid rgba(0,119,204,0.3);color:var(--accent);
|
||||
}
|
||||
|
||||
/* top cards */
|
||||
.top-row{display:flex;gap:.75rem;flex-wrap:wrap;}
|
||||
.top-card{
|
||||
flex:1;min-width:140px;
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.08);border-radius:4px;
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;
|
||||
padding:.9rem 1.1rem;display:flex;align-items:center;gap:.75rem;
|
||||
transition:border-color .2s;
|
||||
}
|
||||
.top-card:hover{border-color:rgba(0,255,224,.2);}
|
||||
.top-card.gold{border-color:rgba(255,184,0,.3);background:rgba(255,184,0,.04);}
|
||||
.top-card.silver{border-color:rgba(180,180,180,.2);}
|
||||
.top-card.bronze{border-color:rgba(160,90,30,.2);}
|
||||
.top-card:hover{border-color:rgba(0,119,204,0.35);}
|
||||
.top-card.gold{border-color:rgba(255,184,0,.35);background:rgba(255,184,0,.04);}
|
||||
.top-card.silver{border-color:rgba(180,180,180,.25);}
|
||||
.top-card.bronze{border-color:rgba(160,90,30,.25);}
|
||||
.top-medal{font-size:1.6rem;}
|
||||
.top-info .top-name{font-weight:700;font-size:.9rem;margin-bottom:.15rem;}
|
||||
.top-info .top-score{
|
||||
font-family:var(--sans);font-weight:900;font-size:1.2rem;
|
||||
letter-spacing:-0.02em;color:var(--accent);line-height:1;
|
||||
}
|
||||
.top-info .top-score{font-family:var(--sans);font-weight:900;font-size:1.2rem;letter-spacing:-0.02em;color:var(--accent);line-height:1;}
|
||||
.top-info .top-meta{font-family:var(--mono);font-size:.45rem;color:var(--muted);margin-top:.15rem;}
|
||||
|
||||
/* sessions */
|
||||
.tabs{display:flex;gap:.5rem;margin-bottom:1.75rem;flex-wrap:wrap;border-bottom:1px solid rgba(0,119,204,0.12);padding-bottom:1rem;}
|
||||
.tab-btn{
|
||||
padding:.45rem 1.1rem;border-radius:2px;font-family:var(--mono);font-size:.62rem;
|
||||
letter-spacing:.1em;border:1px solid rgba(0,119,204,0.2);
|
||||
background:transparent;color:var(--muted);cursor:pointer;transition:all .15s;
|
||||
}
|
||||
.tab-btn.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700;}
|
||||
.tab-btn:hover:not(.active){border-color:var(--accent);color:var(--accent);}
|
||||
.tab-panel{display:none;}
|
||||
.tab-panel.active{display:block;}
|
||||
|
||||
.table-wrap{background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;overflow:hidden;}
|
||||
table{width:100%;border-collapse:collapse;}
|
||||
thead th{
|
||||
text-align:left;font-family:var(--mono);font-size:.5rem;letter-spacing:.12em;
|
||||
color:var(--muted);padding:.65rem 1.1rem;
|
||||
border-bottom:1px solid rgba(0,119,204,0.1);
|
||||
}
|
||||
tbody td{padding:.6rem 1.1rem;font-size:.85rem;border-bottom:1px solid rgba(0,0,0,0.04);}
|
||||
tbody tr:last-child td{border:none;}
|
||||
tbody tr:hover{background:var(--surface2);}
|
||||
.td-rank{font-family:var(--mono);font-weight:700;color:var(--muted);font-size:.8rem;}
|
||||
.td-score{font-family:var(--sans);font-weight:800;color:var(--accent);font-size:.95rem;}
|
||||
.td-medal{font-size:1rem;min-width:28px;}
|
||||
.td-dept{font-family:var(--mono);font-size:.7rem;color:var(--muted);}
|
||||
|
||||
/* ── GROUP SCORE TABLE ── */
|
||||
.group-score-intro{
|
||||
font-family:var(--mono);font-size:.6rem;color:var(--muted);
|
||||
margin-bottom:1.25rem;line-height:1.6;letter-spacing:.03em;
|
||||
padding:.75rem 1rem;
|
||||
background:rgba(0,119,204,0.04);border:1px solid rgba(0,119,204,0.12);border-radius:4px;
|
||||
}
|
||||
.group-score-intro strong{color:var(--accent);}
|
||||
|
||||
.prize-bar-wrap{display:flex;align-items:center;gap:.6rem;min-width:120px;}
|
||||
.prize-bar{height:6px;border-radius:3px;background:rgba(0,119,204,0.12);flex:1;overflow:hidden;}
|
||||
.prize-bar-fill{height:100%;border-radius:3px;transition:width .4s ease;}
|
||||
.prize-pts{
|
||||
font-family:var(--mono);font-weight:700;font-size:.8rem;
|
||||
min-width:36px;text-align:right;
|
||||
}
|
||||
|
||||
.td-pct{font-family:var(--mono);font-size:.72rem;color:var(--muted);}
|
||||
.td-prize{font-family:var(--sans);font-weight:800;font-size:1rem;}
|
||||
|
||||
/* row highlight for top group */
|
||||
tbody tr.group-winner td{background:rgba(255,184,0,.06);}
|
||||
tbody tr.group-winner .td-prize{color:var(--warn);}
|
||||
|
||||
/* ── DEPT BLOCKS (individual breakdown) ── */
|
||||
.dept-block{margin-bottom:1.25rem;border-radius:4px;overflow:hidden;border:1px solid rgba(0,119,204,0.15);}
|
||||
.dept-header{
|
||||
display:flex;align-items:center;gap:.7rem;
|
||||
padding:.7rem 1.1rem;background:var(--surface);
|
||||
border-bottom:1px solid rgba(0,119,204,0.1);
|
||||
}
|
||||
.dept-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
|
||||
.dept-name{font-family:var(--sans);font-weight:800;font-size:.95rem;}
|
||||
.dept-count{font-family:var(--mono);font-size:.5rem;color:var(--muted);margin-left:auto;}
|
||||
|
||||
.session-card{
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.08);border-radius:4px;
|
||||
background:var(--surface);border:1px solid rgba(0,119,204,0.15);border-radius:4px;
|
||||
margin-bottom:.75rem;overflow:hidden;
|
||||
}
|
||||
.session-header{
|
||||
@@ -115,33 +163,14 @@ main{position:relative;z-index:1;max-width:960px;margin:0 auto;padding:2rem;}
|
||||
.session-badge{
|
||||
font-family:var(--mono);font-size:.5rem;
|
||||
padding:.2rem .55rem;border-radius:2px;
|
||||
background:rgba(0,255,224,.06);border:1px solid rgba(0,255,224,.15);color:var(--accent);
|
||||
background:rgba(0,119,204,0.1);border:1px solid rgba(0,119,204,0.3);color:var(--accent);
|
||||
}
|
||||
.session-right{display:flex;align-items:center;gap:.75rem;}
|
||||
.session-rounds{font-family:var(--mono);font-size:.5rem;color:var(--muted);}
|
||||
.chevron{color:var(--muted);transition:transform .2s;font-size:.9rem;}
|
||||
|
||||
.session-body{display:none;border-top:1px solid rgba(0,255,224,.06);}
|
||||
.session-body{display:none;border-top:1px solid rgba(0,119,204,0.1);}
|
||||
.session-body.open{display:block;}
|
||||
|
||||
table{width:100%;border-collapse:collapse;}
|
||||
thead th{
|
||||
text-align:left;font-family:var(--mono);font-size:.5rem;letter-spacing:.12em;
|
||||
color:var(--muted);padding:.65rem 1.1rem;
|
||||
border-bottom:1px solid rgba(0,255,224,.06);
|
||||
}
|
||||
tbody td{padding:.6rem 1.1rem;font-size:.85rem;border-bottom:1px solid rgba(255,255,255,.02);}
|
||||
tbody tr:last-child td{border:none;}
|
||||
.td-rank{font-family:var(--mono);font-weight:700;color:var(--muted);font-size:.8rem;}
|
||||
.td-score{font-family:var(--sans);font-weight:800;color:var(--accent);font-size:.95rem;}
|
||||
.td-medal{font-size:1rem;}
|
||||
|
||||
/* global leaderboard table */
|
||||
.global-table-wrap{
|
||||
background:var(--surface);border:1px solid rgba(0,255,224,.08);border-radius:4px;overflow:hidden;
|
||||
}
|
||||
|
||||
/* fade-up */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
.fade-up{animation:fadeUp .4s ease both;}
|
||||
</style>
|
||||
@@ -166,7 +195,7 @@ tbody tr:last-child td{border:none;}
|
||||
<div id="content">
|
||||
|
||||
<!-- TOP PLAYERS -->
|
||||
<div class="section" id="section-top">
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<h2>🏆 All-Time Top Players</h2>
|
||||
<span class="pill">BEST SCORE</span>
|
||||
@@ -174,18 +203,26 @@ tbody tr:last-child td{border:none;}
|
||||
<div class="top-row" id="top-row"></div>
|
||||
</div>
|
||||
|
||||
<!-- GLOBAL RANKING -->
|
||||
<div class="section">
|
||||
<!-- TABS -->
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('global')">📊 GLOBAL RANKING</button>
|
||||
<button class="tab-btn" onclick="switchTab('dept')">🏢 BY DEPARTMENT</button>
|
||||
<button class="tab-btn" onclick="switchTab('sessions')">📋 SESSIONS</button>
|
||||
</div>
|
||||
|
||||
<!-- GLOBAL TAB -->
|
||||
<div class="tab-panel active" id="tab-global">
|
||||
<div class="section-title">
|
||||
<h2>📊 Global Ranking</h2>
|
||||
<span class="pill">ALL SESSIONS</span>
|
||||
<span class="pill">ALL SESSIONS · BEST SCORE</span>
|
||||
</div>
|
||||
<div class="global-table-wrap">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>PLAYER</th>
|
||||
<th>DEPARTMENT</th>
|
||||
<th>BEST SCORE</th>
|
||||
<th>SESSIONS</th>
|
||||
<th>AVG SCORE</th>
|
||||
@@ -196,10 +233,52 @@ tbody tr:last-child td{border:none;}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SESSIONS -->
|
||||
<div class="section">
|
||||
<!-- DEPT TAB -->
|
||||
<div class="tab-panel" id="tab-dept">
|
||||
|
||||
<!-- GROUP PRIZE SCORE -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<h2>🎯 Group Prize Score</h2>
|
||||
<span class="pill">AVG · MAX 25 PTS</span>
|
||||
</div>
|
||||
<div class="group-score-intro">
|
||||
Prize points are based on each group's <strong>average individual score</strong> across all rounds.<br>
|
||||
<strong>3,000 avg = 25 pts</strong> · Score scales linearly downward · Max score per player: <strong>4,500 pts</strong>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>DEPARTMENT</th>
|
||||
<th>PLAYERS</th>
|
||||
<th>AVG SCORE</th>
|
||||
<th>% OF MAX</th>
|
||||
<th>PRIZE PTS</th>
|
||||
<th style="min-width:140px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="group-score-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INDIVIDUAL BREAKDOWN BY DEPT -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<h2>🏢 Player Breakdown by Department</h2>
|
||||
<span class="pill">BEST SCORE PER PLAYER</span>
|
||||
</div>
|
||||
<div id="dept-list"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SESSIONS TAB -->
|
||||
<div class="tab-panel" id="tab-sessions">
|
||||
<div class="section-title">
|
||||
<h2>📋 Sessions</h2>
|
||||
<h2>📋 Session History</h2>
|
||||
<span class="pill" id="session-count-pill">0 GAMES</span>
|
||||
</div>
|
||||
<div id="sessions-list"></div>
|
||||
@@ -211,6 +290,30 @@ tbody tr:last-child td{border:none;}
|
||||
<script>
|
||||
const medals = ['🥇','🥈','🥉'];
|
||||
const rankClass = ['gold','silver','bronze'];
|
||||
const DEPT_COLORS = [
|
||||
'#0077cc','#cc0033','#ffb800','#cc44ff','#00994d','#ff6b00','#5b9cf6','#f472b6'
|
||||
];
|
||||
|
||||
// Scoring constants (must match main.py)
|
||||
const MAX_SCORE = 4500;
|
||||
const PRIZE_THRESHOLD = 3000;
|
||||
const MAX_PRIZE = 25;
|
||||
|
||||
function switchTab(name) {
|
||||
const names = ['global','dept','sessions'];
|
||||
document.querySelectorAll('.tab-btn').forEach((b,i) => {
|
||||
b.classList.toggle('active', names[i] === name);
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
}
|
||||
|
||||
function prizeColor(pts) {
|
||||
if (pts >= 23) return '#ffb800'; // gold zone
|
||||
if (pts >= 18) return '#0077cc'; // blue
|
||||
if (pts >= 12) return '#00994d'; // green
|
||||
return '#cc0033'; // red / low
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const res = await fetch('api/scores');
|
||||
@@ -230,19 +333,21 @@ async function load() {
|
||||
const playerMap = {};
|
||||
data.forEach(s => {
|
||||
s.players.forEach(p => {
|
||||
if (!playerMap[p.player]) {
|
||||
playerMap[p.player] = { best: p.score, total: p.score, sessions: 1 };
|
||||
const key = p.player;
|
||||
if (!playerMap[key]) {
|
||||
playerMap[key] = { best: p.score, total: p.score, sessions: 1, dept: p.dept || '—' };
|
||||
} else {
|
||||
if (p.score > playerMap[p.player].best) playerMap[p.player].best = p.score;
|
||||
playerMap[p.player].total += p.score;
|
||||
playerMap[p.player].sessions++;
|
||||
if (p.score > playerMap[key].best) playerMap[key].best = p.score;
|
||||
playerMap[key].total += p.score;
|
||||
playerMap[key].sessions++;
|
||||
if (p.dept) playerMap[key].dept = p.dept;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sorted = Object.entries(playerMap).sort((a,b) => b[1].best - a[1].best);
|
||||
|
||||
// ── TOP CARDS ─────────────────────────────────────────────────────────────
|
||||
// ── TOP 5 CARDS ───────────────────────────────────────────────────────────
|
||||
const topRow = document.getElementById('top-row');
|
||||
sorted.slice(0, 5).forEach(([name, info], i) => {
|
||||
const card = document.createElement('div');
|
||||
@@ -253,7 +358,7 @@ async function load() {
|
||||
<div class="top-info">
|
||||
<div class="top-name">${name}</div>
|
||||
<div class="top-score">${info.best.toLocaleString()}</div>
|
||||
<div class="top-meta">${info.sessions} SESSION${info.sessions>1?'S':''}</div>
|
||||
<div class="top-meta">${info.dept} · ${info.sessions} SESSION${info.sessions>1?'S':''}</div>
|
||||
</div>`;
|
||||
topRow.appendChild(card);
|
||||
});
|
||||
@@ -266,12 +371,85 @@ async function load() {
|
||||
tr.innerHTML = `
|
||||
<td class="td-medal">${i<3?medals[i]:''}<span class="td-rank" style="${i>=3?'':'display:none'}">#${i+1}</span></td>
|
||||
<td style="font-weight:600">${name}</td>
|
||||
<td class="td-dept">${info.dept}</td>
|
||||
<td class="td-score">${info.best.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${info.sessions}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${avg.toLocaleString()}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// ── BUILD DEPT MAP ────────────────────────────────────────────────────────
|
||||
const depts = {};
|
||||
sorted.forEach(([name, info]) => {
|
||||
const d = info.dept || '—';
|
||||
if (!depts[d]) depts[d] = [];
|
||||
depts[d].push({ name, best: info.best, total: info.total, sessions: info.sessions });
|
||||
});
|
||||
|
||||
// ── GROUP PRIZE SCORE TABLE ───────────────────────────────────────────────
|
||||
// Compute avg score per dept using best scores (fairest cross-session metric)
|
||||
const groupRows = Object.entries(depts).map(([dept, players]) => {
|
||||
const avgScore = Math.round(players.reduce((s, p) => s + p.best, 0) / players.length);
|
||||
const pct = ((avgScore / MAX_SCORE) * 100).toFixed(1);
|
||||
const prize = Math.min(MAX_PRIZE, Math.round((avgScore / PRIZE_THRESHOLD) * MAX_PRIZE));
|
||||
return { dept, players: players.length, avgScore, pct, prize };
|
||||
}).sort((a, b) => b.avgScore - a.avgScore);
|
||||
|
||||
const groupTbody = document.getElementById('group-score-tbody');
|
||||
groupRows.forEach((row, i) => {
|
||||
const color = prizeColor(row.prize);
|
||||
const barPct = Math.min(100, (row.prize / MAX_PRIZE) * 100);
|
||||
const tr = document.createElement('tr');
|
||||
if (i === 0) tr.className = 'group-winner';
|
||||
tr.innerHTML = `
|
||||
<td class="td-medal">${i<3 ? medals[i] : ''}<span class="td-rank" style="${i>=3?'':'display:none'}">#${i+1}</span></td>
|
||||
<td style="font-weight:700">${row.dept}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${row.players}</td>
|
||||
<td class="td-score">${row.avgScore.toLocaleString()}</td>
|
||||
<td class="td-pct">${row.pct}%</td>
|
||||
<td class="td-prize" style="color:${color}">${row.prize}</td>
|
||||
<td>
|
||||
<div class="prize-bar-wrap">
|
||||
<div class="prize-bar">
|
||||
<div class="prize-bar-fill" style="width:${barPct}%;background:${color}"></div>
|
||||
</div>
|
||||
<span class="prize-pts" style="color:${color}">${row.prize}/${MAX_PRIZE}</span>
|
||||
</div>
|
||||
</td>`;
|
||||
groupTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// ── DEPT INDIVIDUAL BREAKDOWN ─────────────────────────────────────────────
|
||||
const deptsSorted = Object.entries(depts).sort((a,b) => b[1][0].best - a[1][0].best);
|
||||
const deptList = document.getElementById('dept-list');
|
||||
deptsSorted.forEach(([dept, players], di) => {
|
||||
const color = DEPT_COLORS[di % DEPT_COLORS.length];
|
||||
const block = document.createElement('div');
|
||||
block.className = 'dept-block fade-up';
|
||||
block.style.animationDelay = `${di*.05}s`;
|
||||
block.innerHTML = `
|
||||
<div class="dept-header">
|
||||
<div class="dept-dot" style="background:${color}"></div>
|
||||
<div class="dept-name">${dept}</div>
|
||||
<div class="dept-count">${players.length} PLAYER${players.length>1?'S':''}</div>
|
||||
</div>
|
||||
<div class="table-wrap" style="border:none;border-radius:0;">
|
||||
<table>
|
||||
<thead><tr><th></th><th>PLAYER</th><th>BEST SCORE</th><th>SESSIONS</th></tr></thead>
|
||||
<tbody>
|
||||
${players.map((p, i) => `
|
||||
<tr>
|
||||
<td class="td-medal">${i<3?medals[i]:''}<span class="td-rank" style="${i>=3?'':'display:none'}">#${i+1}</span></td>
|
||||
<td style="font-weight:600">${p.name}</td>
|
||||
<td class="td-score">${p.best.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.75rem;color:var(--muted)">${p.sessions}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
deptList.appendChild(block);
|
||||
});
|
||||
|
||||
// ── SESSIONS ──────────────────────────────────────────────────────────────
|
||||
const list = document.getElementById('sessions-list');
|
||||
data.forEach(session => {
|
||||
@@ -295,12 +473,13 @@ async function load() {
|
||||
</div>
|
||||
<div class="session-body">
|
||||
<table>
|
||||
<thead><tr><th></th><th>PLAYER</th><th>SCORE</th><th>RANK</th></tr></thead>
|
||||
<thead><tr><th></th><th>PLAYER</th><th>DEPARTMENT</th><th>SCORE</th><th>RANK</th></tr></thead>
|
||||
<tbody>
|
||||
${session.players.map((p,i) => `
|
||||
<tr>
|
||||
<td class="td-medal">${i<3?medals[i]:''}</td>
|
||||
<td style="font-weight:600">${p.player}</td>
|
||||
<td class="td-dept">${p.dept || '—'}</td>
|
||||
<td class="td-score">${p.score.toLocaleString()}</td>
|
||||
<td style="font-family:var(--mono);font-size:.7rem;color:var(--muted)">#${p.rank}</td>
|
||||
</tr>`).join('')}
|
||||
|
||||
Reference in New Issue
Block a user