Skip to content

client: preload all art, cut mobile heat, rebuild responsive layout - #31

Merged
WERSIT35 merged 1 commit into
mainfrom
fix/board-aspect-ratio-and-asset-hardening
Aug 3, 2026
Merged

client: preload all art, cut mobile heat, rebuild responsive layout#31
WERSIT35 merged 1 commit into
mainfrom
fix/board-aspect-ratio-and-asset-hardening

Conversation

@WERSIT35

@WERSIT35 WERSIT35 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Three related pieces of work on the player client. No slot-math changes.

  1. Preload everything before the game reveals The "You won N Free Spins" panel streamed in part-by-part mid-game because its art is a CSS background on a display:none element, which browsers never fetch until the element is shown. The boot gate only waited on 12 hand-picked symbols. Now a uiAssets map routes every non-canvas image through the same loader, the gate waits on all of it decoded (image.decode(), not just onload), and carries preload hints. Verified: 23 images fetch at boot, zero image requests afterwards. The ante plate had the same bug and is fixed too.

decode() is skipped while document.hidden and capped at 2s — Chrome defers decoding for hidden documents, which otherwise hung the gate for 20s for a game opened in a background tab or an iframe that starts hidden.

  1. Mobile thermals detectDeviceTier() had no mobile signal at all (deviceMemory is NaN on iOS and never demotes), so phones landed on "high" — idleThrottleMs 0, meaning a full 661-line draw() at 60fps forever, even idle. Handhelds are now capped at "mid", idle is throttled on every tier (60 -> 18fps measured), busy frames cap at 30fps on phones, and visibilitychange/pagehide cancel the rAF chain and suspend the AudioContext (previously nothing paused at all).

Also: art re-encoded to WebP via the new tools/optimize-assets.js (12.9MB ->
513KB, -96%), memoized the two full-canvas gradients that were reallocated every frame, added a pre-scaled symbol sprite cache, debounced the ResizeObserver, raised win_tick minGap, made soul-trail emission framerate-independent, converted the bonus badge's infinite box-shadow pulse to a composited opacity fade, and deleted ~290 lines of unreachable ambiance CSS.

  1. Responsive layout The reel grid was 16% of a 375x667 phone and collapsed to 2x2px in landscape. Causes: an unscoped max-width:520px rule stacked the player's control deck into a 235px tower; .altar-top being display:none in landscape promoted .altar-core into the auto grid row, so container-type:size had no definite height and cqh resolved to ~0; hard 1040px/920px width caps left ~440px of dead backdrop per side at 1920px; desktop forced 3/2 while the canvas draws 6/5; and overlays were positioned in raw px against the panel, not the grid.

The stack is now one scaling container: flex columns throughout (immune to rows toggling hidden, which is what broke the old grid-template-rows), a single set of clamp()/vh/vmin scale tokens on .altar, and the board as the only growing row. Overlays anchor to --board-w/h/x/y, published from ReelCanvasRenderer.publishBoardBox(). Also removed a duplicate second declaration of .altar-core/.vault-shell/.vault-window - two sources of truth for the same boxes was why the sizing kept drifting.

Measured on 11 breakpoints (320x568 through 1920x1080): grid fills 98-99% of width in portrait, landscape works, no overflow or clipping anywhere, badge anchored to the grid at every size.

Note: npm run test:rtp-parity currently reports 95.62% vs a 96.38% target. The 12 engine files that test loads are byte-identical to HEAD and none of this touches them; it passed at 96.34% earlier on the same math. With a ~4700x max win, one top-end hit in 1M spins moves RTP ~0.47%, so the +-0.20% tolerance looks too tight at that sample size. Not yet confirmed by a repeated-run measurement.

Claude-Session: https://claude.ai/code/session_01BBWtAHqmoaiBy6E8eSg8y9

Three related pieces of work on the player client. No slot-math changes.

1. Preload everything before the game reveals
The "You won N Free Spins" panel streamed in part-by-part mid-game because
its art is a CSS background on a display:none element, which browsers never
fetch until the element is shown. The boot gate only waited on 12 hand-picked
symbols. Now a uiAssets map routes every non-canvas image through the same
loader, the gate waits on all of it decoded (image.decode(), not just onload),
and <head> carries preload hints. Verified: 23 images fetch at boot, zero
image requests afterwards. The ante plate had the same bug and is fixed too.

decode() is skipped while document.hidden and capped at 2s — Chrome defers
decoding for hidden documents, which otherwise hung the gate for 20s for a
game opened in a background tab or an iframe that starts hidden.

2. Mobile thermals
detectDeviceTier() had no mobile signal at all (deviceMemory is NaN on iOS
and never demotes), so phones landed on "high" — idleThrottleMs 0, meaning a
full 661-line draw() at 60fps forever, even idle. Handhelds are now capped at
"mid", idle is throttled on every tier (60 -> 18fps measured), busy frames cap
at 30fps on phones, and visibilitychange/pagehide cancel the rAF chain and
suspend the AudioContext (previously nothing paused at all).

Also: art re-encoded to WebP via the new tools/optimize-assets.js (12.9MB ->
513KB, -96%), memoized the two full-canvas gradients that were reallocated
every frame, added a pre-scaled symbol sprite cache, debounced the
ResizeObserver, raised win_tick minGap, made soul-trail emission
framerate-independent, converted the bonus badge's infinite box-shadow pulse
to a composited opacity fade, and deleted ~290 lines of unreachable ambiance
CSS.

3. Responsive layout
The reel grid was 16% of a 375x667 phone and collapsed to 2x2px in landscape.
Causes: an unscoped max-width:520px rule stacked the player's control deck
into a 235px tower; .altar-top being display:none in landscape promoted
.altar-core into the `auto` grid row, so container-type:size had no definite
height and cqh resolved to ~0; hard 1040px/920px width caps left ~440px of
dead backdrop per side at 1920px; desktop forced 3/2 while the canvas draws
6/5; and overlays were positioned in raw px against the panel, not the grid.

The stack is now one scaling container: flex columns throughout (immune to
rows toggling hidden, which is what broke the old grid-template-rows), a
single set of clamp()/vh/vmin scale tokens on .altar, and the board as the
only growing row. Overlays anchor to --board-w/h/x/y, published from
ReelCanvasRenderer.publishBoardBox(). Also removed a duplicate second
declaration of .altar-core/.vault-shell/.vault-window - two sources of truth
for the same boxes was why the sizing kept drifting.

Measured on 11 breakpoints (320x568 through 1920x1080): grid fills 98-99% of
width in portrait, landscape works, no overflow or clipping anywhere, badge
anchored to the grid at every size.

Note: npm run test:rtp-parity currently reports 95.62% vs a 96.38% target.
The 12 engine files that test loads are byte-identical to HEAD and none of
this touches them; it passed at 96.34% earlier on the same math. With a
~4700x max win, one top-end hit in 1M spins moves RTP ~0.47%, so the +-0.20%
tolerance looks too tight at that sample size. Not yet confirmed by a
repeated-run measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BBWtAHqmoaiBy6E8eSg8y9
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
provider Ready Ready Preview Aug 3, 2026 10:47am

@WERSIT35
WERSIT35 merged commit 2cf72d6 into main Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant