v3 — ballons volants d'ambiance + bouton 🎈 HUD (lance le jeu des ballons), superset v2.1
This commit is contained in:
62
game.js
62
game.js
@@ -143,7 +143,10 @@ function pickVoice() {
|
|||||||
if ("speechSynthesis" in window) { pickVoice(); speechSynthesis.onvoiceschanged = pickVoice; }
|
if ("speechSynthesis" in window) { pickVoice(); speechSynthesis.onvoiceschanged = pickVoice; }
|
||||||
function say(text, { interrupt = true } = {}) {
|
function say(text, { interrupt = true } = {}) {
|
||||||
if (!state.soundOn || !("speechSynthesis" in window)) return;
|
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;
|
if (!clean) return;
|
||||||
try {
|
try {
|
||||||
if (interrupt) speechSynthesis.cancel();
|
if (interrupt) speechSynthesis.cancel();
|
||||||
@@ -287,9 +290,16 @@ function recomputeFocus(announce = false) {
|
|||||||
state.focus = f;
|
state.focus = f;
|
||||||
renderClue();
|
renderClue();
|
||||||
applyGuide();
|
applyGuide();
|
||||||
if (announce && changed && f) {
|
if (announce && changed && f) setTimeout(announceWord, 300);
|
||||||
setTimeout(() => mascotSay("Écris : " + f + " ! ✏️", "happy"), 250);
|
}
|
||||||
}
|
// É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() {
|
function applyGuide() {
|
||||||
elKeyboard.querySelectorAll(".key.guide").forEach(k => k.classList.remove("guide"));
|
elKeyboard.querySelectorAll(".key.guide").forEach(k => k.classList.remove("guide"));
|
||||||
@@ -304,7 +314,7 @@ function applyGuide() {
|
|||||||
/* Saisie */
|
/* Saisie */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
function eraseLetter() { state.current = ""; renderWord(); bump(); recomputeFocus(); }
|
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) {
|
function inputLetter(ch) {
|
||||||
ch = ch.toUpperCase();
|
ch = ch.toUpperCase();
|
||||||
if (!/^[A-Z]$/.test(ch)) return;
|
if (!/^[A-Z]$/.test(ch)) return;
|
||||||
@@ -313,7 +323,7 @@ function inputLetter(ch) {
|
|||||||
state.current += ch;
|
state.current += ch;
|
||||||
renderWord(true);
|
renderWord(true);
|
||||||
sndType();
|
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);
|
spawnBubbles(window.innerWidth / 2, window.innerHeight * 0.32, 6);
|
||||||
checkWord();
|
checkWord();
|
||||||
recomputeFocus();
|
recomputeFocus();
|
||||||
@@ -375,18 +385,8 @@ function clearHint() { document.querySelectorAll(".chip.hint").forEach(c => c.cl
|
|||||||
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) {
|
// Si rien n'est tapé, on rappelle (et on redit) le mot à écrire, régulièrement
|
||||||
const sinceHint = (now() - (state.lastHint || 0)) / 1000;
|
if (idle > 4 && now() - (state.lastPrompt || 0) > 6000) announceWord();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
@@ -394,8 +394,8 @@ function checkIdle() {
|
|||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
let nextMiniGame = 0;
|
let nextMiniGame = 0;
|
||||||
function maybeStartMiniGame() { if (!state.miniGame && state.running && now() >= nextMiniGame) startMiniGame(); }
|
function maybeStartMiniGame() { if (!state.miniGame && state.running && now() >= nextMiniGame) startMiniGame(); }
|
||||||
function startMiniGame() {
|
function startMiniGame(kind) {
|
||||||
const kind = Math.random() < 0.5 ? "ballons" : "etoiles";
|
kind = kind || (Math.random() < 0.5 ? "ballons" : "etoiles");
|
||||||
state.miniGame = { kind, items: [], end: now() + 10000, spawn: 0, earned: 0 };
|
state.miniGame = { 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");
|
||||||
@@ -435,7 +435,7 @@ function hitMiniGame(x, y) {
|
|||||||
/* Canvas FX */
|
/* Canvas FX */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
const cv = $("fx"), ctx2d = cv.getContext("2d");
|
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() {
|
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;
|
||||||
@@ -475,6 +475,23 @@ function loop(ts) {
|
|||||||
ctx2d.globalAlpha = 1; bubbles = bubbles.filter(b => b.life > 0 && b.y > -60);
|
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(); }
|
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);
|
||||||
|
// 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);
|
updateMiniGame(dt);
|
||||||
if (state.miniGame) {
|
if (state.miniGame) {
|
||||||
for (const it of state.miniGame.items) {
|
for (const it of state.miniGame.items) {
|
||||||
@@ -557,7 +574,7 @@ function startCategory(cat) {
|
|||||||
blinkLoop();
|
blinkLoop();
|
||||||
try { ac().resume(); } catch (e) {}
|
try { ac().resume(); } catch (e) {}
|
||||||
mascotSay(greeting(), "happy");
|
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();
|
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-mini").addEventListener("click", () => { if (state.running && !state.miniGame) startMiniGame("ballons"); });
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (e.target.closest("[data-close-drawer]")) $("sticker-drawer").classList.add("hidden");
|
if (e.target.closest("[data-close-drawer]")) $("sticker-drawer").classList.add("hidden");
|
||||||
if (e.target.closest("[data-close-parent]")) $("parent").classList.add("hidden");
|
if (e.target.closest("[data-close-parent]")) $("parent").classList.add("hidden");
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<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">
|
||||||
|
<button id="btn-mini" class="icon-btn" title="Jeu des ballons">🎈</button>
|
||||||
<button id="btn-sound" class="icon-btn" title="Son">🔊</button>
|
<button id="btn-sound" class="icon-btn" title="Son">🔊</button>
|
||||||
<button id="btn-stickers" class="icon-btn" title="Mes autocollants">🏅</button>
|
<button id="btn-stickers" class="icon-btn" title="Mes autocollants">🏅</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ html, body {
|
|||||||
transition: transform .2s ease, background .2s ease, opacity .2s ease;
|
transition: transform .2s ease, background .2s ease, opacity .2s ease;
|
||||||
}
|
}
|
||||||
.chip.found { background: var(--green); color: #fff; opacity: .6; text-decoration: line-through; }
|
.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 ---------- */
|
/* ---------- Clavier tactile ---------- */
|
||||||
.keyboard {
|
.keyboard {
|
||||||
@@ -362,13 +362,12 @@ html, body {
|
|||||||
animation: bounceIn .4s ease;
|
animation: bounceIn .4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Lettre guidée ---------- */
|
/* ---------- Lettre guidée (surbrillance fixe, sans clignotement) ---------- */
|
||||||
.key.guide {
|
.key.guide {
|
||||||
outline: 4px solid #fff;
|
outline: 4px solid #fff;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
transform: scale(1.08);
|
filter: brightness(1.18);
|
||||||
filter: brightness(1.25);
|
box-shadow: 0 0 0 6px rgba(255,255,255,0.35), 0 5px 0 rgba(0,0,0,0.22);
|
||||||
box-shadow: 0 0 0 4px rgba(106,92,255,.45), 0 8px 18px rgba(0,0,0,.25);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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-v4";
|
const CACHE = "lettres-magiques-v5";
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
".",
|
".",
|
||||||
"index.html",
|
"index.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user