Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RETRO BALL

Game expansion: Specification · Phased implementation checklist.

Play RETRO BALL in your browser

image image image

A multi-course, isometric marble game in the spirit of Marble Madness, rebuilt as a Synthwave / Outrun fever dream: glowing grid slabs floating in a purple nebula, sweeping laser grids, data voids, light-elevators, jump pads, VCR scanlines and a synth soundtrack whose kick drum drives the glow.

npm install
npm run dev        # http://localhost:5173/retro-ball/
npm run build      # type-checks, then bundles to dist/
Key Action
Arrows / WASD Roll (relative to the isometric camera)
R Reboot at the last checkpoint / re-run
M Mute
F (or ⛶) Enter / exit browser fullscreen
Esc (or ⚙) Open / close the settings panel
Space Boot from the intro screen

Screen-up pushes the marble "into" the screen (world -x, -z), so following a corridor that runs up-right means holding Up + Right, exactly as in the original arcade cabinet's trackball logic.


Why these libraries

Rendering: Three.js (WebGL 2) + custom GLSL

The brief asked for a fixed isometric orthographic view, neon wireframe / translucent grid surfaces, chromatic bloom, VCR artefacts and a marble that lights the track. That list is a post-processing pipeline plus a handful of bespoke shaders, and Three.js is the best fit for exactly that:

  • OrthographicCamera with a fixed direction (src/render/Renderer.ts) gives the true isometric look. The camera never rotates, so player input can be mapped through a constant ground basis (groundBasis()), which is what makes controls feel predictable.
  • EffectComposer + UnrealBloomPass + a custom ShaderPass (src/render/PostFX.ts). Bloom provides the soft neon halo; the VCR pass adds radial chromatic aberration, scanlines, an aperture grille, tracking slips, block glitches (spiked on boot, death and win), a rolling VHS band, grain, vignette and flicker. OutputPass performs ACES tone mapping last so bright neon cores burn toward white the way real phosphor does.
  • Bespoke ShaderMaterials instead of stock PBR for the level. The grid material (src/render/GridMaterial.ts) draws anti-aliased 1-tile grid lines from a per-vertex tile-space attribute, tints top faces differently from sides, adds a slow travelling light band, and, crucially, adds a distance-based glow from a uMarblePos uniform so the marble's pink light visibly washes over the slabs beneath it. Doing this in the shader is cheaper and far more controllable than relying on a point light alone (a point light is still attached to the marble for the chrome shell itself).
  • Fat lines (LineSegments2 / LineMaterial) for slab edges. WebGL's native lines are 1 px; the addons' screen-space lines give crisp 2 px neon edges that bloom evenly at any resolution.
  • Geometry merging. Static pieces are merged into one draw call per colour tone, and their 12 edges each into one fat-line batch. A 60-piece course renders in a handful of draw calls, leaving the frame budget for the full-screen bloom chain.
  • PMREM environment for the marble. buildNeonEnvironment() bakes a tiny scene of pink/cyan light strips into a prefiltered environment map so the chrome marble reflects neon bands instead of a flat colour.

Babylon.js would also have worked, but its strengths (a full scene editor pipeline, node materials, built-in glow layers) are heavier than needed for a single hand-authored track, and Three.js's post-processing addons made the VCR/bloom look a two-file job. WebGPU was ruled out for portability.

Physics: Rapier (@dimforge/rapier3d-compat)

The marble has to feel like heavy chrome, move fast, bounce predictably and never tunnel through a thin ledge at 20+ m/s. Rapier was chosen over cannon-es / Ammo / a hand-rolled solver because:

  • Continuous collision detection is a one-liner (setCcdEnabled(true)), and the marble is small and fast, which is precisely the tunnelling case.
  • Rust-compiled WebAssembly solver with a fixed 120 Hz sub-step (src/physics/Physics.ts) keeps contact response stable on ramps and edges regardless of the display refresh rate.
  • Kinematic position-based bodies make the light-elevators trivial: the platform is moved each step with setNextKinematicTranslation and friction carries the marble up naturally.
  • Restitution combine rules let walls bounce harder than floors (Max combine rule, 0.75 on bumpers vs 0.35 on track) so chicane rebounds feel arcade-like while landings stay grounded.
  • The -compat build inlines the WASM as base64, so npm run dev works with no special bundler configuration.

Physics tuning (the "arcade feel")

All numbers live in TUNING (src/physics/Physics.ts):

  • Gravity is -36 (about 3.7 g). Heavier gravity makes ramps and landings read as weighty and keeps jump arcs short and readable in an isometric view.
  • Input applies an impulse at the centre of mass plus a matching torque, so the sphere both slides and spins up; friction converts one into the other and the result is ~20 m/s² of effective acceleration, i.e. about a second from rest to top speed. Air control is about a third of that.
  • A soft speed cap only cancels the component of input that would push the marble beyond maxSpeed (16 u/s). Braking and steering always stay available, and downhill ramps can still exceed the cap, which is where the "the marble feels fast" moments come from.
  • Moderate linear damping (0.25) and angular damping (0.35) produce a heavy coast-down that still settles when you release the keys.
  • Jump pads solve the ballistic launch analytically for their target so every launch lands on the same spot, no matter the entry speed.

Level: the circuit

