/* ✨ Lettres Magiques — jeu web pour enfants - Catégories / imagier (famille, animaux, fruits, couleurs…) - Espace parent (édite les mots + prénom de l'enfant, protégé par un calcul) - Personnalisation au prénom de l'enfant - Mode guidé (surligne la prochaine lettre + lit chaque lettre) Fonctionne au clavier (ordi) et au toucher (téléphone/tablette). */ "use strict"; /* ================================================================== */ /* Données par défaut (catégories + mots) */ /* Pour un mot "names" : clue = phrase magique. Pour "imagier" : emoji. */ /* ================================================================== */ const DEFAULTS = { childName: "", guided: true, categories: [ { id: "famille", label: "La famille", emoji: "👨‍👩‍👧‍👦", kind: "names", show: true, words: [ { w: "PEPE", clue: "Pépé, la reine des bisous magiques !" }, { w: "GEGE", clue: "Gégé, le champion des chatouilles !" }, { w: "NICO", clue: "Nico, le super héros rigolo !" }, { w: "JULIE", clue: "Julie, la fée des câlins tout doux !" }, { w: "LILWENN", clue: "Lilwenn, la princesse des étoiles filantes !" }, { w: "COME", clue: "Côme, le pirate des doudous perdus !" }, { w: "GWEN", clue: "Gwen, la magicienne des sourires !" }, { w: "ISABELLE", clue: "Isabelle, la reine des gâteaux rigolos !" }, { w: "BAPTISTE", clue: "Baptiste, le roi des voitures qui font vroum !" }, { w: "TRUFFE", clue: "Truffe, le chien qui fait des bisous baveux !" }, ], }, { id: "animaux", label: "Les animaux", emoji: "🐾", kind: "imagier", show: true, words: [ { w: "CHAT", clue: "🐱" }, { w: "CHIEN", clue: "🐶" }, { w: "VACHE", clue: "🐮" }, { w: "LION", clue: "🦁" }, { w: "LAPIN", clue: "🐰" }, { w: "POULE", clue: "🐔" }, { w: "OURS", clue: "🐻" }, { w: "SINGE", clue: "🐵" }, { w: "CANARD", clue: "🦆" }, { w: "POISSON", clue: "🐟" }, ], }, { id: "fruits", label: "Fruits & légumes", emoji: "🍎", kind: "imagier", show: true, words: [ { w: "POMME", clue: "🍎" }, { w: "BANANE", clue: "🍌" }, { w: "FRAISE", clue: "🍓" }, { w: "CERISE", clue: "🍒" }, { w: "RAISIN", clue: "🍇" }, { w: "CITRON", clue: "🍋" }, { w: "CAROTTE", clue: "🥕" }, { w: "TOMATE", clue: "🍅" }, { w: "POIRE", clue: "🍐" }, { w: "ORANGE", clue: "🍊" }, ], }, { id: "couleurs", label: "Les couleurs", emoji: "🌈", kind: "imagier", show: true, words: [ { w: "ROUGE", clue: "🟥" }, { w: "BLEU", clue: "🟦" }, { w: "VERT", clue: "🟩" }, { w: "JAUNE", clue: "🟨" }, { w: "ORANGE", clue: "🟧" }, { w: "VIOLET", clue: "🟪" }, { w: "ROSE", clue: "🌸" }, { w: "NOIR", clue: "⬛" }, { w: "BLANC", clue: "⬜" }, { w: "MARRON", clue: "🟫" }, ], }, ], }; const STORE_KEY = "lm_data_v1"; const BATCH_SIZE = 6; const STICKERS = ["🦄","⭐","🌈","🐱","🚀","🍭","🎈","🐠","🦋","🌸","🐢","🍩","🎸","🐧","🌟"]; const ENCOURAGEMENTS = ["Super ! 🌟","Génial ! 🎉","Bravo ! 👏","Magnifique ! ✨","Tu es fort ! 💪","Youpi ! 🎈","Fantastique ! 🌈","Trop bien ! 🚀"]; const ERREURS = ["Oups ! Essaie encore ! 🤗","Pas grave ! Recommence ! 😊","Presque ! Tu vas y arriver ! 💪","C'est pas facile ! Essaie ! 🌟"]; /* ================================================================== */ /* Persistance (localStorage) */ /* ================================================================== */ function clone(o) { return JSON.parse(JSON.stringify(o)); } let data = loadData(); function loadData() { try { const raw = localStorage.getItem(STORE_KEY); if (raw) { const d = JSON.parse(raw); if (d && Array.isArray(d.categories) && d.categories.length) return d; } } catch (e) {} return clone(DEFAULTS); } function saveData() { try { localStorage.setItem(STORE_KEY, JSON.stringify(data)); } catch (e) {} } /* ================================================================== */ /* État de jeu */ /* ================================================================== */ const state = { running: false, cat: null, // catégorie en cours score: 0, current: "", // lettres tapées targets: [], // mots du lot courant (chaînes) found: new Set(), totalFound: 0, stickers: 0, focus: null, // mot visé (pour guide + imagier) soundOn: true, lastAction: 0, hintShown: false, miniGame: null, }; /* ================================================================== */ /* Raccourcis DOM */ /* ================================================================== */ const $ = (id) => document.getElementById(id); const elScore = $("score"), elStickersCount = $("stickers-count"); const elClue = $("clue"), elWordZone = $("word-zone"), elWordList = $("word-list"); const elKeyboard = $("keyboard"), elBlob = $("blob"), elSpeech = $("speech"); const elMini = $("minigame"), elMgTitle = $("mg-title"), elMgTime = $("mg-time"); /* ================================================================== */ /* Son (WebAudio) + voix (Web Speech) */ /* ================================================================== */ let audioCtx = null; function ac() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); return audioCtx; } function beep(freq, dur = 0.12, type = "sine", vol = 0.18, delay = 0) { if (!state.soundOn) return; try { const ctx = ac(), o = ctx.createOscillator(), g = ctx.createGain(); o.type = type; o.frequency.value = freq; const t0 = ctx.currentTime + delay; g.gain.setValueAtTime(0, t0); g.gain.linearRampToValueAtTime(vol, t0 + 0.02); g.gain.exponentialRampToValueAtTime(0.001, t0 + dur); o.connect(g).connect(ctx.destination); o.start(t0); o.stop(t0 + dur + 0.02); } catch (e) {} } const sndType = () => beep(420 + Math.random() * 120, 0.08, "triangle", 0.12); const sndPop = () => beep(700, 0.10, "square", 0.14); const sndError = () => { beep(300, 0.14, "sawtooth", 0.10); beep(220, 0.18, "sawtooth", 0.10, 0.08); }; const sndSuccess = () => { [523, 659, 784, 1047].forEach((f, i) => beep(f, 0.22, "triangle", 0.16, i * 0.09)); }; const sndSticker = () => { [784, 988, 1175].forEach((f, i) => beep(f, 0.25, "sine", 0.16, i * 0.10)); }; let frVoice = null; function pickVoice() { const voices = (window.speechSynthesis && speechSynthesis.getVoices()) || []; frVoice = voices.find(v => /fr(-|_)?/i.test(v.lang)) || voices[0] || null; } if ("speechSynthesis" in window) { pickVoice(); speechSynthesis.onvoiceschanged = pickVoice; } function say(text, { interrupt = true } = {}) { if (!state.soundOn || !("speechSynthesis" in window)) return; const clean = String(text).replace(/[^\p{L}\p{N} !?.,']/gu, "").trim(); if (!clean) return; try { if (interrupt) speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(clean); if (frVoice) u.voice = frVoice; u.lang = "fr-FR"; u.rate = 0.92; u.pitch = 1.15; speechSynthesis.speak(u); } catch (e) {} } /* ================================================================== */ /* Helpers */ /* ================================================================== */ const rand = (a) => a[Math.floor(Math.random() * a.length)]; function shuffle(a) { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function now() { return performance.now(); } const nom = () => (data.childName || "").trim(); function clueOf(w) { const it = state.cat && state.cat.words.find(x => x.w === w); return it ? it.clue : ""; } /* ================================================================== */ /* Mascotte */ /* ================================================================== */ let speechTimer = null, blinkTimer = null; function mascotSet(s) { elBlob.classList.remove("happy","sad","sleep","dance"); if (s) elBlob.classList.add(s); } function mascotSay(text, mood = "happy") { mascotSet(mood); elSpeech.textContent = text; elSpeech.classList.add("show"); clearTimeout(speechTimer); speechTimer = setTimeout(() => elSpeech.classList.remove("show"), 3400); say(text); if (mood === "dance") setTimeout(() => mascotSet("happy"), 2500); } function mascotError() { mascotSay(rand(ERREURS), "sad"); } function blinkLoop() { clearTimeout(blinkTimer); blinkTimer = setTimeout(() => { elBlob.classList.add("blink"); setTimeout(() => elBlob.classList.remove("blink"), 150); blinkLoop(); }, 1500 + Math.random() * 3000); } /* Phrases personnalisées ----------------------------------------- */ function greeting() { const n = nom(); const h = new Date().getHours(); if (n) { if (h >= 6 && h < 12) return `Bonjour ${n} ! Bien dormi ? On joue ? 🌞`; if (h >= 17 && h < 21) return `Bonsoir ${n} ! Une petite partie avant dodo ? 🌙`; return rand([`Coucou ${n} ! On s'amuse ? 🎮`, `${n} est là ! Youpi ! 🎉`, `Salut ${n} ! Prêt ? 🌟`]); } return "Coucou ! On apprend les lettres en s'amusant ? 😊"; } function successPhrase(word) { const n = nom(); // Catégorie famille : on garde la phrase magique du prénom if (state.cat.kind === "names") { const c = clueOf(word); return "Bravo ! " + (c || (word + " !")) + " 🎉"; } // Paliers if (state.totalFound === 1) return n ? `Ton 1er mot ${n} ! ${word} ! Génial ! 🎊` : `Ton 1er mot ! ${word} ! 🎊`; if (state.totalFound === 5) return n ? `Déjà 5 mots ${n} ! Tu es super fort ! 🚀` : `Déjà 5 mots ! Bravo ! 🚀`; if (state.totalFound === 10) return n ? `10 MOTS ! ${n} est un GÉNIE ! 🧠✨` : `10 MOTS ! Un génie ! 🧠✨`; const base = rand([`Bravo ! ${word} ! 🏆`, `Oui ! C'est ${word} ! ⭐`, `Super ! ${word} ! 🌈`]); return n && Math.random() < 0.4 ? base.replace("!", `${n} !`) : base; } function encourage() { let e = rand(ENCOURAGEMENTS); const n = nom(); if (n && Math.random() < 0.35) e = e.replace("Tu", `${n}, tu`).replace("Bravo", `Bravo ${n}`); return e; } /* ================================================================== */ /* Menu de catégories */ /* ================================================================== */ function buildMenu() { const grid = $("cat-grid"); grid.innerHTML = ""; data.categories.filter(c => c.show && c.words.length).forEach(cat => { const b = document.createElement("button"); b.className = "cat-card"; b.innerHTML = `${cat.emoji}${cat.label}`; b.addEventListener("click", () => startCategory(cat)); grid.appendChild(b); }); } /* ================================================================== */ /* Lot de mots, affichage */ /* ================================================================== */ function newBatch() { const words = state.cat.words.map(x => x.w); state.targets = shuffle(words).slice(0, Math.min(BATCH_SIZE, words.length)); state.found.clear(); state.current = ""; renderList(); renderWord(); recomputeFocus(true); } const LETTER_COLORS = ["#ff6bcb","#6a5cff","#2ee6e6","#ffd23f","#3ddc84","#ff8c42"]; function renderWord(popLast = false) { elWordZone.innerHTML = ""; [...state.current].forEach((ch, i) => { const t = document.createElement("span"); t.className = "tile" + (popLast && i === state.current.length - 1 ? " pop" : ""); t.textContent = ch; t.style.color = LETTER_COLORS[i % LETTER_COLORS.length]; elWordZone.appendChild(t); }); } function renderList() { elWordList.innerHTML = ""; const imagier = state.cat.kind === "imagier"; state.targets.forEach((w) => { const c = document.createElement("span"); c.className = "chip" + (state.found.has(w) ? " found" : ""); c.dataset.word = w; c.textContent = imagier ? (clueOf(w) + " " + w) : w; elWordList.appendChild(c); }); } function renderClue() { if (state.cat.kind === "imagier" && state.focus) { elClue.textContent = clueOf(state.focus); elClue.classList.remove("hidden"); } else { elClue.classList.add("hidden"); } } /* Mot visé (guide + imagier) -------------------------------------- */ function recomputeFocus(announce = false) { const left = state.targets.filter(w => !state.found.has(w)); let f = null; if (state.current) f = left.find(w => w.startsWith(state.current)); if (!f) f = left.find(w => w.startsWith(state.current)) || left[0] || null; const changed = f !== state.focus; state.focus = f; renderClue(); applyGuide(); if (announce && changed && f) { setTimeout(() => mascotSay("Écris : " + f + " ! ✏️", "happy"), 250); } } function applyGuide() { elKeyboard.querySelectorAll(".key.guide").forEach(k => k.classList.remove("guide")); if (!data.guided || !state.focus) return; const next = state.focus[state.current.length]; if (!next) return; const key = elKeyboard.querySelector(`.key[data-ch="${next}"]`); if (key) key.classList.add("guide"); } /* ================================================================== */ /* Saisie */ /* ================================================================== */ function eraseLetter() { state.current = ""; renderWord(); bump(); recomputeFocus(); } function bump() { state.lastAction = now(); state.hintShown = false; clearHint(); } function inputLetter(ch) { ch = ch.toUpperCase(); if (!/^[A-Z]$/.test(ch)) return; bump(); mascotSet("happy"); state.current += ch; renderWord(true); sndType(); if (data.guided) say(ch.toLowerCase(), { interrupt: true }); // lit la lettre (minuscule = pas de "majuscule") spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.32, 6); checkWord(); recomputeFocus(); } function checkWord() { const w = state.current; if (state.targets.includes(w) && !state.found.has(w)) { wordFound(w); return; } const possible = state.targets.some(t => !state.found.has(t) && t.startsWith(w)); if (!possible) { mascotError(); sndError(); setTimeout(() => { state.current = ""; renderWord(); recomputeFocus(); }, 380); } } function wordFound(w) { state.found.add(w); state.totalFound++; state.score += 100 * w.length; elScore.textContent = state.score; renderList(); celebrate(); sndSuccess(); mascotSay(successPhrase(w), "dance"); if (state.totalFound % 3 === 0) { earnSticker(); confettiBurst(120); } setTimeout(() => { state.current = ""; renderWord(); recomputeFocus(true); }, 750); if (state.found.size >= state.targets.length) { confettiBurst(200); const n = nom(); setTimeout(() => mascotSay(n ? `Tu as tout trouvé ${n} ! 🥳` : "Tu as tout trouvé ! 🥳", "dance"), 800); setTimeout(newBatch, 1700); } } /* ================================================================== */ /* Autocollants */ /* ================================================================== */ function earnSticker() { if (state.stickers >= STICKERS.length) return; state.stickers++; elStickersCount.textContent = state.stickers; sndSticker(); const emoji = STICKERS[state.stickers - 1]; setTimeout(() => mascotSay("Nouvel autocollant ! " + emoji, "dance"), 900); } function renderStickerGrid() { const grid = $("sticker-grid"); grid.innerHTML = ""; STICKERS.forEach((emoji, i) => { const d = document.createElement("div"); const ok = i < state.stickers; d.className = "sticker " + (ok ? "unlocked" : "locked"); d.textContent = ok ? emoji : "❔"; grid.appendChild(d); }); } /* ================================================================== */ /* Inactivité : indice puis dodo */ /* ================================================================== */ function clearHint() { document.querySelectorAll(".chip.hint").forEach(c => c.classList.remove("hint")); } function checkIdle() { if (!state.running || state.miniGame) return; const idle = (now() - state.lastAction) / 1000; if (idle > 5) { const sinceHint = (now() - (state.lastHint || 0)) / 1000; if (sinceHint > 6) { state.lastHint = now(); const w = state.focus || state.targets.find(x => !state.found.has(x)); if (w) { const chip = elWordList.querySelector(`.chip[data-word="${w}"]`); if (chip) chip.classList.add("hint"); mascotSay("Écris : " + w + " ! 💡", "happy"); } } } } /* ================================================================== */ /* Mini-jeux (ballons / étoiles) */ /* ================================================================== */ let nextMiniGame = 0; function maybeStartMiniGame() { if (!state.miniGame && state.running && now() >= nextMiniGame) startMiniGame(); } function startMiniGame() { const kind = Math.random() < 0.5 ? "ballons" : "etoiles"; state.miniGame = { kind, items: [], end: now() + 10000, spawn: 0, earned: 0 }; elMgTitle.textContent = kind === "ballons" ? "🎈 Éclate les ballons !" : "⭐ Attrape les étoiles !"; elMini.classList.remove("hidden"); mascotSay("Mini-jeu bonus ! 🎮", "dance"); } function endMiniGame() { if (!state.miniGame) return; const earned = state.miniGame.earned; state.miniGame = null; elMini.classList.add("hidden"); nextMiniGame = now() + 35000; if (earned > 0) { state.score += earned; elScore.textContent = state.score; mascotSay(encourage(), "dance"); } } function updateMiniGame(dt) { const mg = state.miniGame; if (!mg) return; if (now() > mg.end) { endMiniGame(); return; } elMgTime.textContent = Math.ceil((mg.end - now()) / 1000); mg.spawn -= dt; if (mg.spawn <= 0) { mg.spawn = 0.55; mg.items.push({ x: 40 + Math.random() * (window.innerWidth - 80), y: window.innerHeight + 30, r: 26 + Math.random() * 14, vy: 90 + Math.random() * 70, hue: Math.floor(Math.random() * 360), kind: mg.kind }); } for (const it of mg.items) it.y -= it.vy * dt; mg.items = mg.items.filter(it => it.y > -60); } function hitMiniGame(x, y) { const mg = state.miniGame; if (!mg) return; for (let i = mg.items.length - 1; i >= 0; i--) { const it = mg.items[i]; if (Math.hypot(it.x - x, it.y - y) < it.r + 16) { mg.items.splice(i, 1); mg.earned += 20; sndPop(); spawnBubbles(x, y, 10); confettiAt(x, y, 8); return; } } } /* ================================================================== */ /* Canvas FX */ /* ================================================================== */ const cv = $("fx"), ctx2d = cv.getContext("2d"); let W = 0, H = 0, DPR = 1, stars = [], bubbles = [], confetti = [], rainbow = 0; function resize() { DPR = Math.min(window.devicePixelRatio || 1, 2); W = window.innerWidth; H = window.innerHeight; cv.width = W * DPR; cv.height = H * DPR; cv.style.width = W + "px"; cv.style.height = H + "px"; ctx2d.setTransform(DPR, 0, 0, DPR, 0, 0); layoutSafeAreas(); } function initStars() { stars = []; const n = Math.min(160, Math.floor((W * H) / 9000)); for (let i = 0; i < n; i++) stars.push({ x: Math.random()*W, y: Math.random()*H, r: 0.6+Math.random()*2.2, phase: Math.random()*Math.PI*2, sp: 0.01+Math.random()*0.04, col: rand(["#ffffff","#ffd23f","#2ee6e6","#ff6bcb"]) }); } function spawnBubbles(x, y, n) { for (let i=0;i 0) { rainbow -= dt * 0.5; const cols = ["#ff5c5c","#ff8c42","#ffd23f","#3ddc84","#2ee6e6","#6a5cff","#ff6bcb"]; ctx2d.globalAlpha = Math.max(0, rainbow) * 0.5; ctx2d.lineWidth = 16; cols.forEach((c, i) => { ctx2d.strokeStyle = c; ctx2d.beginPath(); ctx2d.arc(W/2, H*1.1, W*0.5 - i*18, Math.PI, Math.PI*2); ctx2d.stroke(); }); ctx2d.globalAlpha = 1; } for (const b of bubbles) { b.y -= b.vy; b.x += Math.sin(b.y*0.02)*b.drift; b.life -= 0.01; ctx2d.globalAlpha = Math.max(0,b.life)*0.6; ctx2d.strokeStyle = b.col; ctx2d.lineWidth = 2; ctx2d.beginPath(); ctx2d.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx2d.stroke(); } ctx2d.globalAlpha = 1; bubbles = bubbles.filter(b => b.life > 0 && b.y > -60); for (const c of confetti) { c.x += c.vx; c.y += c.vy; c.vy += 0.12; c.rot += c.vr; ctx2d.save(); ctx2d.translate(c.x, c.y); ctx2d.rotate(c.rot*Math.PI/180); ctx2d.fillStyle = c.col; ctx2d.fillRect(-c.size/2, -c.size/2, c.size, c.size*0.6); ctx2d.restore(); } confetti = confetti.filter(c => c.y < H + 40); updateMiniGame(dt); if (state.miniGame) { for (const it of state.miniGame.items) { if (it.kind === "ballons") { ctx2d.fillStyle = `hsl(${it.hue} 85% 60%)`; ctx2d.beginPath(); ctx2d.ellipse(it.x, it.y, it.r*0.85, it.r, 0, 0, Math.PI*2); ctx2d.fill(); ctx2d.strokeStyle = "rgba(255,255,255,0.5)"; ctx2d.lineWidth = 1; ctx2d.beginPath(); ctx2d.moveTo(it.x, it.y+it.r); ctx2d.lineTo(it.x, it.y+it.r+22); ctx2d.stroke(); } else { drawStar(it.x, it.y, it.r, `hsl(${it.hue} 90% 60%)`); } } } requestAnimationFrame(loop); } function drawStar(cx, cy, r, color) { ctx2d.fillStyle = color; ctx2d.beginPath(); for (let i = 0; i < 10; i++) { const ang = (Math.PI/5)*i - Math.PI/2; const rad = i%2===0?r:r*0.45; const x = cx+Math.cos(ang)*rad, y = cy+Math.sin(ang)*rad; i===0?ctx2d.moveTo(x,y):ctx2d.lineTo(x,y); } ctx2d.closePath(); ctx2d.fill(); } /* ================================================================== */ /* Clavier tactile */ /* ================================================================== */ const KB_ROWS = ["ABCDEFG", "HIJKLMN", "OPQRSTU", "VWXYZ"]; function buildKeyboard() { elKeyboard.innerHTML = ""; KB_ROWS.forEach((row, ri) => { const r = document.createElement("div"); r.className = "kb-row"; [...row].forEach((ch, i) => { const b = document.createElement("button"); b.className = "key"; b.textContent = ch; b.dataset.ch = ch; b.style.background = `linear-gradient(135deg, hsl(${(ri*40+i*22)%360} 80% 62%), hsl(${(ri*40+i*22+30)%360} 80% 52%))`; b.addEventListener("pointerdown", (e) => { e.preventDefault(); inputLetter(ch); }, { passive: false }); r.appendChild(b); }); if (ri === KB_ROWS.length - 1) { const e = document.createElement("button"); e.className = "key wide erase"; e.textContent = "⌫ Effacer"; e.addEventListener("pointerdown", (ev) => { ev.preventDefault(); eraseLetter(); }, { passive: false }); r.appendChild(e); } elKeyboard.appendChild(r); }); layoutSafeAreas(); } function layoutSafeAreas() { const h = elKeyboard.classList.contains("hidden") ? 0 : elKeyboard.offsetHeight; document.documentElement.style.setProperty("--kb-h", h + "px"); } /* ================================================================== */ /* Entrées globales */ /* ================================================================== */ window.addEventListener("keydown", (e) => { if (!state.running) return; if (e.key === "Backspace" || e.key === " ") { e.preventDefault(); eraseLetter(); return; } if (/^[a-zA-Z]$/.test(e.key)) inputLetter(e.key); }); window.addEventListener("pointerdown", (e) => { if (state.miniGame) hitMiniGame(e.clientX, e.clientY); }); /* ================================================================== */ /* Navigation : démarrage catégorie / retour menu */ /* ================================================================== */ const GAME_UI = ["hud","word-zone","word-list","keyboard","mascot"]; function showMenu() { state.running = false; $("start").classList.add("hidden"); GAME_UI.forEach(id => $(id).classList.add("hidden")); elClue.classList.add("hidden"); buildMenu(); $("menu").classList.remove("hidden"); } function startCategory(cat) { state.cat = cat; state.running = true; state.score = 0; state.totalFound = 0; state.stickers = 0; state.current = ""; state.found.clear(); state.focus = null; state.lastAction = now(); nextMiniGame = now() + 30000; elScore.textContent = "0"; elStickersCount.textContent = "0"; $("menu").classList.add("hidden"); $("start").classList.add("hidden"); GAME_UI.forEach(id => $(id).classList.remove("hidden")); buildKeyboard(); newBatch(); blinkLoop(); try { ac().resume(); } catch (e) {} mascotSay(greeting(), "happy"); recomputeFocus(true); } /* ================================================================== */ /* Espace parents (protégé par un calcul) */ /* ================================================================== */ function openParents() { const a = 2 + Math.floor(Math.random() * 7), b = 2 + Math.floor(Math.random() * 7); const body = $("parent-body"); body.innerHTML = `

⚙️ Espace parents

Pour entrer, combien font ${a} + ${b} ?

`; $("parent").classList.remove("hidden"); const input = $("gate-input"); input.focus(); const check = () => { if (parseInt(input.value, 10) === a + b) renderParentEditor(); else { input.value = ""; input.placeholder = "Réessaie"; } }; $("gate-ok").addEventListener("click", check); input.addEventListener("keydown", e => { if (e.key === "Enter") check(); }); } function renderParentEditor() { const body = $("parent-body"); body.innerHTML = `

⚙️ Espace parents

`; renderCatsEditor(); $("parent-save").addEventListener("click", saveParents); $("parent-reset").addEventListener("click", () => { if (confirm("Remettre toutes les listes par défaut ? (le prénom de l'enfant est conservé)")) { const keepName = $("child-name").value; data = clone(DEFAULTS); data.childName = keepName; saveData(); renderParentEditor(); } }); } function renderCatsEditor() { const wrap = $("cats-editor"); wrap.innerHTML = ""; data.categories.forEach((cat, ci) => { const sec = document.createElement("div"); sec.className = "cat-edit"; const isNames = cat.kind === "names"; sec.innerHTML = `
`; wrap.appendChild(sec); const wbox = sec.querySelector(`[data-words="${ci}"]`); cat.words.forEach((it, wi) => wbox.appendChild(wordRow(ci, wi, it, isNames))); sec.querySelector(`[data-add="${ci}"]`).addEventListener("click", () => { cat.words.push({ w: "", clue: "" }); wbox.appendChild(wordRow(ci, cat.words.length - 1, cat.words[cat.words.length - 1], isNames)); }); sec.querySelector(`[data-show="${ci}"]`).addEventListener("change", e => { cat.show = e.target.checked; }); }); } function wordRow(ci, wi, it, isNames) { const row = document.createElement("div"); row.className = "word-row"; const ph = isNames ? "Phrase magique (ex : Pépé, le roi des câlins !)" : "Emoji (ex : 🐱)"; row.innerHTML = ` `; row.querySelector(".w-name").addEventListener("input", e => { it.w = e.target.value.toUpperCase().replace(/[^A-Z]/g, ""); e.target.value = it.w; }); row.querySelector(".w-clue").addEventListener("input", e => { it.clue = e.target.value; }); row.querySelector(".del").addEventListener("click", () => { data.categories[ci].words.splice(wi, 1); renderCatsEditor(); }); return row; } function saveParents() { data.childName = $("child-name").value.trim(); data.guided = $("guided-toggle").checked; // nettoyer les mots vides data.categories.forEach(c => { c.words = c.words.filter(x => x.w && x.w.length >= 2); }); saveData(); $("parent").classList.add("hidden"); buildMenu(); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, m => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[m])); } /* ================================================================== */ /* Boutons globaux */ /* ================================================================== */ $("btn-play").addEventListener("click", showMenu); $("btn-parents").addEventListener("click", openParents); $("btn-parents-menu").addEventListener("click", openParents); $("btn-home").addEventListener("click", showMenu); $("btn-sound").addEventListener("click", () => { state.soundOn = !state.soundOn; $("btn-sound").textContent = state.soundOn ? "🔊" : "🔇"; if (!state.soundOn && "speechSynthesis" in window) speechSynthesis.cancel(); }); $("btn-stickers").addEventListener("click", () => { renderStickerGrid(); $("sticker-drawer").classList.remove("hidden"); }); document.addEventListener("click", (e) => { if (e.target.closest("[data-close-drawer]")) $("sticker-drawer").classList.add("hidden"); if (e.target.closest("[data-close-parent]")) $("parent").classList.add("hidden"); }); /* ================================================================== */ /* Init */ /* ================================================================== */ window.addEventListener("resize", () => { resize(); initStars(); }); resize(); initStars(); requestAnimationFrame(loop); if ("serviceWorker" in navigator) window.addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));