Files
comarques-de-catalunya/index.html

1776 lines
91 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Les Comarques de Muntanya 🏔️</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@400;600;700;800&display=swap');
:root {
--orange:#F4A535; --dark-orange:#C97D10; --green:#4DBD6E;
--red:#F05C5C; --blue:#5B9CF4; --purple:#A87FE8; --text:#2A2A2A;
--shadow:0 4px 18px rgba(0,0,0,.13);
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:'Lexend','Arial Rounded MT Bold',Arial,sans-serif;
background:linear-gradient(160deg,#FFF3DC 0%,#FFE9B8 100%);
min-height:100vh;color:var(--text);font-size:20px;letter-spacing:.03em;line-height:1.6;}
/* ── Header ── */
header{background:var(--orange);padding:13px 18px;display:flex;align-items:center;
justify-content:space-between;box-shadow:var(--shadow);position:sticky;top:0;z-index:50;}
header h1{font-size:1.25rem;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px;}
.hdr-right{display:flex;align-items:center;gap:12px;}
#header-stars{font-size:1.1rem;color:#fff;font-weight:700;}
#hdr-player{font-size:1.5rem;cursor:pointer;line-height:1;
background:rgba(255,255,255,.2);border-radius:50%;width:36px;height:36px;
display:flex;align-items:center;justify-content:center;}
#hdr-player:hover{background:rgba(255,255,255,.35);}
/* ── Screens ── */
.screen{display:none;padding:18px 14px;max-width:860px;margin:0 auto;}
.screen.active{display:block;}
/* ── WELCOME ── */
.welcome-title{text-align:center;font-size:1.7rem;font-weight:800;
color:var(--dark-orange);margin:18px 0 6px;}
.welcome-sub{text-align:center;font-size:.95rem;color:#888;margin-bottom:22px;}
.players-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:14px;margin-bottom:24px;}
.player-card{background:#fff;border-radius:18px;padding:18px 12px;text-align:center;
cursor:pointer;box-shadow:var(--shadow);border:3px solid transparent;
transition:transform .15s,border-color .15s;}
.player-card:hover{transform:translateY(-4px);border-color:var(--orange);}
.player-card:active{transform:scale(.96);}
.player-card .p-avatar{font-size:3rem;display:block;margin-bottom:6px;}
.player-card .p-name{font-weight:700;font-size:.95rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.player-card .p-stars{font-size:.8rem;color:#aaa;margin-top:2px;}
.welcome-actions{display:flex;flex-direction:column;gap:10px;align-items:center;}
.admin-link{font-size:.8rem;color:#bbb;cursor:pointer;margin-top:8px;text-decoration:underline;}
.admin-link:hover{color:#888;}
/* ── REGISTER ── */
.reg-title{font-size:1.5rem;font-weight:800;color:var(--dark-orange);margin-bottom:18px;text-align:center;}
.avatar-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:14px 0 20px;}
.avatar-btn{font-size:2.2rem;background:#fff;border:3px solid #ddd;border-radius:14px;
padding:10px;cursor:pointer;transition:border-color .15s,transform .1s;aspect-ratio:1;
display:flex;align-items:center;justify-content:center;}
.avatar-btn:hover{border-color:var(--orange);transform:scale(1.06);}
.avatar-btn.selected{border-color:var(--orange);background:#FFF3DC;transform:scale(1.1);}
/* ── HOME ── */
.home-title{text-align:center;font-size:1.8rem;font-weight:800;color:var(--dark-orange);margin:16px 0 4px;}
.home-sub{text-align:center;font-size:.9rem;color:#888;margin-bottom:22px;}
.level-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px;}
.level-card{background:#fff;border-radius:18px;padding:18px 14px;text-align:center;
cursor:pointer;box-shadow:var(--shadow);border:3px solid transparent;
transition:transform .15s,border-color .15s;}
.level-card:hover{transform:translateY(-3px);border-color:var(--orange);}
.level-card:active{transform:scale(.97);}
.level-card .emoji{font-size:2.3rem;display:block;margin-bottom:7px;}
.level-card .lvl-name{font-weight:700;font-size:.92rem;}
.level-card .lvl-desc{font-size:.75rem;color:#999;margin-top:3px;}
.map-card{grid-column:span 2;background:linear-gradient(135deg,#1A5C38,#2E9A5C);color:#fff;}
.map-card .lvl-name,.map-card .lvl-desc{color:rgba(255,255,255,.92);}
.map-card:hover{border-color:#fff;}
.home-actions{display:flex;gap:10px;justify-content:center;margin-top:18px;flex-wrap:wrap;}
/* ── STATS ── */
.stats-hero{background:var(--orange);border-radius:20px;padding:22px;text-align:center;color:#fff;margin-bottom:18px;}
.stats-avatar{font-size:4rem;display:block;margin-bottom:6px;}
.stats-name{font-size:1.5rem;font-weight:800;}
.stats-stars{font-size:1.1rem;margin-top:4px;opacity:.9;}
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:18px;}
.stat-box{background:#fff;border-radius:14px;padding:16px;text-align:center;box-shadow:var(--shadow);}
.stat-box .stat-val{font-size:2rem;font-weight:800;color:var(--dark-orange);}
.stat-box .stat-lbl{font-size:.78rem;color:#aaa;margin-top:2px;}
.level-bars{background:#fff;border-radius:14px;padding:18px;box-shadow:var(--shadow);margin-bottom:18px;}
.level-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;}
.level-bar-row:last-child{margin-bottom:0;}
.lbr-icon{font-size:1.3rem;width:28px;flex-shrink:0;text-align:center;}
.lbr-name{font-size:.82rem;font-weight:700;width:70px;flex-shrink:0;color:#666;}
.lbr-track{flex:1;background:#eee;border-radius:50px;height:14px;overflow:hidden;}
.lbr-fill{height:100%;border-radius:50px;transition:width .6s ease;}
.lbr-pct{font-size:.8rem;font-weight:700;width:36px;text-align:right;flex-shrink:0;}
.recent-list{background:#fff;border-radius:14px;padding:16px;box-shadow:var(--shadow);}
.recent-title{font-weight:700;font-size:.85rem;color:#aaa;letter-spacing:.1em;margin-bottom:12px;}
.recent-item{display:flex;align-items:center;gap:10px;padding:8px 0;
border-bottom:1px solid #f0f0f0;}
.recent-item:last-child{border-bottom:none;}
.ri-lvl{font-size:1.3rem;width:28px;text-align:center;}
.ri-score{flex:1;font-size:.9rem;font-weight:600;}
.ri-stars{font-size:.9rem;}
.ri-date{font-size:.72rem;color:#bbb;}
/* ── ADMIN ── */
.admin-pin-wrap{max-width:320px;margin:0 auto 24px;}
/* ── Manual ── */
#screen-help{padding-bottom:40px;}
.help-hero{text-align:center;padding:20px 0 10px;font-size:3.5rem;line-height:1;}
.help-hero-title{font-size:1.6rem;font-weight:900;color:var(--green);margin:6px 0 4px;}
.help-hero-sub{font-size:.95rem;color:#666;margin-bottom:20px;}
.help-section{margin:18px 0;}
.help-section-title{font-size:1.05rem;font-weight:800;color:#fff;background:var(--orange);
border-radius:12px;padding:8px 16px;margin-bottom:10px;display:flex;align-items:center;gap:8px;}
.help-card{background:#fff;border-radius:16px;padding:16px;margin-bottom:10px;
box-shadow:0 2px 10px rgba(0,0,0,.07);border-left:5px solid var(--orange);display:flex;gap:14px;align-items:flex-start;}
.help-card.green{border-left-color:var(--green);}
.help-card.blue{border-left-color:#1a6bb5;}
.help-card.purple{border-left-color:#6a1e8a;}
.help-card.red{border-left-color:#e03030;}
.help-icon{font-size:2.4rem;flex-shrink:0;line-height:1;margin-top:2px;}
.help-text strong{display:block;font-size:.95rem;margin-bottom:3px;}
.help-text p{font-size:.85rem;color:#555;margin:0;line-height:1.5;}
.help-tip{background:linear-gradient(135deg,#fffbe6,#fff3cc);border-radius:14px;
padding:14px 16px;margin:10px 0;font-size:.88rem;color:#7a5a00;border:1.5px dashed #f0c040;}
.help-tip .tip-icon{font-size:1.5rem;display:block;margin-bottom:4px;}
.help-stars-demo{display:flex;gap:16px;justify-content:center;margin:10px 0;flex-wrap:wrap;}
.help-star-item{text-align:center;background:#fff;border-radius:14px;padding:12px 16px;
box-shadow:0 2px 8px rgba(0,0,0,.08);min-width:80px;}
.help-star-item .stars{font-size:1.3rem;}
.help-star-item .star-desc{font-size:.75rem;color:#888;margin-top:4px;}
.help-comarca-demo{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:10px 0;}
.help-comarca-pill{background:#f0f4ff;border-radius:50px;padding:5px 13px;font-size:.8rem;
font-weight:600;color:#2255aa;border:1.5px solid #c0d0ff;}
/* ── Fi Manual ── */
.group-filter-grid{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0;}
.group-chip{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:50px;border:2px solid #ddd;cursor:pointer;font-size:.9rem;font-weight:600;transition:all .2s;user-select:none;background:#fff;}
.group-chip.active{border-color:var(--orange);background:var(--orange);color:#fff;}
.group-chip input{display:none;}
.filter-preview{font-size:.82rem;color:#888;margin-top:6px;min-height:20px;}
.filter-save-btn{margin-top:14px;}
.pin-input{width:100%;font-size:2rem;letter-spacing:.5em;text-align:center;
padding:14px;border-radius:14px;border:3px solid #ddd;font-family:inherit;
font-weight:700;outline:none;transition:border-color .2s;}
.pin-input:focus{border-color:var(--orange);}
.admin-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.admin-tbl{width:100%;border-collapse:collapse;font-size:.85rem;}
table.admin-tbl th{background:var(--orange);color:#fff;padding:10px 8px;
text-align:center;font-weight:700;white-space:nowrap;}
table.admin-tbl th:first-child{text-align:left;border-radius:10px 0 0 0;}
table.admin-tbl th:last-child{border-radius:0 10px 0 0;}
table.admin-tbl td{padding:10px 8px;text-align:center;border-bottom:1px solid #f0f0f0;}
table.admin-tbl td:first-child{text-align:left;}
table.admin-tbl tr:nth-child(even) td{background:#FAFAFA;}
.pct-cell{border-radius:8px;padding:4px 8px;font-weight:700;font-size:.8rem;}
.pct-green{background:#D4F7DF;color:#1a7a3c;}
.pct-orange{background:#FFF3DC;color:#C97D10;}
.pct-red{background:#FFE0E0;color:#a02020;}
.pct-gray{background:#F0F0F0;color:#aaa;}
/* ── MAP ── */
#map-screen{padding-bottom:30px;}
.map-mode-tabs{display:flex;gap:8px;margin-bottom:14px;}
.map-tab{flex:1;padding:10px;border-radius:11px;font-family:inherit;font-size:.85rem;
font-weight:700;border:2px solid #ddd;background:#fff;cursor:pointer;transition:all .15s;}
.map-tab.active{background:var(--green);color:#fff;border-color:var(--green);}
.map-q-box{background:#fff;border-radius:15px;padding:14px 18px;
box-shadow:var(--shadow);margin-bottom:14px;text-align:center;}
#map-svg-container{width:100%;background:#D8EEF8;border-radius:16px;
overflow:hidden;box-shadow:var(--shadow);}
#map-svg{width:100%;height:auto;display:block;cursor:pointer;}
.comarca-bg{fill:#C5DEB0;stroke:#a8c890;stroke-width:.8;transition:fill .2s;}
.comarca-mountain{stroke:#fff;stroke-width:1.4;cursor:pointer;
transition:fill .25s,filter .2s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.18));}
.comarca-mountain:hover{filter:drop-shadow(0 2px 8px rgba(0,0,0,.35)) brightness(1.08);}
.comarca-mountain.wrong-shake{animation:wShake .4s ease;}
@keyframes wShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
.comarca-label{font-family:'Lexend',Arial,sans-serif;font-size:8.5px;font-weight:700;
fill:#fff;text-anchor:middle;dominant-baseline:middle;pointer-events:none;
paint-order:stroke fill;stroke:rgba(0,0,0,.45);stroke-width:2.5px;}
.comarca-label.small{font-size:6.5px;}
.capital-dot{fill:#fff;stroke:rgba(0,0,0,.4);stroke-width:.8;pointer-events:none;}
.map-score-row{display:flex;justify-content:space-between;font-size:.83rem;color:#aaa;margin-bottom:8px;}
.map-score-row span:last-child{font-weight:700;color:var(--green);}
/* ── Shared ── */
.card{background:#fff;border-radius:18px;padding:22px 18px;box-shadow:var(--shadow);margin-bottom:16px;}
.prog-wrap{background:#ddd;border-radius:50px;height:12px;margin:8px 0 14px;overflow:hidden;}
.prog-bar{background:linear-gradient(90deg,var(--orange),#f4c535);height:100%;border-radius:50px;transition:width .4s ease;}
.q-label{font-size:.75rem;font-weight:700;color:#ccc;letter-spacing:.12em;margin-bottom:3px;}
.q-text{font-size:1.5rem;font-weight:800;color:var(--dark-orange);line-height:1.2;}
.options-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-top:16px;}
.opt-btn{padding:14px 11px;border-radius:13px;font-family:inherit;font-size:.95rem;
font-weight:600;border:3px solid #ddd;background:#fff;cursor:pointer;
transition:background .1s,border-color .1s;min-height:54px;line-height:1.3;}
.opt-btn:hover:not(:disabled){border-color:var(--orange);background:#FFF3DC;}
.opt-btn.correct{background:#D4F7DF;border-color:var(--green);color:#1a7a3c;}
.opt-btn.wrong{background:#FFE0E0;border-color:var(--red);color:#a02020;}
.fc-wrap{perspective:900px;width:100%;height:210px;cursor:pointer;margin:18px auto;}
.fc-inner{width:100%;height:100%;transform-style:preserve-3d;transition:transform .5s;position:relative;}
.fc-inner.flipped{transform:rotateY(180deg);}
.fc-face{position:absolute;inset:0;border-radius:20px;display:flex;flex-direction:column;
align-items:center;justify-content:center;backface-visibility:hidden;padding:22px;}
.fc-front{background:var(--orange);color:#fff;}
.fc-back{background:var(--blue);color:#fff;transform:rotateY(180deg);}
.fc-lbl{font-size:.75rem;opacity:.7;font-weight:600;margin-bottom:4px;}
.fc-main{font-size:1.8rem;font-weight:800;text-align:center;}
.fc-hint{text-align:center;color:#bbb;font-size:.82rem;margin-top:5px;}
.match-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:12px;}
.m-item{padding:12px 13px;border-radius:11px;background:#fff;border:3px solid #ddd;
cursor:pointer;font-weight:600;font-size:.9rem;text-align:center;
transition:border-color .15s,background .15s;min-height:48px;
display:flex;align-items:center;justify-content:center;}
.m-item:hover:not(.matched){border-color:var(--orange);background:#FFF3DC;}
.m-item.selected{border-color:var(--purple);background:#F0E8FF;}
.m-item.matched{border-color:var(--green);background:#D4F7DF;color:#1a7a3c;cursor:default;}
.m-item.bad{animation:badFlash .4s;}
@keyframes badFlash{0%,100%{border-color:#ddd;background:#fff;}50%{border-color:var(--red);background:#FFE0E0;}}
.btn{display:inline-block;padding:12px 26px;border-radius:50px;font-family:inherit;
font-size:.95rem;font-weight:700;border:none;cursor:pointer;transition:transform .12s,box-shadow .12s;}
.btn:active{transform:scale(.96);}
.btn-primary{background:var(--orange);color:#fff;box-shadow:0 4px 0 var(--dark-orange);}
.btn-primary:hover{box-shadow:0 6px 0 var(--dark-orange);transform:translateY(-2px);}
.btn-green{background:var(--green);color:#fff;box-shadow:0 4px 0 #2d8a4e;}
.btn-back{background:#eee;color:#666;padding:9px 16px;font-size:.88rem;margin-bottom:14px;}
.info-box{background:linear-gradient(135deg,#FFF3DC,#FFE8B2);border-radius:14px;
padding:14px 16px;margin-top:14px;border-left:5px solid var(--orange);}
.info-box p{font-size:.82rem;color:#777;}
.result-emoji{font-size:4.2rem;text-align:center;display:block;margin:12px 0;}
.result-title{font-size:1.7rem;font-weight:800;text-align:center;color:var(--dark-orange);}
.result-score{text-align:center;font-size:1.1rem;margin:9px 0 20px;color:#777;}
.stars-earned{text-align:center;font-size:2.2rem;letter-spacing:4px;margin-bottom:18px;}
.mtn-deco{text-align:center;font-size:2.6rem;margin:10px 0;animation:float 3s ease-in-out infinite;}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
#feedback{position:fixed;inset:0;display:flex;flex-direction:column;
align-items:center;justify-content:center;font-size:4.2rem;
pointer-events:none;z-index:999;opacity:0;transition:opacity .28s;}
#feedback.show{opacity:1;}
#feedback .fb-msg{font-size:1.6rem;font-weight:800;margin-top:10px;text-shadow:0 2px 8px rgba(0,0,0,.2);}
#confetti-canvas{position:fixed;inset:0;pointer-events:none;z-index:998;}
@media(max-width:520px){
.level-grid,.options-grid,.match-grid,.stats-grid{grid-template-columns:1fr;}
.map-card{grid-column:span 1;}
.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>
<canvas id="confetti-canvas"></canvas>
<header>
<h1>🏔️ Comarques</h1>
<div class="hdr-right">
<div id="header-stars">⭐ 0</div>
<div id="hdr-player" title="Les meves estadístiques" onclick="goToStats()" style="display:none;"></div>
</div>
</header>
<div id="feedback">
<span id="fb-emoji"></span>
<span class="fb-msg" id="fb-msg"></span>
</div>
<!-- ═══════════════════ WELCOME ═══════════════════ -->
<div id="screen-welcome" class="screen">
<div class="mtn-deco">🏔️⛰️🗻</div>
<div class="welcome-title">Qui juga avui?</div>
<div class="welcome-sub">Toca el teu nom per entrar 👇</div>
<div class="players-grid" id="players-grid"></div>
<div class="welcome-actions">
<button class="btn btn-primary" onclick="showScreen('screen-register')"> Nou jugador</button>
<button class="btn" onclick="showScreen('screen-help')" style="background:#1a6bb5;color:#fff;">📖 Com es juga?</button>
<div class="admin-link" onclick="showScreen('screen-admin')">⚙️ Accés administrador</div>
</div>
</div>
<!-- ═══════════════════ REGISTER ═══════════════════ -->
<div id="screen-register" class="screen">
<button class="btn btn-back" onclick="showScreen('screen-welcome')">← Tornar</button>
<div class="card">
<div class="reg-title">Crea el teu perfil! 🎉</div>
<div class="q-label">COM ET DIUS?</div>
<input type="text" id="reg-name" maxlength="20" placeholder="El teu nom..."
style="width:100%;padding:13px 16px;border-radius:12px;border:3px solid #ddd;
font-family:inherit;font-size:1.2rem;font-weight:700;outline:none;
margin-top:6px;transition:border-color .2s;"
oninput="validateReg()">
<div id="reg-name-err" style="color:var(--red);font-size:.8rem;min-height:20px;margin-top:4px;"></div>
<div class="q-label" style="margin-top:16px;">TRIA EL TEU AVATAR</div>
<div class="avatar-grid" id="avatar-grid"></div>
<button class="btn btn-primary" id="reg-btn" onclick="registerPlayer()" disabled
style="width:100%;margin-top:6px;">
Jugar! 🚀
</button>
</div>
</div>
<!-- ═══════════════════ HOME ═══════════════════ -->
<div id="screen-home" class="screen">
<div class="mtn-deco">🏔️⛰️🗻</div>
<div class="home-title" id="home-greeting">Aprèn les Comarques!</div>
<div class="home-sub">Tria un nivell per començar 👇</div>
<div class="level-grid">
<div class="level-card" onclick="startLevel(1)">
<span class="emoji">👀</span>
<div class="lvl-name">Nivell 1 · Descobreix</div>
<div class="lvl-desc">Targetes comarca i capital</div>
</div>
<div class="level-card" onclick="startLevel(2)">
<span class="emoji">🧩</span>
<div class="lvl-name">Nivell 2 · Tria</div>
<div class="lvl-desc">Tria la capital correcta</div>
</div>
<div class="level-card" onclick="startLevel(3)">
<span class="emoji">🔗</span>
<div class="lvl-name">Nivell 3 · Uneix</div>
<div class="lvl-desc">Uneix comarca amb capital</div>
</div>
<div class="level-card" onclick="startLevel(4)">
<span class="emoji">✏️</span>
<div class="lvl-name">Nivell 4 · Escriu</div>
<div class="lvl-desc">Escriu la capital de memòria</div>
</div>
<div class="level-card map-card" onclick="startLevel(5)" style="grid-column:span 1">
<span class="emoji">🗺️</span>
<div class="lvl-name">Nivell 5 · El Gran Mapa</div>
<div class="lvl-desc">Toca la comarca al mapa real!</div>
</div>
<div class="level-card map-card" onclick="startLevel(6)" style="grid-column:span 1;background:linear-gradient(135deg,#2c1654,#6a1e8a);">
<span class="emoji">🔮</span>
<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;">
📊 Les meves estadístiques
</button>
<button class="btn" onclick="goToFilter()" id="btn-filter" style="display:none;background:#6a1e8a;color:#fff;">
⚙️ Les meves comarques
</button>
<button class="btn btn-back" onclick="goWelcome()" id="btn-change" style="display:none;">
🔄 Canviar jugador
</button>
</div>
<div class="info-box">
<p>💡 <strong>Consell:</strong> Comença pel Nivell 1. Cada resposta correcta = una ⭐</p>
</div>
</div>
<!-- ═══════════════════ LEVEL 1 ═══════════════════ -->
<div id="screen-l1" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
<div class="card">
<div class="q-label">NIVELL 1 · DESCOBREIX</div>
<div style="font-size:.95rem;font-weight:600;margin-bottom:4px;">Toca la targeta per veure la capital 👆</div>
<div class="prog-wrap"><div class="prog-bar" id="l1-prog"></div></div>
<span id="l1-ctr" style="color:#ccc;font-size:.82rem;"></span>
<div class="fc-wrap" onclick="flipCard()">
<div class="fc-inner" id="fc-inner">
<div class="fc-face fc-front">
<div class="fc-lbl">COMARCA</div>
<div class="fc-main" id="fc-comarca"></div>
</div>
<div class="fc-face fc-back">
<div class="fc-lbl">CAPITAL</div>
<div class="fc-main" id="fc-capital"></div>
</div>
</div>
</div>
<div class="fc-hint" id="fc-hint">Toca per veure la capital</div>
<div style="display:flex;gap:10px;justify-content:center;margin-top:16px;flex-wrap:wrap;">
<button class="btn btn-back" onclick="prevCard()">⬅ Anterior</button>
<button class="btn btn-primary" onclick="nextCard()">Següent ➡</button>
</div>
</div>
</div>
<!-- ═══════════════════ LEVEL 2 ═══════════════════ -->
<div id="screen-l2" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
<div class="card">
<div class="q-label">NIVELL 2 · TRIA LA CAPITAL</div>
<div class="prog-wrap"><div class="prog-bar" id="l2-prog"></div></div>
<span id="l2-ctr" style="color:#ccc;font-size:.82rem;"></span>
<div style="margin:14px 0;">
<div class="q-label">La capital de...</div>
<div class="q-text" id="l2-q"></div>
</div>
<div class="options-grid" id="l2-opts"></div>
</div>
</div>
<!-- ═══════════════════ LEVEL 3 ═══════════════════ -->
<div id="screen-l3" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
<div class="card">
<div class="q-label">NIVELL 3 · UNEIX COMARCA ↔ CAPITAL</div>
<div class="prog-wrap"><div class="prog-bar" id="l3-prog"></div></div>
<div style="display:flex;justify-content:space-between;font-size:.78rem;color:#ccc;margin-bottom:6px;">
<span>Comarques ←</span><span>→ Capitals</span>
</div>
<div class="match-grid" id="l3-grid"></div>
</div>
</div>
<!-- ═══════════════════ LEVEL 4 ═══════════════════ -->
<div id="screen-l4" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
<div class="card">
<div class="q-label">NIVELL 4 · ESCRIU LA CAPITAL</div>
<div class="prog-wrap"><div class="prog-bar" id="l4-prog"></div></div>
<span id="l4-ctr" style="color:#ccc;font-size:.82rem;"></span>
<div style="margin:14px 0;">
<div class="q-label">Quina és la capital de...</div>
<div class="q-text" id="l4-q"></div>
</div>
<input type="text" id="l4-input" placeholder="Escriu aquí..."
style="width:100%;padding:13px 16px;border-radius:11px;border:3px solid #ddd;
font-family:inherit;font-size:1.15rem;font-weight:600;outline:none;transition:border-color .2s;"
onkeydown="if(event.key==='Enter')checkL4()">
<div id="l4-fb" style="min-height:24px;font-size:.9rem;font-weight:700;margin-top:9px;"></div>
<div style="display:flex;gap:9px;flex-wrap:wrap;margin-top:12px;">
<button class="btn btn-primary" onclick="checkL4()">Comprovar ✅</button>
<button class="btn btn-back" onclick="skipL4()">Saltar ⏭</button>
</div>
</div>
</div>
<!-- ═══════════════════ MAP ═══════════════════ -->
<div id="map-screen" class="screen">
<button class="btn btn-back" onclick="goHome()">← Tornar</button>
<div class="map-mode-tabs">
<button class="map-tab active" id="tab-name" onclick="setMapMode('name')">🏷️ Troba pel nom</button>
<button class="map-tab" id="tab-cap" onclick="setMapMode('capital')">📍 Troba per la capital</button>
</div>
<div class="map-q-box">
<div class="q-label" id="map-qlabel">TOCA LA COMARCA...</div>
<div class="q-text" id="map-qtext" style="font-size:1.65rem;margin:4px 0;"></div>
<div id="map-qhint" style="font-size:.9rem;color:#aaa;min-height:22px;"></div>
</div>
<div class="prog-wrap"><div class="prog-bar" id="map-prog"></div></div>
<div class="map-score-row">
<span id="map-ctr"></span>
<span id="map-score-live"></span>
</div>
<div id="map-svg-container">
<svg id="map-svg" viewBox="0 0 820 600" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;justify-content:center;font-size:.8rem;color:#888;">
<span>🟫 Comarques de muntanya &nbsp;·&nbsp; 🟩 Resta de Catalunya</span>
</div>
</div>
<!-- ═══════════════════ RESULT ═══════════════════ -->
<div id="screen-result" class="screen">
<div class="card" style="text-align:center;">
<span class="result-emoji" id="result-emoji">🎉</span>
<div class="result-title" id="result-title"></div>
<div class="result-score" id="result-score"></div>
<div class="stars-earned" id="result-stars"></div>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="repeatLevel()">🔄 Repetir</button>
<button class="btn btn-back" onclick="goHome()">🏠 Inici</button>
</div>
</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>
<div id="stats-content"><div style="text-align:center;padding:40px;color:#aaa;">Carregant…</div></div>
</div>
<!-- ═══════════════════ HELP / MANUAL ═══════════════════ -->
<div id="screen-help" class="screen">
<button class="btn btn-back" onclick="showScreen('screen-welcome')">← Tornar</button>
<div class="help-hero">🏔️🗺️⛰️</div>
<div class="help-hero-title">Com es juga?</div>
<div class="help-hero-sub">Aprèn les comarques de Catalunya jugant! 🎮</div>
<!-- ── Pas 1: Registre ── -->
<div class="help-section">
<div class="help-section-title" style="background:#1a6bb5;">👤 Primer de tot...</div>
<div class="help-card blue">
<div class="help-icon"></div>
<div class="help-text">
<strong>Crea el teu perfil</strong>
<p>Toca <b>«Nou jugador»</b>, escriu el teu nom i tria un emoji que t'agradi com a avatar. Cada jugador té les seves pròpies estreles i estadístiques!</p>
</div>
</div>
<div class="help-card blue">
<div class="help-icon">👆</div>
<div class="help-text">
<strong>Entra al joc</strong>
<p>La propera vegada, toca el teu nom a la pantalla inicial i ja estàs dins! No cal cap contrasenya 🎉</p>
</div>
</div>
</div>
<!-- ── Pas 2: Nivells ── -->
<div class="help-section">
<div class="help-section-title">🎯 Els 6 nivells</div>
<div class="help-card green">
<div class="help-icon">👀</div>
<div class="help-text">
<strong>Nivell 1 · Descobreix</strong>
<p>Veus targetes amb el nom de cada comarca i la seva capital. Toca la targeta per girar-la i descobrir la capital! Perfecte per començar a memoritzar.</p>
</div>
</div>
<div class="help-card green">
<div class="help-icon">🧩</div>
<div class="help-text">
<strong>Nivell 2 · Tria</strong>
<p>Et diuen el nom d'una comarca i has de triar la capital correcta entre 4 opcions. Toca la resposta que creus que és correcta!</p>
</div>
</div>
<div class="help-card green">
<div class="help-icon">🔗</div>
<div class="help-text">
<strong>Nivell 3 · Uneix</strong>
<p>A l'esquerra hi ha comarques i a la dreta capitals barrejades. Toca una comarca i després toca la seva capital per unir-les. Veuràs una línia que les connecta!</p>
</div>
</div>
<div class="help-card">
<div class="help-icon">✏️</div>
<div class="help-text">
<strong>Nivell 4 · Escriu</strong>
<p>Has d'escriure el nom de la capital de memòria, sense ajuda! No et preocupis pels accents, el joc és flexible amb l'ortografia.</p>
</div>
</div>
<div class="help-card" style="border-left-color:#1a7a3c;">
<div class="help-icon">🗺️</div>
<div class="help-text">
<strong>Nivell 5 · El Gran Mapa</strong>
<p>Apareix el mapa real de Catalunya amb les comarques acolorides. El joc et diu quin nom o capital buscar, i tu has de tocar la comarca correcta al mapa. Pots triar entre dos modes:</p>
<p>🏷️ <b>Troba pel nom</b> — et donen el nom, tu toques el lloc<br>
📍 <b>Troba per la capital</b> — et donen la capital, tu trobes la comarca</p>
</div>
</div>
<div class="help-card purple">
<div class="help-icon">🔮</div>
<div class="help-text">
<strong>Nivell 6 · Mapa Cec</strong>
<p>Com el Nivell 5, però el mapa no té cap nom escrit! Has d'identificar les comarques només per la seva forma i posició. El repte màxim per als experts! 🧠</p>
</div>
</div>
<div class="help-tip">
<span class="tip-icon">💡</span>
<b>Consell:</b> Comença sempre pel Nivell 1 per aprendre, i quan te'ls sàpigues de memòria prova el Nivell 6. Cada nivell que passes et fa més fort!
</div>
</div>
<!-- ── Estreles ── -->
<div class="help-section">
<div class="help-section-title" style="background:#d4a017;">⭐ Les estreles</div>
<div class="help-card" style="border-left-color:#d4a017;">
<div class="help-icon">🏆</div>
<div class="help-text">
<strong>Guanya estreles en cada partida</strong>
<p>Al final de cada nivell reps entre 1 i 3 estreles segons com ho has fet. S'acumulen totes!</p>
</div>
</div>
<div class="help-stars-demo">
<div class="help-star-item">
<div class="stars"></div>
<div class="star-desc">Bon intent!<br>Has practicat 💪</div>
</div>
<div class="help-star-item">
<div class="stars">⭐⭐</div>
<div class="star-desc">Molt bé!<br>Ja ho domines 🌟</div>
</div>
<div class="help-star-item">
<div class="stars">⭐⭐⭐</div>
<div class="star-desc">Perfecte!<br>Ets un expert! 🏆</div>
</div>
</div>
</div>
<!-- ── Comarques ── -->
<div class="help-section">
<div class="help-section-title" style="background:#6a1e8a;">⚙️ Tria les teves comarques</div>
<div class="help-card purple">
<div class="help-icon">🗺️</div>
<div class="help-text">
<strong>Personalitza el joc!</strong>
<p>Un cop seleccionat el teu perfil, toca el botó <b>«⚙️ Les meves comarques»</b> per triar quines comarques vols practicar. Pots combinar grups!</p>
</div>
</div>
<div class="help-comarca-demo">
<span class="help-comarca-pill">⛰️ Muntanya</span>
<span class="help-comarca-pill">🏙️ Barcelona</span>
<span class="help-comarca-pill">🌊 Girona</span>
<span class="help-comarca-pill">🏔️ Lleida</span>
<span class="help-comarca-pill">☀️ Tarragona</span>
<span class="help-comarca-pill">🗺️ Totes (42!)</span>
</div>
<div class="help-tip">
<span class="tip-icon">🎓</span>
<b>Per als experts:</b> Activa <b>«Totes»</b> per practicar les 42 comarques de Catalunya! Perfecte quan ja domines les de muntanya.
</div>
</div>
<!-- ── Stats ── -->
<div class="help-section">
<div class="help-section-title" style="background:#1a7a3c;">📊 Les meves estadístiques</div>
<div class="help-card green">
<div class="help-icon">📈</div>
<div class="help-text">
<strong>Segueix el teu progrés</strong>
<p>Toca <b>«📊 Les meves estadístiques»</b> per veure quants encerts tens a cada nivell. Les barres canvien de color:</p>
<p>🟢 <b>Verd</b> = 80% o més — Excel·lent!<br>
🟠 <b>Taronja</b> = 5079% — Molt bé, segueix practicant<br>
🔴 <b>Vermell</b> = menys del 50% — Necessites practicar més</p>
</div>
</div>
</div>
<!-- ── Final ── -->
<div style="text-align:center;padding:20px 0 10px;">
<div style="font-size:2.5rem;">🚀🌟🏔️</div>
<div style="font-size:1rem;font-weight:700;color:var(--green);margin:8px 0 4px;">Ja estàs llest per jugar!</div>
<div style="font-size:.88rem;color:#888;">Recorda: la pràctica fa al mestre 💪</div>
<button class="btn btn-primary" onclick="showScreen('screen-welcome')" style="margin-top:16px;">
🎮 Anar a jugar!
</button>
</div>
</div>
<!-- ═══════════════════ FILTER ═══════════════════ -->
<div id="screen-filter" class="screen">
<button class="btn btn-back" onclick="showScreen('screen-home')">← Tornar</button>
<div class="card">
<div class="q-label" style="margin-bottom:6px;">⚙️ LES MEVES COMARQUES</div>
<div style="font-size:.88rem;color:#666;margin-bottom:4px;" id="filter-player-name"></div>
<div style="font-size:.85rem;color:#888;margin-bottom:12px;">
Tria quines comarques vols practicar. Pots combinar grups.
</div>
<div class="group-filter-grid" id="group-chips-player"></div>
<div class="filter-preview" id="filter-preview-player">10 comarques seleccionades</div>
</div>
</div>
<!-- ═══════════════════ ADMIN ═══════════════════ -->
<div id="screen-admin" class="screen">
<button class="btn btn-back" onclick="goWelcome()">← Tornar</button>
<div class="card">
<div class="q-label">ACCÉS ADMINISTRADOR</div>
<div style="font-size:1.1rem;font-weight:700;margin-bottom:14px;">Introdueix el PIN de 4 dígits</div>
<div class="admin-pin-wrap">
<input type="password" inputmode="numeric" maxlength="4" id="admin-pin"
class="pin-input" placeholder="····"
oninput="if(this.value.length===4)loadAdmin()">
</div>
<div id="admin-err" style="color:var(--red);text-align:center;font-size:.9rem;min-height:22px;"></div>
</div>
<div id="admin-content"></div>
</div>
<!-- ═══════════════════ SCRIPTS ═══════════════════ -->
<script src="comarca-paths.js"></script>
<script>
/* ══════════════════════════════════════════════════════
DADES
══════════════════════════════════════════════════════ */
// Full catalogue — 41 comarques with province and mountain flag
// province: 'B'=Barcelona · 'G'=Girona · 'L'=Lleida · 'T'=Tarragona
const ALL_COMARQUES = [
// ── Lleida ──────────────────────────────────────────────────────────────
{ name:"Val d'Aran", capital:"Vielha", emoji:"🏔️", province:"L", mountain:true },
{ name:"Pallars Sobirà", capital:"Sort", emoji:"⛰️", province:"L", mountain:true },
{ name:"Alta Ribagorça", capital:"el Pont de Suert", emoji:"🌊", province:"L", mountain:true },
{ name:"Pallars Jussà", capital:"Tremp", emoji:"🦅", province:"L", mountain:true },
{ name:"Alt Urgell", capital:"la Seu d'Urgell", emoji:"⛪", province:"L", mountain:true },
{ name:"Solsonès", capital:"Solsona", emoji:"🏰", province:"L", mountain:true },
{ name:"Noguera", capital:"Balaguer", emoji:"🌾", province:"L", mountain:false },
{ name:"Segrià", capital:"Lleida", emoji:"🏛️", province:"L", mountain:false },
{ name:"Urgell", capital:"Tàrrega", emoji:"🌻", province:"L", mountain:false },
{ name:"Garrigues", capital:"les Borges Blanques", emoji:"🫒", province:"L", mountain:false },
{ name:"Pla d'Urgell", capital:"Mollerussa", emoji:"🌽", province:"L", mountain:false },
{ name:"Segarra", capital:"Cervera", emoji:"🌿", province:"L", mountain:false },
// ── Girona ──────────────────────────────────────────────────────────────
{ name:"Cerdanya", capital:"Puigcerdà", emoji:"🌄", province:"G", mountain:true },
{ name:"Ripollès", capital:"Ripoll", emoji:"🌲", province:"G", mountain:true },
{ name:"Garrotxa", capital:"Olot", emoji:"🌋", province:"G", mountain:true },
{ name:"Alt Empordà", capital:"Figueres", emoji:"🌬️", province:"G", mountain:false },
{ name:"Baix Empordà", capital:"la Bisbal d'Empordà", emoji:"🏖️", province:"G", mountain:false },
{ name:"Gironès", capital:"Girona", emoji:"🦁", province:"G", mountain:false },
{ name:"Pla de l'Estany", capital:"Banyoles", emoji:"🦆", province:"G", mountain:false },
{ name:"Selva", capital:"Santa Coloma de Farners", emoji:"🌿", province:"G", mountain:false },
// ── Barcelona ────────────────────────────────────────────────────────────
{ name:"Berguedà", capital:"Berga", emoji:"🏕️", province:"B", mountain:true },
{ name:"Osona", capital:"Vic", emoji:"🐄", province:"B", mountain:false },
{ name:"Bages", capital:"Manresa", emoji:"⛏️", province:"B", mountain:false },
{ name:"Moianès", capital:"Moià", emoji:"🪨", province:"B", mountain:false, noMap:true },
{ name:"Anoia", capital:"Igualada", emoji:"🏭", province:"B", mountain:false },
{ name:"Vallès Oriental", capital:"Granollers", emoji:"🌳", province:"B", mountain:false },
{ name:"Vallès Occidental", capital:"Sabadell", emoji:"🏙️", province:"B", mountain:false },
{ name:"Barcelonès", capital:"Barcelona", emoji:"🗼", province:"B", mountain:false },
{ name:"Baix Llobregat", capital:"Sant Feliu de Llobregat", emoji:"🏘️", province:"B", mountain:false },
{ name:"Maresme", capital:"Mataró", emoji:"⛵", province:"B", mountain:false },
{ name:"Garraf", capital:"Vilanova i la Geltrú", emoji:"🌊", province:"B", mountain:false },
{ name:"Alt Penedès", capital:"Vilafranca del Penedès", emoji:"🍇", province:"B", mountain:false },
// ── Tarragona ────────────────────────────────────────────────────────────
{ name:"Conca de Barberà", capital:"Montblanc", emoji:"🏰", province:"T", mountain:false },
{ name:"Priorat", capital:"Falset", emoji:"🍷", province:"T", mountain:false },
{ name:"Ribera d'Ebre", capital:"Móra d'Ebre", emoji:"🌊", province:"T", mountain:false },
{ name:"Terra Alta", capital:"Gandesa", emoji:"🌄", province:"T", mountain:false },
{ name:"Montsià", capital:"Amposta", emoji:"🦩", province:"T", mountain:false },
{ name:"Baix Ebre", capital:"Tortosa", emoji:"🏛️", province:"T", mountain:false },
{ name:"Tarragonès", capital:"Tarragona", emoji:"🏛️", province:"T", mountain:false },
{ name:"Baix Camp", capital:"Reus", emoji:"🎨", province:"T", mountain:false },
{ name:"Alt Camp", capital:"Valls", emoji:"🧅", province:"T", mountain:false },
{ name:"Baix Penedès", capital:"el Vendrell", emoji:"🎸", province:"T", mountain:false },
];
// ── Group filter — persisted in localStorage, scoped per player ───────────────
// activeGroups is a Set of: 'mountain' | 'B' | 'G' | 'L' | 'T' | 'all'
// Default: only mountain comarques (original behaviour)
function filterKey(playerId) {
return playerId ? `activeGroups_${playerId}` : 'activeGroups_default';
}
function loadActiveGroups(playerId) {
try {
const saved = JSON.parse(localStorage.getItem(filterKey(playerId)));
if (Array.isArray(saved) && saved.length) return new Set(saved);
} catch {}
return new Set(['mountain']);
}
function saveActiveGroups(set, playerId) {
localStorage.setItem(filterKey(playerId), JSON.stringify([...set]));
}
let activeGroups = loadActiveGroups(null);
// Derive the active COMARQUES subset from the current filter
function getActiveComarques() {
const all = activeGroups.has('all');
return ALL_COMARQUES.filter(c => {
if (all) return true;
if (activeGroups.has('mountain') && c.mountain) return true;
if (activeGroups.has(c.province)) return true;
return false;
});
}
// Legacy alias — always reflects the current filter
let COMARQUES = getActiveComarques();
const AVATARS = ['🦁','🐯','🐻','🦊','🐼','🐨','🐸','🦋','🌟','🚀','🦄','🐬','🐧','🌈','🎠','🐉'];
const MTN_COLORS = {
"Val d'Aran":"#A0522D","Pallars Sobirà":"#B8621A","Alta Ribagorça":"#C47322",
"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'};
/* ══════════════════════════════════════════════════════
ESTAT
══════════════════════════════════════════════════════ */
let currentPlayer = null;
let offlineMode = false;
let totalStars = 0;
let sessionStart = null;
let currentLevel=1, currentQ=0, score=0, questions=[];
let fcFlipped=false, fcIndex=0;
let mapMode='name', mapTarget=null, mapAnswered=false, mapBuilt=false, mapBlind=false;
let l3Round=0, l3Sel=null, l3BTotal=0, l3BDone=0;
const L3B=5;
/* ══════════════════════════════════════════════════════
API
══════════════════════════════════════════════════════ */
async function api(method, path, body) {
try {
const opts = { method, headers: {} };
if (body) { opts.headers['Content-Type']='application/json'; opts.body=JSON.stringify(body); }
const r = await fetch(path, opts);
if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status });
return r.json();
} catch(e) { return null; }
}
const apiGet = path => api('GET', path);
const apiPost = (path, body)=> api('POST', path, body);
async function apiAdmin(pin) {
try {
const r = await fetch('/api/admin/stats', { headers: {'x-admin-pin': pin} });
if (r.status === 401) return { error: 'PIN incorrecte' };
if (!r.ok) return null;
return r.json();
} catch { return null; }
}
/* ══════════════════════════════════════════════════════
UTILS
══════════════════════════════════════════════════════ */
const shuffle=a=>{let b=[...a];for(let i=b.length-1;i>0;i--){const j=~~(Math.random()*(i+1));[b[i],b[j]]=[b[j],b[i]];}return b;};
const normalize=s=>s.trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z\s]/g,'');
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo(0,0);
}
function updateHeader() {
document.getElementById('header-stars').textContent = '⭐ ' + totalStars;
const ph = document.getElementById('hdr-player');
if (currentPlayer) {
ph.textContent = currentPlayer.avatar;
ph.style.display='flex';
} else {
ph.style.display='none';
}
}
function addStars(n) {
totalStars += n;
// localStorage kept only as offline fallback — source of truth is the API
if (currentPlayer) localStorage.setItem('stars_'+currentPlayer.id, totalStars);
updateHeader();
}
async function saveSession(level, score, total, stars) {
if (offlineMode || !currentPlayer) return;
const duration = sessionStart ? Math.round((Date.now()-sessionStart)/1000) : 0;
await apiPost('/api/sessions', { player_id: currentPlayer.id, level, score, total, stars, duration_seconds: duration });
}
/* ══════════════════════════════════════════════════════
INIT
══════════════════════════════════════════════════════ */
window.addEventListener('load', async () => {
// Test backend availability
const health = await apiGet('/api/health');
offlineMode = !health;
if (offlineMode) {
// No backend → mode directe, stars from localStorage
totalStars = parseInt(localStorage.getItem('stars') || '0');
updateHeader();
showScreen('screen-home');
return;
}
// Build welcome
await renderWelcome();
showScreen('screen-welcome');
});
/* ══════════════════════════════════════════════════════
WELCOME / PLAYER MANAGEMENT
══════════════════════════════════════════════════════ */
async function renderWelcome() {
const players = await apiGet('/api/players') || [];
const grid = document.getElementById('players-grid');
grid.innerHTML = '';
if (!players.length) {
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#aaa;font-size:.9rem;padding:20px;">Cap jugador registrat encara.<br>Crea el primer perfil! 👇</div>';
return;
}
players.forEach(p => {
const card = document.createElement('div');
card.className = 'player-card';
// Star count shown on card — loaded from API after render
card.innerHTML = `
<span class="p-avatar">${p.avatar}</span>
<div class="p-name">${p.name}</div>
<div class="p-stars" id="card-stars-${p.id}">⭐ …</div>`;
card.onclick = () => selectPlayer(p);
// Load real star count from API asynchronously
apiGet(`/api/players/${p.id}/stats`).then(data => {
const el = document.getElementById(`card-stars-${p.id}`);
if (el && data) el.textContent = `${data.totalStars}`;
});
grid.appendChild(card);
});
}
async function selectPlayer(p) {
currentPlayer = p;
// Load this player's comarca filter
activeGroups = loadActiveGroups(p.id);
COMARQUES = getActiveComarques();
mapBuilt = false;
// Load star count from API; fall back to localStorage for offline mode
if (!offlineMode) {
const data = await apiGet(`/api/players/${p.id}/stats`);
totalStars = data ? data.totalStars : parseInt(localStorage.getItem('stars_'+p.id) || '0');
} else {
totalStars = parseInt(localStorage.getItem('stars_'+p.id) || '0');
}
updateHeader();
document.getElementById('home-greeting').textContent = `Hola, ${p.name}! 👋`;
document.getElementById('btn-stats').style.display = 'inline-block';
document.getElementById('btn-filter').style.display = 'inline-block';
document.getElementById('btn-change').style.display = 'inline-block';
showScreen('screen-home');
}
function goWelcome() {
currentPlayer = null;
updateHeader();
document.getElementById('btn-stats').style.display = 'none';
document.getElementById('btn-filter').style.display = 'none';
document.getElementById('btn-change').style.display = 'none';
document.getElementById('home-greeting').textContent = 'Aprèn les Comarques!';
renderWelcome();
showScreen('screen-welcome');
}
/* ── Register ── */
(function buildAvatarGrid() {
const grid = document.getElementById('avatar-grid');
let selected = null;
AVATARS.forEach(av => {
const btn = document.createElement('button');
btn.className = 'avatar-btn';
btn.textContent = av;
btn.onclick = () => {
document.querySelectorAll('.avatar-btn').forEach(b=>b.classList.remove('selected'));
btn.classList.add('selected');
selected = av;
validateReg();
};
grid.appendChild(btn);
});
// expose selected getter
window._getSelectedAvatar = () => selected;
})();
function validateReg() {
const name = document.getElementById('reg-name').value.trim();
const av = window._getSelectedAvatar();
document.getElementById('reg-btn').disabled = !(name.length >= 2 && av);
}
async function registerPlayer() {
const name = document.getElementById('reg-name').value.trim();
const avatar = window._getSelectedAvatar();
if (!name || !avatar) return;
const result = await apiPost('/api/players', { name, avatar });
if (!result) {
document.getElementById('reg-name-err').textContent = 'Error de connexió. Torna-ho a provar.';
return;
}
if (result.error) {
document.getElementById('reg-name-err').textContent = result.error;
return;
}
document.getElementById('reg-name-err').textContent = '';
selectPlayer(result);
}
/* ══════════════════════════════════════════════════════
STATS SCREEN
══════════════════════════════════════════════════════ */
function goToStats() {
if (!currentPlayer && !offlineMode) { showScreen('screen-welcome'); return; }
showScreen('screen-stats');
loadStats();
}
async function loadStats() {
if (offlineMode || !currentPlayer) {
document.getElementById('stats-content').innerHTML =
'<div class="card" style="text-align:center;color:#aaa;">Estadístiques no disponibles en mode sense connexió.</div>';
return;
}
const data = await apiGet(`/api/players/${currentPlayer.id}/stats`);
if (!data) {
document.getElementById('stats-content').innerHTML =
'<div class="card" style="text-align:center;color:var(--red);">Error en carregar les estadístiques.</div>';
return;
}
const ls = data.levelStats || {};
const recentRows = (data.recent || []).map(s => {
const d = new Date(s.date);
const dateStr = `${d.getDate()}/${d.getMonth()+1} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
const pct = Math.round(s.score/s.total*100);
return `<div class="recent-item">
<span class="ri-lvl">${LEVEL_ICONS[s.level]}</span>
<span class="ri-score">${LEVEL_NAMES[s.level]} · ${pct}%</span>
<span class="ri-stars">${'⭐'.repeat(s.stars)}</span>
<span class="ri-date">${dateStr}</span>
</div>`;
}).join('') || '<div style="color:#aaa;font-size:.85rem;text-align:center;padding:12px;">Cap sessió registrada</div>';
const barRows = [1,2,3,4,5,6,7].map(lvl => {
const s = ls[lvl];
const pct = s?.bestPct ?? null;
const color = pct===null ? '#ddd' : pct>=80 ? '#4DBD6E' : pct>=50 ? '#F4A535' : '#F05C5C';
const pctStr = pct===null ? '—' : pct+'%';
return `<div class="level-bar-row">
<span class="lbr-icon">${LEVEL_ICONS[lvl]}</span>
<span class="lbr-name">${LEVEL_NAMES[lvl]}</span>
<div class="lbr-track">
<div class="lbr-fill" style="width:${pct??0}%;background:${color};"></div>
</div>
<span class="lbr-pct" style="color:${color};">${pctStr}</span>
</div>`;
}).join('');
document.getElementById('stats-content').innerHTML = `
<div class="stats-hero">
<span class="stats-avatar">${data.player.avatar}</span>
<div class="stats-name">${data.player.name}</div>
<div class="stats-stars">⭐ ${data.totalStars} estreles acumulades</div>
</div>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-val">${data.totalSessions}</div>
<div class="stat-lbl">Partides jugades</div>
</div>
<div class="stat-box">
<div class="stat-val">${data.totalStars}</div>
<div class="stat-lbl">Estreles totals</div>
</div>
</div>
<div class="level-bars">
<div class="q-label" style="margin-bottom:14px;">MILLOR RESULTAT PER NIVELL</div>
${barRows}
</div>
${data.recent?.length ? `
<div class="recent-list">
<div class="recent-title">ÚLTIMES PARTIDES</div>
${recentRows}
</div>` : ''}`;
}
/* ══════════════════════════════════════════════════════
ADMIN — GROUP FILTER CONFIG
══════════════════════════════════════════════════════ */
const GROUP_DEFS = [
{ key:'mountain', label:'⛰️ Muntanya', desc:'Les 10 comarques de muntanya' },
{ key:'B', label:'🏙️ Barcelona', desc:'12 comarques de la demarcació' },
{ key:'G', label:'🌊 Girona', desc:'8 comarques de la demarcació' },
{ key:'L', label:'🏔️ Lleida', desc:'12 comarques de la demarcació' },
{ key:'T', label:'☀️ Tarragona', desc:'10 comarques de la demarcació' },
{ key:'all', label:'🗺️ Totes', desc:'Les 42 comarques de Catalunya' },
];
function renderAdminConfig() {
const chips = GROUP_DEFS.map(g => `
<div class="group-chip ${activeGroups.has(g.key)?'active':''}"
id="chip-${g.key}" onclick="toggleGroup('${g.key}')" title="${g.desc}">
${g.label}
</div>`).join('');
const count = getActiveComarques().length;
return `
<div class="card" style="margin-bottom:0;">
<div class="q-label" style="margin-bottom:10px;">⚙️ CONFIGURACIÓ DEL JOC</div>
<div style="font-size:.88rem;color:#666;margin-bottom:4px;">
Selecciona quines comarques s'han de practicar. Pots combinar grups.
</div>
<div class="group-filter-grid" id="group-chips">${chips}</div>
<div class="filter-preview" id="filter-preview">
${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}
</div>
</div>`;
}
function toggleGroup(key) {
// 'all' is exclusive — selecting it deselects others; selecting others deselects 'all'
if (key === 'all') {
activeGroups = new Set(['all']);
} else {
activeGroups.delete('all');
if (activeGroups.has(key)) {
activeGroups.delete(key);
if (activeGroups.size === 0) activeGroups.add('mountain'); // at least one
} else {
activeGroups.add(key);
}
}
saveActiveGroups(activeGroups, currentPlayer?.id ?? null);
syncChips();
// Refresh COMARQUES and force map rebuild on next level start
COMARQUES = getActiveComarques();
mapBuilt = false; // force SVG rebuild with new active set
}
function syncChips() {
// Sync both the admin chips and the player chips
['chip-', 'pchip-'].forEach(prefix => {
GROUP_DEFS.forEach(g => {
const el = document.getElementById(prefix + g.key);
if (!el) return;
el.classList.toggle('active', activeGroups.has(g.key));
});
});
const count = getActiveComarques().length;
const text = `${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}`;
['filter-preview', 'filter-preview-player'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = text;
});
}
function goToFilter() {
if (!currentPlayer) return;
document.getElementById('filter-player-name').textContent =
`${currentPlayer.avatar} ${currentPlayer.name}`;
// Render player chips with prefix 'pchip-'
const chips = GROUP_DEFS.map(g => `
<div class="group-chip ${activeGroups.has(g.key)?'active':''}"
id="pchip-${g.key}" onclick="toggleGroup('${g.key}')" title="${g.desc}">
${g.label}
</div>`).join('');
document.getElementById('group-chips-player').innerHTML = chips;
const count = getActiveComarques().length;
document.getElementById('filter-preview-player').textContent =
`${count} ${count!==1?'comarques seleccionades':'comarca seleccionada'}`;
showScreen('screen-filter');
}
/* ══════════════════════════════════════════════════════
ADMIN SCREEN
══════════════════════════════════════════════════════ */
async function loadAdmin() {
const pin = document.getElementById('admin-pin').value;
const data = await apiAdmin(pin);
const err = document.getElementById('admin-err');
if (!data) { err.textContent='Error de connexió.'; return; }
if (data.error) { err.textContent=data.error; return; }
err.textContent = '';
const rows = data.map(p => {
const cells = [1,2,3,4,5,6].map(lvl => {
const s = p.levelStats[lvl];
if (!s) return `<td><span class="pct-cell pct-gray">—</span></td>`;
const pct = s.bestPct;
const cls = pct>=80?'pct-green':pct>=50?'pct-orange':'pct-red';
return `<td><span class="pct-cell ${cls}">${pct}%</span></td>`;
}).join('');
const lastSeen = p.lastActivity
? new Date(p.lastActivity).toLocaleDateString('ca-ES')
: '—';
return `<tr>
<td><span style="font-size:1.2rem">${p.avatar}</span> <strong>${p.name}</strong></td>
<td>${p.totalSessions}</td>
<td>⭐ ${p.totalStars}</td>
${cells}
<td style="font-size:.75rem;color:#aaa;">${lastSeen}</td>
</tr>`;
}).join('');
document.getElementById('admin-content').innerHTML = `
${renderAdminConfig()}
<div class="admin-table-wrap" style="margin-top:24px;">
<table class="admin-tbl">
<thead><tr>
<th>Jugador</th><th>Sessions</th><th>⭐</th>
<th>👀 N1</th><th>🧩 N2</th><th>🔗 N3</th><th>✏️ N4</th><th>🗺️ N5</th><th>🔮 N6</th>
<th>Darrera activitat</th>
</tr></thead>
<tbody>${rows || '<tr><td colspan="10" style="color:#aaa;text-align:center;padding:20px;">Sense jugadors</td></tr>'}</tbody>
</table>
</div>`;
// Restore chip active state after render
syncChips();
}
/* ══════════════════════════════════════════════════════
FEEDBACK & SOUNDS
══════════════════════════════════════════════════════ */
const OKS=['Molt bé! 🎉','Excel·lent! 🌟','Perfecte! 👏','Genial! 💪','Fantàstic! 🚀','Ets una crack! 🏆','Increïble! 🌈'];
const NOS=['Quasi! Intenta-ho 💪','No passa res! 🙂','Poc a poc! 🐢','Ja ho tindràs! 😊'];
function showFeedback(ok) {
const el=document.getElementById('feedback');
document.getElementById('fb-emoji').textContent=ok?['🎊','🌟','🎉','✨','👑'][~~(Math.random()*5)]:'😅';
document.getElementById('fb-msg').textContent=ok?OKS[~~(Math.random()*OKS.length)]:NOS[~~(Math.random()*NOS.length)];
el.style.background=ok?'rgba(77,189,110,.9)':'rgba(240,92,92,.8)';
el.classList.add('show'); playSound(ok);
setTimeout(()=>el.classList.remove('show'),1100);
}
function playSound(ok){
try{const ctx=new(window.AudioContext||window.webkitAudioContext)();
const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);
if(ok){o.frequency.setValueAtTime(523,ctx.currentTime);o.frequency.setValueAtTime(659,ctx.currentTime+.1);o.frequency.setValueAtTime(784,ctx.currentTime+.2);}
else{o.frequency.setValueAtTime(300,ctx.currentTime);o.frequency.setValueAtTime(200,ctx.currentTime+.18);}
g.gain.setValueAtTime(.28,ctx.currentTime);g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.45);
o.start();o.stop(ctx.currentTime+.45);}catch(e){}
}
function launchConfetti(){
const c=document.getElementById('confetti-canvas'),cx=c.getContext('2d');
c.width=window.innerWidth;c.height=window.innerHeight;
const p=Array.from({length:130},()=>({x:Math.random()*c.width,y:-20,r:5+Math.random()*8,alpha:1,
color:['#F4A535','#5BC97A','#5B9CF4','#A87FE8','#F05C5C','#F4D535'][~~(Math.random()*6)],
vx:(Math.random()-.5)*4,vy:2+Math.random()*4}));
let fr;function draw(){cx.clearRect(0,0,c.width,c.height);let alive=false;
p.forEach(q=>{q.x+=q.vx;q.y+=q.vy;q.alpha-=.009;if(q.alpha>0)alive=true;
cx.globalAlpha=Math.max(0,q.alpha);cx.fillStyle=q.color;cx.beginPath();cx.arc(q.x,q.y,q.r,0,Math.PI*2);cx.fill();});
cx.globalAlpha=1;if(alive)fr=requestAnimationFrame(draw);else cx.clearRect(0,0,c.width,c.height);}
cancelAnimationFrame(fr);draw();
}
/* ══════════════════════════════════════════════════════
RESULT
══════════════════════════════════════════════════════ */
async function showResult(level) {
const pct=Math.round(score/questions.length*100);
let emoji,title,stars;
if(pct===100){emoji='🏆';title='PERFECTE! Ets una campiona!';stars=3;}
else if(pct>=70){emoji='🌟';title='Molt bé! Segueix així!';stars=2;}
else{emoji='💪';title='Bon intent! Practica una mica més';stars=1;}
document.getElementById('result-emoji').textContent=emoji;
document.getElementById('result-title').textContent=title;
document.getElementById('result-score').textContent=`${score} de ${questions.length} correctes (${pct}%)`;
document.getElementById('result-stars').textContent='⭐'.repeat(stars);
addStars(stars);
await saveSession(level, score, questions.length, stars);
if(pct===100)launchConfetti();
showScreen('screen-result');
}
/* ══════════════════════════════════════════════════════
NAVEGACIÓ
══════════════════════════════════════════════════════ */
function goHome(){ showScreen('screen-home'); }
function startLevel(n){
currentLevel=n; currentQ=0; score=0;
COMARQUES = getActiveComarques(); // refresh from current filter
// For map levels (5 & 6), exclude comarques without SVG path (noMap:true)
const mapComarques = COMARQUES.filter(c => !c.noMap && COMARCA_PATHS[c.name]);
questions=shuffle(n>=5 ? mapComarques : COMARQUES);
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 if(n===6){mapBlind=true;initMap();}
else{initMill();}
}
function repeatLevel(){ startLevel(currentLevel); }
/* ══════════════════════════════════════════════════════
L1 FLASHCARDS
══════════════════════════════════════════════════════ */
function initL1(){fcIndex=0;showScreen('screen-l1');renderCard();}
function renderCard(){
const c=questions[fcIndex];
document.getElementById('fc-inner').classList.remove('flipped');fcFlipped=false;
document.getElementById('fc-comarca').textContent=c.emoji+' '+c.name;
document.getElementById('fc-capital').textContent='📍 '+c.capital;
document.getElementById('fc-hint').textContent='Toca per veure la capital';
document.getElementById('l1-prog').style.width=((fcIndex+1)/questions.length*100)+'%';
document.getElementById('l1-ctr').textContent=`${fcIndex+1} / ${questions.length}`;
}
function flipCard(){fcFlipped=!fcFlipped;document.getElementById('fc-inner').classList.toggle('flipped',fcFlipped);document.getElementById('fc-hint').textContent=fcFlipped?'Toca per amagar':'Toca per veure la capital';}
function nextCard(){
if(fcIndex<questions.length-1){fcIndex++;renderCard();}
else{
addStars(1);
saveSession(1,questions.length,questions.length,1);
document.getElementById('result-emoji').textContent='📚';
document.getElementById('result-title').textContent='Has vist totes les comarques!';
document.getElementById('result-score').textContent='Ara prova el Nivell 2 per practicar!';
document.getElementById('result-stars').textContent='⭐';
showScreen('screen-result');
}
}
function prevCard(){if(fcIndex>0){fcIndex--;renderCard();}}
/* ══════════════════════════════════════════════════════
L2 MULTI-CHOICE
══════════════════════════════════════════════════════ */
function initL2(){showScreen('screen-l2');renderL2();}
function renderL2(){
if(currentQ>=questions.length){showResult(2);return;}
const q=questions[currentQ];
document.getElementById('l2-prog').style.width=(currentQ/questions.length*100)+'%';
document.getElementById('l2-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
document.getElementById('l2-q').textContent=q.emoji+' '+q.name;
const wrong=shuffle(COMARQUES.filter(c=>c.name!==q.name)).slice(0,3);
const opts=shuffle([q,...wrong]);
const grid=document.getElementById('l2-opts');grid.innerHTML='';
opts.forEach(opt=>{
const btn=document.createElement('button');btn.className='opt-btn';btn.textContent=opt.capital;
btn.onclick=()=>handleL2(btn,opt.capital===q.capital,q.capital);grid.appendChild(btn);});
}
function handleL2(btn,ok,right){
document.querySelectorAll('.opt-btn').forEach(b=>b.disabled=true);
btn.classList.add(ok?'correct':'wrong');
if(!ok)document.querySelectorAll('.opt-btn').forEach(b=>{if(b.textContent===right)b.classList.add('correct');});
if(ok)score++;showFeedback(ok);
setTimeout(()=>{currentQ++;renderL2();},1400);
}
/* ══════════════════════════════════════════════════════
L3 MATCH
══════════════════════════════════════════════════════ */
function initL3(){l3Round=0;score=0;showScreen('screen-l3');renderL3();}
function renderL3(){
l3Sel=null;const batch=questions.slice(l3Round*L3B,(l3Round+1)*L3B);
if(!batch.length){showResult(3);return;}
document.getElementById('l3-prog').style.width=(l3Round*L3B/questions.length*100)+'%';
const left=shuffle(batch.map(c=>({text:c.emoji+' '+c.name,key:c.name,type:'C'})));
const right=shuffle(batch.map(c=>({text:'📍 '+c.capital,key:c.name,type:'K'})));
const all=[];for(let i=0;i<batch.length;i++){all.push(left[i],right[i]);}
const grid=document.getElementById('l3-grid');grid.innerHTML='';
all.forEach(item=>{const d=document.createElement('div');d.className='m-item';d.textContent=item.text;
d.dataset.key=item.key;d.dataset.type=item.type;d.onclick=()=>handleL3(d);grid.appendChild(d);});
l3BTotal=batch.length;l3BDone=0;
}
function handleL3(div){
if(div.classList.contains('matched'))return;
if(!l3Sel){document.querySelectorAll('.m-item.selected').forEach(e=>e.classList.remove('selected'));div.classList.add('selected');l3Sel=div;return;}
if(l3Sel===div){div.classList.remove('selected');l3Sel=null;return;}
const ok=l3Sel.dataset.key===div.dataset.key&&l3Sel.dataset.type!==div.dataset.type;
if(ok){l3Sel.classList.remove('selected');l3Sel.classList.add('matched');div.classList.add('matched');l3Sel=null;l3BDone++;score++;playSound(true);
if(l3BDone===l3BTotal)setTimeout(()=>{l3Round++;renderL3();if(l3Round*L3B>=questions.length)showResult(3);},600);}
else{l3Sel.classList.remove('selected');[l3Sel,div].forEach(e=>{e.classList.add('bad');setTimeout(()=>e.classList.remove('bad'),400);});playSound(false);l3Sel=null;}
}
/* ══════════════════════════════════════════════════════
L4 TYPE
══════════════════════════════════════════════════════ */
function initL4(){showScreen('screen-l4');renderL4();}
function renderL4(){
if(currentQ>=questions.length){showResult(4);return;}
const q=questions[currentQ];
document.getElementById('l4-prog').style.width=(currentQ/questions.length*100)+'%';
document.getElementById('l4-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
document.getElementById('l4-q').textContent=q.emoji+' '+q.name;
document.getElementById('l4-input').value='';
document.getElementById('l4-input').style.borderColor='#ddd';
document.getElementById('l4-fb').textContent='';
document.getElementById('l4-input').focus();
}
function checkL4(){
const q=questions[currentQ],val=document.getElementById('l4-input').value;
const ok=normalize(val)===normalize(q.capital)||normalize(val)===normalize(q.capital.replace(/^(el |la |l')/i,''));
const inp=document.getElementById('l4-input'),fb=document.getElementById('l4-fb');
if(ok){inp.style.borderColor='#5BC97A';fb.style.color='#1a7a3c';fb.textContent='✅ Correcte! La capital és '+q.capital;score++;showFeedback(true);setTimeout(()=>{currentQ++;renderL4();},1400);}
else if(!val.trim()){fb.style.color='#aaa';fb.textContent='Escriu alguna cosa!';}
else{inp.style.borderColor='#F05C5C';fb.style.color='#a02020';fb.textContent='❌ La capital és '+q.capital;showFeedback(false);setTimeout(()=>{currentQ++;renderL4();},2000);}
}
function skipL4(){const q=questions[currentQ];document.getElementById('l4-fb').style.color='#999';document.getElementById('l4-fb').textContent='➡ '+q.name+' → '+q.capital;setTimeout(()=>{currentQ++;renderL4();},1900);}
/* ══════════════════════════════════════════════════════
L5 MAP
══════════════════════════════════════════════════════ */
function buildMap(){
// Always rebuild when switching between normal and blind mode
const needRebuild = mapBuilt && (document.getElementById('map-svg').dataset.blind !== String(mapBlind));
if(mapBuilt && !needRebuild)return;
mapBuilt=true;
document.getElementById('map-svg').dataset.blind = String(mapBlind);
const svg=document.getElementById('map-svg'); svg.innerHTML='';
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);
// Active set — comarques that are clickable in this session
const activeSet = new Set(COMARQUES.map(c => c.name));
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);
if(activeSet.has(name)){
// Clickable comarca — use MTN_COLORS if available, else a neutral green-blue
const fill=MTN_COLORS[name]||'#4A90A4';
path.setAttribute('fill',fill);path.setAttribute('class','comarca-mountain');
path.setAttribute('data-name',name);path.style.setProperty('--mfill',fill);
path.setAttribute('id','mp_'+info.id);
path.addEventListener('click',()=>handleMapClick(path));
if(!mapBlind){const title=document.createElementNS('http://www.w3.org/2000/svg','title');title.textContent=name;path.appendChild(title);}
}else{path.setAttribute('class','comarca-bg');}
svg.appendChild(path);
}
// Level 6 (blind mode): no labels, no capital dots — identify by shape only
if(!mapBlind){
for(const c of COMARQUES){
const info=COMARCA_PATHS[c.name]; if(!info)continue;
const small=c.name.length>12;
const words=c.name.split(' ');
if(words.length>2&&small){addLabel(svg,info.cx,info.cy-5,words.slice(0,~~(words.length/2+.5)).join(' '),true);addLabel(svg,info.cx,info.cy+7,words.slice(~~(words.length/2+.5)).join(' '),true);}
else{addLabel(svg,info.cx,info.cy,c.name,small);}
const dot=document.createElementNS('http://www.w3.org/2000/svg','circle');
dot.setAttribute('cx',info.cx);dot.setAttribute('cy',info.cy+14);dot.setAttribute('r','2.5');dot.setAttribute('class','capital-dot');svg.appendChild(dot);
}
}
const g=document.createElementNS('http://www.w3.org/2000/svg','g');g.setAttribute('transform','translate(785,35)');
g.innerHTML=`<circle cx="0" cy="0" r="22" fill="rgba(255,255,255,.9)" stroke="#aaa" stroke-width="1"/>
<text x="0" y="-12" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">N</text>
<text x="0" y="14" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">S</text>
<text x="-13" y="2" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">O</text>
<text x="13" y="2" text-anchor="middle" dominant-baseline="middle" font-size="9" font-weight="700" fill="#555" font-family="Lexend,Arial">E</text>
<polygon points="0,-16 3,-5 0,-8 -3,-5" fill="#E33"/><polygon points="0,16 3,5 0,8 -3,5" fill="#555"/>`;
svg.appendChild(g);
addTextSVG(svg,420,18,'ANDORRA · FRANÇA',7,'#999','middle');
addTextSVG(svg,36,310,'ARAGÓ',7,'#999','middle','rotate(-90,36,310)');
addTextSVG(svg,790,430,'Mar Mediterrani',7,'#6BAFD6','middle','rotate(90,790,430)');
addTextSVG(svg,320,575,'COMUNITAT VALENCIANA',7,'#999','middle');
}
function addLabel(svg,x,y,text,small){
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y);t.setAttribute('class','comarca-label'+(small?' small':''));t.textContent=text;svg.appendChild(t);
}
function addTextSVG(svg,x,y,text,size,fill,anchor,transform){
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',x);t.setAttribute('y',y);t.setAttribute('font-size',size);t.setAttribute('fill',fill);
t.setAttribute('text-anchor',anchor||'middle');t.setAttribute('font-family','Lexend,Arial');t.setAttribute('dominant-baseline','middle');
if(transform)t.setAttribute('transform',transform);t.textContent=text;svg.appendChild(t);
}
function resetMapPaths(){
document.querySelectorAll('.comarca-mountain').forEach(p=>{const name=p.getAttribute('data-name');p.style.fill=MTN_COLORS[name]||'#C97D10';p.classList.remove('wrong-shake');p.style.filter='';p.style.opacity='1';});
}
function initMap(){
currentQ=0;score=0;mapAnswered=false;
// Both levels show mode tabs — level 6 just removes labels from the map
const tabs=document.querySelector('.map-mode-tabs');
tabs.style.display='flex';
showScreen('map-screen');
buildMap();
resetMapPaths();
nextMapQ();
}
function setMapMode(mode){mapMode=mode;document.getElementById('tab-name').classList.toggle('active',mode==='name');document.getElementById('tab-cap').classList.toggle('active',mode==='capital');initMap();}
function nextMapQ(){
if(currentQ>=questions.length){showResult(5);return;}
mapAnswered=false;mapTarget=questions[currentQ];
document.getElementById('map-prog').style.width=(currentQ/questions.length*100)+'%';
document.getElementById('map-ctr').textContent=`Pregunta ${currentQ+1} de ${questions.length}`;
document.getElementById('map-score-live').textContent=`${score} correctes`;
if(mapMode==='name'){document.getElementById('map-qlabel').textContent='TOCA LA COMARCA...';document.getElementById('map-qtext').textContent=mapTarget.emoji+' '+mapTarget.name;}
else{document.getElementById('map-qlabel').textContent='QUINA COMARCA TÉ LA CAPITAL...';document.getElementById('map-qtext').textContent='📍 '+mapTarget.capital;}
document.getElementById('map-qhint').textContent='';resetMapPaths();
}
function handleMapClick(path){
if(mapAnswered)return;
const clicked=path.getAttribute('data-name');const ok=clicked===mapTarget.name;mapAnswered=true;
if(ok){
path.style.fill='#4DBD6E';path.style.filter='drop-shadow(0 0 12px #4DBD6E)';
score++;showFeedback(true);document.getElementById('map-qhint').textContent='✅ Capital: '+mapTarget.capital;
setTimeout(()=>{path.style.fill=MTN_COLORS[clicked]||'#C97D10';path.style.filter='';currentQ++;nextMapQ();},1600);
}else{
path.classList.add('wrong-shake');path.style.fill='#F05C5C';showFeedback(false);
const cp=document.getElementById('mp_'+COMARCA_PATHS[mapTarget.name]?.id);
if(cp){cp.style.fill='#4DBD6E';cp.style.filter='drop-shadow(0 0 10px rgba(77,189,110,.9))';}
document.getElementById('map-qhint').textContent='❌ Era: '+mapTarget.name+' · Capital: '+mapTarget.capital;
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>