From b9404dd5b2a93da59e543ccb262fe27dd2eea2d8 Mon Sep 17 00:00:00 2001 From: Jaume Garriga Maestre Date: Wed, 13 May 2026 22:29:45 +0200 Subject: [PATCH] feat: add Level 8 Trencaclosques (drag-and-drop puzzle) - Blind base map (grey slots for all comarques) - Shuffled tray at the bottom with each comarca's shape + name - Drag-and-drop with pointer events (works on touch + mouse) - Hit test: SVG isPointInFill (precise) + 65px center fallback for small comarques - Correct placement: comarca snaps to map with color + label - Wrong drop: piece shakes and returns to tray (counts errors) - Score = total pieces - errors; stars via showResult(8) - Respects active group filter (same piece count as other map levels) --- index.html | 263 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index d7e996b..8ffaa18 100644 --- a/index.html +++ b/index.html @@ -253,6 +253,34 @@ .avatar-grid{grid-template-columns:repeat(4,1fr);} } + /* ── Nivell 8 · Trencaclosques ── */ + #screen-puzzle { padding-bottom:20px; } + #puzzle-map-wrap { width:100%; background:#D8EEF8; border-radius:16px; + overflow:hidden; box-shadow:var(--shadow); } + #puzzle-tray-wrap { margin-top:10px; background:#f2ede3; border-radius:14px; + padding:8px 8px 10px; box-shadow:var(--shadow); } + #puzzle-tray-label { font-size:.72rem; font-weight:700; color:#888; + text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px; padding-left:2px; } + #puzzle-tray { display:flex; gap:8px; overflow-x:auto; overflow-y:hidden; + padding:4px 2px 6px; scroll-snap-type:x mandatory; } + #puzzle-tray::-webkit-scrollbar{ height:4px; } + #puzzle-tray::-webkit-scrollbar-thumb{ background:#ccc; border-radius:4px; } + .puzzle-piece { flex:0 0 auto; display:flex; flex-direction:column; align-items:center; + background:#fff; border-radius:12px; padding:5px 7px 4px; cursor:grab; + user-select:none; touch-action:none; border:2px solid #e0d8cc; + scroll-snap-align:start; transition:transform .15s, box-shadow .15s, opacity .2s; } + .puzzle-piece:hover { transform:translateY(-4px); box-shadow:0 5px 14px rgba(0,0,0,.22); } + .puzzle-piece svg { width:80px; height:60px; display:block; } + .puzzle-piece .pp-name { font-size:.6rem; font-weight:700; text-align:center; + margin-top:3px; color:#555; font-family:'Lexend',Arial,sans-serif; + max-width:84px; line-height:1.2; } + #puzzle-drag-ghost { display:none; position:fixed; pointer-events:none; z-index:9999; + transform:translate(-50%,-50%); filter:drop-shadow(0 6px 16px rgba(0,0,0,.45)); + transition:none; } + #puzzle-drag-ghost svg { width:110px; height:82px; display:block; } + .puzzle-snap-ok { animation:pSnap .35s ease forwards; } + @keyframes pSnap { 0%{filter:brightness(1.8)} 100%{filter:brightness(1)} } + /* ── Nivell 7 · Milionari ── */ #screen-mill { background: linear-gradient(180deg, #08082a 0%, #140a3e 100%); @@ -412,6 +440,11 @@
Nivell 7 · Milionari
Qui vol ser milionari? 4 opcions, 10 preguntes!
+
+ 🧩 +
Nivell 8 · Trencaclosques
+
Arrossega cada comarca fins al seu lloc al mapa!
+
+ +
+ +
+ + +
+
+ +
+
+
🧩 Arrossega cada comarca fins al seu lloc al mapa
+
+
+ +
+
+
@@ -878,8 +930,8 @@ const MTN_COLORS = { "Pallars Jussà":"#D4851F","Alt Urgell":"#E09828","Cerdanya":"#E8A82E", "Ripollès":"#C86820","Garrotxa":"#B05818","Berguedà":"#D07B25","Solsonès":"#E8B835", }; -const LEVEL_ICONS = {1:'👀',2:'🧩',3:'🔗',4:'✏️',5:'🗺️',6:'🔮',7:'💰'}; -const LEVEL_NAMES = {1:'Descobreix',2:'Tria',3:'Uneix',4:'Escriu',5:'El Mapa',6:'Mapa Cec',7:'Milionari'}; +const LEVEL_ICONS = {1:'👀',2:'🧩',3:'🔗',4:'✏️',5:'🗺️',6:'🔮',7:'💰',8:'🧩'}; +const LEVEL_NAMES = {1:'Descobreix',2:'Tria',3:'Uneix',4:'Escriu',5:'El Mapa',6:'Mapa Cec',7:'Milionari',8:'Trencaclosques'}; /* ══════════════════════════════════════════════════════ ESTAT @@ -1372,7 +1424,8 @@ function startLevel(n){ 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 if(n===6){mapBlind=true;initMap();} - else{initMill();} + else if(n===7){initMill();} + else if(n===8){initPuzzle();} } function repeatLevel(){ startLevel(currentLevel); } @@ -1860,6 +1913,210 @@ function buildMillSVG(comarca) { svg.setAttribute('viewBox', '0 0 820 600'); } } + +/* ══════════════════════════════════════════════════════ + NIVELL 8 · TRENCACLOSQUES + Drag-and-drop: arrossega el contorn de la comarca + fins al seu lloc al mapa cec. +══════════════════════════════════════════════════════ */ +let puzzlePlaced = new Set(); // noms de comarques ja col·locades +let puzzleTotal = 0; // total de peces d'aquesta partida +let puzzleWrong = 0; // nombre de deixades incorrectes +let puzzleDrag = null; // {name, trayEl} mentre es fa drag + +/** Entry point cridat per startLevel(8). */ +function initPuzzle() { + puzzlePlaced = new Set(); + puzzleWrong = 0; + puzzleDrag = null; + // questions ja conté les comarques filtrades amb path (assignades a startLevel) + puzzleTotal = questions.length; + showScreen('screen-puzzle'); + _buildPuzzleBaseMap(); + _buildPuzzleTray(); + _updatePuzzleHUD(); +} + +/** Construeix el mapa de fons: totes les comarques en gris (slots buits). */ +function _buildPuzzleBaseMap() { + const svg = document.getElementById('puzzle-map-svg'); + svg.innerHTML = ''; + // Fons de mar + const sea = document.createElementNS('http://www.w3.org/2000/svg','rect'); + sea.setAttribute('width','820'); sea.setAttribute('height','600'); + sea.setAttribute('fill','#C8E8F5'); svg.appendChild(sea); + // Totes les comarques amb path: slot gris clar (placa buida) + for (const [name, info] of Object.entries(COMARCA_PATHS)) { + const path = document.createElementNS('http://www.w3.org/2000/svg','path'); + path.setAttribute('d', info.d); + path.setAttribute('id', 'pslot_'+info.id); + path.setAttribute('data-name', name); + path.setAttribute('fill', '#E2E8E0'); + path.setAttribute('stroke', '#B8C8B0'); + path.setAttribute('stroke-width', '0.7'); + svg.appendChild(path); + } + // Textos de veïns + addTextSVG(svg,420,18,'ANDORRA · FRANÇA',7,'#aaa','middle'); + addTextSVG(svg,36,310,'ARAGÓ',7,'#aaa','middle','rotate(-90,36,310)'); + addTextSVG(svg,790,430,'Mar Mediterrani',7,'#80AACC','middle','rotate(90,790,430)'); + addTextSVG(svg,320,575,'COMUNITAT VALENCIANA',7,'#aaa','middle'); +} + +/** Construeix la safata inferior amb les peces barrejades. */ +function _buildPuzzleTray() { + const tray = document.getElementById('puzzle-tray'); + tray.innerHTML = ''; + for (const c of questions) { // questions ja és shuffle() + const info = COMARCA_PATHS[c.name]; + if (!info) continue; + // viewBox centrat al centroide de la comarca (finestra de 120×90 unitats SVG) + const vx = info.cx - 60, vy = info.cy - 45; + // Color de la peça: igual que al mapa de joc + const fill = MTN_COLORS[c.name] || '#C97D10'; + const el = document.createElement('div'); + el.className = 'puzzle-piece'; + el.dataset.name = c.name; + el.innerHTML = ` + + + +
${c.name}
`; + el.addEventListener('pointerdown', _puzzleDragStart, {passive:false}); + tray.appendChild(el); + } +} + +/** Comença el drag en prémer una peça de la safata. */ +function _puzzleDragStart(e) { + e.preventDefault(); + const trayEl = e.currentTarget; + const name = trayEl.dataset.name; + const info = COMARCA_PATHS[name]; + const fill = MTN_COLORS[name] || '#C97D10'; + // Atenua la peça original a la safata + trayEl.style.opacity = '0.35'; + trayEl.style.pointerEvents = 'none'; + // Crea el fantasma flotant + const ghost = document.getElementById('puzzle-drag-ghost'); + const vx = info.cx - 60, vy = info.cy - 45; + ghost.innerHTML = ` + + `; + ghost.style.display = 'block'; + ghost.style.left = e.clientX + 'px'; + ghost.style.top = e.clientY + 'px'; + puzzleDrag = {name, trayEl}; + document.addEventListener('pointermove', _puzzleDragMove); + document.addEventListener('pointerup', _puzzleDragEnd); + document.addEventListener('pointercancel', _puzzleDragCancel); +} + +function _puzzleDragMove(e) { + if (!puzzleDrag) return; + const ghost = document.getElementById('puzzle-drag-ghost'); + ghost.style.left = e.clientX + 'px'; + ghost.style.top = e.clientY + 'px'; +} + +function _puzzleDragEnd(e) { + if (!puzzleDrag) return; + _puzzleCleanListeners(); + const ghost = document.getElementById('puzzle-drag-ghost'); + ghost.style.display = 'none'; + const {name, trayEl} = puzzleDrag; + puzzleDrag = null; + // Converteix coordenades pantalla → SVG + const svg = document.getElementById('puzzle-map-svg'); + const rect = svg.getBoundingClientRect(); + const svgX = (e.clientX - rect.left) / rect.width * 820; + const svgY = (e.clientY - rect.top) / rect.height * 600; + // Hit-test 1: SVG isPointInFill (precís, usa la forma real) + const slot = document.getElementById('pslot_' + COMARCA_PATHS[name].id); + let hit = false; + if (slot) { + try { + const pt = svg.createSVGPoint(); + pt.x = svgX; pt.y = svgY; + hit = slot.isPointInFill(pt); + } catch(_) {} + } + // Hit-test 2: fallback per a comarques petites (radi generós 65px SVG) + if (!hit) { + const info = COMARCA_PATHS[name]; + hit = Math.hypot(svgX - info.cx, svgY - info.cy) < 65; + } + if (hit) { + _puzzlePlace(name); + trayEl.remove(); + } else { + // Deixada incorrecta — torna a la safata + puzzleWrong++; + trayEl.style.opacity = '1'; + trayEl.style.pointerEvents = ''; + trayEl.style.animation = 'wShake .4s ease'; + setTimeout(() => trayEl.style.animation = '', 420); + _updatePuzzleHUD(); + } +} + +function _puzzleDragCancel() { + if (!puzzleDrag) return; + _puzzleCleanListeners(); + document.getElementById('puzzle-drag-ghost').style.display = 'none'; + const {trayEl} = puzzleDrag; + puzzleDrag = null; + trayEl.style.opacity = '1'; + trayEl.style.pointerEvents = ''; +} + +function _puzzleCleanListeners() { + document.removeEventListener('pointermove', _puzzleDragMove); + document.removeEventListener('pointerup', _puzzleDragEnd); + document.removeEventListener('pointercancel',_puzzleDragCancel); +} + +/** Fixa la comarca al mapa amb color i etiqueta. */ +function _puzzlePlace(name) { + puzzlePlaced.add(name); + const info = COMARCA_PATHS[name]; + const fill = MTN_COLORS[name] || '#C97D10'; + const svg = document.getElementById('puzzle-map-svg'); + // Omple el slot del mapa amb color + const slot = document.getElementById('pslot_'+info.id); + if (slot) { + slot.setAttribute('fill', fill); + slot.setAttribute('stroke', 'white'); + slot.setAttribute('stroke-width', '1.4'); + slot.classList.add('puzzle-snap-ok'); + } + // Afegeix l'etiqueta amb el nom de la comarca + const c = questions.find(x => x.name === name); + const small = name.length > 12; + const words = name.split(' '); + if (words.length > 2 && small) { + const mid = Math.ceil(words.length / 2); + addLabel(svg, info.cx, info.cy - 5, words.slice(0, mid).join(' '), true); + addLabel(svg, info.cx, info.cy + 7, words.slice(mid).join(' '), true); + } else { + addLabel(svg, info.cx, info.cy, name, small); + } + _updatePuzzleHUD(); + // Fi del joc? + if (puzzlePlaced.size >= puzzleTotal) { + // score = total peces - errors (mínim 0) → % per a showResult + score = Math.max(0, puzzleTotal - puzzleWrong); + questions = {length: puzzleTotal}; // showResult usa questions.length + setTimeout(() => showResult(8), 900); + } +} + +function _updatePuzzleHUD() { + document.getElementById('puzzle-ctr').textContent = + `Col·locades: ${puzzlePlaced.size} de ${puzzleTotal}`; + document.getElementById('puzzle-score-live').textContent = + puzzleWrong > 0 ? `❌ ${puzzleWrong} errors` : ''; +}