v2 — catégories (imagier), espace parents protégé, perso prénom enfant, mode guidé
This commit is contained in:
747
game.js
747
game.js
@@ -1,80 +1,125 @@
|
|||||||
/* ✨ Lettres Magiques — jeu web pour enfants
|
/* ✨ Lettres Magiques — jeu web pour enfants
|
||||||
Réécriture HTML5 du jeu pygame "Super Lettres Magiques".
|
- 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). */
|
Fonctionne au clavier (ordi) et au toucher (téléphone/tablette). */
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Données du jeu */
|
/* Données par défaut (catégories + mots) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* Pour un mot "names" : clue = phrase magique. Pour "imagier" : emoji. */
|
||||||
// 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 DEFAULTS = {
|
||||||
const NAMES = ["PEPE", "GEGE", "NICO", "JULIE", "LILWENN", "COME", "GWEN", "ISABELLE", "BAPTISTE", "TRUFFE"];
|
childName: "",
|
||||||
const NAME_PHRASES = {
|
guided: true,
|
||||||
PEPE: "Pépé, la reine des bisous magiques !",
|
categories: [
|
||||||
GEGE: "Gégé, le champion des chatouilles !",
|
{
|
||||||
NICO: "Nico, le super héros rigolo !",
|
id: "famille", label: "La famille", emoji: "👨👩👧👦", kind: "names", show: true,
|
||||||
JULIE: "Julie, la fée des câlins tout doux !",
|
words: [
|
||||||
LILWENN: "Lilwenn, la princesse des étoiles filantes !",
|
{ w: "PEPE", clue: "Pépé, la reine des bisous magiques !" },
|
||||||
COME: "Côme, le pirate des doudous perdus !",
|
{ w: "GEGE", clue: "Gégé, le champion des chatouilles !" },
|
||||||
GWEN: "Gwen, la magicienne des sourires !",
|
{ w: "NICO", clue: "Nico, le super héros rigolo !" },
|
||||||
ISABELLE: "Isabelle, la reine des gâteaux rigolos !",
|
{ w: "JULIE", clue: "Julie, la fée des câlins tout doux !" },
|
||||||
BAPTISTE: "Baptiste, le roi des voitures qui font vroum !",
|
{ w: "LILWENN", clue: "Lilwenn, la princesse des étoiles filantes !" },
|
||||||
TRUFFE: "Truffe, le chien qui fait des bisous baveux !",
|
{ 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 WORD_POOL = NAMES;
|
|
||||||
const BATCH_SIZE = NAMES.length; // on affiche toute la famille
|
const STORE_KEY = "lm_data_v1";
|
||||||
|
const BATCH_SIZE = 6;
|
||||||
const STICKERS = ["🦄","⭐","🌈","🐱","🚀","🍭","🎈","🐠","🦋","🌸","🐢","🍩","🎸","🐧","🌟"];
|
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 ! 🌟"];
|
||||||
|
|
||||||
const ENCOURAGEMENTS = [
|
/* ================================================================== */
|
||||||
"Super ! 🌟", "Génial ! 🎉", "Bravo ! 👏", "Magnifique ! ✨",
|
/* Persistance (localStorage) */
|
||||||
"Tu es fort ! 💪", "Youpi ! 🎈", "Fantastique ! 🌈", "Trop bien ! 🚀",
|
/* ================================================================== */
|
||||||
];
|
function clone(o) { return JSON.parse(JSON.stringify(o)); }
|
||||||
const ERREURS = [
|
let data = loadData();
|
||||||
"Oups ! Essaie encore ! 🤗", "Pas grave ! Recommence ! 😊",
|
function loadData() {
|
||||||
"Presque ! Tu vas y arriver ! 💪", "C'est pas facile ! Essaie ! 🌟",
|
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 */
|
/* État de jeu */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
const state = {
|
const state = {
|
||||||
running: false,
|
running: false,
|
||||||
|
cat: null, // catégorie en cours
|
||||||
score: 0,
|
score: 0,
|
||||||
level: 1,
|
current: "", // lettres tapées
|
||||||
current: "", // mot en cours de frappe
|
targets: [], // mots du lot courant (chaînes)
|
||||||
targets: [], // mots à trouver (batch courant)
|
found: new Set(),
|
||||||
found: new Set(), // mots du batch déjà trouvés
|
|
||||||
totalFound: 0,
|
totalFound: 0,
|
||||||
stickers: 0,
|
stickers: 0,
|
||||||
|
focus: null, // mot visé (pour guide + imagier)
|
||||||
soundOn: true,
|
soundOn: true,
|
||||||
lastAction: 0,
|
lastAction: 0,
|
||||||
hintShown: false,
|
hintShown: false,
|
||||||
miniGame: null,
|
miniGame: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Raccourcis DOM */
|
/* Raccourcis DOM */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const elScore = $("score"), elLevel = $("level"), elStickersCount = $("stickers-count");
|
const elScore = $("score"), elStickersCount = $("stickers-count");
|
||||||
const elWordZone = $("word-zone"), elWordList = $("word-list");
|
const elClue = $("clue"), elWordZone = $("word-zone"), elWordList = $("word-list");
|
||||||
const elKeyboard = $("keyboard"), elMascot = $("mascot"), elBlob = $("blob"), elSpeech = $("speech");
|
const elKeyboard = $("keyboard"), elBlob = $("blob"), elSpeech = $("speech");
|
||||||
const elMini = $("minigame"), elMgTitle = $("mg-title"), elMgTime = $("mg-time");
|
const elMini = $("minigame"), elMgTitle = $("mg-title"), elMgTime = $("mg-time");
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Son (synthèse WebAudio + voix) */
|
/* Son (WebAudio) + voix (Web Speech) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
function ac() {
|
function ac() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); return audioCtx; }
|
||||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
||||||
return audioCtx;
|
|
||||||
}
|
|
||||||
function beep(freq, dur = 0.12, type = "sine", vol = 0.18, delay = 0) {
|
function beep(freq, dur = 0.12, type = "sine", vol = 0.18, delay = 0) {
|
||||||
if (!state.soundOn) return;
|
if (!state.soundOn) return;
|
||||||
try {
|
try {
|
||||||
const ctx = ac();
|
const ctx = ac(), o = ctx.createOscillator(), g = ctx.createGain();
|
||||||
const o = ctx.createOscillator(), g = ctx.createGain();
|
|
||||||
o.type = type; o.frequency.value = freq;
|
o.type = type; o.frequency.value = freq;
|
||||||
const t0 = ctx.currentTime + delay;
|
const t0 = ctx.currentTime + delay;
|
||||||
g.gain.setValueAtTime(0, t0);
|
g.gain.setValueAtTime(0, t0);
|
||||||
@@ -82,7 +127,7 @@ function beep(freq, dur = 0.12, type = "sine", vol = 0.18, delay = 0) {
|
|||||||
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
|
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
|
||||||
o.connect(g).connect(ctx.destination);
|
o.connect(g).connect(ctx.destination);
|
||||||
o.start(t0); o.stop(t0 + dur + 0.02);
|
o.start(t0); o.stop(t0 + dur + 0.02);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
const sndType = () => beep(420 + Math.random() * 120, 0.08, "triangle", 0.12);
|
const sndType = () => beep(420 + Math.random() * 120, 0.08, "triangle", 0.12);
|
||||||
const sndPop = () => beep(700, 0.10, "square", 0.14);
|
const sndPop = () => beep(700, 0.10, "square", 0.14);
|
||||||
@@ -92,65 +137,115 @@ const sndSticker = () => { [784, 988, 1175].forEach((f, i) => beep(f, 0.25, "sin
|
|||||||
|
|
||||||
let frVoice = null;
|
let frVoice = null;
|
||||||
function pickVoice() {
|
function pickVoice() {
|
||||||
const voices = speechSynthesis.getVoices();
|
const voices = (window.speechSynthesis && speechSynthesis.getVoices()) || [];
|
||||||
frVoice = voices.find(v => /fr(-|_)?/i.test(v.lang)) || voices[0] || null;
|
frVoice = voices.find(v => /fr(-|_)?/i.test(v.lang)) || voices[0] || null;
|
||||||
}
|
}
|
||||||
if ("speechSynthesis" in window) {
|
if ("speechSynthesis" in window) { pickVoice(); speechSynthesis.onvoiceschanged = pickVoice; }
|
||||||
pickVoice();
|
function say(text, { interrupt = true } = {}) {
|
||||||
speechSynthesis.onvoiceschanged = pickVoice;
|
|
||||||
}
|
|
||||||
function say(text) {
|
|
||||||
if (!state.soundOn || !("speechSynthesis" in window)) return;
|
if (!state.soundOn || !("speechSynthesis" in window)) return;
|
||||||
// Retirer les emojis / garder lettres, chiffres, ponctuation simple
|
const clean = String(text).replace(/[^\p{L}\p{N} !?.,']/gu, "").trim();
|
||||||
const clean = text.replace(/[^\p{L}\p{N} !?.,']/gu, "").trim();
|
|
||||||
if (!clean) return;
|
if (!clean) return;
|
||||||
try {
|
try {
|
||||||
speechSynthesis.cancel();
|
if (interrupt) speechSynthesis.cancel();
|
||||||
const u = new SpeechSynthesisUtterance(clean);
|
const u = new SpeechSynthesisUtterance(clean);
|
||||||
if (frVoice) u.voice = frVoice;
|
if (frVoice) u.voice = frVoice;
|
||||||
u.lang = "fr-FR"; u.rate = 0.92; u.pitch = 1.15;
|
u.lang = "fr-FR"; u.rate = 0.92; u.pitch = 1.15;
|
||||||
speechSynthesis.speak(u);
|
speechSynthesis.speak(u);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Mascotte « Blob » */
|
/* 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;
|
let speechTimer = null, blinkTimer = null;
|
||||||
function mascotSet(stateName) {
|
function mascotSet(s) { elBlob.classList.remove("happy","sad","sleep","dance"); if (s) elBlob.classList.add(s); }
|
||||||
elBlob.classList.remove("happy", "sad", "sleep", "dance");
|
|
||||||
if (stateName) elBlob.classList.add(stateName);
|
|
||||||
}
|
|
||||||
function mascotSay(text, mood = "happy") {
|
function mascotSay(text, mood = "happy") {
|
||||||
mascotSet(mood);
|
mascotSet(mood);
|
||||||
elSpeech.textContent = text;
|
elSpeech.textContent = text;
|
||||||
elSpeech.classList.add("show");
|
elSpeech.classList.add("show");
|
||||||
clearTimeout(speechTimer);
|
clearTimeout(speechTimer);
|
||||||
speechTimer = setTimeout(() => elSpeech.classList.remove("show"), 3200);
|
speechTimer = setTimeout(() => elSpeech.classList.remove("show"), 3400);
|
||||||
say(text);
|
say(text);
|
||||||
if (mood === "dance") setTimeout(() => mascotSet("happy"), 2500);
|
if (mood === "dance") setTimeout(() => mascotSet("happy"), 2500);
|
||||||
}
|
}
|
||||||
function mascotEncourage() { mascotSay(rand(ENCOURAGEMENTS), "dance"); }
|
|
||||||
function mascotError() { mascotSay(rand(ERREURS), "sad"); }
|
function mascotError() { mascotSay(rand(ERREURS), "sad"); }
|
||||||
function blinkLoop() {
|
function blinkLoop() {
|
||||||
clearTimeout(blinkTimer);
|
clearTimeout(blinkTimer);
|
||||||
const next = 1500 + Math.random() * 3000;
|
|
||||||
blinkTimer = setTimeout(() => {
|
blinkTimer = setTimeout(() => {
|
||||||
elBlob.classList.add("blink");
|
elBlob.classList.add("blink");
|
||||||
setTimeout(() => elBlob.classList.remove("blink"), 150);
|
setTimeout(() => elBlob.classList.remove("blink"), 150);
|
||||||
blinkLoop();
|
blinkLoop();
|
||||||
}, next);
|
}, 1500 + Math.random() * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* Phrases personnalisées ----------------------------------------- */
|
||||||
/* Helpers */
|
function greeting() {
|
||||||
/* ------------------------------------------------------------------ */
|
const n = nom();
|
||||||
const rand = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
const h = new Date().getHours();
|
||||||
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; }
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Affichage : mot en cours, liste, HUD */
|
/* 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 = `<span class="cat-emoji">${cat.emoji}</span><span class="cat-label">${cat.label}</span>`;
|
||||||
|
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"];
|
const LETTER_COLORS = ["#ff6bcb","#6a5cff","#2ee6e6","#ffd23f","#3ddc84","#ff8c42"];
|
||||||
function renderWord(popLast = false) {
|
function renderWord(popLast = false) {
|
||||||
elWordZone.innerHTML = "";
|
elWordZone.innerHTML = "";
|
||||||
@@ -164,159 +259,142 @@ function renderWord(popLast = false) {
|
|||||||
}
|
}
|
||||||
function renderList() {
|
function renderList() {
|
||||||
elWordList.innerHTML = "";
|
elWordList.innerHTML = "";
|
||||||
|
const imagier = state.cat.kind === "imagier";
|
||||||
state.targets.forEach((w) => {
|
state.targets.forEach((w) => {
|
||||||
const c = document.createElement("span");
|
const c = document.createElement("span");
|
||||||
c.className = "chip" + (state.found.has(w) ? " found" : "");
|
c.className = "chip" + (state.found.has(w) ? " found" : "");
|
||||||
c.dataset.word = w;
|
c.dataset.word = w;
|
||||||
c.textContent = w;
|
c.textContent = imagier ? (clueOf(w) + " " + w) : w;
|
||||||
elWordList.appendChild(c);
|
elWordList.appendChild(c);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function updateHud() {
|
function renderClue() {
|
||||||
elScore.textContent = state.score;
|
if (state.cat.kind === "imagier" && state.focus) {
|
||||||
elLevel.textContent = state.level;
|
elClue.textContent = clueOf(state.focus);
|
||||||
elStickersCount.textContent = state.stickers;
|
elClue.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
elClue.classList.add("hidden");
|
||||||
}
|
}
|
||||||
function newBatch() {
|
|
||||||
state.targets = shuffle(WORD_POOL).slice(0, BATCH_SIZE);
|
|
||||||
state.found.clear();
|
|
||||||
renderList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* Mot visé (guide + imagier) -------------------------------------- */
|
||||||
/* Saisie d'une lettre */
|
function recomputeFocus(announce = false) {
|
||||||
/* ------------------------------------------------------------------ */
|
const left = state.targets.filter(w => !state.found.has(w));
|
||||||
function eraseLetter() {
|
let f = null;
|
||||||
state.current = "";
|
if (state.current) f = left.find(w => w.startsWith(state.current));
|
||||||
renderWord();
|
if (!f) f = left.find(w => w.startsWith(state.current)) || left[0] || null;
|
||||||
bump();
|
const changed = f !== state.focus;
|
||||||
|
state.focus = f;
|
||||||
|
renderClue();
|
||||||
|
applyGuide();
|
||||||
|
if (announce && changed && f && state.cat.kind === "imagier") {
|
||||||
|
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) {
|
function inputLetter(ch) {
|
||||||
ch = ch.toUpperCase();
|
ch = ch.toUpperCase();
|
||||||
if (!/^[A-Z]$/.test(ch)) return;
|
if (!/^[A-Z]$/.test(ch)) return;
|
||||||
state.lastAction = now();
|
bump();
|
||||||
state.hintShown = false;
|
|
||||||
clearHint();
|
|
||||||
mascotSet("happy");
|
mascotSet("happy");
|
||||||
|
|
||||||
state.current += ch;
|
state.current += ch;
|
||||||
renderWord(true);
|
renderWord(true);
|
||||||
sndType();
|
sndType();
|
||||||
spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.28, 6);
|
if (data.guided) say(ch, { interrupt: true }); // lit la lettre
|
||||||
|
spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.32, 6);
|
||||||
checkWord();
|
checkWord();
|
||||||
|
recomputeFocus();
|
||||||
}
|
}
|
||||||
function checkWord() {
|
function checkWord() {
|
||||||
const w = state.current;
|
const w = state.current;
|
||||||
if (state.targets.includes(w) && !state.found.has(w)) {
|
if (state.targets.includes(w) && !state.found.has(w)) { wordFound(w); return; }
|
||||||
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));
|
const possible = state.targets.some(t => !state.found.has(t) && t.startsWith(w));
|
||||||
if (!possible) {
|
if (!possible) {
|
||||||
mascotError();
|
mascotError(); sndError();
|
||||||
sndError();
|
setTimeout(() => { state.current = ""; renderWord(); recomputeFocus(); }, 380);
|
||||||
setTimeout(() => { state.current = ""; renderWord(); }, 350);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function wordFound(w) {
|
function wordFound(w) {
|
||||||
state.found.add(w);
|
state.found.add(w);
|
||||||
state.totalFound++;
|
state.totalFound++;
|
||||||
state.score += 100 * w.length;
|
state.score += 100 * w.length;
|
||||||
updateHud();
|
elScore.textContent = state.score;
|
||||||
renderList();
|
renderList();
|
||||||
celebrate();
|
celebrate(); sndSuccess();
|
||||||
sndSuccess();
|
mascotSay(successPhrase(w), "dance");
|
||||||
mascotSay("Bravo ! " + (NAME_PHRASES[w] || (w + " !")) + " 🎉", "dance");
|
if (state.totalFound % 3 === 0) { earnSticker(); confettiBurst(120); }
|
||||||
|
setTimeout(() => { state.current = ""; renderWord(); recomputeFocus(true); }, 750);
|
||||||
// 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) {
|
if (state.found.size >= state.targets.length) {
|
||||||
confettiBurst(200);
|
confettiBurst(200);
|
||||||
setTimeout(() => mascotSay("Tu connais toute la famille ! 🥳", "dance"), 800);
|
const n = nom();
|
||||||
setTimeout(newBatch, 1600);
|
setTimeout(() => mascotSay(n ? `Tu as tout trouvé ${n} ! 🥳` : "Tu as tout trouvé ! 🥳", "dance"), 800);
|
||||||
|
setTimeout(newBatch, 1700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Stickers */
|
/* Autocollants */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
function earnSticker() {
|
function earnSticker() {
|
||||||
if (state.stickers >= STICKERS.length) return;
|
if (state.stickers >= STICKERS.length) return;
|
||||||
state.stickers++;
|
state.stickers++;
|
||||||
updateHud();
|
elStickersCount.textContent = state.stickers;
|
||||||
sndSticker();
|
sndSticker();
|
||||||
const emoji = STICKERS[state.stickers - 1];
|
const emoji = STICKERS[state.stickers - 1];
|
||||||
setTimeout(() => mascotSay("Nouvel autocollant ! " + emoji, "dance"), 900);
|
setTimeout(() => mascotSay("Nouvel autocollant ! " + emoji, "dance"), 900);
|
||||||
renderStickerGrid();
|
|
||||||
}
|
}
|
||||||
function renderStickerGrid() {
|
function renderStickerGrid() {
|
||||||
const grid = $("sticker-grid");
|
const grid = $("sticker-grid");
|
||||||
grid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
STICKERS.forEach((emoji, i) => {
|
STICKERS.forEach((emoji, i) => {
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
const unlocked = i < state.stickers;
|
const ok = i < state.stickers;
|
||||||
d.className = "sticker " + (unlocked ? "unlocked" : "locked");
|
d.className = "sticker " + (ok ? "unlocked" : "locked");
|
||||||
d.textContent = unlocked ? emoji : "❔";
|
d.textContent = ok ? emoji : "❔";
|
||||||
grid.appendChild(d);
|
grid.appendChild(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Inactivité : indice puis dodo */
|
/* Inactivité : indice puis dodo */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
function clearHint() {
|
function clearHint() { document.querySelectorAll(".chip.hint").forEach(c => c.classList.remove("hint")); }
|
||||||
document.querySelectorAll(".chip.hint").forEach(c => c.classList.remove("hint"));
|
|
||||||
}
|
|
||||||
function checkIdle() {
|
function checkIdle() {
|
||||||
if (!state.running || state.miniGame) return;
|
if (!state.running || state.miniGame) return;
|
||||||
const idle = (now() - state.lastAction) / 1000;
|
const idle = (now() - state.lastAction) / 1000;
|
||||||
if (idle > 5 && !state.hintShown) {
|
if (idle > 5 && !state.hintShown) {
|
||||||
state.hintShown = true;
|
state.hintShown = true;
|
||||||
const remaining = state.targets.filter(w => !state.found.has(w));
|
const w = state.focus || state.targets.find(x => !state.found.has(x));
|
||||||
if (remaining.length) {
|
if (w) {
|
||||||
const w = rand(remaining);
|
|
||||||
const chip = elWordList.querySelector(`.chip[data-word="${w}"]`);
|
const chip = elWordList.querySelector(`.chip[data-word="${w}"]`);
|
||||||
if (chip) chip.classList.add("hint");
|
if (chip) chip.classList.add("hint");
|
||||||
mascotSay("Essaie d'écrire " + w + " ! 💡", "happy");
|
mascotSay("Essaie d'écrire " + w + " ! 💡", "happy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idle > 12) {
|
if (idle > 13) { mascotSet("sleep"); elSpeech.textContent = "Zzz… joue avec moi ! 😴"; elSpeech.classList.add("show"); }
|
||||||
mascotSet("sleep");
|
|
||||||
elSpeech.textContent = "Zzz… joue avec moi ! 😴";
|
|
||||||
elSpeech.classList.add("show");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Mini-jeux (ballons / étoiles) — taper pour éclater */
|
/* Mini-jeux (ballons / étoiles) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
let nextMiniGame = 0;
|
let nextMiniGame = 0;
|
||||||
function maybeStartMiniGame() {
|
function maybeStartMiniGame() { if (!state.miniGame && state.running && now() >= nextMiniGame) startMiniGame(); }
|
||||||
if (state.miniGame || !state.running) return;
|
|
||||||
if (now() < nextMiniGame) return;
|
|
||||||
startMiniGame();
|
|
||||||
}
|
|
||||||
function startMiniGame() {
|
function startMiniGame() {
|
||||||
const kind = Math.random() < 0.5 ? "ballons" : "etoiles";
|
const kind = Math.random() < 0.5 ? "ballons" : "etoiles";
|
||||||
state.miniGame = {
|
state.miniGame = { kind, items: [], end: now() + 10000, spawn: 0, earned: 0 };
|
||||||
kind,
|
|
||||||
items: [],
|
|
||||||
end: now() + 10000,
|
|
||||||
spawn: 0,
|
|
||||||
earned: 0,
|
|
||||||
};
|
|
||||||
elMgTitle.textContent = kind === "ballons" ? "🎈 Éclate les ballons !" : "⭐ Attrape les étoiles !";
|
elMgTitle.textContent = kind === "ballons" ? "🎈 Éclate les ballons !" : "⭐ Attrape les étoiles !";
|
||||||
elMini.classList.remove("hidden");
|
elMini.classList.remove("hidden");
|
||||||
mascotSay("Mini-jeu bonus ! 🎮", "dance");
|
mascotSay("Mini-jeu bonus ! 🎮", "dance");
|
||||||
@@ -326,213 +404,109 @@ function endMiniGame() {
|
|||||||
const earned = state.miniGame.earned;
|
const earned = state.miniGame.earned;
|
||||||
state.miniGame = null;
|
state.miniGame = null;
|
||||||
elMini.classList.add("hidden");
|
elMini.classList.add("hidden");
|
||||||
nextMiniGame = now() + 30000;
|
nextMiniGame = now() + 35000;
|
||||||
if (earned > 0) { state.score += earned; updateHud(); mascotEncourage(); }
|
if (earned > 0) { state.score += earned; elScore.textContent = state.score; mascotSay(encourage(), "dance"); }
|
||||||
}
|
}
|
||||||
function updateMiniGame(dt) {
|
function updateMiniGame(dt) {
|
||||||
const mg = state.miniGame;
|
const mg = state.miniGame; if (!mg) return;
|
||||||
if (!mg) return;
|
|
||||||
if (now() > mg.end) { endMiniGame(); return; }
|
if (now() > mg.end) { endMiniGame(); return; }
|
||||||
elMgTime.textContent = Math.ceil((mg.end - now()) / 1000);
|
elMgTime.textContent = Math.ceil((mg.end - now()) / 1000);
|
||||||
mg.spawn -= dt;
|
mg.spawn -= dt;
|
||||||
if (mg.spawn <= 0) {
|
if (mg.spawn <= 0) {
|
||||||
mg.spawn = 0.55;
|
mg.spawn = 0.55;
|
||||||
mg.items.push({
|
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 });
|
||||||
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;
|
for (const it of mg.items) it.y -= it.vy * dt;
|
||||||
mg.items = mg.items.filter(it => it.y > -60);
|
mg.items = mg.items.filter(it => it.y > -60);
|
||||||
}
|
}
|
||||||
function hitMiniGame(x, y) {
|
function hitMiniGame(x, y) {
|
||||||
const mg = state.miniGame;
|
const mg = state.miniGame; if (!mg) return;
|
||||||
if (!mg) return false;
|
|
||||||
for (let i = mg.items.length - 1; i >= 0; i--) {
|
for (let i = mg.items.length - 1; i >= 0; i--) {
|
||||||
const it = mg.items[i];
|
const it = mg.items[i];
|
||||||
if (Math.hypot(it.x - x, it.y - y) < it.r + 14) {
|
if (Math.hypot(it.x - x, it.y - y) < it.r + 16) {
|
||||||
mg.items.splice(i, 1);
|
mg.items.splice(i, 1); mg.earned += 20; sndPop(); spawnBubbles(x, y, 10); confettiAt(x, y, 8); return;
|
||||||
mg.earned += 20;
|
|
||||||
sndPop();
|
|
||||||
spawnBubbles(x, y, 10);
|
|
||||||
confettiAt(x, y, 8);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Canvas : fond, étoiles, bulles, confettis, arc-en-ciel */
|
/* Canvas FX */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
const cv = $("fx"), ctx2d = cv.getContext("2d");
|
const cv = $("fx"), ctx2d = cv.getContext("2d");
|
||||||
let W = 0, H = 0, DPR = 1;
|
let W = 0, H = 0, DPR = 1, stars = [], bubbles = [], confetti = [], rainbow = 0;
|
||||||
let stars = [], bubbles = [], confetti = [], rainbow = 0;
|
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
DPR = Math.min(window.devicePixelRatio || 1, 2);
|
DPR = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
W = window.innerWidth; H = window.innerHeight;
|
W = window.innerWidth; H = window.innerHeight;
|
||||||
cv.width = W * DPR; cv.height = H * DPR;
|
cv.width = W * DPR; cv.height = H * DPR; cv.style.width = W + "px"; cv.style.height = H + "px";
|
||||||
cv.style.width = W + "px"; cv.style.height = H + "px";
|
|
||||||
ctx2d.setTransform(DPR, 0, 0, DPR, 0, 0);
|
ctx2d.setTransform(DPR, 0, 0, DPR, 0, 0);
|
||||||
layoutSafeAreas();
|
layoutSafeAreas();
|
||||||
}
|
}
|
||||||
function initStars() {
|
function initStars() {
|
||||||
stars = [];
|
stars = [];
|
||||||
const n = Math.min(160, Math.floor((W * H) / 9000));
|
const n = Math.min(160, Math.floor((W * H) / 9000));
|
||||||
for (let i = 0; i < n; i++) {
|
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"]) });
|
||||||
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 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 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 confettiAt(x, y, n) { for (let i=0;i<n;i++) confetti.push(mkConfetti(x, y)); }
|
||||||
function 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"]) }; }
|
||||||
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 celebrate() { confettiBurst(90); spawnBubbles(W/2, H/2, 24); }
|
||||||
|
|
||||||
function drawBackground() {
|
function drawBackground() {
|
||||||
const g = ctx2d.createLinearGradient(0, 0, 0, H);
|
const g = ctx2d.createLinearGradient(0, 0, 0, H);
|
||||||
g.addColorStop(0, "#5b6cff");
|
g.addColorStop(0, "#5b6cff"); g.addColorStop(0.5, "#8a63ff"); g.addColorStop(1, "#ff8fd0");
|
||||||
g.addColorStop(0.5, "#8a63ff");
|
ctx2d.fillStyle = g; ctx2d.fillRect(0, 0, W, H);
|
||||||
g.addColorStop(1, "#ff8fd0");
|
|
||||||
ctx2d.fillStyle = g;
|
|
||||||
ctx2d.fillRect(0, 0, W, H);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loop(ts) {
|
function loop(ts) {
|
||||||
if (!loop.last) loop.last = ts;
|
if (!loop.last) loop.last = ts;
|
||||||
const dt = Math.min(0.05, (ts - loop.last) / 1000);
|
const dt = Math.min(0.05, (ts - loop.last) / 1000); loop.last = ts;
|
||||||
loop.last = ts;
|
|
||||||
|
|
||||||
drawBackground();
|
drawBackground();
|
||||||
|
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(); }
|
||||||
// É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;
|
ctx2d.globalAlpha = 1;
|
||||||
|
|
||||||
// Arc-en-ciel lors d'un succès
|
|
||||||
if (rainbow > 0) {
|
if (rainbow > 0) {
|
||||||
rainbow -= dt * 0.5;
|
rainbow -= dt * 0.5;
|
||||||
const cols = ["#ff5c5c","#ff8c42","#ffd23f","#3ddc84","#2ee6e6","#6a5cff","#ff6bcb"];
|
const cols = ["#ff5c5c","#ff8c42","#ffd23f","#3ddc84","#2ee6e6","#6a5cff","#ff6bcb"];
|
||||||
ctx2d.globalAlpha = Math.max(0, rainbow) * 0.5;
|
ctx2d.globalAlpha = Math.max(0, rainbow) * 0.5; ctx2d.lineWidth = 16;
|
||||||
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(); });
|
||||||
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;
|
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(); }
|
||||||
// Bulles
|
ctx2d.globalAlpha = 1; bubbles = bubbles.filter(b => b.life > 0 && b.y > -60);
|
||||||
for (const b of bubbles) {
|
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(); }
|
||||||
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);
|
confetti = confetti.filter(c => c.y < H + 40);
|
||||||
|
|
||||||
// Mini-jeu : ballons / étoiles
|
|
||||||
updateMiniGame(dt);
|
updateMiniGame(dt);
|
||||||
if (state.miniGame) {
|
if (state.miniGame) {
|
||||||
for (const it of state.miniGame.items) {
|
for (const it of state.miniGame.items) {
|
||||||
if (it.kind === "ballons") {
|
if (it.kind === "ballons") {
|
||||||
ctx2d.fillStyle = `hsl(${it.hue} 85% 60%)`;
|
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.beginPath();
|
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();
|
||||||
ctx2d.ellipse(it.x, it.y, it.r * 0.85, it.r, 0, 0, Math.PI * 2);
|
} else { drawStar(it.x, it.y, it.r, `hsl(${it.hue} 90% 60%)`); }
|
||||||
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);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
function drawStar(cx, cy, r, color) {
|
function drawStar(cx, cy, r, color) {
|
||||||
ctx2d.fillStyle = color;
|
ctx2d.fillStyle = color; ctx2d.beginPath();
|
||||||
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); }
|
||||||
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();
|
ctx2d.closePath(); ctx2d.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Clavier tactile (alphabétique, gros boutons) */
|
/* Clavier tactile */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
const KB_ROWS = ["ABCDEFG", "HIJKLMN", "OPQRSTU", "VWXYZ"];
|
const KB_ROWS = ["ABCDEFG", "HIJKLMN", "OPQRSTU", "VWXYZ"];
|
||||||
function buildKeyboard() {
|
function buildKeyboard() {
|
||||||
elKeyboard.innerHTML = "";
|
elKeyboard.innerHTML = "";
|
||||||
KB_ROWS.forEach((row, ri) => {
|
KB_ROWS.forEach((row, ri) => {
|
||||||
const r = document.createElement("div");
|
const r = document.createElement("div"); r.className = "kb-row";
|
||||||
r.className = "kb-row";
|
|
||||||
[...row].forEach((ch, i) => {
|
[...row].forEach((ch, i) => {
|
||||||
const b = document.createElement("button");
|
const b = document.createElement("button");
|
||||||
b.className = "key";
|
b.className = "key"; b.textContent = ch; b.dataset.ch = ch;
|
||||||
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.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 });
|
b.addEventListener("pointerdown", (e) => { e.preventDefault(); inputLetter(ch); }, { passive: false });
|
||||||
r.appendChild(b);
|
r.appendChild(b);
|
||||||
});
|
});
|
||||||
if (ri === KB_ROWS.length - 1) {
|
if (ri === KB_ROWS.length - 1) {
|
||||||
const e = document.createElement("button");
|
const e = document.createElement("button"); e.className = "key wide erase"; e.textContent = "⌫ Effacer";
|
||||||
e.className = "key wide erase";
|
|
||||||
e.textContent = "⌫ Effacer";
|
|
||||||
e.addEventListener("pointerdown", (ev) => { ev.preventDefault(); eraseLetter(); }, { passive: false });
|
e.addEventListener("pointerdown", (ev) => { ev.preventDefault(); eraseLetter(); }, { passive: false });
|
||||||
r.appendChild(e);
|
r.appendChild(e);
|
||||||
}
|
}
|
||||||
@@ -541,68 +515,159 @@ function buildKeyboard() {
|
|||||||
layoutSafeAreas();
|
layoutSafeAreas();
|
||||||
}
|
}
|
||||||
function layoutSafeAreas() {
|
function layoutSafeAreas() {
|
||||||
// Hauteur réelle du clavier → positionnement de la liste de mots / mascotte
|
|
||||||
const h = elKeyboard.classList.contains("hidden") ? 0 : elKeyboard.offsetHeight;
|
const h = elKeyboard.classList.contains("hidden") ? 0 : elKeyboard.offsetHeight;
|
||||||
document.documentElement.style.setProperty("--kb-h", h + "px");
|
document.documentElement.style.setProperty("--kb-h", h + "px");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Entrées globales */
|
/* Entrées globales */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
function now() { return performance.now(); }
|
|
||||||
function bump() { state.lastAction = now(); state.hintShown = false; clearHint(); }
|
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (!state.running) return;
|
if (!state.running) return;
|
||||||
if (e.key === "Backspace" || e.key === " ") { e.preventDefault(); eraseLetter(); return; }
|
if (e.key === "Backspace" || e.key === " ") { e.preventDefault(); eraseLetter(); return; }
|
||||||
if (/^[a-zA-Z]$/.test(e.key)) inputLetter(e.key);
|
if (/^[a-zA-Z]$/.test(e.key)) inputLetter(e.key);
|
||||||
});
|
});
|
||||||
|
window.addEventListener("pointerdown", (e) => { if (state.miniGame) hitMiniGame(e.clientX, e.clientY); });
|
||||||
|
|
||||||
// Tap dans la zone de jeu = éclater ballon/étoile pendant le mini-jeu
|
/* ================================================================== */
|
||||||
window.addEventListener("pointerdown", (e) => {
|
/* Navigation : démarrage catégorie / retour menu */
|
||||||
if (state.miniGame) hitMiniGame(e.clientX, e.clientY);
|
/* ================================================================== */
|
||||||
});
|
const GAME_UI = ["hud","word-zone","word-list","keyboard","mascot"];
|
||||||
|
function showMenu() {
|
||||||
/* ------------------------------------------------------------------ */
|
state.running = false;
|
||||||
/* Démarrage / boucle de jeu */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
function startGame() {
|
|
||||||
$("start").classList.add("hidden");
|
$("start").classList.add("hidden");
|
||||||
["hud", "word-zone", "word-list", "keyboard", "mascot"].forEach(id => $(id).classList.remove("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.running = true;
|
||||||
state.lastAction = now();
|
state.score = 0; state.totalFound = 0; state.stickers = 0;
|
||||||
nextMiniGame = now() + 30000;
|
state.current = ""; state.found.clear(); state.focus = null;
|
||||||
newBatch();
|
state.lastAction = now(); nextMiniGame = now() + 30000;
|
||||||
updateHud();
|
elScore.textContent = "0"; elStickersCount.textContent = "0";
|
||||||
|
$("menu").classList.add("hidden"); $("start").classList.add("hidden");
|
||||||
|
GAME_UI.forEach(id => $(id).classList.remove("hidden"));
|
||||||
buildKeyboard();
|
buildKeyboard();
|
||||||
|
newBatch();
|
||||||
blinkLoop();
|
blinkLoop();
|
||||||
try { ac().resume(); } catch (e) {}
|
try { ac().resume(); } catch (e) {}
|
||||||
mascotSay("Coucou ! Écris les prénoms de la famille ! 😊", "happy");
|
mascotSay(greeting(), "happy");
|
||||||
setInterval(checkIdle, 1000);
|
if (cat.kind === "imagier") recomputeFocus(true);
|
||||||
setInterval(maybeStartMiniGame, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
/* Boutons UI */
|
/* Espace parents (protégé par un calcul) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
$("btn-play").addEventListener("click", startGame);
|
function openParents() {
|
||||||
|
const a = 2 + Math.floor(Math.random() * 7), b = 2 + Math.floor(Math.random() * 7);
|
||||||
|
const body = $("parent-body");
|
||||||
|
body.innerHTML = `
|
||||||
|
<h2>⚙️ Espace parents</h2>
|
||||||
|
<p class="gate-q">Pour entrer, combien font <b>${a} + ${b}</b> ?</p>
|
||||||
|
<input id="gate-input" class="field" inputmode="numeric" type="number" placeholder="?" />
|
||||||
|
<div class="row-btns">
|
||||||
|
<button class="big-btn small" id="gate-ok">Valider</button>
|
||||||
|
<button class="big-btn small ghost" data-close-parent>Annuler</button>
|
||||||
|
</div>`;
|
||||||
|
$("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 = `
|
||||||
|
<h2>⚙️ Espace parents</h2>
|
||||||
|
<label class="field-label">Prénom de l'enfant</label>
|
||||||
|
<input id="child-name" class="field" type="text" maxlength="14" placeholder="ex : Léa" value="${escapeHtml(data.childName || "")}" />
|
||||||
|
<label class="check"><input type="checkbox" id="guided-toggle" ${data.guided ? "checked" : ""} /> Mode guidé (surligne la lettre + lit à voix haute)</label>
|
||||||
|
<div id="cats-editor"></div>
|
||||||
|
<div class="row-btns sticky-save">
|
||||||
|
<button class="big-btn small" id="parent-save">💾 Enregistrer</button>
|
||||||
|
<button class="big-btn small ghost" id="parent-reset">Réinitialiser</button>
|
||||||
|
<button class="big-btn small ghost" data-close-parent>Fermer</button>
|
||||||
|
</div>`;
|
||||||
|
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 = `
|
||||||
|
<div class="cat-edit-head">
|
||||||
|
<label class="check"><input type="checkbox" data-show="${ci}" ${cat.show ? "checked" : ""} /> <b>${cat.emoji} ${escapeHtml(cat.label)}</b></label>
|
||||||
|
</div>
|
||||||
|
<div class="words" data-words="${ci}"></div>
|
||||||
|
<button class="add-word" data-add="${ci}">+ Ajouter un mot</button>`;
|
||||||
|
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 = `
|
||||||
|
<input class="field w-name" type="text" value="${escapeHtml(it.w)}" placeholder="MOT" />
|
||||||
|
<input class="field w-clue" type="text" value="${escapeHtml(it.clue)}" placeholder="${ph}" />
|
||||||
|
<button class="del" title="Supprimer">🗑️</button>`;
|
||||||
|
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", () => {
|
$("btn-sound").addEventListener("click", () => {
|
||||||
state.soundOn = !state.soundOn;
|
state.soundOn = !state.soundOn;
|
||||||
$("btn-sound").textContent = state.soundOn ? "🔊" : "🔇";
|
$("btn-sound").textContent = state.soundOn ? "🔊" : "🔇";
|
||||||
if (!state.soundOn && "speechSynthesis" in window) speechSynthesis.cancel();
|
if (!state.soundOn && "speechSynthesis" in window) speechSynthesis.cancel();
|
||||||
});
|
});
|
||||||
$("btn-stickers").addEventListener("click", () => { renderStickerGrid(); $("sticker-drawer").classList.remove("hidden"); });
|
$("btn-stickers").addEventListener("click", () => { renderStickerGrid(); $("sticker-drawer").classList.remove("hidden"); });
|
||||||
$("btn-close-stickers").addEventListener("click", () => $("sticker-drawer").classList.add("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 */
|
/* Init */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ================================================================== */
|
||||||
window.addEventListener("resize", () => { resize(); initStars(); });
|
window.addEventListener("resize", () => { resize(); initStars(); });
|
||||||
resize();
|
resize(); initStars(); requestAnimationFrame(loop);
|
||||||
initStars();
|
if ("serviceWorker" in navigator) window.addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
|
||||||
requestAnimationFrame(loop);
|
|
||||||
|
|
||||||
// PWA : service worker (installable, jouable hors-ligne)
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
window.addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
|
|
||||||
}
|
|
||||||
|
|||||||
27
index.html
27
index.html
@@ -18,17 +18,26 @@
|
|||||||
<div id="start" class="screen">
|
<div id="start" class="screen">
|
||||||
<div class="start-card">
|
<div class="start-card">
|
||||||
<h1 class="title">✨ Lettres Magiques ✨</h1>
|
<h1 class="title">✨ Lettres Magiques ✨</h1>
|
||||||
<p class="subtitle">Écris les prénoms de la famille !</p>
|
<p class="subtitle" id="start-sub">Apprends à écrire en t'amusant !</p>
|
||||||
<button id="btn-play" class="big-btn">🎮 Jouer</button>
|
<button id="btn-play" class="big-btn">🎮 Jouer</button>
|
||||||
<p class="hint-line">🔊 Le son aide les petits — laisse-le activé.</p>
|
<button id="btn-parents" class="text-btn">⚙️ Espace parents</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu de catégories -->
|
||||||
|
<div id="menu" class="screen hidden">
|
||||||
|
<div class="menu-card">
|
||||||
|
<h2 class="menu-title">Choisis un jeu !</h2>
|
||||||
|
<div id="cat-grid" class="cat-grid"></div>
|
||||||
|
<button id="btn-parents-menu" class="text-btn">⚙️ Espace parents</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Interface de jeu -->
|
<!-- Interface de jeu -->
|
||||||
<div id="hud" class="hud hidden">
|
<div id="hud" class="hud hidden">
|
||||||
<div class="hud-left">
|
<div class="hud-left">
|
||||||
|
<button id="btn-home" class="icon-btn" title="Menu">🏠</button>
|
||||||
<div class="badge">⭐ <span id="score">0</span></div>
|
<div class="badge">⭐ <span id="score">0</span></div>
|
||||||
<div class="badge">🚀 Niv. <span id="level">1</span></div>
|
|
||||||
<div class="badge">🏅 <span id="stickers-count">0</span>/15</div>
|
<div class="badge">🏅 <span id="stickers-count">0</span>/15</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hud-right">
|
<div class="hud-right">
|
||||||
@@ -37,10 +46,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Indice imagier (grande image du mot à écrire) -->
|
||||||
|
<div id="clue" class="clue hidden"></div>
|
||||||
|
|
||||||
<!-- Mot en cours -->
|
<!-- Mot en cours -->
|
||||||
<div id="word-zone" class="word-zone hidden"></div>
|
<div id="word-zone" class="word-zone hidden"></div>
|
||||||
|
|
||||||
<!-- Liste des mots à trouver (aide permanente) -->
|
<!-- Liste des mots à trouver -->
|
||||||
<div id="word-list" class="word-list hidden"></div>
|
<div id="word-list" class="word-list hidden"></div>
|
||||||
|
|
||||||
<!-- Clavier tactile -->
|
<!-- Clavier tactile -->
|
||||||
@@ -66,10 +78,15 @@
|
|||||||
<div class="drawer-card">
|
<div class="drawer-card">
|
||||||
<h2>🏅 Mes autocollants</h2>
|
<h2>🏅 Mes autocollants</h2>
|
||||||
<div id="sticker-grid" class="sticker-grid"></div>
|
<div id="sticker-grid" class="sticker-grid"></div>
|
||||||
<button id="btn-close-stickers" class="big-btn small">Fermer</button>
|
<button class="big-btn small" data-close-drawer>Fermer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Espace parents (protégé) -->
|
||||||
|
<div id="parent" class="drawer hidden">
|
||||||
|
<div class="drawer-card parent-card" id="parent-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="game.js"></script>
|
<script src="game.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
99
style.css
99
style.css
@@ -119,7 +119,7 @@ html, body {
|
|||||||
/* ---------- Mot en cours ---------- */
|
/* ---------- Mot en cours ---------- */
|
||||||
.word-zone {
|
.word-zone {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16vh;
|
top: 22vh;
|
||||||
left: 0; right: 0;
|
left: 0; right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -302,3 +302,100 @@ html, body {
|
|||||||
.key { max-width: 56px; }
|
.key { max-width: 56px; }
|
||||||
.speech { font-size: 16px; }
|
.speech { font-size: 16px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Bouton texte (accueil) ---------- */
|
||||||
|
.text-btn {
|
||||||
|
display: block;
|
||||||
|
margin: 18px auto 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6a5cff;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
.text-btn:active { transform: scale(.96); }
|
||||||
|
|
||||||
|
/* ---------- Menu de catégories ---------- */
|
||||||
|
.menu-card {
|
||||||
|
background: rgba(255,255,255,0.94);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 26px 22px;
|
||||||
|
max-width: 540px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 18px 60px rgba(0,0,0,0.35);
|
||||||
|
animation: pop .4s ease;
|
||||||
|
}
|
||||||
|
.menu-title { text-align: center; margin: 0 0 18px; font-size: clamp(22px, 6vw, 30px); color: var(--ink); }
|
||||||
|
.cat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
|
||||||
|
.cat-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 20px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: #fff;
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||||
|
background: linear-gradient(135deg, var(--purple), var(--pink));
|
||||||
|
box-shadow: 0 8px 0 rgba(0,0,0,0.15), 0 12px 22px rgba(106,92,255,0.35);
|
||||||
|
transition: transform .08s ease, box-shadow .08s ease;
|
||||||
|
}
|
||||||
|
.cat-card:nth-child(4n+2) { background: linear-gradient(135deg, #2ee6e6, #6a5cff); }
|
||||||
|
.cat-card:nth-child(4n+3) { background: linear-gradient(135deg, #3ddc84, #2ee6e6); }
|
||||||
|
.cat-card:nth-child(4n+4) { background: linear-gradient(135deg, #ff8c42, #ff6bcb); }
|
||||||
|
.cat-card:active { transform: translateY(5px); box-shadow: 0 3px 0 rgba(0,0,0,0.15); }
|
||||||
|
.cat-emoji { font-size: clamp(40px, 12vw, 60px); line-height: 1; }
|
||||||
|
.cat-label { font-weight: 800; font-size: clamp(15px, 4vw, 19px); }
|
||||||
|
|
||||||
|
/* ---------- Indice imagier (grande image) ---------- */
|
||||||
|
.clue {
|
||||||
|
position: fixed;
|
||||||
|
top: 6vh; left: 0; right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
text-align: center;
|
||||||
|
font-size: clamp(52px, 17vw, 120px);
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: drop-shadow(0 8px 14px rgba(0,0,0,0.3));
|
||||||
|
animation: bounceIn .4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Lettre guidée ---------- */
|
||||||
|
.key.guide {
|
||||||
|
animation: guidePulse .8s ease infinite;
|
||||||
|
outline: 4px solid #fff;
|
||||||
|
outline-offset: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
@keyframes guidePulse { 0%,100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.12); filter: brightness(1.35); } }
|
||||||
|
|
||||||
|
/* ---------- Espace parents ---------- */
|
||||||
|
.parent-card { max-width: 560px; width: 100%; max-height: 88vh; overflow-y: auto; text-align: left; }
|
||||||
|
.parent-card h2 { margin: 0 0 14px; text-align: center; }
|
||||||
|
.gate-q { font-size: 18px; text-align: center; margin: 10px 0; }
|
||||||
|
.field {
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid #ded7ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fbfaff;
|
||||||
|
}
|
||||||
|
.field:focus { outline: none; border-color: var(--purple); }
|
||||||
|
.field-label { display: block; font-weight: 700; margin: 6px 0 4px; font-size: 14px; }
|
||||||
|
.check { display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 8px 0 14px; cursor: pointer; }
|
||||||
|
.check input { width: 20px; height: 20px; }
|
||||||
|
.cat-edit { border: 1px solid #eee; border-radius: 14px; padding: 12px; margin-bottom: 12px; background: #faf9ff; }
|
||||||
|
.cat-edit-head { margin-bottom: 8px; }
|
||||||
|
.word-row { display: flex; gap: 6px; margin-bottom: 6px; align-items: center; }
|
||||||
|
.word-row .w-name { flex: 0 0 32%; margin: 0; text-transform: uppercase; }
|
||||||
|
.word-row .w-clue { flex: 1; margin: 0; }
|
||||||
|
.word-row .del { border: none; background: #ffe1e8; border-radius: 10px; width: 38px; height: 38px; cursor: pointer; font-size: 16px; flex: 0 0 auto; }
|
||||||
|
.add-word { border: none; background: #e9e4ff; color: var(--purple); font: inherit; font-weight: 700; border-radius: 10px; padding: 8px 12px; cursor: pointer; }
|
||||||
|
.row-btns { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 14px; }
|
||||||
|
.sticky-save { position: sticky; bottom: -22px; background: linear-gradient(180deg, rgba(255,255,255,0), #fff 30%); padding-top: 12px; padding-bottom: 4px; }
|
||||||
|
.big-btn.ghost { background: #efeefb; color: var(--ink); box-shadow: 0 6px 0 rgba(0,0,0,0.08); }
|
||||||
|
|||||||
2
sw.js
2
sw.js
@@ -1,5 +1,5 @@
|
|||||||
/* Service worker — network-first (frais en ligne, cache en secours hors-ligne) */
|
/* Service worker — network-first (frais en ligne, cache en secours hors-ligne) */
|
||||||
const CACHE = "lettres-magiques-v2";
|
const CACHE = "lettres-magiques-v3";
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
".",
|
".",
|
||||||
"index.html",
|
"index.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user