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);
// 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) {
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");
if (cat.kind === "imagier") 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"); });
$("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");
});
/* ================================================================== */
/* Init */
/* ================================================================== */
window.addEventListener("resize", () => { resize(); initStars(); });
resize(); initStars(); requestAnimationFrame(loop);
if ("serviceWorker" in navigator) window.addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));