Skip to content

Hall: live trailer screens, spatial cabinet sound, warm marquees and a Hall of Fame - #63

Merged
dmccoystephenson merged 6 commits into
mainfrom
feat/hall-atmosphere
Oct 31, 2026
Merged

dmccoystephenson merged 6 commits into
mainfrom
feat/hall-atmosphere

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Makes the walkable hall feel alive: the nearest screens play their game's trailer, each nearby cabinet plays a soft attract loop placed in the room, the most played (and liked) games' marquees glow a little warmer, and a Hall of Fame board by the door lists the best score on each leaderboard. Four commits, each reviewable on its own.

Walking up to Tidewater: its screen plays the trailer

1. Live trailers on the screens (81aa57f, 068387b)

  • The screen of the cabinet nearest the visitor plays the game's trailer (silent, looping) instead of its poster, and goes back to the poster on walking away. Two screens at most on a desktop, one on a phone or in economy (at 10 fps there), only screens in front of the visitor and facing them, with a little hysteresis so the edge of range does not flicker.
  • None plays under reduced motion, while a game is open, while the page is hidden, or with ?trailers=0.
  • Clips: hall_assets/clips/<slug>.mp4, 29 clips, 1.4 MB in all (384x216, 15 fps, H.264, no audio, faststart), re-encoded by the new tools/make_hall_clips.py from danielstephenson.dev/trailers. They are served same-origin, like the posters, because that host sends no CORS header and a cross-origin video would taint the canvas. The CSP needed no change (default-src 'self' covers same-origin media).
  • Hall assets now answer single byte ranges (206/416, Accept-Ranges: bytes): Safari plays a video only from a server that does. Compressed variants are never ranged. server.byteRange() is unit-tested.
  • Frames reach the texture asynchronously through VideoFrame.copyTo({format: "RGBA"}) where available, so the hall's frame never stalls on a GPU readback. A synchronous canvas drawImage/getImageData was measured at 70–140 ms per frame under SwiftShader; the async path costs about 3–5 ms per upload. Elsewhere frames go through a small canvas.
  • No engine change: hall_texture already updates a same-sized texture in place and regenerates its mipmaps.

iPhone 13: Tidewater's trailer on its screen

2. Spatial attract-loop sound (ff708d9)

  • With Sound on (the existing button / "Arcade hum" setting; still off by default), the nearest cabinets (3, or 2 on a phone or in economy) each play a short chiptune phrase synthesised with WebAudio, with no audio files. The phrase is made up from the slug, so it is the same on every visit, and flavoured by the game: a tak text adventure gets a slow minor triangle-wave figure, a Halloween-tagged game a minor one, and the rest a brighter pulse, bell or bass arpeggio. Each plays two bars, then rests for two.
  • Placement: a soft inverse-square falloff (silent past ~11 m), panned by direction as the visitor turns, and quieter and low-passed from behind a cabinet. Everything pauses with the hum while a game is open or the page is hidden.
  • Measured through an analyser on the output in Chromium: at a cabinet the peaks reach about 0.07, against about 0.04 from across the hall (hum alone, RMS ~0.013). The loops are audible up close and fade to nothing far away.

3. Popularity in the room (0307d03)

  • Warm marquees: plays as a share of the most played, plus likes from arcade-social's public /v1/likes/counts, weighted by how many likes exist overall (up to a third, in full from 30 likes in all; with today's 3 likes they barely count). Only the top end warms: today that is env-lib-cpp (0.98), Tidewater (0.29), Beyond Nations (0.13) and FishE (0.05). Dark cabinets in a filtered hall stay dark.
  • Hall of Fame board on the near wall, right of the door and across from the counter (layout.json room.fame). It shows each leaderboard's best score (player, game, board, value; at most six, with the most-played games first), plus a "Still open:" line for boards nobody has topped yet. It reads /v1/games/<slug>/boards and then /v1/boards/<slug>/<board>?limit=1 without credentials, three at a time, 2.5 s after load, and keeps the public result in sessionStorage for 10 minutes.
Live data (one score so far) With sample scores (local only)

Marquees: env-lib-cpp, the most played, glows warmest

Verification

  • python3 -m pytest: 375 passed, 1 skipped (after merging main). New tests:
    • node tests hall_trailers_test.cjs (screen choice) and hall_fame_test.cjs (warmth, board rows, fetch and cache, service down), plus additions to hall_life_test.cjs (tunes, spatial).
    • pytest: clips are 16:9 384x216 faststart mp4s under 256 KB each; byte ranges; layout.json names only clips that exist; the Hall of Fame position.
  • Browser (tests/browser/hall_atmosphere_check.cjs, Chromium + SwiftShader; this setup forces economy). Desktop 1024x640, iPhone 13, economy, reduce motion and ?trailers=0 were all checked with no console errors. The trailer was shown in every profile except reduce motion and ?trailers=0, where none plays.
  • Frame rate A/B in SwiftShader on a shared, noisy box, three runs each, medians:
    • iPhone 13: 40–47 fps with everything on vs 36–49 off.
    • Desktop: 17–25 vs 21–22 off. The desktop shows 2 screens at 15 fps; with SwiftShader every texture upload and mip build runs on the CPU.

