From 379aaac74d669f3183234545c297362a54bc2757 Mon Sep 17 00:00:00 2001 From: Poulpe Date: Wed, 17 Jun 2026 08:43:53 +0000 Subject: [PATCH] =?UTF-8?q?Lettres=20Magiques=20=E2=80=94=20jeu=20de=20pr?= =?UTF-8?q?=C3=A9noms=20HTML5=20(mobile+ordi)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +++ game.js | 608 +++++++++++++++++++++++++++++++++++++++++++ icons/icon-192.png | Bin 0 -> 4840 bytes icons/icon-512.png | Bin 0 -> 13155 bytes index.html | 75 ++++++ manifest.webmanifest | 15 ++ style.css | 304 ++++++++++++++++++++++ sw.js | 36 +++ 8 files changed, 1079 insertions(+) create mode 100644 README.md create mode 100644 game.js create mode 100644 icons/icon-192.png create mode 100644 icons/icon-512.png create mode 100644 index.html create mode 100644 manifest.webmanifest create mode 100644 style.css create mode 100644 sw.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..62250c1 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# ✨ Lettres Magiques + +Jeu web pour enfants (4+) pour apprendre les lettres en s'amusant. +Réécriture HTML5 du jeu pygame « Super Lettres Magiques » — fonctionne **au clavier sur ordinateur** et **au toucher sur téléphone/tablette**. + +## Jouer + +Ouvre `index.html` dans un navigateur, ou va sur l'URL publique : +**https://laboratoire.freeboxos.fr/lettres-magiques/** + +## Comment ça marche + +- L'enfant tape/touche les lettres pour écrire les **prénoms de la famille** affichés (PEPE, JULIE, LILWENN, BAPTISTE, TRUFFE le chien…), chacun avec sa petite phrase magique. La liste se personnalise en haut de `game.js` (`NAMES` / `NAME_PHRASES`). +- Mascotte « Blob » qui parle et encourage (voix française via Web Speech API). +- Confettis, bulles, étoiles, arc-en-ciel à chaque réussite. +- Autocollants à collectionner (1 tous les 3 mots). +- Mini-jeux bonus (éclate les ballons / attrape les étoiles) toutes les ~30 s. +- Aucune pression temporelle, aide permanente (liste des mots + indice après quelques secondes). + +## Technique + +- 100 % statique : HTML + CSS + JS, aucune dépendance, aucun build. +- PWA : installable sur l'écran d'accueil et jouable hors-ligne (`manifest.webmanifest` + `sw.js`). +- Sons synthétisés en WebAudio (pas de fichiers audio lourds). + +## Fichiers + +| Fichier | Rôle | +|---|---| +| `index.html` | Structure de la page | +| `style.css` | Style responsive (mobile + desktop) | +| `game.js` | Logique du jeu, mascotte, effets, mini-jeux | +| `manifest.webmanifest`, `sw.js` | PWA | +| `icons/` | Icônes de l'app | +| `make_icons.py` | (dev) régénère les icônes | + +## Régénérer les icônes + +```bash +python make_icons.py +``` diff --git a/game.js b/game.js new file mode 100644 index 0000000..5dcca18 --- /dev/null +++ b/game.js @@ -0,0 +1,608 @@ +/* ✨ Lettres Magiques — jeu web pour enfants + Réécriture HTML5 du jeu pygame "Super Lettres Magiques". + Fonctionne au clavier (ordi) et au toucher (téléphone/tablette). */ + +"use strict"; + +/* ------------------------------------------------------------------ */ +/* Données du jeu */ +/* ------------------------------------------------------------------ */ +// Prénoms de la famille (repris du jeu d'origine) + leur petite phrase magique. +// 👉 Personnalisable : ajoute/retire des prénoms ici, c'est tout. +const NAMES = ["PEPE", "GEGE", "NICO", "JULIE", "LILWENN", "COME", "GWEN", "ISABELLE", "BAPTISTE", "TRUFFE"]; +const NAME_PHRASES = { + PEPE: "Pépé, la reine des bisous magiques !", + GEGE: "Gégé, le champion des chatouilles !", + NICO: "Nico, le super héros rigolo !", + JULIE: "Julie, la fée des câlins tout doux !", + LILWENN: "Lilwenn, la princesse des étoiles filantes !", + COME: "Côme, le pirate des doudous perdus !", + GWEN: "Gwen, la magicienne des sourires !", + ISABELLE: "Isabelle, la reine des gâteaux rigolos !", + BAPTISTE: "Baptiste, le roi des voitures qui font vroum !", + TRUFFE: "Truffe, le chien qui fait des bisous baveux !", +}; +const WORD_POOL = NAMES; +const BATCH_SIZE = NAMES.length; // on affiche toute la famille +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 ! 🌟", +]; + +/* ------------------------------------------------------------------ */ +/* État */ +/* ------------------------------------------------------------------ */ +const state = { + running: false, + score: 0, + level: 1, + current: "", // mot en cours de frappe + targets: [], // mots à trouver (batch courant) + found: new Set(), // mots du batch déjà trouvés + totalFound: 0, + stickers: 0, + soundOn: true, + lastAction: 0, + hintShown: false, + miniGame: null, +}; + +/* ------------------------------------------------------------------ */ +/* Raccourcis DOM */ +/* ------------------------------------------------------------------ */ +const $ = (id) => document.getElementById(id); +const elScore = $("score"), elLevel = $("level"), elStickersCount = $("stickers-count"); +const elWordZone = $("word-zone"), elWordList = $("word-list"); +const elKeyboard = $("keyboard"), elMascot = $("mascot"), elBlob = $("blob"), elSpeech = $("speech"); +const elMini = $("minigame"), elMgTitle = $("mg-title"), elMgTime = $("mg-time"); + +/* ------------------------------------------------------------------ */ +/* Son (synthèse WebAudio + voix) */ +/* ------------------------------------------------------------------ */ +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(); + const 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) { /* ignore */ } +} +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 = 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) { + if (!state.soundOn || !("speechSynthesis" in window)) return; + // Retirer les emojis / garder lettres, chiffres, ponctuation simple + const clean = text.replace(/[^\p{L}\p{N} !?.,']/gu, "").trim(); + if (!clean) return; + try { + 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) { /* ignore */ } +} + +/* ------------------------------------------------------------------ */ +/* Mascotte « Blob » */ +/* ------------------------------------------------------------------ */ +let speechTimer = null, blinkTimer = null; +function mascotSet(stateName) { + elBlob.classList.remove("happy", "sad", "sleep", "dance"); + if (stateName) elBlob.classList.add(stateName); +} +function mascotSay(text, mood = "happy") { + mascotSet(mood); + elSpeech.textContent = text; + elSpeech.classList.add("show"); + clearTimeout(speechTimer); + speechTimer = setTimeout(() => elSpeech.classList.remove("show"), 3200); + say(text); + if (mood === "dance") setTimeout(() => mascotSet("happy"), 2500); +} +function mascotEncourage() { mascotSay(rand(ENCOURAGEMENTS), "dance"); } +function mascotError() { mascotSay(rand(ERREURS), "sad"); } +function blinkLoop() { + clearTimeout(blinkTimer); + const next = 1500 + Math.random() * 3000; + blinkTimer = setTimeout(() => { + elBlob.classList.add("blink"); + setTimeout(() => elBlob.classList.remove("blink"), 150); + blinkLoop(); + }, next); +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ +const rand = (arr) => arr[Math.floor(Math.random() * arr.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; } + +/* ------------------------------------------------------------------ */ +/* Affichage : mot en cours, liste, HUD */ +/* ------------------------------------------------------------------ */ +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 = ""; + state.targets.forEach((w) => { + const c = document.createElement("span"); + c.className = "chip" + (state.found.has(w) ? " found" : ""); + c.dataset.word = w; + c.textContent = w; + elWordList.appendChild(c); + }); +} +function updateHud() { + elScore.textContent = state.score; + elLevel.textContent = state.level; + elStickersCount.textContent = state.stickers; +} +function newBatch() { + state.targets = shuffle(WORD_POOL).slice(0, BATCH_SIZE); + state.found.clear(); + renderList(); +} + +/* ------------------------------------------------------------------ */ +/* Saisie d'une lettre */ +/* ------------------------------------------------------------------ */ +function eraseLetter() { + state.current = ""; + renderWord(); + bump(); +} +function inputLetter(ch) { + ch = ch.toUpperCase(); + if (!/^[A-Z]$/.test(ch)) return; + state.lastAction = now(); + state.hintShown = false; + clearHint(); + mascotSet("happy"); + + state.current += ch; + renderWord(true); + sndType(); + spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.28, 6); + checkWord(); +} +function checkWord() { + const w = state.current; + if (state.targets.includes(w) && !state.found.has(w)) { + wordFound(w); + return; + } + // Erreur : ces lettres ne peuvent plus mener à aucun prénom restant + const possible = state.targets.some(t => !state.found.has(t) && t.startsWith(w)); + if (!possible) { + mascotError(); + sndError(); + setTimeout(() => { state.current = ""; renderWord(); }, 350); + } +} +function wordFound(w) { + state.found.add(w); + state.totalFound++; + state.score += 100 * w.length; + updateHud(); + renderList(); + celebrate(); + sndSuccess(); + mascotSay("Bravo ! " + (NAME_PHRASES[w] || (w + " !")) + " 🎉", "dance"); + + // Sticker tous les 3 mots + if (state.totalFound % 3 === 0) earnSticker(); + + // Niveau suivant tous les 3 mots + if (state.totalFound % 3 === 0) { + state.level++; + updateHud(); + confettiBurst(150); + } + + setTimeout(() => { state.current = ""; renderWord(); }, 700); + + // Toute la famille trouvée → grande fête puis on recommence + if (state.found.size >= state.targets.length) { + confettiBurst(200); + setTimeout(() => mascotSay("Tu connais toute la famille ! 🥳", "dance"), 800); + setTimeout(newBatch, 1600); + } +} + +/* ------------------------------------------------------------------ */ +/* Stickers */ +/* ------------------------------------------------------------------ */ +function earnSticker() { + if (state.stickers >= STICKERS.length) return; + state.stickers++; + updateHud(); + sndSticker(); + const emoji = STICKERS[state.stickers - 1]; + setTimeout(() => mascotSay("Nouvel autocollant ! " + emoji, "dance"), 900); + renderStickerGrid(); +} +function renderStickerGrid() { + const grid = $("sticker-grid"); + grid.innerHTML = ""; + STICKERS.forEach((emoji, i) => { + const d = document.createElement("div"); + const unlocked = i < state.stickers; + d.className = "sticker " + (unlocked ? "unlocked" : "locked"); + d.textContent = unlocked ? 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 && !state.hintShown) { + state.hintShown = true; + const remaining = state.targets.filter(w => !state.found.has(w)); + if (remaining.length) { + const w = rand(remaining); + const chip = elWordList.querySelector(`.chip[data-word="${w}"]`); + if (chip) chip.classList.add("hint"); + mascotSay("Essaie d'écrire " + w + " ! 💡", "happy"); + } + } + if (idle > 12) { + mascotSet("sleep"); + elSpeech.textContent = "Zzz… joue avec moi ! 😴"; + elSpeech.classList.add("show"); + } +} + +/* ------------------------------------------------------------------ */ +/* Mini-jeux (ballons / étoiles) — taper pour éclater */ +/* ------------------------------------------------------------------ */ +let nextMiniGame = 0; +function maybeStartMiniGame() { + if (state.miniGame || !state.running) return; + if (now() < nextMiniGame) return; + 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() + 30000; + if (earned > 0) { state.score += earned; updateHud(); mascotEncourage(); } +} +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 false; + 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 + 14) { + mg.items.splice(i, 1); + mg.earned += 20; + sndPop(); + spawnBubbles(x, y, 10); + confettiAt(x, y, 8); + return true; + } + } + return false; +} + +/* ------------------------------------------------------------------ */ +/* Canvas : fond, étoiles, bulles, confettis, arc-en-ciel */ +/* ------------------------------------------------------------------ */ +const cv = $("fx"), ctx2d = cv.getContext("2d"); +let W = 0, H = 0, DPR = 1; +let 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 < n; i++) { + bubbles.push({ + x: x + (Math.random() - 0.5) * 80, y, + r: 6 + Math.random() * 16, + vy: 0.6 + Math.random() * 1.8, + life: 1, drift: (Math.random() - 0.5) * 1.5, + col: rand(["#2ee6e6", "#6a5cff", "#7af5f5"]), + }); + } +} +function confettiBurst(n) { for (let i = 0; i < n; i++) confetti.push(mkConfetti(Math.random() * W, -20)); rainbow = 1; } +function confettiAt(x, y, n) { for (let i = 0; i < n; i++) confetti.push(mkConfetti(x, y)); } +function mkConfetti(x, y) { + return { + x, y, + vx: (Math.random() - 0.5) * 6, + vy: 2 + Math.random() * 4, + rot: Math.random() * 360, vr: (Math.random() - 0.5) * 20, + size: 8 + Math.random() * 10, + col: rand(["#ff6bcb", "#6a5cff", "#2ee6e6", "#ffd23f", "#3ddc84", "#ff8c42"]), + }; +} +function celebrate() { confettiBurst(90); spawnBubbles(W / 2, H / 2, 24); } + +function drawBackground() { + const g = ctx2d.createLinearGradient(0, 0, 0, H); + g.addColorStop(0, "#5b6cff"); + g.addColorStop(0.5, "#8a63ff"); + g.addColorStop(1, "#ff8fd0"); + ctx2d.fillStyle = g; + ctx2d.fillRect(0, 0, W, H); +} + +function loop(ts) { + if (!loop.last) loop.last = ts; + const dt = Math.min(0.05, (ts - loop.last) / 1000); + loop.last = ts; + + drawBackground(); + + // Étoiles scintillantes + for (const s of stars) { + s.phase += s.sp; + const lum = (Math.sin(s.phase) + 1) / 2; + ctx2d.globalAlpha = 0.2 + lum * 0.8; + ctx2d.fillStyle = s.col; + ctx2d.beginPath(); ctx2d.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx2d.fill(); + } + ctx2d.globalAlpha = 1; + + // Arc-en-ciel lors d'un succès + if (rainbow > 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; + } + + // Bulles + 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); + + // Confettis + 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); + + // Mini-jeu : ballons / étoiles + 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 (alphabétique, gros boutons) */ +/* ------------------------------------------------------------------ */ +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.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() { + // Hauteur réelle du clavier → positionnement de la liste de mots / mascotte + const h = elKeyboard.classList.contains("hidden") ? 0 : elKeyboard.offsetHeight; + document.documentElement.style.setProperty("--kb-h", h + "px"); +} + +/* ------------------------------------------------------------------ */ +/* Entrées globales */ +/* ------------------------------------------------------------------ */ +function now() { return performance.now(); } +function bump() { state.lastAction = now(); state.hintShown = false; clearHint(); } + +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); +}); + +// Tap dans la zone de jeu = éclater ballon/étoile pendant le mini-jeu +window.addEventListener("pointerdown", (e) => { + if (state.miniGame) hitMiniGame(e.clientX, e.clientY); +}); + +/* ------------------------------------------------------------------ */ +/* Démarrage / boucle de jeu */ +/* ------------------------------------------------------------------ */ +function startGame() { + $("start").classList.add("hidden"); + ["hud", "word-zone", "word-list", "keyboard", "mascot"].forEach(id => $(id).classList.remove("hidden")); + state.running = true; + state.lastAction = now(); + nextMiniGame = now() + 30000; + newBatch(); + updateHud(); + buildKeyboard(); + blinkLoop(); + try { ac().resume(); } catch (e) {} + mascotSay("Coucou ! Écris les prénoms de la famille ! 😊", "happy"); + setInterval(checkIdle, 1000); + setInterval(maybeStartMiniGame, 1000); +} + +/* ------------------------------------------------------------------ */ +/* Boutons UI */ +/* ------------------------------------------------------------------ */ +$("btn-play").addEventListener("click", startGame); +$("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"); }); +$("btn-close-stickers").addEventListener("click", () => $("sticker-drawer").classList.add("hidden")); + +/* ------------------------------------------------------------------ */ +/* Init */ +/* ------------------------------------------------------------------ */ +window.addEventListener("resize", () => { resize(); initStars(); }); +resize(); +initStars(); +requestAnimationFrame(loop); + +// PWA : service worker (installable, jouable hors-ligne) +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {})); +} diff --git a/icons/icon-192.png b/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..d22ea3f62014f143009ba1eddf10c4443896275f GIT binary patch literal 4840 zcmVP) zd2m$6nZWzaeM^G`LP#JXSwi=PkV0f3#AU!Xhc1kq*x26PzmilXcEwd>QI ztF~(YiYu7QUUS%BgdBro%n=L*1V};{gANW!UlB_e4w(>>Gu-ur%4 zUonl|yf@wN`}+I7e%<}J7k}nPjKSOw0*J#QMo>}KH&AwY;%3DtR1~fXN)LF`3r>m# z0E!Rzsw+T;TK*6dz#T5L6s_+XBT0L}LgFL>5qfAQ-q=%K<>~0a1Db z5;*`UKR^>VjuGH`08oBF)JA}l1cUMeqBjD6Jpd>_APFPzCkFuK2P9(zz7B(e@&jVP z9f%MJlpm0sCz!4Qfbs*9Gy)aJdDU%Den7HDKw~`s05F3$1AyKikhB+=$^bz50m&Ny z?KQywK$Xd+LV#5OvhV_s1Ou{WA_IWlACQd~2;`bz01%xA0BZnbiXN z93H^?dH?|M3o+;xcmM`^A*slJZdo%+9eF06gA#dGg+0;I}*2^a9`k09kth?yc7! zK@I>+$lEVJi)iq>9Y=z8Ef@v`m{3Go*MtXQ3_OfsS$-cxh2*al^g90j4G4h%@BHqi zz=lI_?+m`T;o;tX!v;A3Fm2v?={ZD_f7@AwXaXS+17U`k zzY{P5{v5RH0S3Up4G^nr!!QE=yR?7*r~zP`G60AeSP%pF#nzub@81_0TPOoL00VAV zjDl-Cw12-KFc%1*06oqrN%$k`BVMq%fl`t7XGm7@aX3^rwEf+ zF#rX>IW7pi1PGClPe}U_ zLOKJ77G$W=)k_12Ro>k_qUhY@fBW@M){XxIBYFo(FyW*sVvf=yB+2}dhS>!8zjl8* zt?%&1jfFP|+Vy}WH~?f4?EM9zW0k1B`G5AL&;P}Xh%#@C^yGgWQD|Q}a^X9|IaqK4 zkOL5mC)H|%lC#Jg08ieMaMRGBqn~!n%=4$+hp^7Uf1ihTT|DxqUw5N|5wLf$9ButX8WzJf^jcg|I;muSo@b#2T(Z zQS(&+ZMThO%d-z}|F&gzcv5UYFC-%rk2i(`!EJ-IJWarFRzX2Q`aRp{e~0_!CZ@9x z0uex=KqxsU@CLw>Uo_inZ*ujkRodWOP~yOI00Lvlami|AFa;-;++10)>{`>|6ST+A zIf)2(00N{2A^Z^t4GlHTh;46&)Qt?V9sm=gMS)@x0yp|I5hdj-a;pAvtvND0X?{>I zEI}Xy!gQf2xf;X;d0pM&#f#J0ZO8sjJAB|IAP@rKz#){Jt3@=(E1GR_H91{sm5$C2 zkO;6Q7=yV_sgcD>rwKP#ZdzW~bod07i%ST2Q8osA_(O}3kPu^7?EQPf{2ahPAP4X> zcGMd<6q`bKD88g(Rc>2HLlf;dniH`o_^A*ezPG`G94%4?cIJau^7Hdk+U$Qj1NRU4 zt;3K6`$-a|9+|Z_%t<=)AYC$He% zz!x@G6xTKLPlg$oTaUyi-~s#~X%T*ByL0Exzcw~Dwk}z+BopU67K_DbjqSM4$FqR% zz%Zzfs5Kml%g_)pfc^XTe`T}T`f$%xQt`~9yWd}J#Qsq-1d?DMlcxAgjmHRZIDPtb zE#Le1_4PUT?%n$(?oG_g%S&$SI}XV(=X(+iIe?Ebq&;^Kj?sD*f+of zOt+^{aVS2EZ1|HnH$Nl78XI-1c1Sfm?8ctcP_k)F-ktmPH)!{&oQMQ+fN92x^HS7E zYryMY+uGVXzxd*d3qtwn^i6B{b31y6P~+9kN4N%%o12?-x9|8_+Ho}}B7wa@r%3{V z)rfh2euJhf2wxV132 zaN)wY4Gj$~Lb))54}X4Oc+ia+FZT^)6yohHXEvJ+*0^p+hNlVS0N%!l6>b!tLnQhA ztMJ)so*kYxzcBa7Yqi#KMD|rIePM0>t$VdMXvf!_2n0ePFD`OUf(98Zcr!)@1_lN{ z{P4psgmOVz*(xQc{D-#hY8ymtw;p0A!#O!QiLK6)XK9BQoCpNAM)bl{x%i!ORaMoQ z2M-?f2<4*8O=~9GlCrH{p`R0jeMyvCJU`s^o!z0+7z49guS5V3Fh%d6Hz`om0xi+x zH!da#+5Nk+x$soLCSA3{xa5Bd_bXPI!WYciSiA#vTEaDS%;JirvaLtGAi^zgw49~|Rz z0RFR5p;i^7ZCv54SNgs|gQfferJJ8y+}vJ!opzkfF)UyJlQhh%SPe3VE6I-iC#?Lj zMQ;u9c2nBFbD^FtyA$`?+1c3%cU`Byp{-;b!vY2{NyEeuNPg?<>zit7YQ7W7MVS?z zv43xZh7r1b#p(Ho%c5m*GoAg9`e-W}$EYYc&W%9Ew}D2FP;`ox=-9v6Fe7AP!m=Mt zx2@aj-9%R1msgZ6z1UoJ9PY7u!ghuy!4nXZp+`zJUI)z2`Ps8)&rzXVm{zh2sD9Pj9rKy{v17e^qcnf_6I__IAeO@ z&sh2Xs>*=3Zo}M22p+kpG3FYF9iq-z~PJ_;2)Kc4p zJsQN5yDWaD!)3G6?r}NB1d`yf36Tb&=nQ-V_~@gL{^oQ#JyV+%oKjJgv2m5ZNoZ$- z%1A01zR+Ct6(qxMNP;ILDpiNnI=){ZH};ybVV!BgvP^@) zptHo=yYOo#Eg%FM;|^nT@N>?aH*elObLLFFP+p$tU-%;kKWn0`Za_&S8LnLa;~T9t z4Yb1!jv)a97-vrN!MIJa>`yf^-%{_LJeFvkgIi(KQR?E>DW= zyW0E6sIw5sEyRkvLm&>J=qw@+T17``S;>;KO-E$Eo3yOAMhuJ7BE6N!GbYi&C&Q*h zSC5?UKazV65S=5>-d~`+a^ut2?wqfueIICwY(pT8UV&yM$t(GNl9ZH`*xN%FYiQpK znj+gIcuuAPDU@=p{SV}>NRG7E_1H}M5Hpe?vh@H_G7o@zK(!MV=M0SsWlo}4rN|o2t z2X7=NC&#o5*IY(2M0QDVM5@7K1i*L}tw@b}RM*{W)`wc;)Hbrq0U`;6Kf}Yr!}I3N zD{F0S?Ld^-x^?SMw{6?@FSzF^DJ@y@*1`WivNo-91CkN48U^ALUrQAJ;wt`4k1QxZ z&-U%xtA!Wu;h@!O)uyCj+3gx3t0XvbA(`ZN?6ODN@SAT{S66>c;#}pGB~RDg{^p9T zl3>~v7%WjLITVqOKlX34+4?^DMoMy($vtNlHqJX>nh^Li=6?Q)H6^ge4h} zN{>J2wQt|P;|_b1xu z8J5j3Xhb%#@Mm<-pWOI@T-wUd<8~v|am7yLTpKoSC_LHt*>NNjWHAh?3ss@e`S=sx znwy*3Pn|kdE0ot{m3j93VF6piu-;wlc5@u>4{(!Vw*=SfUVocm|A@*)!5V@jjO0V0S=nad$?qKN z+{o;hMH#f~_N2In-01$5ZtV97j3&cPpGkfLEzuglKfk8+dW^=XM4<`zTORUTo%6$= zLV0c0#%B~G*(Ph^XyAw}bkJxtYGbm(LAySI77_0aio6~Zo@qp!$HehCc<|sU{-qZ} zd2Lq7$jh$T6Gw*&wcfZ#CK;|QU2?JI>r1rj6i5-}REU3i?Qw|9|2x%j;aTLvpRAbN zjJaX+qG-3lRO0*=5y|=!5)z`DxvP?1Cm_lYXe^T*W0|Kzx*2LBb8U_tJ9b_uKR1&6 zvMjydx|B|bYXH0Qh);&IV!G`QyJ^=cP$D`5c=grs0~~yLqT|8|CBOWuPKDW)8kJVF zGHq=!?Kbcx?*3uaez}9lyT#V6Usv!|qjZyDNz4U$vJOhE0)-|SiO>8jPFY!K)|rey zqA{gnJEbo3*tz$=YjM8_bLA-K-zU8Da;uFFMGE4c?1Xk`2!~RDbYXryz=!YK z(dI2ipN{vKHUP+RJ%H5e0|XXP4eJ4b>^#6GAHp9V*6RbX1@i{rD^jZu5LiSx8E)VE zk48V2Zy7)Jy<&@T+}{CUDpX>c-?x9qh+=c&|F7)j@kSt~IstTHIs_^mqu2)o<(~lI zZ^H2`*9l+?=0|~e<(^9UpF9=-e1W_9AJ@f~1OR{>LLdMLG@?p^0f02%0RUMe%I5$8 zK+*&7K@eC0AWN7Z1_gjX0yzL6YnWdU0|0>pJOChT1OtFX7?2|zcmP1wz;ggV){Na6 z5yBuqPIxpQLLflaJZS*n1pqlPs`iir0J3JH5D2^gAZ5m>Jmdg?teI><40r)Riu{AN zt6(NHAZMlufxrs@Qer|?_m%^I5dczPQZ}0_Pysgw416bOs}U=c2udxL@z04(8`CgVN_ z03!fcz;87su^s@70H6t0A5Q8?uy7jKWeW%v9E*E{!Z0Zi3^UpklpkE O0000WdBM;t`R^FrQ}(%d+*;xIhZ7RCjVA*eqgY;Jmy+IEV=)~ky*p+F9(?F} zs?&8tB{p^W>1!R8g-stFyK3KZHB;8aUp^*a1Md=4ZNp@oN&qUW{l5SnV(txRAfzXq(z*4)L z2JHJWxv7s_JquJq(wV_lqfL735XC%7bvt_%x87W@@jMPl5}|NMQ@3(#W%S9srPO6FwXFi6Ckx zxPt_ta+1UU#|v%MC?MA1kk)G39#-LuhEKmB0>}O0A?|0xdT&hDgOlnXy69|x32t77~=%xscHaZ zHOr_<0Vh>^0Io#ac@!XuX6_*XE>47gcC7$>t5|LZ>Y^6i61|7I%c@xYvbVhBKNVllws`2k33(oOqte2+i} zcRS=Nj{Wg(M!KK^gHbJ2x8cfcWK(mFrHqLEO%IYJ*$ zWAe}dL~xe$QXDcD<6V8ivx<=g0M4+>jKG8p0PRu*gb&b}6;mCa z0BYVCXkh@Tj42GCL0S-zKtPEeC4vIL;-k1ZIBc-iND1regEZkTri}fg3DERW#2_H6 zf>cWX+suBr=$(D}NfiJ-v)TVg(Db~tvjGwS41EtR?IAIzkf=SlZ|`r-F6D>T~ zD)(prHZU^q$G_<<{duKf$2nA^E+k;5hcJ2V*& zyZZ(xFq;}cARoOm&imj>YGI@2;rx~td5-@cf`;H}+r-4zJr^RoY%y%c29@y>D~9*c zT_g>VZiyajHX^6hZgR+-cHItUIGnE$zSBLEM|MRs`EJ;Ao1p<}o8|gFJ77u&dEh6p zmMQ@NDulGvBvMV-F5>4#AV9{mW>m=`OI{EbX$zNy)S0%hhc<}Q1)&FM!Th;$L?i7h z*k88jqLMhUvb0vjL=XEduIz{N_s!RrPyw(LX5~KRKx+9}E}e8v`_Oc8{vu7%coM8y z;d9+)9_O5g@40{)CpkF>TX_l!!1YX(Vkr_511ze60FfE&{F2IyAw|y)2FmzNKLjB7 zEpeI~g0%Owr{00w`tula_^1MGsbqmPVD6fbF9%pYWv)O_00wO_4Dq4H zd$>C93Ma+qZ|G|Um``PC2lfh3N(B0(>Qr^BI&`f9dhvov&$bNU`B-1q@Vx69{U)vW z$~+4kWNknxGuv_HEM#@FDW$a4<(`0MI4pG4#EGsH4?I!_Ck#9os5@o+qB56qsjJ=ys#WY)zh_oFCWG}GV0fGa`*AO>$Y@p$kgZmzJKQuKCN zS}EMZS^)l85LSwobSs;95aRieiUBaK_O7&>=n%q9&Y7uGw)~NVB1M}+eKm*lHU9(g z8C+bcO_%pI-Hszg{d*{QnD0u0t}1^ftUV8D4m?Gp*J}G#bKemP5w?e<-~9$FMMwH@ zNROoTwiXDS@N+^ysUA|s4tqQ4d?3DtC*T^%KXR6BRs31~{r^N6P6c)N?^})h40dTy ziH)w`$STRSs$CE6zvL>M_5o4`FiA&4@*5y;{*yVtxn2zQrRbNH-~Ls?;!GbR;gD06 z*fjJ=Di!8`1SS-&hM5_PI>&$f@PR~Qe(ZxSq>V}2Rm=R4g+ml0V0K+hOT-%M{N_O5 z76C7$cww;bV_!XKJu0_wW~1pP$$59tinN{PzyC=z;C?5kTr2Sff;ZRYjmgv1!QF#v zGm!*ybp|fpgONRdPfbsmz^EcBFm$7V(vGt8+e?JIm&^11@2I!~VlApLX!k3>mLR5- z(VFk~+vAw;JH&U~X1E9{ln;M2UZdNQAZ;PVeW6MLtG%c+^t=m?06Wi*Qk`cHb?;Qr zYskV>`_&EL@1}=Wvvy}DGGdCoc%`j{cG)P}u3nzr@9$K^i&7t(EjS}xaPO_&T#)g9LMzu^)9dpg;JFp-O~K^=`? z=(Fsg#+r`8U0_-kN5yA0Bc#F*g~=}EKBAvDzSAO?H9mWd#o>L4`&KlXj*l|yejaxqD2+}wr03GyCf?Wf;7f{y1}r~IEMBqm#Qs#~ z<;XUBCf@xa{Nu`3aX25c(n#A8f$HQGPf90e97`Jh6+YG7Ypt}j`NXIPPSb<@(%U$? zi7|_kRNmZ9f%O{JAbxxn$$>q&QGdKJct-UrSPl5{S}ClgE%0ThM!8PwLcf zYgouw8{k}wA6{cOQ|G{T_-$HHwhww@ObAO;-+q#P!_rdJN#85Ps44RsIe>Lw&DwG$ zp-{a`ZKK^*q;23A8z^Fdd~Q8JbQr~xAH9r>WIT4`%FBYh!u$_%0@jRa8Dl4{bMI0G zu!laruvkwUhCr`dnbaywy(b$wLDCeVd`Ksb^pdS4sI%Y;>`Lt89IsEUh$HILRtXa?Ime?QT^tSEkwJo#fT~(1|z> zu%|LlH%bpa`q9zR;T)zM5Y~!lZ=wLw7xcrl?=!yNk+QJEmRzTaa+9T)h2{!Nnb}Mg zcX><0Ne1_mJhxAlP56ocz0kcde4rt{ni3@~7Rlin<4nU4h>aou7~UxK4VF-0R35_{DN4TW>L{-S{8C-hk8?K>ptkMV=GcX)WO8=B>@r(TGjE~3Gj?f4b zFTbV)uvS`sjS0LQ@zUNNy!a80h>kC?*NVliUM5zis2he|1ZMY4kA{%HJ(TPcr?PWN#ZX)bzz1tiRe;Aw+{ zK_gv_?Z!r3slOT={rpETh3V8l8e8-X_46dKG_bHZ2pvD}dSy1tZ*)x|R-T$7BZL94P$ga>o{>qc zh<(+B(f&E$BDcf?9e@Z23)L}lqCNu`x?;;CzG32sD`RvW5*x0J1eP1C^JJ3jWMtRHuFtJu0M{H#VNsfvXDl8L*KG5ya$7C!c`fi9`#o^b2xpYK+c zPiER8^Gpis`1twx^C&;%FXgpRT!9hFeDI$s%_d!U95ygoC+)m{ zadmaI$M;~yl`}7!FtNYbM5!sOqp3i*7K!U7V_U4zL51J$dO-oX*6FwGs}XShTAkvy zzY3CKthp3mB)RojGW`(BDP`O|JV^ujW~G^g?XMnxRi3O(HaVJ^nbpQuc4GK960Fmc z;CWceTbQm*%yuMgPgFk6EFo++SWA1MFQZ;(%%vu{&Q1n|Ap)JP5#Z^C+x&(2_yslv z&!2Cl$iqnYI{~{h64%lvCnvS9d*6`mmVma`Qre~_f3kk0!e!!jnSE~^4cS79){jiw z*1eIcrmRcH%EmS|7>;msa4`{X%-$;UQNw870~nq4J$Z#nJ>xy}-{CK+i;IgqBYR)p zS_PO*XL?+R%J?>ODf}Nsu&pBcGG>|G$-G? zPbQ3$M^sc!mzN7ve8@E$?ZN{w}qX3Q~Qb zS0D1kH;18ZZ}Z{vqU;X-FvK>Wl~75{(IQqDH6Cr_=GI$NSBtt%);&(#bW&eWQ@6;u ztQ&108;;QJInFYF$}H^xCMcNBcUAY&rAt%90`$lvVaK2CvgBScX@Y(5U4P`yo7XZX z=Vo=(`~zDkktq!nAQWYhp?yCphJfR*U5g$EqN6gqmmcU`i_qO)_ina}?h}6*78b^* z{AAhF#ax{U#aa$9v<%02GS$Al`FS*(ypfvdMs7y$_NXWX==l9u?3K4~!elPMeTEf9 z%VW7O@xP8I)q8I|m`QS}+YP9+WwkMQeI%sw-4k+J|KN+hncm2ZXXw1wtdQ58hms^9 zG*bAww>D$ezT=&ODxrlOtoq>K2UYZLO<isE`eZXKDayNjn=dCx4(0b%*~cHehPR7bp#lt|3+q@J#sEsb9XtW@gf$WIUYRsfj2{ZjY)P$bqX?im=AZe-B~i*W=u7eR6&Tv-%MMhGm>;^7F_) z%*pr#a#Z|@0+R;oE2GF~x5E({hh~(+9Avd#iTx7Ky26_@deWMuA`vH%j4-Nk;y9BFI~^LMFPYaVZF zYy0{qN{n}J=U6R5FU!hcf;#47Hka#@!bbe?NIcYsB5`OKd4i+D-DYda-mkLB=9Vh` z>3GMaOZwr6ZC>*Z+h>6u-L$k`DE)U8+52gy1`DkUy%q~9m|m!!mXTEOZtqTvRgE2_ zJr~rsJH~Ehxbsl?8f@>gleONF($4MuR<_(7R+Lo8ZCku~vI4;& zb?0F9&-#Q*1(dH7#Z+K4hg^gSyZ-u%zMl1TUbv=GVg%cNn~+ry(yjD{qXG?K9Kr2KOo)3)g?$ zuKk(*`r*~q*HtTDjv=2!$z_ z^mn=Gdk4Ljg>PNA!|wE|o#ukoCxA=6U~6mc08ag`_4Ql`qUZb!wZ?Ry@!*aaa?fMi zH=gb`GqpHkio5! z3EK6oC#18x@x{PHaLI>8`toHRlrUj4?A`eA@bDgCad3snGh%|1Eg@mQ&$eeLpd(Vy zeeGUtB@T@~j3fdzpgxz4?@P5eNAheN8W{yWBC2}KFMV2*QD@B{sQ+a8IYtc?dP2QC zxD$g_KYw%Dhc7f0mH(mSa_bpVxI9rqh@7b0;}<{~-MZrr}n%6~B5_z=C~mMvR>sDpurt{n>1LIFOD z6z{mqw8f98IuKR8`!Za-)ayQSo{L-bME-E1>(*!R`gpK?Xy~v^(=%%r(QAJf9lfuX zLXS%Oi>%uvGfA{G=*j<_;;2{aMse+djP+%G_l|wxO#-^4h2qszT;O6vI5jXov#i$jEAXP~kZE zS?H~lRUO}Q!YiZ*=LeDPE@_{$b5VU~K0yUgo6_68%*V!H0`m@&Yd1D>@BBuzjPJWN zn|^+y`0*?I&6C?K$UqEwGw%FVPX}kC+c5JuBf;ZUlYp#9L9&LEm&a?(EAD;|E-FAH z>xL4p3eBoJwW$TiRl6EG>tck3uip+s6rAVumZ5p4<%XgLpEEVqq-tLg`sE}uhPm$Cl#TepCZ#+sc2xTXYrw>5Ois4+nAuFsgO zs?6Sv{Y5Uwby27&qj{4%U$q;1UZs8|w0{cZ!xGnm$Y8dBFbCOx>$jbv=hDz)=;>PN zYJaV}J%~{BJ>NwyMXhF9_xNl7+$pJ9RH-%7v?VoSjOFe0E&t#)-_ULhq z<)2PRHnr@qrFKo9tud0Y#9?s>?G+h7u4h{zqLs)C%Esm8wR`rpxdjeKng}u8H*al@ z9<+*7EUC+ebz)qP2Da*@JC?LGhbO7{PoSaH|1l>!@O5qWXhng&GaAA<+bMrm*CDa0pAxgcB@eq}Gh zX!yuT=6MB~%(?kw`>D8&E8~Y&O8e&D!OXB0%I{RBuwo^v-E;;Wak_-y0xu~>_B@;C zn@ZK*4SKBn>Czp{tHwWgRS@Vd>|=+ubrhq1F?9SJx@Kwd#Xq44$>2j5CtXn>A;hx1kVhV5OnFW^>hk*ee> zLPx7ARGjf`%n>$M=fl#fF(qj0sjSOn9sh1?bR(nY#OFsN<^HY&e7`x20=VS;EgbYO z>*mc>W)5cCemTR%@LTcb(F-!T&r^ zCCWY-7m^`WnhH10rS;_nb3@_Ww80|pKc#*lM|Y4w`K>#STPb+6-jXuu5`LYosvyuj z9FgbXY;3B|Auz1gXQF32mF1T1sS^>VOE_aMX;JCwzP&t_{U>VSccx3>!IGG5e(F?b zo#dMV(bmKGu3~Y1Si-nV2pW{|b(lqBG8tI*aA`0ebfORXX40-9>$7(D=0>P*m$8Mj zq337CGoOMPj=wfLL0-ra6yA=aPM4TGe*?2cIjNSraz$ojDyo3w^0P_*cM2lh%2ayB zYj?be*^VOXEr)zR$n$N$-lmyB}zztA;FK>Eh`&tXC7sA&L;l;{zE*|IkWfzwC#Q4&M$wqnyB5kjSWNvZ_X~;I?7lZ{k<86 z5KJv!uzNf6hT}Vdwbvj2XJy-isD@R8p?dA+qDH_>ZBs0H-h1}MYZy27lr*>1?1fnP z$ly|TN@P`#7gZ9yl~B4?-U{(%@ZMrpd;rEiqn=p;c6ifSe*ISnj49MDLxDg8t^u{v zUg+J$yZeg3qa{bAv5h;)n!tk752&AbtEwQL{knznl5`j6UoGfCOF9FG?nPhYUrLzt zqW#NIS^Ko(LEZ#CRJeY4s?^}*efH+waK-_R(@K9VU|~a$-5kVd8=P@tSU?X=$uwyH z+&(lUC=w?&-Pq6Riu_oxkRT;a*WBOOv1%BCJ{~FiZdCAb+raTN*$_LZ((4l9jd1G!r-fn&bJ zRKV$`%U-WknlD)}kIU73K?ORot%sD9=8NiT<=I3jfm|->wF71LBexi6&8K47%Lk%2 zH7L&n-2KxhG~;`|3?6{eSzv8m+eoEJTlG zjGb|6J*-up>x*t7eM5w$T1kJ|j8x~3IwAik86E*2w3+2F4Sah(nTCu#-h}b(s=X>B z;PkQu@!eyy7F~;A!9{dpkOtxo&f_{)=UZ)`-4(wFtW}SZ8|+gM(8ry4%p>3=tSo+6 zcyk@zYX~?{GjjJ8o>`+vLEKe^BgOq&To?J1VVSPdPYSk)!`s5JXYBaJ^UvXT;V&~&kM2)Y)6t*v)=2TwGpVLN0 zMxKgJL#v-ztr)ys+MrCMsKD9&9KGdna`~f?^OXTapP>FJSDu9tP@Tcap|fA))FlW- z#dE`c(=yOVK3n#PRt~wsOr2WMXD-DvD(>xD^qbh&UK6@yhMlGQnw{vW5LZQze~B8r zxyr+vW8>qcMWS3DtuiQmJk_jj9Q{3VbASG=gn79c-R#D0jZn^g0|wgb#Q<|(mU3dh z9!B?LzruS?nU_2Leq$}oW%w-|Q31=)9Fu0C=!Hf@o)1$vo9ccRxxoF;x1c;#&t)|= zx7qf`*IZ?+?@wlL_*}cxI}{PL3v(vc!?>Z&!HS; zaDrioyGNt-1^rDTIo+)B$}xhKd+7dLqmk^Kv=r1t@TZR-KN>$qSv3;heWa$`E0T4QE6Ml zm$tpuqw9ZZwqEZ8Pm6=v%jg!-*0B1jhr{n9eVKH)Wc{D@+Xni=h~blLbK6bw%6RS9 z&B>4OsJBc`xwQV>Z%j^%C-S~ap^=v2M-QyJ@MaZXqfEDiN9MZGC|wbsTdvi;P& z&Tn@hDoT*(lA@>g$y~%?Ce?cTSZ$FjzCm}J7urKtnrPA$V;G$D%%NNwwn-B;-lb1| zzLD7fej+wC?iHyUCuZv{Www2%dKU8t)N-wkZJ2vJ;af}0zmb4U8~DY_BCT^HS6A5X zXGcV#*$J18N$MTvqrwMV8u zkbkzdeO`h!UCl~c>iY~l@?|c|dK;I6wno*wO%xR5!&ewgUp)=##7OgcBCvX5mnMw_ zTE#uySostBnP5_vVcm0f?QIIv(R}9IFkJ`rh0GaA6?ZMEv+q1s3F>pqP7&Q&IT(_1SuFudyWKE@7#eA!Qcw=|F!1Yck0NnzlQ&K7=UO7D_*v zz?RD${(UvK1oa1_jTRXRn!2VQ9v*37d}+}AeTVj1@HKT~?vQ-_(C;y5&1en_f3%+M zWq+b?UX8fO(CE3U!NHL z)>K-MYIo;vS3_iG|DIHa`tEeB!GvmsJEw+Sr+Wtm7A|qD*RR{$*uOK*6@Bf5*!H)| z!wZ!EvWZyUw0COv=Y?Y1dDvC*CS$q#O-D}Eak-vq!btz99muXp)}U!CTj`gRy5LRe znP6HUKvacklRHc%5*4>^r425YlIGg-YL#ivQT6|gp5{d2QxFqMHO_Jxj-Br8Gs-Ln zGLcu}(=?U$V$|T@<5@^TwG8da$f59;;E0wS9K< z(WFUB(c7H}OxOyWp1x((lbL3C?Zh0S`T>o{1neT4XA|N1^^b`Y@XP%wJtLqX1+i_w z5Gp$L7|T@n*}itqrGMQQS#MRnnnyOuGk(NO{roPnnJtL^f!SehzsozSB8lcfNm2W4 zj}BNKj4E#%9h<{QKr9S;>vH!t(Ra2geUE%+ga3!?qSqHZ`Qf!6Q}HA5_;4Gn;GvGd z0<31_PQ1);#l5ReIi#Ae`j=0My5x@<;13hl?Cfm2Zztr=lv+rw=FKn<0}6)>>#uP?yz!ucVJ`&V7B4;ltB`|Jgib!J~Y8`*T zJGxtD+&}S(h!L>tp%2gDSK1%H(1{qQxqWOF!+^Qu-<2+p_Vqckyk9tSSTJo0=PZO} z_FpjzFdg}&4}~E^_!z^du-GqAqSStKqXQ{0GU*f`F(MW@>E3-X?CQBH`H=Ya;Tz@5 zz@yG@#*bq{?=0Z`v6M&VyEGNA(VCZ)3-9&3{(c$sfHj`;Ed1L) zekArDu2Hc3ATzhlP&|NKbI+cx2@`rGZR>uLX{CE{(Ne{iu|4hus**yI;-lfR!jh78 z=Stf;8HY}x$E2YljWVNP&59@^$CrtZuayKMX|_IHAst*4?c?2i$R|13eLq=ko&%j> zLf|^Nk92PeV}E5xjwN|w!lIXRKLkHmYr-^5oPAeJUgG0pi-0H~oV1kWOSu`cef*oW zEcq&;QC~-rqPU{Vc#X!WOzmsgUoFh<)COv9HHdZ)E*KGB9@t=aH|BJ`wO8Op4>ASx zWaJ6ftE0jrG|s z^HGbrdT@;ky*lP>(ds!vzRAm#W1~-u(j|9^o1*q6F64F%=2njoE{Hs(9=&3RdWxh6 zPZQzgOG!zI`2N;k)uYP^4YeV&3@b^h(AG3Curqm~^k9v!?Mrxr-cE=K;_uqRw(F(3 z6~Ds$ATJ*dT>RUUUxiRByJ>1CFI?S%wNnqT74m^h_=63;$EhPN%O4{a#ICWCzW1vl zm&wG`=LR7i2@eUT0?|80()q7c&J@oqpmm%{nfC9tG#d<}lN3)TWWUe7YLpf`!IqxQ zwR%wQna@XaZ{18CaG~$JG;WWqyhB_Of zJeR9xR&cJ)ZS&w8I+Y+HJi5(axJ3@pChe#~K`kkwvJRX%x?FnQ!>%RD|Mhcpgw*L( zuW`2M+v-eQ;yHu3z+E%j%U9Fp2sDE=7vQ=-H?$lL?mXyfSbysf7dv*lYW=40P{h$_ z9cYYi;A;OIbOi@7vv(PM^EZCHyRqF>bS|X4k-(zr0pQp^E|WDUA|`r`0bY2${1FM8;p1gQm@~+*2fD;l<_}`%L>AfToQLP z*G;G2r%9ATRH#5ZNeSOs*W|nnoY-)vW&zN zK>kzRajnQKb50$<>K*8jZ2_T+TbLfX6ECUg~lx`%JVtneYhZ(aZ7p_S;kVnI2oF$;MV zR!7l#<0sxk$=WFu&RVtkL6^RT<7F3x#iz=H-8#7ye`|u00F+Li*JZSo=zLFnZf*s- z%)Np&f`(i<_^en<*NbHjSI|RT%)f1% znTnPfUzaJHjs#$Gx9%YwEuFE%*F4zTP_n`ZT$voei{g_kYzGLeHw25RXb$j$WSI8< zL(!oU=FWaZ6ixb4fCz}Nfe^|JYH;*D69CCU|2rhc?sqs1(4;d5pl{0xf>>a6w-hqk#g|jwu^PTGjUk!(LLlLu+L5a zDG0sYDA?wLwJ4Ck8=>}i1FAO zOzWOt2jK19L&Pa3teU`27^*=G<9~qI!^yEhke85!V-)FM#o_-;jBwQd0BolXAkF{+ z5gE;4TP4I~9ezUeA+QLAL;%Tgif;6KZWaJMyZ?Xi399eA|HBOz*)GwbGjH$(RU8a-_wvgIW5X*{=w_QvTzEK zUvZH`_Wv_VCqpc$c!UE}AOdAMr%~?z!TxPMtiX`ZIsh@;^-H&rtLX!GhcoQq`*%ZW z3s_~rSOosgg+1A?@~uR1%APZBhlh% zKlzJVE&?!m-9U1-TmrLI1l4N0JD(48qSBpS;C1Y`{!d4NE7vm;(iD_M=L7IV8|Ok} z4o(O`aVcTh6hcv_yO}ZojQY_{)t6ML!D21|9FJ~(4U_=3Iv0=7A_VqmcW!A?W4X@K u0)f4>?9h`OU?8RO9vu)km^&5sn504U)O+n!HF$X9;Hs96=I4vn&;AcdJ6gK{ literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b8f99f0 --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + + + ✨ Lettres Magiques ✨ + + + + + + + + + +
+
+

