From f99eef4fccdd286db93671859207cec1c34c5d07 Mon Sep 17 00:00:00 2001 From: Poulpe Date: Thu, 18 Jun 2026 05:30:56 +0000 Subject: [PATCH] =?UTF-8?q?v3=20=E2=80=94=20ballons=20volants=20d'ambiance?= =?UTF-8?q?=20+=20bouton=20=F0=9F=8E=88=20HUD=20(lance=20le=20jeu=20des=20?= =?UTF-8?q?ballons),=20superset=20v2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game.js | 62 +++++++++++++++++++++++++++++++++++------------------- index.html | 1 + style.css | 9 ++++---- sw.js | 2 +- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/game.js b/game.js index 5933ff7..abcf00f 100644 --- a/game.js +++ b/game.js @@ -143,7 +143,10 @@ function pickVoice() { 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(); + const clean = String(text) + .replace(/[^\p{L}\p{N} !?.,']/gu, "") + .replace(/[A-Z]{2,}/g, (m) => m.toLowerCase()) // évite que la voix annonce « majuscule » + .trim(); if (!clean) return; try { if (interrupt) speechSynthesis.cancel(); @@ -287,9 +290,16 @@ function recomputeFocus(announce = false) { state.focus = f; renderClue(); applyGuide(); - if (announce && changed && f) { - setTimeout(() => mascotSay("Écris : " + f + " ! ✏️", "happy"), 250); - } + if (announce && changed && f) setTimeout(announceWord, 300); +} +// Énonce et affiche le mot à écrire (toutes catégories) + surligne sa pastille +function announceWord() { + if (!state.focus) return; + state.lastPrompt = now(); + clearHint(); + const chip = elWordList.querySelector(`.chip[data-word="${state.focus}"]`); + if (chip) chip.classList.add("hint"); + mascotSay("Écris : " + state.focus + " ✏️", "happy"); } function applyGuide() { elKeyboard.querySelectorAll(".key.guide").forEach(k => k.classList.remove("guide")); @@ -304,7 +314,7 @@ function applyGuide() { /* Saisie */ /* ================================================================== */ function eraseLetter() { state.current = ""; renderWord(); bump(); recomputeFocus(); } -function bump() { state.lastAction = now(); state.hintShown = false; clearHint(); } +function bump() { state.lastAction = now(); state.lastPrompt = now(); state.hintShown = false; clearHint(); } function inputLetter(ch) { ch = ch.toUpperCase(); if (!/^[A-Z]$/.test(ch)) return; @@ -313,7 +323,7 @@ function inputLetter(ch) { state.current += ch; renderWord(true); sndType(); - if (data.guided) say(ch.toLowerCase(), { interrupt: true }); // lit la lettre (minuscule = pas de "majuscule") + if (data.guided) say(ch.toLowerCase(), { interrupt: true }); // lit la lettre (minuscule) spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.32, 6); checkWord(); recomputeFocus(); @@ -375,18 +385,8 @@ function clearHint() { document.querySelectorAll(".chip.hint").forEach(c => c.cl 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"); - } - } - } + // Si rien n'est tapé, on rappelle (et on redit) le mot à écrire, régulièrement + if (idle > 4 && now() - (state.lastPrompt || 0) > 6000) announceWord(); } /* ================================================================== */ @@ -394,8 +394,8 @@ function checkIdle() { /* ================================================================== */ let nextMiniGame = 0; function maybeStartMiniGame() { if (!state.miniGame && state.running && now() >= nextMiniGame) startMiniGame(); } -function startMiniGame() { - const kind = Math.random() < 0.5 ? "ballons" : "etoiles"; +function startMiniGame(kind) { + kind = 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"); @@ -435,7 +435,7 @@ function hitMiniGame(x, y) { /* Canvas FX */ /* ================================================================== */ const cv = $("fx"), ctx2d = cv.getContext("2d"); -let W = 0, H = 0, DPR = 1, stars = [], bubbles = [], confetti = [], rainbow = 0; +let W = 0, H = 0, DPR = 1, stars = [], bubbles = [], confetti = [], rainbow = 0, ambient = []; function resize() { DPR = Math.min(window.devicePixelRatio || 1, 2); W = window.innerWidth; H = window.innerHeight; @@ -475,6 +475,23 @@ function loop(ts) { 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); + // Ballons volants d'ambiance (décoratifs) + loop.amb = (loop.amb || 0) + dt; + if (loop.amb > 2.4 && ambient.length < 6) { + loop.amb = 0; + ambient.push({ x: 40 + Math.random() * (W - 80), y: H + 50, vy: 16 + Math.random() * 16, hue: Math.floor(Math.random() * 360), sway: Math.random() * Math.PI * 2 }); + } + for (const b of ambient) { + b.y -= b.vy * dt; b.sway += dt; + const x = b.x + Math.sin(b.sway) * 14; + ctx2d.globalAlpha = 0.85; + ctx2d.fillStyle = `hsl(${b.hue} 80% 62%)`; + ctx2d.beginPath(); ctx2d.ellipse(x, b.y, 16, 20, 0, 0, Math.PI * 2); ctx2d.fill(); + ctx2d.strokeStyle = "rgba(255,255,255,0.45)"; ctx2d.lineWidth = 1; + ctx2d.beginPath(); ctx2d.moveTo(x, b.y + 20); ctx2d.lineTo(x, b.y + 40); ctx2d.stroke(); + ctx2d.globalAlpha = 1; + } + ambient = ambient.filter(b => b.y > -70); updateMiniGame(dt); if (state.miniGame) { for (const it of state.miniGame.items) { @@ -557,7 +574,7 @@ function startCategory(cat) { blinkLoop(); try { ac().resume(); } catch (e) {} mascotSay(greeting(), "happy"); - recomputeFocus(true); + if (cat.kind === "imagier") recomputeFocus(true); } /* ================================================================== */ @@ -662,6 +679,7 @@ $("btn-sound").addEventListener("click", () => { if (!state.soundOn && "speechSynthesis" in window) speechSynthesis.cancel(); }); $("btn-stickers").addEventListener("click", () => { renderStickerGrid(); $("sticker-drawer").classList.remove("hidden"); }); +$("btn-mini").addEventListener("click", () => { if (state.running && !state.miniGame) startMiniGame("ballons"); }); 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"); diff --git a/index.html b/index.html index 8a82cbb..2cee539 100644 --- a/index.html +++ b/index.html @@ -41,6 +41,7 @@
🏅 0/15
+
diff --git a/style.css b/style.css index 0bbf79c..5b882d1 100644 --- a/style.css +++ b/style.css @@ -164,7 +164,7 @@ html, body { 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); } +.chip.hint { background: var(--yellow); box-shadow: 0 0 0 3px rgba(255,210,63,.55); } /* ---------- Clavier tactile ---------- */ .keyboard { @@ -362,13 +362,12 @@ html, body { animation: bounceIn .4s ease; } -/* ---------- Lettre guidée ---------- */ +/* ---------- Lettre guidée (surbrillance fixe, sans clignotement) ---------- */ .key.guide { outline: 4px solid #fff; outline-offset: -2px; - transform: scale(1.08); - filter: brightness(1.25); - box-shadow: 0 0 0 4px rgba(106,92,255,.45), 0 8px 18px rgba(0,0,0,.25); + filter: brightness(1.18); + box-shadow: 0 0 0 6px rgba(255,255,255,0.35), 0 5px 0 rgba(0,0,0,0.22); z-index: 1; } diff --git a/sw.js b/sw.js index 10e00a5..9a14183 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ /* Service worker — network-first (frais en ligne, cache en secours hors-ligne) */ -const CACHE = "lettres-magiques-v4"; +const CACHE = "lettres-magiques-v5"; const ASSETS = [ ".", "index.html",