diff --git a/client/assets/symbols/ANTE_ACTIVE-removebg-preview.webp b/client/assets/symbols/ANTE_ACTIVE-removebg-preview.webp new file mode 100644 index 0000000..263d001 Binary files /dev/null and b/client/assets/symbols/ANTE_ACTIVE-removebg-preview.webp differ diff --git a/client/assets/symbols/ANTE_INACTIVE-removebg-preview.webp b/client/assets/symbols/ANTE_INACTIVE-removebg-preview.webp new file mode 100644 index 0000000..dc3e316 Binary files /dev/null and b/client/assets/symbols/ANTE_INACTIVE-removebg-preview.webp differ diff --git a/client/assets/symbols/BET-removebg-preview.webp b/client/assets/symbols/BET-removebg-preview.webp new file mode 100644 index 0000000..4c99f37 Binary files /dev/null and b/client/assets/symbols/BET-removebg-preview.webp differ diff --git a/client/assets/symbols/BUY_FEATURE-removebg-preview.webp b/client/assets/symbols/BUY_FEATURE-removebg-preview.webp new file mode 100644 index 0000000..facc96b Binary files /dev/null and b/client/assets/symbols/BUY_FEATURE-removebg-preview.webp differ diff --git a/client/assets/symbols/Background.webp b/client/assets/symbols/Background.webp new file mode 100644 index 0000000..d100eca Binary files /dev/null and b/client/assets/symbols/Background.webp differ diff --git a/client/assets/symbols/BlueGem.webp b/client/assets/symbols/BlueGem.webp new file mode 100644 index 0000000..258ed3f Binary files /dev/null and b/client/assets/symbols/BlueGem.webp differ diff --git a/client/assets/symbols/Chaile.webp b/client/assets/symbols/Chaile.webp new file mode 100644 index 0000000..28ec5c6 Binary files /dev/null and b/client/assets/symbols/Chaile.webp differ diff --git a/client/assets/symbols/Common.webp b/client/assets/symbols/Common.webp new file mode 100644 index 0000000..bd96cce Binary files /dev/null and b/client/assets/symbols/Common.webp differ diff --git a/client/assets/symbols/Crown.webp b/client/assets/symbols/Crown.webp new file mode 100644 index 0000000..a5e7f84 Binary files /dev/null and b/client/assets/symbols/Crown.webp differ diff --git a/client/assets/symbols/Epic.webp b/client/assets/symbols/Epic.webp new file mode 100644 index 0000000..eff4415 Binary files /dev/null and b/client/assets/symbols/Epic.webp differ diff --git a/client/assets/symbols/FREESPINS_INFO-removebg-preview.webp b/client/assets/symbols/FREESPINS_INFO-removebg-preview.webp new file mode 100644 index 0000000..0c37ee9 Binary files /dev/null and b/client/assets/symbols/FREESPINS_INFO-removebg-preview.webp differ diff --git a/client/assets/symbols/GreenGem.webp b/client/assets/symbols/GreenGem.webp new file mode 100644 index 0000000..aebbe6b Binary files /dev/null and b/client/assets/symbols/GreenGem.webp differ diff --git a/client/assets/symbols/HourGlass.webp b/client/assets/symbols/HourGlass.webp new file mode 100644 index 0000000..f99e244 Binary files /dev/null and b/client/assets/symbols/HourGlass.webp differ diff --git a/client/assets/symbols/Legendary.webp b/client/assets/symbols/Legendary.webp new file mode 100644 index 0000000..3831e76 Binary files /dev/null and b/client/assets/symbols/Legendary.webp differ diff --git a/client/assets/symbols/Mythic.webp b/client/assets/symbols/Mythic.webp new file mode 100644 index 0000000..8ddfffd Binary files /dev/null and b/client/assets/symbols/Mythic.webp differ diff --git a/client/assets/symbols/PurpleGem.webp b/client/assets/symbols/PurpleGem.webp new file mode 100644 index 0000000..0634b85 Binary files /dev/null and b/client/assets/symbols/PurpleGem.webp differ diff --git a/client/assets/symbols/REEL.webp b/client/assets/symbols/REEL.webp new file mode 100644 index 0000000..9ad2827 Binary files /dev/null and b/client/assets/symbols/REEL.webp differ diff --git a/client/assets/symbols/Rare.webp b/client/assets/symbols/Rare.webp new file mode 100644 index 0000000..0c07543 Binary files /dev/null and b/client/assets/symbols/Rare.webp differ diff --git a/client/assets/symbols/RedGem.webp b/client/assets/symbols/RedGem.webp new file mode 100644 index 0000000..fbc3b63 Binary files /dev/null and b/client/assets/symbols/RedGem.webp differ diff --git a/client/assets/symbols/Ring.webp b/client/assets/symbols/Ring.webp new file mode 100644 index 0000000..8cfee9e Binary files /dev/null and b/client/assets/symbols/Ring.webp differ diff --git a/client/assets/symbols/SPIN.webp b/client/assets/symbols/SPIN.webp new file mode 100644 index 0000000..229ad19 Binary files /dev/null and b/client/assets/symbols/SPIN.webp differ diff --git a/client/assets/symbols/Scatter.webp b/client/assets/symbols/Scatter.webp new file mode 100644 index 0000000..7916a56 Binary files /dev/null and b/client/assets/symbols/Scatter.webp differ diff --git a/client/assets/symbols/YellowGem.webp b/client/assets/symbols/YellowGem.webp new file mode 100644 index 0000000..8dbfd89 Binary files /dev/null and b/client/assets/symbols/YellowGem.webp differ diff --git a/client/engine/ambiance.js b/client/engine/ambiance.js index ab6dee9..efb2a60 100644 --- a/client/engine/ambiance.js +++ b/client/engine/ambiance.js @@ -23,7 +23,7 @@ // Optional real-character art. Leave null to use the CSS idol. const MASCOT_IMG = null; // e.g. "assets/mascot/zeus.png" - const CROWN_IMG = "assets/symbols/Crown.png"; + const CROWN_IMG = "assets/symbols/Crown.webp"; const reduceMotion = root.matchMedia && root.matchMedia("(prefers-reduced-motion: reduce)").matches; @@ -139,7 +139,7 @@ node.style.setProperty("--delay", `${-Math.random() * 16}s`); node.style.setProperty("--spin", `${Math.random() < 0.5 ? -1 : 1}`); if (useImg) { - node.src = `assets/symbols/${gems[i % gems.length]}.png`; + node.src = `assets/symbols/${gems[i % gems.length]}.webp`; node.alt = ""; node.classList.add("fx-deco-gem"); } else { diff --git a/client/engine/audio.js b/client/engine/audio.js index da66a86..d0587e7 100644 --- a/client/engine/audio.js +++ b/client/engine/audio.js @@ -54,7 +54,9 @@ loading: new Map(), // name -> Promise lastPlay: new Map(), // name -> timestamp (debounce identical rapid triggers) listeners: new Set(), // change subscribers (UI sync) - warmed: false // whether the audio pipeline has been pre-warmed + warmed: false, // whether the audio pipeline has been pre-warmed + suspended: false, // parked because the tab/app is in the background + wasPlaying: false // music state to restore when we come back }; function ensureContext() { @@ -192,7 +194,11 @@ } }, win_tick: { - minGap: 8, + // The win-meter count-up drives this every ~16ms, so a minGap below that + // meant a fresh oscillator + gain node ~60x a second for the whole + // count-up. 45ms still reads as a continuous ticking run but builds a + // third of the audio graph. + minGap: 45, synth: (c, d, o = {}) => { const f = clamp(520 + (o.progress || 0) * 900, 520, 1700); tone(c, d, { type: "triangle", freq: f, dur: 0.04, gain: 0.12 }); @@ -458,6 +464,11 @@ const def = SFX[name]; if (!def) return; if (prefs.muted) return; + // Parked in the background: drop the sound rather than resuming the context. + // Timer-driven game state (autoplay, win count-ups) keeps advancing while + // hidden, and without this every one of those steps would un-suspend the + // audio hardware we just parked. + if (state.suspended) return; const ctx = ensureContext(); if (!ctx) return; if (ctx.state === "suspended") ctx.resume(); @@ -514,6 +525,38 @@ return () => state.listeners.delete(fn); } + // --- background suspend ---------------------------------------------------- + // The music scheduler re-arms a setTimeout every 130ms for the entire session + // and the AudioContext is never suspended, so a backgrounded game kept the + // audio render thread and the hardware clock alive indefinitely. Worse, a + // hidden tab clamps setTimeout to ~1s while the scheduler only looks 0.35s + // ahead, so it under-schedules and then burst-catches-up on every tick. + // + // suspendAudio() parks all of it; resumeAudio() puts it back exactly as it + // was. `wasPlaying` remembers whether music was running so we don't start it + // for a player who had muted or never triggered the first gesture. + function suspendAudio() { + if (state.suspended) return; + state.suspended = true; + state.wasPlaying = Boolean(music); + stopMusic(); + const ctx = state.ctx; + if (ctx && ctx.state === "running") { + try { ctx.suspend(); } catch (e) { /* ignore */ } + } + } + + function resumeAudio() { + if (!state.suspended) return; + state.suspended = false; + const ctx = state.ctx; + if (ctx && ctx.state === "suspended") { + try { ctx.resume(); } catch (e) { /* ignore */ } + } + if (state.wasPlaying && !prefs.muted && state.unlocked) startMusic(); + state.wasPlaying = false; + } + // Self-install a one-shot unlock on the first user gesture as a safety net, // even if main.js forgets to call unlock(). function installAutoUnlock() { @@ -535,6 +578,10 @@ onChange, startMusic, stopMusic, + suspendAudio, + resumeAudio, + isSuspended: () => Boolean(state.suspended), + contextState: () => (state.ctx ? state.ctx.state : "none"), isMuted: () => prefs.muted, getVolume: () => prefs.volume, names: () => Object.keys(SFX) diff --git a/client/index.html b/client/index.html index 40f0bd3..2e8934c 100644 --- a/client/index.html +++ b/client/index.html @@ -19,7 +19,16 @@ })(); - + + + + +