✨ Lettres Magiques ✨

+

Écris les prénoms de la famille !

+ +

🔊 Le son aide les petits — laisse-le activé.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..009e6b3 --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1,15 @@ +{ + "name": "Lettres Magiques", + "short_name": "Lettres", + "description": "Un jeu pour apprendre les lettres en s'amusant.", + "start_url": ".", + "scope": "./", + "display": "standalone", + "orientation": "portrait", + "background_color": "#5b6cff", + "theme_color": "#6a5cff", + "icons": [ + { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, + { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } + ] +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..7dbe2bb --- /dev/null +++ b/style.css @@ -0,0 +1,304 @@ +:root { + --pink: #ff6bcb; + --purple: #6a5cff; + --cyan: #2ee6e6; + --yellow: #ffd23f; + --green: #3ddc84; + --ink: #2a2350; + --radius: 22px; + --safe-b: env(safe-area-inset-bottom, 0px); +} + +* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } + +html, body { + margin: 0; + height: 100%; + overflow: hidden; + font-family: "Baloo 2", "Comic Sans MS", "Segoe UI", system-ui, sans-serif; + color: var(--ink); + background: #1b1640; + touch-action: manipulation; + user-select: none; + -webkit-user-select: none; +} + +#fx { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + z-index: 0; + display: block; +} + +.hidden { display: none !important; } + +/* ---------- Écran d'accueil ---------- */ +.screen { + position: fixed; + inset: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.start-card { + background: rgba(255,255,255,0.92); + border-radius: var(--radius); + padding: 32px 28px 28px; + text-align: center; + box-shadow: 0 18px 60px rgba(0,0,0,0.35); + max-width: 420px; + width: 100%; + animation: pop 0.5s ease; +} +.title { + font-size: clamp(28px, 8vw, 44px); + margin: 0 0 8px; + background: linear-gradient(90deg, var(--pink), var(--purple), var(--cyan)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: 800; +} +.subtitle { font-size: clamp(15px, 4.5vw, 20px); margin: 0 0 22px; color: #5b5480; } +.hint-line { font-size: 13px; color: #8a84a8; margin: 16px 0 0; } + +.big-btn { + font: inherit; + font-weight: 800; + font-size: clamp(20px, 6vw, 26px); + color: #fff; + border: none; + border-radius: 999px; + padding: 16px 38px; + cursor: pointer; + background: linear-gradient(135deg, var(--pink), var(--purple)); + box-shadow: 0 10px 0 rgba(0,0,0,0.18), 0 14px 24px rgba(106,92,255,0.45); + transition: transform .08s ease, box-shadow .08s ease; +} +.big-btn:active { transform: translateY(6px); box-shadow: 0 4px 0 rgba(0,0,0,0.18); } +.big-btn.small { font-size: 18px; padding: 12px 26px; } + +/* ---------- HUD ---------- */ +.hud { + position: fixed; + top: 0; left: 0; right: 0; + z-index: 20; + display: flex; + justify-content: space-between; + align-items: center; + padding: max(10px, env(safe-area-inset-top)) 12px 10px; + gap: 8px; + pointer-events: none; +} +.hud-left, .hud-right { display: flex; gap: 8px; pointer-events: auto; } +.badge { + background: rgba(255,255,255,0.92); + border-radius: 999px; + padding: 7px 14px; + font-weight: 800; + font-size: clamp(13px, 3.6vw, 17px); + box-shadow: 0 6px 16px rgba(0,0,0,0.2); + white-space: nowrap; +} +.icon-btn { + background: rgba(255,255,255,0.92); + border: none; + border-radius: 50%; + width: 44px; height: 44px; + font-size: 22px; + cursor: pointer; + box-shadow: 0 6px 16px rgba(0,0,0,0.2); + display: grid; place-items: center; +} +.icon-btn:active { transform: scale(0.92); } + +/* ---------- Mot en cours ---------- */ +.word-zone { + position: fixed; + top: 16vh; + left: 0; right: 0; + z-index: 10; + display: flex; + justify-content: center; + align-items: center; + gap: clamp(6px, 2vw, 14px); + min-height: 22vh; + flex-wrap: wrap; + padding: 0 12px; +} +.tile { + font-weight: 900; + font-size: clamp(46px, 16vw, 110px); + line-height: 1; + color: #fff; + text-shadow: 0 6px 0 rgba(0,0,0,0.18); + animation: bounceIn .45s cubic-bezier(.2,1.4,.5,1); + filter: drop-shadow(0 8px 14px rgba(0,0,0,0.25)); +} +.tile.pop { animation: tilePop .4s ease; } + +/* ---------- Liste de mots ---------- */ +.word-list { + position: fixed; + z-index: 10; + left: 0; right: 0; + bottom: calc(var(--kb-h, 230px) + 10px + var(--safe-b)); + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + padding: 0 12px; +} +.chip { + background: rgba(255,255,255,0.85); + border-radius: 999px; + padding: 6px 14px; + font-weight: 800; + font-size: clamp(13px, 3.8vw, 18px); + letter-spacing: 2px; + box-shadow: 0 4px 10px rgba(0,0,0,0.15); + transition: transform .2s ease, background .2s ease, opacity .2s ease; +} +.chip.found { background: var(--green); color: #fff; opacity: .6; text-decoration: line-through; } +.chip.hint { animation: chipHint .6s ease infinite; background: var(--yellow); } + +/* ---------- Clavier tactile ---------- */ +.keyboard { + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 15; + padding: 8px 6px calc(8px + var(--safe-b)); + background: linear-gradient(180deg, rgba(27,22,64,0) 0%, rgba(27,22,64,0.55) 30%); + display: flex; + flex-direction: column; + gap: 6px; +} +.kb-row { display: flex; gap: 6px; justify-content: center; } +.key { + flex: 1 1 0; + max-width: 64px; + aspect-ratio: 1 / 1.15; + border: none; + border-radius: 14px; + font: inherit; + font-weight: 900; + font-size: clamp(16px, 4.6vw, 24px); + color: #fff; + cursor: pointer; + box-shadow: 0 5px 0 rgba(0,0,0,0.22); + transition: transform .06s ease, box-shadow .06s ease, filter .1s ease; + display: grid; place-items: center; +} +.key:active, .key.down { transform: translateY(4px); box-shadow: 0 1px 0 rgba(0,0,0,0.22); filter: brightness(1.15); } +.key.wide { flex: 1.6 1 0; max-width: 110px; aspect-ratio: auto; padding: 0 8px; font-size: 18px; } +.key.erase { background: linear-gradient(135deg, #ff7a7a, #ff5c8a); } + +/* ---------- Mascotte ---------- */ +.mascot { + position: fixed; + right: max(10px, env(safe-area-inset-right)); + bottom: calc(var(--kb-h, 230px) + 8px + var(--safe-b)); + z-index: 16; + width: 116px; height: 116px; + pointer-events: none; +} +.blob { + position: absolute; + inset: 0; + border-radius: 48% 52% 47% 53% / 55% 50% 50% 45%; + background: radial-gradient(circle at 35% 30%, #7af5f5, var(--cyan) 60%, #1bb8c9); + box-shadow: 0 12px 24px rgba(0,0,0,0.3), inset 0 -10px 16px rgba(0,0,0,0.12); + animation: blobFloat 3s ease-in-out infinite; +} +.blob .eye { + position: absolute; top: 34%; + width: 26px; height: 26px; + background: #fff; border-radius: 50%; + display: grid; place-items: center; + box-shadow: inset 0 -2px 3px rgba(0,0,0,0.1); +} +.blob .eye.left { left: 24%; } +.blob .eye.right { right: 24%; } +.blob .eye span { width: 11px; height: 11px; background: var(--ink); border-radius: 50%; transition: transform .2s ease; } +.blob.blink .eye { height: 4px; } +.blob.blink .eye span { opacity: 0; } +.blob .mouth { + position: absolute; left: 50%; bottom: 24%; + width: 34px; height: 18px; + transform: translateX(-50%); + border-bottom: 5px solid var(--ink); + border-radius: 0 0 40px 40px; + transition: all .2s ease; +} +.blob.happy .mouth { height: 26px; border-bottom-width: 6px; } +.blob.sad .mouth { border-bottom: none; border-top: 5px solid var(--ink); border-radius: 40px 40px 0 0; bottom: 20%; } +.blob.sleep .mouth { width: 16px; height: 10px; } +.blob.dance { animation: blobDance .5s ease-in-out infinite; } +.speech { + position: absolute; + right: 60%; + bottom: 70%; + min-width: 130px; + max-width: 60vw; + background: #fff; + border-radius: 16px; + padding: 10px 14px; + font-weight: 700; + font-size: 15px; + box-shadow: 0 8px 20px rgba(0,0,0,0.25); + opacity: 0; + transform: translateY(8px) scale(.9); + transition: opacity .2s ease, transform .2s ease; + pointer-events: none; +} +.speech.show { opacity: 1; transform: translateY(0) scale(1); } +.speech::after { + content: ""; position: absolute; right: 18px; bottom: -9px; + border: 10px solid transparent; border-top-color: #fff; border-bottom: 0; +} + +/* ---------- Mini-jeu ---------- */ +.minigame { position: fixed; inset: 0; z-index: 18; pointer-events: none; } +.mg-banner { + position: absolute; top: 12vh; left: 50%; transform: translateX(-50%); + background: rgba(255,255,255,0.94); + border-radius: 999px; + padding: 10px 22px; + font-weight: 800; + font-size: clamp(16px, 5vw, 22px); + box-shadow: 0 8px 20px rgba(0,0,0,0.25); + white-space: nowrap; +} + +/* ---------- Tiroir autocollants ---------- */ +.drawer { position: fixed; inset: 0; z-index: 40; display: grid; place-items: center; background: rgba(20,16,50,0.6); padding: 20px; } +.drawer-card { + background: #fff; border-radius: var(--radius); padding: 22px; + max-width: 460px; width: 100%; text-align: center; + box-shadow: 0 18px 60px rgba(0,0,0,0.4); +} +.drawer-card h2 { margin: 0 0 16px; } +.sticker-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 18px; } +.sticker { + aspect-ratio: 1; border-radius: 16px; display: grid; place-items: center; + font-size: 28px; background: #eee9ff; +} +.sticker.locked { filter: grayscale(1); opacity: .35; } +.sticker.unlocked { background: linear-gradient(135deg, var(--yellow), var(--pink)); animation: pop .4s ease; } + +/* ---------- Animations ---------- */ +@keyframes pop { from { transform: scale(.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } +@keyframes bounceIn { 0% { transform: scale(0) rotate(-20deg); opacity: 0; } 60% { transform: scale(1.25) rotate(6deg); } 100% { transform: scale(1) rotate(0); opacity: 1; } } +@keyframes tilePop { 0%,100% { transform: scale(1); } 50% { transform: scale(1.3); } } +@keyframes blobFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } +@keyframes blobDance { 0%,100% { transform: translateY(0) rotate(-8deg); } 50% { transform: translateY(-14px) rotate(8deg); } } +@keyframes chipHint { 0%,100% { transform: scale(1); } 50% { transform: scale(1.12); } } + +@media (min-width: 820px) { + .key { max-width: 56px; } + .speech { font-size: 16px; } +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..da07562 --- /dev/null +++ b/sw.js @@ -0,0 +1,36 @@ +/* Service worker — network-first (frais en ligne, cache en secours hors-ligne) */ +const CACHE = "lettres-magiques-v2"; +const ASSETS = [ + ".", + "index.html", + "style.css", + "game.js", + "manifest.webmanifest", + "icons/icon-192.png", + "icons/icon-512.png", +]; + +self.addEventListener("install", (e) => { + e.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting())); +}); + +self.addEventListener("activate", (e) => { + e.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener("fetch", (e) => { + if (e.request.method !== "GET") return; + // Network-first : on tente le réseau, on met à jour le cache, et on retombe + // sur le cache si on est hors-ligne. Garantit des mises à jour immédiates. + e.respondWith( + fetch(e.request).then((resp) => { + const copy = resp.clone(); + caches.open(CACHE).then((c) => c.put(e.request, copy)).catch(() => {}); + return resp; + }).catch(() => caches.match(e.request)) + ); +});