feat: update quiz templates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+98
-99
@@ -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
|
||||
<!-- STARS BG -->
|
||||
<canvas class="pixel-stars" id="stars-canvas"></canvas>
|
||||
|
||||
<!-- LANGUAGE SELECT -->
|
||||
<!-- SPLASH — goes straight to join -->
|
||||
<div class="screen active" id="lang-screen">
|
||||
<div class="title">🌐 NI QUIZ</div>
|
||||
<div class="subtitle">NETWORK INTELLIGENCE · LCPR</div>
|
||||
<button class="lang-btn en" onclick="setLang('en')">🇺🇸 ENGLISH</button>
|
||||
<button class="lang-btn es" onclick="setLang('es')">🇵🇷 ESPAÑOL</button>
|
||||
|
||||
<button class="lang-btn es" onclick="goJoin()">▶ JUGAR / PLAY</button>
|
||||
</div>
|
||||
|
||||
<!-- JOIN -->
|
||||
@@ -180,13 +178,11 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
<label id="lbl-dept">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>
|
||||
<option>Finanzas</option>
|
||||
<option>People</option>
|
||||
<option>Legal</option>
|
||||
<option>B2B</option>
|
||||
<option>B2C</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="join-btn">▶ ENTER GAME</button>
|
||||
@@ -210,7 +206,7 @@ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradie
|
||||
<span class="hud-timer" id="hud-timer">15</span>
|
||||
<span class="hud-q" id="hud-q">Q1/5</span>
|
||||
</div>
|
||||
<div id="question-banner"></div>
|
||||
<div id="projector-hint"></div>
|
||||
</div>
|
||||
<div id="result-flash">
|
||||
<div id="flash-icon"></div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user