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)
This commit is contained in:
263
index.html
263
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 @@
|
||||
<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 class="level-card" onclick="startLevel(8)" style="grid-column:span 2;background:linear-gradient(135deg,#0a2a0a,#1a5c1a);border:2px solid #7CFC00;">
|
||||
<span class="emoji">🧩</span>
|
||||
<div class="lvl-name" style="color:#7CFC00;">Nivell 8 · Trencaclosques</div>
|
||||
<div class="lvl-desc">Arrossega cada comarca fins al seu lloc al mapa!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-actions">
|
||||
<button class="btn btn-green" onclick="goToStats()" id="btn-stats" style="display:none;">
|
||||
@@ -552,6 +585,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ TRENCACLOSQUES (Level 8) ═══════════════════ -->
|
||||
<div id="screen-puzzle" class="screen">
|
||||
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
|
||||
<div class="map-score-row">
|
||||
<span id="puzzle-ctr"></span>
|
||||
<span id="puzzle-score-live"></span>
|
||||
</div>
|
||||
<div id="puzzle-map-wrap">
|
||||
<svg id="puzzle-map-svg" viewBox="0 0 820 600"
|
||||
xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;display:block;"></svg>
|
||||
</div>
|
||||
<div id="puzzle-tray-wrap">
|
||||
<div id="puzzle-tray-label">🧩 Arrossega cada comarca fins al seu lloc al mapa</div>
|
||||
<div id="puzzle-tray"></div>
|
||||
</div>
|
||||
<!-- Floating drag ghost — follows pointer -->
|
||||
<div id="puzzle-drag-ghost"></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>
|
||||
@@ -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 = `
|
||||
<svg viewBox="${vx} ${vy} 120 90" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${info.d}" fill="${fill}" stroke="white" stroke-width="1.8"/>
|
||||
</svg>
|
||||
<div class="pp-name">${c.name}</div>`;
|
||||
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 = `<svg viewBox="${vx} ${vy} 120 90" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${info.d}" fill="${fill}" stroke="white" stroke-width="1.8"/>
|
||||
</svg>`;
|
||||
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` : '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user