From ce4209068f4ca08e77e046efabbef83e118e0977 Mon Sep 17 00:00:00 2001 From: Carlos Lugo Date: Thu, 4 Jun 2026 02:13:37 +0000 Subject: [PATCH] feat: update quiz templates Co-Authored-By: Claude Sonnet 4.6 --- templates/host.html | 43 +++-- templates/player.html | 197 ++++++++++--------- templates/scores.html | 433 +++++++++++++++++++++++++----------------- 3 files changed, 390 insertions(+), 283 deletions(-) diff --git a/templates/host.html b/templates/host.html index 02421e0..6cdc74d 100644 --- a/templates/host.html +++ b/templates/host.html @@ -10,14 +10,14 @@ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=DM+Sans:wght@400;500&display=swap'); *{margin:0;padding:0;box-sizing:border-box;} :root{ - --bg:#000510;--surface:#0d1117;--surface2:#161b22; - --accent:#00c8ff;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff; - --text:#e8edf5;--muted:#6b7a99;--border:rgba(255,255,255,0.08); + --bg:#ffffff;--surface:#f0f4f8;--surface2:#e2e8f0; + --accent:#0077cc;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff; + --text:#0a0f1e;--muted:#4a5568;--border:rgba(0,0,0,0.12); --pixel:'Press Start 2P',monospace; } +html{font-size:17px;} html,body{width:100%;height:100vh;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;overflow:hidden;} -body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.1) 2px,rgba(0,0,0,0.1) 4px);pointer-events:none;z-index:9999;} -.grid-bg{position:fixed;inset:0;background-image:linear-gradient(rgba(0,200,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,200,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;} +.grid-bg{position:fixed;inset:0;background-image: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;} .screen{display:none;height:100vh;padding:1.5rem 2rem;flex-direction:column;} .screen.active{display:flex;} @@ -53,6 +53,8 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie .hbtn-secondary:hover{border-color:var(--muted);color:var(--text);} .hbtn-danger{border-color:var(--red);color:var(--red);background:transparent;} .hbtn-danger:hover{background:rgba(255,51,85,.1);} +.hbtn-warning{border-color:var(--amber);color:var(--amber);background:transparent;} +.hbtn-warning:hover{background:rgba(255,204,0,.1);} /* ── QUESTION ── */ #question-screen{justify-content:space-between;} @@ -62,8 +64,10 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie .timer-big.urgent{color:var(--red);animation:blink .5s step-end infinite;} .q-body{display:flex;gap:1.5rem;flex:1;} .q-left{flex:1;} -.q-text{font-family:var(--pixel);font-size:clamp(.55rem,1.2vw,.75rem);line-height:2;margin-bottom:1.2rem;color:var(--text);} -.opts-grid{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;} +.q-text{margin-bottom:1.2rem;} +.q-es{font-family:var(--pixel);font-size:clamp(.55rem,1.1vw,.72rem);line-height:2;color:var(--text);display:block;} +.q-en{font-family:var(--pixel);font-size:clamp(.45rem,.9vw,.6rem);line-height:1.8;color:var(--muted);display:block;margin-top:.25rem;} +.opts-grid{display:flex;flex-direction:column;gap:.6rem;} .opt-box{background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:.8rem 1rem;font-size:.82rem;display:flex;align-items:center;gap:.6rem;} .opt-box.correct{border-color:var(--green);background:rgba(0,255,136,.07);} .opt-letter{font-family:var(--pixel);font-size:.55rem;min-width:24px;color:var(--muted);} @@ -175,6 +179,7 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
+
@@ -280,11 +285,22 @@ function handleMessage(msg) { else if (msg.type === 'question') { currentQ = msg.number; document.getElementById('q-meta').textContent = `Q ${msg.number} / ${msg.total}`; - document.getElementById('host-q-text').textContent = msg.q; + const qParts = msg.q.split('\n\n[EN]'); + const qEs = qParts[0].trim(); + const qEn = qParts[1] ? qParts[1].trim() : ''; + document.getElementById('host-q-text').innerHTML = + `${qEs}${qEn ? `[EN] ${qEn}` : ''}`; const letters = ['A','B','C','D']; - document.getElementById('host-opts').innerHTML = msg.options.map((o,i) => - `
${letters[i]}${o}
` - ).join(''); + const optColors = ['#5b9cf6','#cc44ff','#00ff88','#ffcc00']; + document.getElementById('host-opts').innerHTML = msg.options.map((o,i) => { + const parts = o.split(' / '); + const oEs = parts[0] || o; + const oEn = parts.slice(1).join(' / ') || ''; + return `
+ ${letters[i]} + ${oEs}${oEn ? `${oEn}` : ''} +
`; + }).join(''); document.getElementById('ans-count').textContent = '0'; document.getElementById('ans-sub').textContent = `of ${currentTotal} players`; document.getElementById('ans-bar').style.width = '0%'; @@ -357,6 +373,11 @@ async function clearScores() { else alert('Error al borrar scores.'); } +function clearLobby() { + if (!confirm('¿Remover todos los jugadores del lobby? Los jugadores serán enviados de regreso al join screen.')) return; + ws.send(JSON.stringify({type: 'clear_lobby'})); +} + function buildPodium(lb) { const medals = ['🥇','🥈','🥉']; const pbClass = ['pb1','pb2','pb3']; diff --git a/templates/player.html b/templates/player.html index b10710f..a7e2dd5 100644 --- a/templates/player.html +++ b/templates/player.html @@ -10,7 +10,7 @@ :root{ --bg:#000510;--surface:#0a0e1a;--surface2:#111827; --accent:#00c8ff;--green:#00ff88;--red:#ff3355;--amber:#ffcc00;--purple:#cc44ff; - --text:#e8edf5;--muted:#4a5568;--border:rgba(255,255,255,0.08); + --text:#e8edf5;--muted:#8a9bb5;--border:rgba(255,255,255,0.08); --pixel:'Press Start 2P',monospace; } html,body{width:100%;height:100%;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;overflow:hidden;touch-action:none;} @@ -39,21 +39,21 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie /* ── JOIN ── */ #join-screen .logo{font-family:var(--pixel);font-size:1rem;color:var(--accent);margin-bottom:.3rem;text-shadow:0 0 20px var(--accent);line-height:1.8;} -#join-screen .sub{font-size:.75rem;color:var(--muted);margin-bottom:2rem;} -.field-wrap{width:100%;max-width:320px;margin-bottom:.8rem;text-align:left;} -.field-wrap label{font-family:var(--pixel);font-size:.45rem;color:var(--muted);letter-spacing:.1em;display:block;margin-bottom:.4rem;} +#join-screen .sub{font-size:.85rem;color:var(--text);margin-bottom:2rem;opacity:.75;} +.field-wrap{width:100%;max-width:380px;margin-bottom:1rem;text-align:left;} +.field-wrap label{font-family:var(--pixel);font-size:.55rem;color:var(--accent);letter-spacing:.12em;display:block;margin-bottom:.5rem;} .field-wrap input, .field-wrap select{ - width:100%;padding:.85rem 1rem; - background:var(--surface2);border:1px solid var(--border);border-radius:4px; - color:var(--text);font-size:.9rem;font-family:'DM Sans',sans-serif; + width:100%;padding:1rem 1.1rem; + background:var(--surface2);border:1.5px solid var(--border);border-radius:6px; + color:var(--text);font-size:1.1rem;font-family:'DM Sans',sans-serif; outline:none; } .field-wrap input:focus,.field-wrap select:focus{border-color:var(--accent);} -.field-wrap input::placeholder{color:var(--muted);} +.field-wrap input::placeholder{color:#5a6a80;} .field-wrap select option{background:var(--surface2);} #join-btn{ - width:100%;max-width:320px;padding:1rem;margin-top:.5rem; - background:var(--accent);border:none;border-radius:4px; + width:100%;max-width:380px;padding:1.1rem;margin-top:.6rem; + background:var(--accent);border:none;border-radius:6px; color:#000;font-family:var(--pixel);font-size:.6rem;letter-spacing:.1em; cursor:pointer; } @@ -96,13 +96,11 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie #score-hud .hud-score{font-family:var(--pixel);font-size:.5rem;color:var(--accent);} #score-hud .hud-timer{font-family:var(--pixel);font-size:.75rem;color:var(--amber);} #score-hud .hud-q{font-family:var(--pixel);font-size:.4rem;color:var(--muted);} -#question-banner{ - font-family:'DM Sans',sans-serif;font-size:.88rem;font-weight:500; - color:var(--text);line-height:1.5;text-align:center; - background:rgba(0,5,16,.9);border:1px solid var(--border); - border-radius:6px;padding:.55rem .9rem; - /* clamp so it never grows past ~25% of viewport */ - max-height:28vh;overflow:hidden; +#projector-hint{ + font-family:var(--pixel);font-size:.42rem; + color:var(--muted);line-height:1.5;text-align:center; + letter-spacing:.08em;padding:.3rem 0; + animation:blink 2s step-end infinite; } /* ── RESULT FLASH ── */ @@ -156,12 +154,12 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie - +
🌐 NI QUIZ
NETWORK INTELLIGENCE · LCPR
- - + +
@@ -180,13 +178,11 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie @@ -210,7 +206,7 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie 15 Q1/5 -
+
@@ -290,16 +286,16 @@ function t(key, vars={}) { return s; } -function setLang(l) { - lang = l; - // Update join labels - document.getElementById('lbl-fname').textContent = t('fname'); - document.getElementById('lbl-lname').textContent = t('lname'); - document.getElementById('lbl-dept').textContent = t('dept'); - document.getElementById('join-btn').textContent = t('enter'); - document.getElementById('inp-dept').options[0].text = t('dept_select'); +function goJoin() { + lang = 'es'; + document.getElementById('lbl-fname').textContent = 'NOMBRE / NAME'; + document.getElementById('lbl-lname').textContent = 'APELLIDO / LAST NAME'; + document.getElementById('lbl-dept').textContent = 'DEPARTAMENTO / DEPARTMENT'; + document.getElementById('join-btn').textContent = '▶ ENTRAR / JOIN'; + document.getElementById('inp-dept').options[0].text = '-- Selecciona / Select --'; show('join-screen'); } +function setLang(l) { goJoin(); } // ── SCREENS ────────────────────────────────────────────────────────────────── function show(id) { @@ -328,6 +324,10 @@ function handleMessage(msg) { } else if (msg.type === 'phase') { if (msg.phase === 'lobby' && myName) show('lobby-screen'); + else if (msg.phase === 'join') { + myName = ''; myScore = 0; myDept = ''; + show('join-screen'); + } } else if (msg.type === 'question') { startGame(msg); @@ -392,62 +392,35 @@ function resizeCanvas() { window.addEventListener('resize', resizeCanvas); resizeCanvas(); -const COL_COLORS = ['#00c8ff','#cc44ff','#00ff88','#ffcc00']; +const COL_COLORS = ['#5b9cf6','#cc44ff','#00ff88','#ffcc00']; // A=blue B=purple C=green D=yellow function startGame(msg) { resizeCanvas(); const W = canvas.width, H = canvas.height; const colW = W / 4; - // Invader block sizing — responsive to screen width - // On 375px wide: colW=93, invW≈77, invH≈90 (enough for 4-5 lines at 13px) - const invW = colW - 12; - const invPadX = 20; // space for letter badge on left - const textAreaW = invW - invPadX - 10; - const fontSize = Math.max(12, Math.min(14, W * 0.034)); // 12–14px - const lineH = fontSize * 1.4; - - // Pre-measure how many lines each option needs, then set uniform height - const measCtx = canvas.getContext('2d'); - measCtx.font = `${fontSize}px 'DM Sans', sans-serif`; - function countLines(text, maxW) { - const words = text.split(' '); - let line = '', count = 1; - words.forEach(w => { - const test = line ? line + ' ' + w : w; - if (measCtx.measureText(test).width > maxW && line) { count++; line = w; } - else line = test; - }); - return count; - } - const maxLines = Math.max(...msg.options.map(o => countLines(o, textAreaW))); - const badgeH = 20; // letter badge row - const invH = badgeH + maxLines * lineH + 20; // 20px vertical padding + // Fixed invader size — no text inside, just letter + const invW = Math.min(colW - 16, 90); + const invH = invW; // square-ish UFO shape gameState = { ship: {x: W/2, targetX: W/2, y: H - 120, w: 36, h: 28}, bullets: [], invaders: msg.options.map((text, idx) => ({ x: colW * idx + colW/2, - y: 120, - targetY: 120, + y: 0, + targetY: 0, w: invW, h: invH, - text, idx, hit: false, hitTimer: 0, - correct: idx === msg.correct_idx_hint, - fontSize, - lineH, - textAreaW, })), particles: [], answered: false, correctIdx: msg.correct_idx_hint ?? -1, timeLeft: msg.time_limit, timerStart: performance.now(), - options: msg.options, correctAnswer: msg.correct_idx_hint, W, H, colW, totalTime: msg.time_limit, @@ -455,18 +428,18 @@ function startGame(msg) { qTotal: msg.total, }; - document.getElementById('question-banner').textContent = msg.q; + document.getElementById('projector-hint').textContent = lang === 'es' ? '👆 MIRA EL PROYECTOR' : '👆 LOOK AT THE SCREEN'; document.getElementById('hud-q').textContent = `${t('q_prefix')}${msg.number}${t('of')}${msg.total}`; document.getElementById('result-flash').classList.remove('active'); // Measure overlay height so invaders don't spawn behind it const overlay = document.getElementById('game-overlay'); const overlayH = overlay ? overlay.getBoundingClientRect().height : 100; - const spawnY = overlayH + invH / 2 + 20; // first visible position just below overlay + const spawnY = overlayH + invH / 2 + 20; - // Invaders start high and slowly descend + // Invaders start above screen and descend into position gameState.invaders.forEach((inv, i) => { - inv.y = -invH - i * 25; + inv.y = -invH - i * 30; inv.targetY = spawnY; }); @@ -545,38 +518,62 @@ function gameFrame(ts) { // Slowly descend after appearing if (!gs.answered) { inv.y += (inv.targetY - inv.y) * 0.08; - inv.targetY += 0.25; // drift down slowly + inv.targetY += 0.25; } const color = COL_COLORS[i]; - // Box - ctx.fillStyle = `rgba(${hexToRgb(color)},0.10)`; + const letters = ['A','B','C','D']; + const urgent = timeLeft <= 3 && !gs.answered && i === gs.correctIdx; + + // Shake offset when urgent + const shakeX = urgent ? Math.sin(ts * 0.045 + i * 1.3) * 4 : 0; + const shakeY = urgent ? Math.cos(ts * 0.05 + i * 0.9) * 3 : 0; + const cx = inv.x + shakeX, cy = inv.y + shakeY; + const r = inv.w / 2; + + // Urgent pulse: glow intensity oscillates fast + const pulse = urgent ? 0.5 + 0.5 * Math.sin(ts * 0.025) : 0; + + // UFO dome (top half ellipse) + ctx.fillStyle = `rgba(${hexToRgb(color)},${0.18 + pulse * 0.22})`; ctx.strokeStyle = color; - ctx.lineWidth = 1.5; - roundRect(ctx, inv.x - inv.w/2, inv.y - inv.h/2, inv.w, inv.h, 8); + ctx.lineWidth = urgent ? 2 + pulse * 2 : 2; + ctx.shadowColor = color; + ctx.shadowBlur = urgent ? 10 + pulse * 24 : 0; + ctx.beginPath(); + ctx.ellipse(cx, cy, r, r * 0.55, 0, Math.PI, 0); ctx.fill(); ctx.stroke(); - // Letter badge — top-left, full row - const letters = ['A','B','C','D']; - const badgeSize = 13; - ctx.fillStyle = color; - ctx.font = `bold ${badgeSize}px 'Press Start 2P', monospace`; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillText(letters[i], inv.x - inv.w/2 + 8, inv.y - inv.h/2 + 7); + // UFO saucer body (full ellipse) + ctx.fillStyle = `rgba(${hexToRgb(color)},${0.25 + pulse * 0.2})`; + ctx.strokeStyle = color; + ctx.lineWidth = urgent ? 2 + pulse * 2 : 2; + ctx.shadowColor = color; + ctx.shadowBlur = urgent ? 14 + pulse * 20 : 0; + ctx.beginPath(); + ctx.ellipse(cx, cy, r, r * 0.32, 0, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + ctx.shadowBlur = 0; - // Answer text — wrap inside block, below badge row - const fz = inv.fontSize || 12; - const lh = inv.lineH || 17; - const maxW = inv.textAreaW || (inv.w - 28); - ctx.fillStyle = '#e8edf5'; - ctx.font = `${fz}px 'DM Sans', sans-serif`; + // Glow under saucer + ctx.shadowColor = color; + ctx.shadowBlur = urgent ? 18 + pulse * 28 : 14; + ctx.beginPath(); + ctx.ellipse(cx, cy + r * 0.1, r * 0.6, r * 0.12, 0, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${hexToRgb(color)},${0.35 + pulse * 0.35})`; + ctx.fill(); + ctx.shadowBlur = 0; + + // Big centered letter + const letterSize = Math.max(18, r * 0.85); + ctx.fillStyle = color; + ctx.font = `bold ${letterSize}px 'Press Start 2P', monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - // Text block starts after badge (20px) + padding (4px) - const textBlockTop = inv.y - inv.h/2 + 24; - const textBlockH = inv.h - 28; - wrapText(ctx, inv.text, inv.x, textBlockTop + textBlockH/2, maxW, lh); + ctx.shadowColor = color; + ctx.shadowBlur = urgent ? 16 + pulse * 20 : 10; + ctx.fillText(letters[i], cx, cy - r * 0.28); + ctx.shadowBlur = 0; }); // ── SHIP ── @@ -612,12 +609,14 @@ function gameFrame(ts) { ctx.fillRect(b.x - 2, b.y, 4, 16); ctx.shadowBlur = 0; - // Collision check + // Collision check — circular hitbox matching UFO saucer radius if (!gs.answered) { gs.invaders.forEach((inv, i) => { if (inv.hit) return; - if (b.x > inv.x - inv.w/2 && b.x < inv.x + inv.w/2 && - b.y > inv.y - inv.h/2 && b.y < inv.y + inv.h/2) { + const r = inv.w / 2; + const dx = b.x - inv.x; + const dy = b.y - inv.y; + if (Math.sqrt(dx*dx + dy*dy) < r) { b.y = -999; // destroy bullet if (!gs.answered) { gs.answered = true; diff --git a/templates/scores.html b/templates/scores.html index 71ca60d..5676066 100644 --- a/templates/scores.html +++ b/templates/scores.html @@ -3,169 +3,251 @@ -NI Quiz · Historial de Scores +NI Quiz · Historial -
-

🌐 NI Quiz · Historial

-

Network Intelligence · LCPR — Scores guardados por sesión

-
-
Cargando historial...
-
📭

Aún no hay sesiones guardadas.
Los scores se guardan al terminar el juego.

+
CARGANDO HISTORIAL
+
📡Aún no hay sesiones guardadas.
Los scores se guardan al terminar el juego.
-