Notes for review / integration

  • Uses Hall: likes, top scores and achievements on the cabinet card #59 (merged) for <meta name="arcade-social"> and connect-src https://api.<domain>; origin/main (with Hall: likes, top scores and achievements on the cabinet card #59) is merged into this branch and the board and likes were re-checked against it with no shim. Without arcade-social the board reads "Scores are resting" and the marquees go by plays only.
  • A trial merge with feat/hall-people conflicts only at the end of tests/test_hall.py (both sides append; keep both).
  • The board makes about one request per deployed game (36 today) once per 10 minutes per tab. A single GET /v1/boards/top aggregate in arcade-social would reduce that to one request; it is left as a possible follow-up.
  • Open questions resolved toward privacy and economy: the board shows only display names that the public boards already show, with no new data held server-side. Reduced motion stops the trailers, not just the camera. Phones get one trailer at 10 fps and two voices.

drafted by Claude on behalf of Daniel Stephenson

🤖 Generated with Claude Code

dmccoystephenson and others added 6 commits October 30, 2026 18:58
The screen of the cabinet nearest the visitor (two on a desktop, one on a
phone or in economy), when it is in front of them and faces them, plays
the game's trailer, silent and looping, in place of its poster, and goes
back to the poster when they walk away. None plays under reduced motion,
while a game is open or the page is hidden, or with ?trailers=0.

Clips are hall_assets/clips/<slug>.mp4 (384x216, 15 fps, H.264, no sound,
faststart; 29 clips, 1.4 MB in all), re-encoded from danielstephenson.dev's
trailers by tools/make_hall_clips.py and served same-origin, since that
host sends no CORS header and a cross-origin video would taint the canvas.
Hall assets now answer single byte ranges (206/416), which Safari needs to
play a video. Frames reach the texture asynchronously through VideoFrame
copyTo where available (no stall of the hall's frame), else a small canvas.

drafted by Claude on behalf of Daniel Stephenson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYNeumobh3ySHVuHYQNDaR
With Sound on, the nearest cabinets (three; two on a phone or in economy)
each play a short chiptune phrase of their own, synthesised with WebAudio
like the hum (no audio files). The phrase is made up from the game's slug,
so it is the same every visit, and flavoured by the game: a tak text
adventure gets a slow minor triangle-wave figure, a Halloween game a minor
one, the rest a brighter pulse, bell or bass arpeggio; two bars on, two off.

Each loop is placed where its cabinet stands: louder near (a soft inverse
square, silent past about 11 m), panned to its side as the visitor turns,
and quieter and low-passed from behind the cabinet. The loops share the
hum's switch (the Sound button and the "Arcade hum" setting) and pause
with it while a game is open or the page is hidden.

drafted by Claude on behalf of Daniel Stephenson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYNeumobh3ySHVuHYQNDaR
… board

The marquees of the most played games glow a little warmer (redder and
brighter, with an amber glow above): plays as a share of the most played,
likes from arcade-social's public /v1/likes/counts weighing in as there
come to be more of them (up to a third, in full from 30 likes in all).
Only the top end warms; the middle of the field is unchanged, and a
filtered hall's dark cabinets stay dark.

A Hall of Fame board hangs on the wall right of the door, across from the
counter (layout.json room.fame): each game's leaderboards' best score
(player, game, board, value; at most six, most played games first) and the
games whose boards nobody has topped yet, drawn into one texture. It reads
arcade-social's public board routes without credentials, three at a time,
a moment after the hall loads, and keeps the public result in
sessionStorage for ten minutes. Without arcade-social (the
<meta name="arcade-social"> and connect-src that arcade PR #59 adds) or
when it is down, the board says the scores are resting and the marquees
go by plays alone.

drafted by Claude on behalf of Daniel Stephenson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYNeumobh3ySHVuHYQNDaR
Each video frame drawn to a screen is a texture upload (and its mipmaps);
on a phone or in the economy tier the screen now takes ten of the clip's
fifteen frames a second.

drafted by Claude on behalf of Daniel Stephenson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYNeumobh3ySHVuHYQNDaR
Both sides appended tests to the end of tests/test_hall.py; both kept.
With #59 the page names arcade-social and allows it in connect-src, so the
Hall of Fame board and the likes' warmth work without the check's shim.

drafted by Claude on behalf of Daniel Stephenson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYNeumobh3ySHVuHYQNDaR
@dmccoystephenson
dmccoystephenson merged commit fd9ba77 into main Oct 31, 2026
4 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