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 @@
@@ -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` : '';
+}