feat: add Level 7 Millionaire quiz with SVG comarca preview and 4-option A/B/C/D format

This commit is contained in:
Jaume Garriga Maestre
2026-05-04 11:22:12 +02:00
parent 1e6a180a3a
commit 63df11b843

View File

@@ -243,6 +243,71 @@
.players-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));}
.avatar-grid{grid-template-columns:repeat(4,1fr);}
}
/* ── Nivell 7 · Milionari ── */
#screen-mill {
background: linear-gradient(180deg, #08082a 0%, #140a3e 100%);
min-height: 100vh;
padding-bottom: 40px;
}
/* Option buttons — show style */
.mill-opt {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px;
border-radius: 12px;
font-family: inherit;
font-size: .92rem;
font-weight: 600;
border: 2px solid #3d7fd4;
background: linear-gradient(135deg, #0a2152, #153a7a);
color: #e8eeff;
cursor: pointer;
text-align: left;
transition: border-color .15s, background .15s;
min-height: 52px;
line-height: 1.3;
}
.mill-opt:hover:not(.disabled) {
border-color: #FFD700;
background: linear-gradient(135deg, #12295e, #1d4a9a);
}
.mill-opt.correct {
border-color: #2ecc71;
background: linear-gradient(135deg, #0a3d1f, #145e2d);
color: #b2ffce;
}
.mill-opt.wrong {
border-color: #e74c3c;
background: linear-gradient(135deg, #3d0a0a, #6e1212);
color: #ffb2b2;
}
.mill-opt.disabled { cursor: default; }
/* Letter badge (A/B/C/D) */
.opt-letter {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid currentColor;
font-size: .82rem;
font-weight: 800;
flex-shrink: 0;
}
/* Progress dots */
.mill-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid #555;
background: #222;
transition: background .3s, border-color .3s;
}
.mill-dot.done { background: #FFD700; border-color: #FFD700; }
.mill-dot.current { background: #fff; border-color: #fff; }
</style>
</head>
<body>
@@ -333,6 +398,11 @@
<div class="lvl-name">Nivell 6 · Mapa Cec</div>
<div class="lvl-desc">Sense noms al mapa. El repte màxim!</div>
</div>
<div class="level-card" onclick="startLevel(7)" style="grid-column:span 2;background:linear-gradient(135deg,#1a1200,#3d2c00);border:2px solid #FFD700;">
<span class="emoji">💰</span>
<div class="lvl-name" style="color:#FFD700;">Nivell 7 · Milionari</div>
<div class="lvl-desc">Qui vol ser milionari? 4 opcions, 10 preguntes!</div>
</div>
</div>
<div class="home-actions">
<button class="btn btn-green" onclick="goToStats()" id="btn-stats" style="display:none;">
@@ -468,6 +538,36 @@
</div>
</div>
<!-- ═══════════════════ MILIONARI (Level 7) ═══════════════════ -->
<div id="screen-mill" class="screen">
<button class="btn btn-back" onclick="goHome()" style="color:#FFD700;background:rgba(255,215,0,.12);border:1px solid #FFD700;">← Tornar</button>
<!-- Progress dots: 10 circles showing answered/current/pending state -->
<div id="mill-dots" style="display:flex;gap:7px;justify-content:center;margin:10px 0 8px;"></div>
<!-- Score header: correct/total -->
<div style="text-align:right;font-size:.9rem;font-weight:700;color:#FFD700;margin-bottom:8px;" id="mill-score-hdr">0/0</div>
<!-- Mini SVG preview map of the target comarca -->
<div id="mill-map-wrap" style="width:100%;border-radius:14px;overflow:hidden;margin-bottom:12px;background:#08082a;border:1.5px solid #3d7fd4;">
<svg id="mill-map-svg" viewBox="0 0 820 600" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;display:block;"></svg>
</div>
<!-- Question text -->
<div id="mill-qtext" style="text-align:center;font-size:1.25rem;font-weight:800;color:#fff;margin-bottom:14px;min-height:56px;line-height:1.35;"></div>
<!-- 4 option buttons (A/B/C/D) -->
<div id="mill-opts" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px;"></div>
<!-- Next button (hidden until answered) -->
<div style="text-align:center;">
<button id="mill-next" class="btn" onclick="millNext()"
style="display:none;background:#FFD700;color:#08082a;font-weight:800;font-size:1rem;padding:13px 32px;border-radius:50px;">
➡ Següent
</button>
</div>
</div>
<!-- ═══════════════════ STATS ═══════════════════ -->
<div id="screen-stats" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
@@ -1249,7 +1349,8 @@ function startLevel(n){
sessionStart=Date.now();
if(n===1)initL1();else if(n===2)initL2();else if(n===3)initL3();
else if(n===4)initL4();else if(n===5){mapBlind=false;initMap();}
else{mapBlind=true;initMap();}
else if(n===6){mapBlind=true;initMap();}
else{initMill();}
}
function repeatLevel(){ startLevel(currentLevel); }
@@ -1459,6 +1560,216 @@ function handleMapClick(path){
setTimeout(()=>{path.classList.remove('wrong-shake');currentQ++;nextMapQ();},2400);
}
}
/* ══════════════════════════════════════════════════════
NIVELL 7 · MILIONARI
"Qui vol ser milionari?" — 10 questions, 4 options A/B/C/D,
alternating between two question types:
type A: "Quina és la capital de <comarca>?" → options = 4 capitals
type B: "De quina comarca és capital <capital>?" → options = 4 comarca names
A mini SVG map highlights the target comarca in gold.
══════════════════════════════════════════════════════ */
const MILL_TOTAL = 10; // questions per session
let millQuestions = []; // selected slice from shuffled pool
let millQ = 0; // current question index (0-based)
let millScore = 0; // correct answers count
/** Escape HTML special chars for safe insertion into data-attributes. */
function escapeHtml(s) {
return String(s)
.replace(/&/g,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
/** Entry point called by startLevel(7). */
function initMill() {
millQ = 0;
millScore = 0;
// millQuestions already assigned as shuffled mapComarques in startLevel()
millQuestions = questions.slice(0, MILL_TOTAL);
showScreen('screen-mill');
renderMill();
}
/** Render the current question. */
function renderMill() {
const comarca = millQuestions[millQ];
// ── Progress dots ──────────────────────────────────────────
const dotsEl = document.getElementById('mill-dots');
dotsEl.innerHTML = '';
for (let i = 0; i < millQuestions.length; i++) {
const d = document.createElement('div');
d.className = 'mill-dot' + (i < millQ ? ' done' : i === millQ ? ' current' : '');
dotsEl.appendChild(d);
}
// ── Score header ──────────────────────────────────────────
document.getElementById('mill-score-hdr').textContent = `${millScore}/${millQ}`;
// ── Mini map ──────────────────────────────────────────────
buildMillSVG(comarca);
// ── Decide question type — alternate randomly ─────────────
// type 'capital': "Quina és la capital de <comarca>?" → options = capitals
// type 'comarca': "De quina comarca és capital <capital>?" → options = comarca names
const qType = Math.random() < 0.5 ? 'capital' : 'comarca';
let questionText, correctAnswer, pool, optionFn;
if (qType === 'capital') {
questionText = `Quina és la capital de ${comarca.emoji} ${comarca.name}?`;
correctAnswer = comarca.capital;
// Distractors: other capitals from the same questions pool
pool = millQuestions.filter(c => c.name !== comarca.name);
optionFn = c => c.capital;
} else {
questionText = `De quina comarca és capital 📍 ${comarca.capital}?`;
correctAnswer = comarca.name;
pool = millQuestions.filter(c => c.name !== comarca.name);
optionFn = c => c.name;
}
document.getElementById('mill-qtext').textContent = questionText;
// Build 4 options: 1 correct + 3 random distractors
const distractors = shuffle(pool).slice(0, 3).map(optionFn);
const options = shuffle([correctAnswer, ...distractors]);
const letters = ['A', 'B', 'C', 'D'];
const optsEl = document.getElementById('mill-opts');
optsEl.innerHTML = options.map((opt, i) => `
<button class="mill-opt"
data-opt="${escapeHtml(opt)}"
data-correct="${escapeHtml(correctAnswer)}"
onclick="millAnswer(this)">
<span class="opt-letter">${letters[i]}</span>${escapeHtml(opt)}
</button>`).join('');
// Hide the Next button until an answer is chosen
document.getElementById('mill-next').style.display = 'none';
}
/** Handle option click. Disables all options, colours correct/wrong, shows Next. */
function millAnswer(btn) {
const chosen = btn.dataset.opt;
const correct = btn.dataset.correct;
// Disable all option buttons
document.querySelectorAll('.mill-opt').forEach(b => {
b.classList.add('disabled');
b.onclick = null;
});
if (chosen === correct) {
btn.classList.add('correct');
millScore++;
score++; // global score used by showResult()
showFeedback(true);
} else {
btn.classList.add('wrong');
// Highlight the correct answer in green
document.querySelectorAll('.mill-opt').forEach(b => {
if (b.dataset.opt === correct) b.classList.add('correct');
});
showFeedback(false);
}
millQ++; // advance AFTER recording the answer
// Update score header immediately
document.getElementById('mill-score-hdr').textContent = `${millScore}/${millQ}`;
// Show Next / Result button
const nextBtn = document.getElementById('mill-next');
nextBtn.style.display = 'block';
nextBtn.textContent = millQ >= millQuestions.length ? '🏆 Veure resultat' : '➡ Següent';
}
/** Advance to next question or show final result. */
function millNext() {
if (millQ >= millQuestions.length) {
// questions.length must match millQuestions.length for showResult() pct calc
questions = millQuestions;
showResult(7);
} else {
renderMill();
}
}
/**
* Build the mini SVG preview map for the Milionari screen.
* Target comarca: gold (#FFD700), others: dark blue (#1c3a5e).
* Red double-circle marks the capital centroid.
* ViewBox zoomed to comarca centroid ±110×82.5 units (220×165 window), clamped.
*/
function buildMillSVG(comarca) {
const svg = document.getElementById('mill-map-svg');
svg.innerHTML = '';
const info = COMARCA_PATHS[comarca.name];
// ── Background sea ──────────────────────────────────────────
const sea = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
sea.setAttribute('width', '820');
sea.setAttribute('height', '600');
sea.setAttribute('fill', '#08082a');
svg.appendChild(sea);
// ── All comarca paths ────────────────────────────────────────
for (const [name, pInfo] of Object.entries(COMARCA_PATHS)) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pInfo.d);
if (name === comarca.name) {
path.setAttribute('fill', '#FFD700');
path.setAttribute('stroke', '#fff');
path.setAttribute('stroke-width', '1.5');
} else {
path.setAttribute('fill', '#1c3a5e');
path.setAttribute('stroke', '#0a1e38');
path.setAttribute('stroke-width', '0.6');
}
svg.appendChild(path);
}
// ── Capital marker: outer ring + inner dot (no CSS animation needed) ─
if (info) {
const cx = info.cx;
const cy = info.cy;
// Outer ring (semi-transparent red)
const outer = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
outer.setAttribute('cx', cx);
outer.setAttribute('cy', cy);
outer.setAttribute('r', '9');
outer.setAttribute('fill', 'rgba(220,50,50,0.35)');
outer.setAttribute('stroke', '#e03030');
outer.setAttribute('stroke-width', '1.5');
svg.appendChild(outer);
// Inner dot (solid red)
const inner = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
inner.setAttribute('cx', cx);
inner.setAttribute('cy', cy);
inner.setAttribute('r', '4');
inner.setAttribute('fill', '#e03030');
svg.appendChild(inner);
// ── Zoom viewBox centred on target comarca ────────────────
// Window: 220 × 165 units; clamp to [0,820] × [0,600]
const W = 220, H = 165;
const x0 = Math.max(0, Math.min(820 - W, cx - W / 2));
const y0 = Math.max(0, Math.min(600 - H, cy - H / 2));
svg.setAttribute('viewBox', `${x0} ${y0} ${W} ${H}`);
} else {
// Fallback: full map view if no centroid data
svg.setAttribute('viewBox', '0 0 820 600');
}
}
</script>
</body>
</html>