Upload files to "templates"

This commit is contained in:
2026-05-13 12:12:13 +00:00
parent a8deb6dd70
commit 55baa1fadd
3 changed files with 1803 additions and 0 deletions
+771
View File
@@ -0,0 +1,771 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Route Rush</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@400;600;800;900&display=swap');
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
:root{
--bg:#070b14;
--surface:#0d1220;
--surface2:#131928;
--accent:#00ffe0;
--accent2:#ff3e6c;
--warn:#ffb800;
--slow:#ff6b00;
--boost:#00ff88;
--blocked:#ff1744;
--text:#ddeeff;
--muted:#3a4a66;
--mono:'Share Tech Mono',monospace;
--sans:'Exo 2',sans-serif;
}
html,body{
width:100%;height:100%;
background:var(--bg);
color:var(--text);
font-family:var(--sans);
overflow:hidden;
touch-action:none;
}
/* scanline */
body::after{
content:'';position:fixed;inset:0;
background:repeating-linear-gradient(0deg,transparent,transparent 3px,rgba(0,255,224,0.015) 3px,rgba(0,255,224,0.015) 4px);
pointer-events:none;z-index:9999;
}
/* grid bg */
body::before{
content:'';position:fixed;inset:0;
background-image:
linear-gradient(rgba(0,255,224,0.03) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,255,224,0.03) 1px,transparent 1px);
background-size:32px 32px;
pointer-events:none;
}
/* ── SCREENS ── */
.screen{display:none;flex-direction:column;align-items:center;justify-content:center;
min-height:100vh;min-height:100dvh;padding:1.5rem;text-align:center;}
.screen.active{display:flex;}
/* ── JOIN ── */
#join-screen{
height:100vh;height:100dvh;
overflow-y:auto;
justify-content:center;
}
/* ── JOIN ── */
#join-screen .logo{
font-family:var(--sans);font-weight:900;font-size:2.8rem;
letter-spacing:-0.02em;line-height:1;margin-bottom:.25rem;
color:var(--accent);text-shadow:0 0 30px rgba(0,255,224,0.5);
}
#join-screen .logo span{color:var(--accent2);}
#join-screen .sub{font-family:var(--mono);font-size:.8rem;color:var(--text);
letter-spacing:.15em;margin-bottom:2.5rem;}
.field{width:100%;max-width:300px;margin-bottom:1rem;text-align:left;}
.field label{font-family:var(--mono);font-size:.75rem;color:var(--text);
letter-spacing:.12em;display:block;margin-bottom:.4rem;}
.field input{
width:100%;padding:.9rem 1rem;
background:var(--surface);
border:1px solid rgba(0,255,224,0.25);
border-radius:3px;
color:var(--text);font-family:var(--sans);font-size:1.1rem;font-weight:600;
outline:none;transition:border-color .2s;
}
.field input:focus{border-color:var(--accent);}
.field input::placeholder{color:var(--muted);}
.field select{
width:100%;padding:.9rem 1rem;
background:var(--surface);
border:1px solid rgba(0,255,224,0.25);
border-radius:3px;
color:var(--text);font-family:var(--sans);font-size:1.1rem;font-weight:600;
outline:none;transition:border-color .2s;cursor:pointer;
}
.field select:focus{border-color:var(--accent);}
.field select option{background:var(--surface2);}
#join-btn{
width:100%;max-width:300px;padding:1.1rem;margin-top:.5rem;
background:transparent;
border:1px solid var(--accent);
border-radius:3px;
color:var(--accent);font-family:var(--mono);font-size:.9rem;letter-spacing:.15em;
cursor:pointer;transition:all .2s;position:relative;overflow:hidden;
}
#join-btn::before{
content:'';position:absolute;inset:0;
background:var(--accent);opacity:0;transition:opacity .2s;
}
#join-btn:hover::before{opacity:.08;}
#join-btn:active{transform:scale(.97);}
/* ── LOBBY ── */
#lobby-screen .callsign{
font-family:var(--mono);font-size:.8rem;color:var(--accent);
letter-spacing:.15em;margin-bottom:.5rem;
}
#lobby-screen .pname{
font-family:var(--sans);font-weight:900;font-size:2.5rem;
letter-spacing:-0.02em;margin-bottom:2rem;
}
.signal-wrap{display:flex;flex-direction:column;align-items:center;gap:.5rem;}
.signal-bars{display:flex;align-items:flex-end;gap:5px;height:40px;}
.signal-bars span{
width:7px;border-radius:1px;background:var(--accent);opacity:.2;
animation:signalPulse 1.2s ease-in-out infinite;
}
.signal-bars span:nth-child(1){height:10px;animation-delay:0s;}
.signal-bars span:nth-child(2){height:18px;animation-delay:.15s;}
.signal-bars span:nth-child(3){height:26px;animation-delay:.3s;}
.signal-bars span:nth-child(4){height:34px;animation-delay:.45s;}
.signal-bars span:nth-child(5){height:40px;animation-delay:.6s;}
.signal-label{font-family:var(--mono);font-size:.75rem;color:var(--text);letter-spacing:.1em;
animation:blink 1.2s step-end infinite;}
/* ── GAME ── */
#game-screen{
height:100vh;height:100dvh;
padding:.5rem .75rem;
justify-content:flex-start;
overflow:hidden;
}
.game-hud{
width:100%;max-width:420px;
display:flex;justify-content:space-between;align-items:center;
padding:.4rem .75rem;
background:var(--surface);
border:1px solid rgba(0,255,224,0.1);
border-radius:3px;
margin-bottom:.4rem;
}
.hud-round{font-family:var(--mono);font-size:.85rem;color:var(--text);}
.hud-score{font-family:var(--mono);font-size:.85rem;color:var(--accent);}
.hud-timer{
font-family:var(--mono);font-size:1.7rem;font-weight:700;
color:var(--warn);min-width:2.5ch;text-align:center;
transition:color .3s;
}
.hud-timer.urgent{color:var(--accent2);animation:blink .4s step-end infinite;}
/* objective bar */
.objective{
width:100%;max-width:420px;
display:flex;align-items:center;justify-content:center;gap:.6rem;
padding:.4rem .75rem;
background:var(--surface);
border:1px solid rgba(0,255,224,0.2);
border-radius:3px;
margin-bottom:.4rem;
font-family:var(--mono);font-size:.85rem;color:var(--text);letter-spacing:.05em;
}
.obj-node{
display:inline-flex;align-items:center;justify-content:center;
width:34px;height:34px;border-radius:50%;
font-weight:800;font-size:1rem;font-family:var(--sans);
}
.obj-start{background:rgba(0,255,224,.15);border:2px solid var(--accent);color:var(--accent);}
.obj-end{background:rgba(255,62,108,.15);border:2px solid var(--accent2);color:var(--accent2);}
.obj-arrow{color:var(--text);}
/* ── GRID CANVAS AREA ── */
.grid-wrap{
width:100%;max-width:420px;
display:flex;align-items:center;justify-content:center;
position:relative;
}
#grid-canvas{
display:block;touch-action:none;
border-radius:4px;
}
/* submit btn */
#submit-btn{
width:100%;max-width:420px;margin-top:.4rem;
padding:.8rem;
background:var(--accent);border:none;border-radius:3px;
color:#070b14;font-family:var(--mono);font-size:.85rem;letter-spacing:.15em;
cursor:pointer;font-weight:700;
transition:all .2s;
}
#submit-btn:disabled{opacity:.3;cursor:not-allowed;}
#submit-btn:not(:disabled):active{transform:scale(.97);}
/* legend */
.legend{
width:100%;max-width:420px;margin-top:.3rem;
display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap;
}
.legend-item{display:flex;align-items:center;gap:.35rem;font-family:var(--mono);font-size:.75rem;color:var(--text);}
.legend-line{width:22px;height:4px;border-radius:2px;}
/* ── RESULT FLASH ── */
#result-screen{background:var(--bg);}
#result-screen .r-icon{font-size:3.5rem;margin-bottom:.75rem;}
#result-screen .r-label{
font-family:var(--mono);font-size:.85rem;letter-spacing:.15em;margin-bottom:.5rem;
}
#result-screen .r-pts{
font-family:var(--sans);font-weight:900;font-size:3.5rem;
letter-spacing:-0.03em;line-height:1;
margin-bottom:.25rem;
}
#result-screen .r-total{font-family:var(--mono);font-size:.8rem;color:var(--text);}
#result-screen .r-reason{
margin-top:1rem;font-family:var(--mono);font-size:.75rem;
color:var(--text);max-width:280px;line-height:1.8;
}
#result-screen .r-wait{
margin-top:2rem;font-family:var(--mono);font-size:.7rem;color:var(--text);
animation:blink 1s step-end infinite;
}
/* ── PODIUM ── */
#podium-screen h2{
font-family:var(--sans);font-weight:900;font-size:2.2rem;
letter-spacing:-0.02em;color:var(--warn);
text-shadow:0 0 20px rgba(255,184,0,.4);
margin-bottom:.25rem;
}
#podium-screen .sub{font-family:var(--mono);font-size:.7rem;color:var(--text);margin-bottom:1.5rem;}
.podium-list{width:100%;max-width:340px;}
.podium-item{
display:flex;align-items:center;gap:.75rem;
padding:.75rem 1rem;margin-bottom:.4rem;
background:var(--surface);
border:1px solid rgba(255,255,255,0.08);
border-radius:3px;
}
.podium-item.r1{border-color:rgba(255,184,0,.4);background:rgba(255,184,0,.05);}
.podium-item.r2{border-color:rgba(180,180,180,.3);}
.podium-item.r3{border-color:rgba(180,100,40,.3);}
.pod-rank{font-size:1.2rem;min-width:28px;}
.pod-name{flex:1;font-weight:700;font-size:1rem;}
.pod-score{font-family:var(--mono);font-size:.75rem;color:var(--accent);}
/* exit btn */
#exit-btn{
width:100%;max-width:340px;margin-top:1rem;
padding:.85rem;
background:transparent;border:1px solid rgba(255,255,255,.2);border-radius:3px;
color:var(--text);font-family:var(--mono);font-size:.75rem;letter-spacing:.15em;
cursor:pointer;transition:all .2s;
}
#exit-btn:active{transform:scale(.97);}
#exit-btn:hover{border-color:var(--accent2);color:var(--accent2);}
/* ── ANIMS ── */
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
@keyframes signalPulse{0%,100%{opacity:.2}50%{opacity:1}}
@keyframes fadeUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
@keyframes popIn{from{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}
.fade-up{animation:fadeUp .4s ease both;}
.pop{animation:popIn .35s cubic-bezier(.34,1.56,.64,1) both;}
</style>
</head>
<body>
<!-- JOIN -->
<div class="screen active" id="join-screen">
<div class="logo">ROUTE<span>RUSH</span></div>
<div class="sub">// NETWORK ROUTING GAME</div>
<div class="field">
<label>FIRST NAME</label>
<input id="inp-fname" type="text" maxlength="20" placeholder="First name" autocomplete="off" autocorrect="off" autocapitalize="words">
</div>
<div class="field">
<label>LAST NAME</label>
<input id="inp-lname" type="text" maxlength="20" placeholder="Last name" autocomplete="off" autocorrect="off" autocapitalize="words">
</div>
<div class="field">
<label>DEPARTMENT</label>
<select id="inp-dept">
<option value="">-- Select --</option>
<option>Network Intelligence</option>
<option>NOC</option>
<option>Network Engineering</option>
<option>Field Operations</option>
<option>IT</option>
<option>Planning</option>
<option>Other / Otro</option>
</select>
</div>
<button id="join-btn">▶ CONNECT</button>
</div>
<!-- LOBBY -->
<div class="screen" id="lobby-screen">
<div class="callsign">CONNECTED AS</div>
<div class="pname" id="lobby-name"></div>
<div class="signal-wrap">
<div class="signal-bars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="signal-label">AWAITING HOST...</div>
</div>
</div>
<!-- GAME -->
<div class="screen" id="game-screen">
<div class="game-hud">
<span class="hud-round" id="hud-round">RND 1/3</span>
<span class="hud-timer" id="hud-timer">30</span>
<span class="hud-score" id="hud-score">0 PTS</span>
</div>
<div class="objective">
<span>ROUTE FROM</span>
<div class="obj-node obj-start" id="obj-start">?</div>
<span class="obj-arrow">——▶</span>
<div class="obj-node obj-end" id="obj-end">?</div>
</div>
<div class="grid-wrap">
<canvas id="grid-canvas"></canvas>
</div>
<button id="submit-btn" disabled>SUBMIT ROUTE</button>
<div class="legend">
<div class="legend-item"><div class="legend-line" style="background:var(--accent);"></div>NORMAL</div>
<div class="legend-item"><div class="legend-line" style="background:var(--slow);"></div>SLOW ×2</div>
<div class="legend-item"><div class="legend-line" style="background:var(--boost);"></div>BOOST ×0.5</div>
<div class="legend-item"><div class="legend-line" style="background:var(--blocked);opacity:.5;"></div>DOWN</div>
</div>
</div>
<!-- RESULT -->
<div class="screen" id="result-screen">
<div class="r-icon" id="r-icon"></div>
<div class="r-label" id="r-label"></div>
<div class="r-pts pop" id="r-pts"></div>
<div class="r-total" id="r-total"></div>
<div class="r-reason" id="r-reason"></div>
<div class="r-wait">WAITING FOR HOST...</div>
</div>
<!-- PODIUM -->
<div class="screen" id="podium-screen">
<h2>GAME OVER</h2>
<div class="sub">// FINAL STANDINGS</div>
<div class="podium-list" id="podium-list"></div>
<button id="exit-btn" onclick="location.reload()">✕ EXIT GAME</button>
</div>
<script>
// ── STATE ────────────────────────────────────────────────────────────────────
const PID = Math.random().toString(36).slice(2) + Date.now().toString(36);
let ws, myName = '', myScore = 0;
let grid = null, path = [], submitted = false;
let timerInterval = null;
// ── CANVAS ───────────────────────────────────────────────────────────────────
const canvas = document.getElementById('grid-canvas');
const ctx = canvas.getContext('2d');
const GRID = 4;
let NODE_R, CELL, OFFSET_X, OFFSET_Y;
const MOD_COLOR = {
normal: '#00ffe0',
slow: '#ff6b00',
boost: '#00ff88',
blocked: '#ff1744',
};
function setupCanvas() {
const screenH = window.visualViewport ? window.visualViewport.height : window.innerHeight;
const screenW = window.visualViewport ? window.visualViewport.width : window.innerWidth;
// Reserve: padding(16) + hud(44) + gap(8) + objective(46) + gap(8) + submit(52) + gap(8) + legend(26) + bottom(8)
const reserved = 216;
const availH = screenH - reserved;
const availW = Math.min(screenW - 24, 420);
const size = Math.min(availW, availH, 380);
canvas.width = size;
canvas.height = size;
CELL = size / GRID;
NODE_R = CELL * 0.18;
OFFSET_X = CELL / 2;
OFFSET_Y = CELL / 2;
}
function nodePos(id) {
const row = Math.floor(id / GRID);
const col = id % GRID;
return {
x: OFFSET_X + col * CELL,
y: OFFSET_Y + row * CELL,
};
}
function drawGrid() {
if (!grid) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ── EDGES ──
grid.edges.forEach(e => {
const a = nodePos(e.a);
const b = nodePos(e.b);
const inPath = isEdgeInPath(e.a, e.b);
const color = MOD_COLOR[e.modifier] || '#00ffe0';
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
if (e.modifier === 'blocked') {
ctx.setLineDash([4, 4]);
ctx.strokeStyle = inPath ? color : 'rgba(255,23,68,0.3)';
ctx.lineWidth = inPath ? 3 : 1.5;
} else {
ctx.setLineDash([]);
ctx.strokeStyle = inPath ? color : `${color}33`;
ctx.lineWidth = inPath ? 3.5 : 1.5;
}
ctx.stroke();
ctx.setLineDash([]);
// modifier label on edge midpoint
if (e.modifier !== 'normal') {
const mx = (a.x + b.x) / 2;
const my = (a.y + b.y) / 2;
const label = e.modifier === 'slow' ? '2×' : e.modifier === 'boost' ? '½×' : '✕';
ctx.fillStyle = 'rgba(7,11,20,0.88)';
ctx.fillRect(mx - 12, my - 8, 24, 16);
ctx.fillStyle = color;
ctx.font = `bold ${Math.max(11, CELL * 0.17)}px 'Share Tech Mono', monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, mx, my);
}
});
// ── PATH LINE highlight ──
if (path.length > 1) {
ctx.beginPath();
const first = nodePos(path[0]);
ctx.moveTo(first.x, first.y);
for (let i = 1; i < path.length; i++) {
const p = nodePos(path[i]);
ctx.lineTo(p.x, p.y);
}
ctx.strokeStyle = 'rgba(0,255,224,0.15)';
ctx.lineWidth = CELL * 0.35;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
}
// ── NODES ──
grid.nodes.forEach(n => {
const {x, y} = nodePos(n.id);
const isStart = n.id === grid.start;
const isEnd = n.id === grid.end;
const inP = path.includes(n.id);
const isHead = path.length > 0 && path[path.length - 1] === n.id;
// outer ring for start/end
if (isStart || isEnd) {
ctx.beginPath();
ctx.arc(x, y, NODE_R * 1.6, 0, Math.PI * 2);
ctx.fillStyle = isStart ? 'rgba(0,255,224,0.08)' : 'rgba(255,62,108,0.08)';
ctx.fill();
ctx.strokeStyle = isStart ? 'var(--accent)' : 'var(--accent2)';
ctx.lineWidth = 1.5;
ctx.setLineDash([3,3]);
ctx.stroke();
ctx.setLineDash([]);
}
// node circle
ctx.beginPath();
ctx.arc(x, y, NODE_R, 0, Math.PI * 2);
if (isStart) {
ctx.fillStyle = '#00ffe0';
} else if (isEnd) {
ctx.fillStyle = '#ff3e6c';
} else if (isHead) {
ctx.fillStyle = '#ffffff';
} else if (inP) {
ctx.fillStyle = 'rgba(0,255,224,0.5)';
} else {
ctx.fillStyle = '#131928';
}
ctx.fill();
ctx.strokeStyle = inP ? '#00ffe0' : 'rgba(0,255,224,0.2)';
ctx.lineWidth = 1.5;
ctx.stroke();
// node label
ctx.fillStyle = (isStart || isEnd) ? '#070b14' : (inP ? '#00ffe0' : '#8899bb');
ctx.font = `bold ${Math.max(12, NODE_R * 1.1)}px 'Exo 2', sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(n.id, x, y);
});
}
function isEdgeInPath(a, b) {
for (let i = 0; i < path.length - 1; i++) {
if ((path[i] === a && path[i+1] === b) || (path[i] === b && path[i+1] === a)) return true;
}
return false;
}
function nodeAtPoint(px, py) {
if (!grid) return null;
let best = null, bestDist = NODE_R * 2.5;
grid.nodes.forEach(n => {
const {x, y} = nodePos(n.id);
const d = Math.hypot(px - x, py - y);
if (d < bestDist) { bestDist = d; best = n.id; }
});
return best;
}
function isAdjacent(a, b) {
return grid.edges.some(e =>
(e.a === a && e.b === b) || (e.a === b && e.b === a)
);
}
function tryAddNode(nid) {
if (submitted || nid === null) return;
if (path.length === 0) {
// shouldn't happen since start is pre-selected, but safety fallback
if (nid !== grid.start) return;
path.push(nid);
} else {
const last = path[path.length - 1];
if (nid === last) return; // tapped same node, ignore
// backtrack
const prev = path[path.length - 2];
if (prev !== undefined && nid === prev) {
path.pop();
} else {
if (!isAdjacent(last, nid)) return;
if (path.includes(nid)) return; // no loops
path.push(nid);
}
}
drawGrid();
updateSubmitBtn();
}
function updateSubmitBtn() {
const btn = document.getElementById('submit-btn');
const reachedEnd = path.length > 0 && path[path.length - 1] === grid.end;
btn.disabled = !reachedEnd || submitted;
}
// Touch / mouse handlers
function getCanvasPoint(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const src = e.touches ? e.touches[0] : e;
return {
x: (src.clientX - rect.left) * scaleX,
y: (src.clientY - rect.top) * scaleY,
};
}
canvas.addEventListener('touchstart', e => {
e.preventDefault();
const {x, y} = getCanvasPoint(e);
tryAddNode(nodeAtPoint(x, y));
}, {passive: false});
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const {x, y} = getCanvasPoint(e);
tryAddNode(nodeAtPoint(x, y));
}, {passive: false});
canvas.addEventListener('mousedown', e => {
const {x, y} = getCanvasPoint(e);
tryAddNode(nodeAtPoint(x, y));
});
// ── SCREENS ──────────────────────────────────────────────────────────────────
function show(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
// ── WS ────────────────────────────────────────────────────────────────────────
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
// Support sub-path proxy (e.g. /route-rush/)
const base = location.pathname.replace(/\/$/, '');
ws = new WebSocket(`${proto}://${location.host}${base}/ws/player/${PID}`);
ws.onmessage = e => handle(JSON.parse(e.data));
ws.onclose = () => setTimeout(connectWS, 2000);
}
function handle(msg) {
if (msg.type === 'phase' && msg.phase === 'lobby' && myName) {
show('lobby-screen');
}
else if (msg.type === 'joined') {
myName = msg.name;
document.getElementById('lobby-name').textContent = myName;
show('lobby-screen');
}
else if (msg.type === 'error') {
alert(msg.msg);
}
else if (msg.type === 'round_start') {
startRound(msg);
}
else if (msg.type === 'path_result') {
myScore = msg.total;
showResult(msg);
}
else if (msg.type === 'round_results') {
if (!submitted) showResult({points: 0, total: myScore, reason: 'timeout'});
else showRoundResults(msg);
}
else if (msg.type === 'podium') {
showPodium(msg.leaderboard);
}
}
// ── JOIN ──────────────────────────────────────────────────────────────────────
document.getElementById('join-btn').addEventListener('click', () => {
const fn = document.getElementById('inp-fname').value.trim();
const ln = document.getElementById('inp-lname').value.trim();
const dept = document.getElementById('inp-dept').value;
// highlight empty fields
const errors = [
!fn && 'inp-fname',
!ln && 'inp-lname',
!dept && 'inp-dept',
].filter(Boolean);
if (errors.length) {
errors.forEach(id => {
const el = document.getElementById(id);
el.style.borderColor = 'var(--accent2)';
setTimeout(() => el.style.borderColor = '', 1200);
});
return;
}
const display = `${fn} ${ln}`;
ws.send(JSON.stringify({type: 'join', name: display, dept, display}));
});
document.getElementById('inp-fname').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('inp-lname').focus();
});
document.getElementById('inp-lname').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('inp-dept').focus();
});
// ── ROUND ─────────────────────────────────────────────────────────────────────
function startRound(msg) {
grid = msg.grid;
path = [grid.start]; // auto-select start node
submitted = false;
show('game-screen');
setupCanvas();
document.getElementById('hud-round').textContent = `RND ${msg.round}/${msg.total_rounds}`;
document.getElementById('hud-score').textContent = `${myScore} PTS`;
document.getElementById('obj-start').textContent = grid.start;
document.getElementById('obj-end').textContent = grid.end;
document.getElementById('submit-btn').disabled = true;
drawGrid();
startTimer(msg.time_limit);
}
function startTimer(secs) {
clearInterval(timerInterval);
let rem = secs;
const el = document.getElementById('hud-timer');
function tick() {
el.textContent = rem;
el.classList.toggle('urgent', rem <= 8);
if (rem <= 0) { clearInterval(timerInterval); autoSubmit(); }
rem--;
}
tick();
timerInterval = setInterval(tick, 1000);
}
function autoSubmit() {
if (!submitted) {
submitted = true;
document.getElementById('submit-btn').disabled = true;
ws.send(JSON.stringify({type: 'submit_path', path}));
}
}
document.getElementById('submit-btn').addEventListener('click', () => {
if (submitted) return;
submitted = true;
clearInterval(timerInterval);
document.getElementById('submit-btn').disabled = true;
ws.send(JSON.stringify({type: 'submit_path', path}));
});
// ── RESULT ────────────────────────────────────────────────────────────────────
function showResult(msg) {
clearInterval(timerInterval);
const ok = msg.points > 0;
document.getElementById('r-icon').textContent = ok ? '📡' : '💀';
document.getElementById('r-label').textContent = ok ? '// ROUTE ACCEPTED' : '// ROUTE FAILED';
document.getElementById('r-label').style.color = ok ? 'var(--boost)' : 'var(--accent2)';
document.getElementById('r-pts').textContent = `+${msg.points}`;
document.getElementById('r-pts').style.color = ok ? 'var(--accent)' : 'var(--accent2)';
document.getElementById('r-total').textContent = `TOTAL: ${msg.total} PTS`;
const reasons = {
ok: 'Route successfully delivered.',
invalid_path: 'Path contains non-adjacent nodes.',
blocked: 'Path passes through a down link.',
timeout: 'Time expired — no route submitted.',
no_path: 'No route submitted.',
};
document.getElementById('r-reason').textContent = reasons[msg.reason] || '';
show('result-screen');
}
function showRoundResults(msg) {
// Could show full grid reveal — for now stay on result screen, host controls flow
}
// ── PODIUM ────────────────────────────────────────────────────────────────────
function showPodium(lb) {
const medals = ['🥇','🥈','🥉'];
const rc = ['r1','r2','r3'];
const list = document.getElementById('podium-list');
list.innerHTML = '';
lb.forEach((p, i) => {
const div = document.createElement('div');
div.className = `podium-item ${rc[i]||''} fade-up`;
div.style.animationDelay = `${i * 0.07}s`;
const isMe = p.name === myName;
div.innerHTML = `
<span class="pod-rank">${i < 3 ? medals[i] : i+1}</span>
<span class="pod-name">${p.name}${isMe ? ' 📡' : ''}</span>
<span class="pod-score">${p.score.toLocaleString()} PTS</span>`;
list.appendChild(div);
});
show('podium-screen');
}
// ── INIT ──────────────────────────────────────────────────────────────────────
window.addEventListener('resize', () => { if (grid) { setupCanvas(); drawGrid(); }});
connectWS();
</script>
</body>
</html>