src/content/levels/legacy.json stores the original course, with src/game/LevelData.ts retained as its regression fixture. The shared document format describes courses declaratively (slabs, ramps, walls, jump pads, elevators, lasers, voids, checkpoints, goal). The route winds through roughly 50 × 65 tiles, several screens in each direction:

  1. Boot sector with a bumper chicane, then a ramp onto the laser plateau (one sweeping beam that parks, safe and blue, at each end of its sweep).
  2. A railed pink ledge with a jog, then checkpoint A and a jump pad over a void.
  3. A corridor to light-elevator 1, a high platform with checkpoint B and a 20-tile downhill speed ramp.
  4. The void field: a wide slab with data voids on its outer lanes. The centre lanes are clear, so the ramp exit is a straight brake run.
  5. A railed ledge to checkpoint C, a timed laser gate, and light-elevator 2.
  6. The elevated skyway (checkpoint D at its start) through three timed laser gates that open in travel order, a second jump pad onto a floating island with checkpoint E, and a walled drop ramp.
  7. A final railed S-bend of ledges, one last sweeping laser, and the GOAL portal.

Difficulty notes: ledges are at least 3 tiles wide (marble diameter is 1) and carry low bumper rails on every open edge, so a drift is a bounce rather than a fall. Lasers are always visible: a live beam burns red, a safe beam turns cool blue, and a beam flickers from blue toward red for 0.5 s before it goes live. Sweeping lasers park, safe, for 2.5 s at each end of their sweep (dwell), which is the crossing window. Gated lasers are safe for 2.8 s of every 4 s. Jump pads solve their own trajectory and their trigger is wider than the pad graphic. Elevators are solid light columns (the shaft is never an open hole). The reference route is verified end to end by the dev autopilot (see below).

Falling off, touching a live beam, or dropping into a void triggers a glitch-burst reset at the last of five checkpoints. The system clock keeps running, so resets cost time.

Audio

The soundtrack, Retro Ball (Main Theme), is an original composition written for this project and dedicated to the public domain (CC0). Rather than ship an MP3, src/audio/Soundtrack.ts performs it live with the Web Audio API: a 112 BPM, eight-bar A-minor loop (Am F C G | Am F Dm E) with a four-on-the-floor kick, claps, hats, a side-chained octave bass, a plucked 16th-note arpeggio through a dotted-eighth delay, detuned saw pads through a convolution reverb, and a lead phrase over the second half of the loop.

Because the sequencer schedules every kick itself, the exact kick times are known in advance. beatEnergy() returns a value that spikes to 1 on each kick and decays exponentially, and it drives:

  • grid-line brightness and edge opacity of every slab,
  • bloom strength and chromatic aberration,
  • the marble's core, halo and point light,
  • jump-pad rings, elevator beams, the goal portal and the nebula.

Sound effects (src/audio/Sfx.ts) are synthesised on the same graph: speed-driven rolling noise and hum, impacts scaled by the velocity change, jump-pad sweeps, per-hazard death sounds, a checkpoint chime and a win arpeggio. Audio unlocks when Space starts the game (browser autoplay policy).

Settings has independent Music and Sound FX switches, saved across reloads. M temporarily mutes both without changing those choices; unmuting restores only enabled categories. Music can be off while the visual beat continues.

Project layout

src/
  main.ts               bootstrap + frame loop
  game/
    LevelData.ts        declarative level definition
    Level.ts            static geometry (merged grid meshes + fat-line edges)
    Dynamics.ts         jump pads, elevators, lasers, voids, checkpoints, goal
    Game.ts             state machine: intro -> play -> reset -> win
    Marble.ts           chrome shell, core, halo, light, trail, env map
    Burst.ts            neon shard particles for death/win
  physics/Physics.ts    Rapier wrapper + tuning constants
  render/
    Renderer.ts         isometric orthographic camera
    GridMaterial.ts     neon grid surface shader
    PostFX.ts           bloom + VCR pass (MSAA target, bloom toggle)
    Background.ts       nebula (optionally offscreen), floor grid, stars, data shards
  settings/Settings.ts  quality settings + localStorage persistence
  audio/                procedural soundtrack, SFX, glue
  input/Input.ts        keyboard -> screen-space axis
  ui/                   HUD, overlays, settings panel
  debug/Autopilot.ts    dev-only waypoint driver used for automated play-tests

Dev hooks

In development builds window.__retro exposes app and the active session’s game, input, autopilot, debug and applySettings. First load a course through the relay or await __retro.app.loadLevel('legacy'); no session exists while at the hub. autopilot.start() drives the marble around the reference route (used to verify the course is completable end to end); debug.stepsPerFrame / debug.fixedDt run the simulation faster than real time for headless testing; game.godMode ignores hazard deaths.

Regression checks

npm test (Node 22.6 or later) checks slab overlap, bend footprints, void clearance and the void field's floor coverage. npm run build checks TypeScript and the production bundle.

For GPU checks, start the dev server, then use an installed agent-browser:

agent-browser --session retro-check open http://127.0.0.1:5173/retro-ball/
agent-browser --session retro-check wait --fn '!!window.__retro'
agent-browser --session retro-check eval "window.__retro.app.loadLevel('legacy')"
agent-browser --session retro-check eval --stdin < tests/rendering.browser.js
agent-browser --session retro-check eval --stdin < tests/voids.browser.js

The browser check reads the HDR scene and post-processing buffers at every checkpoint, elevator and the goal, with antialiasing and bloom on and off. It fails on non-finite pixel values or WebGL errors. Inspect both bends while moving the camera as well: pixel validity alone does not detect depth flicker. The void check drops the marble into each opening from five positions and verifies a void death followed by a respawn at checkpoint B.

For the full rendering/audio/void/course suite, run npm run test:browser. For recorded frame/physics/resource measurements, run npm run benchmark:browser. Both require the dev server and installed agent-browser. See the phase-0 baseline for configuration, budgets and evidence.

Phase 1 runtime and content contracts describe the relay selector, course lifecycle, validation, component factories and checkpoint snapshots. P pauses; the course toolbar provides retry and return-to-relay.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages