Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/BET-removebg-preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Background.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/BlueGem.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Chaile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Common.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Crown.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Epic.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/GreenGem.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/HourGlass.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Legendary.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Mythic.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/PurpleGem.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/REEL.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Rare.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/RedGem.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Ring.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/SPIN.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/Scatter.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/assets/symbols/YellowGem.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions client/engine/ambiance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
51 changes: 49 additions & 2 deletions client/engine/audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand All @@ -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)
Expand Down
23 changes: 16 additions & 7 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@
})();
</script>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="styles.css?v=20260710-1" />
<!-- Start these fetches during HTML parse instead of waiting for main.js to
run. FREESPINS_INFO in particular is a CSS background on a display:none
panel, so the browser would otherwise not request it until the panel is
shown mid-game — where it painted in progressively. Paths are relative
so they resolve through the <base href> above to the same URLs main.js
preloads; a mismatch here would double-fetch. -->
<link rel="preload" as="image" href="assets/symbols/Background.webp" />
<link rel="preload" as="image" href="assets/symbols/FREESPINS_INFO-removebg-preview.webp" />
<link rel="preload" as="image" href="assets/symbols/BET-removebg-preview.webp" />
<link rel="stylesheet" href="styles.css?v=20260803-2" />
</head>
<body>
<div id="bootLoader" class="boot-loader" role="progressbar" aria-label="Loading game" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
Expand Down Expand Up @@ -151,15 +160,15 @@ <h2 id="featureScreenTitle">You won 15 Free Spins</h2>
</div>
<label class="ante-toggle" for="anteToggle">
<input id="anteToggle" type="checkbox" />
<img class="ante-toggle-img" id="anteToggleImg" src="assets/symbols/ANTE_INACTIVE-removebg-preview.png" alt="Ante Bet 1.25x" />
<img class="ante-toggle-img" id="anteToggleImg" src="assets/symbols/ANTE_INACTIVE-removebg-preview.webp" alt="Ante Bet 1.25x" />
</label>
</div>
<button id="spinBtn" type="button" class="img-btn img-btn-spin" aria-label="Spin">
<img src="assets/symbols/SPIN.png" alt="Spin" />
<img src="assets/symbols/SPIN.webp" alt="Spin" />
</button>
<div class="deck-right">
<button id="buyFreeBtn" type="button" class="img-btn img-btn-buy" aria-label="Buy Free Spins">
<img src="assets/symbols/BUY_FEATURE-removebg-preview.png" alt="Buy Free Spins" />
<img src="assets/symbols/BUY_FEATURE-removebg-preview.webp" alt="Buy Free Spins" />
</button>
<button id="simulateBtn" type="button" class="admin-only">Imitate 1M Spins</button>
<button id="simulateBonusBtn" type="button" class="admin-only">Imitate 1M Bonus Rounds</button>
Expand Down Expand Up @@ -346,8 +355,8 @@ <h2 id="rulesTitle">Game Rules</h2>
<script src="engine/spin-engine.js?v=20260523-1"></script>
<script src="engine/simulator.js?v=20260523-1"></script>
<script src="engine/spin-lock.js?v=20260523-1"></script>
<script src="engine/audio.js?v=20260622-4"></script>
<script src="engine/ambiance.js?v=20260622-4"></script>
<script src="main.js?v=20260622-5"></script>
<script src="engine/audio.js?v=20260803-1"></script>
<script src="engine/ambiance.js?v=20260803-1"></script>
<script src="main.js?v=20260803-2"></script>
</body>
</html>
Loading
Loading