← back to Wild Orbs Opus
README.md
116 lines
# WILD ORBS
Asteroids' skeleton, except every orb detonates into a **different disaster** when you
shoot it. One self-contained HTML file — no assets, no CDNs, no build step, no network.
Open `index.html` in any modern browser.
> **Arena provenance.** This is the **Claude Opus** entry: a rebuild from the Claude
> Opus Model-Arena artifact for the "Wild Orbs" challenge. A separate session ran the
> *same* challenge starting from **Qwen3 14B's** artifact; that entry lives in
> `~/Projects/wild-orbs` and is untouched. The two are parallel arena entries, not
> competing versions of one project — neither supersedes the other. `db229b2` appears
> in this repo's history only because this branch was originally cut from that repo;
> its content is unrelated to this build.
## Controls
| | |
|---|---|
| `←` `→` / `A` `D` | rotate |
| `↑` / `W` | thrust |
| `↓` / `S` | brake |
| `SPACE` | fire (hold to auto-fire) |
| `SHIFT` | pulse bomb — clears the room |
| `H` | hyperspace — teleport to the safest open space |
| `P` / `ESC` | pause · `M` mute · `K` screen shake |
Also supports **gamepad** (stick to steer, A thrust, X fire, B hyperspace, Y pulse) and
**touch** — left thumb is a virtual stick that steers *and* thrusts, right side fires,
plus on-screen PULSE / WARP buttons.
## The thirteen wild orbs
| Orb | What shooting it does |
|---|---|
| **SWARM** | bursts into homing hunters that chase your hull |
| **GRAVITY WELL** | collapses into a well that drags your ship *and your bullets* |
| **PHASE** | blinks to a new position on every non-lethal hit — 3 tags to kill |
| **VOID** | opens a black hole that eats orbs, bullets and you |
| **CHAOS** | reverses your steering for six seconds |
| **SHARD** | shatters into ricocheting glass that kills on contact |
| **CHRONO** | drops a slow-time bubble; everything inside crawls |
| **CHAIN BOMB** | detonates every orb nearby on a staggered fuse — cascades hard |
| **MIRROR** | clones itself twice (twice), then finally breaks |
| **RUBBER** | never wraps — bounces off the walls 50% faster on every hit, 5 hits to pop |
| **NOVA** | erupts a shockwave ring that shatters whatever it crosses — and chains into other novas |
| **SENTINEL** | deploys tracking turrets that shoot back |
| **WARP** | rips space open and swaps *your ship* into the orb's position |
**Elder orbs** (tier 4) appear every 5th wave: quadruple size, huge HP, and their wild
effect fires at double strength. They always drop a powerup.
## Powerups
`W` spread shot · `R` rapid fire · `S` absorb-one-hit shield · `P` +1 pulse charge · `1` extra life
## What changed from the starting artifact
**Correctness**
- `continue` inside a nested `for…of` over black holes continued the *inner* loop, so a
bullet was spliced and then execution fell through using the freed object — later
splices then removed the wrong bullet.
- Respawn ran on `setTimeout`, so the ship could respawn *while paused* and a pending
timer could fire across a restart. Now frame-counted.
- Ship trail drew straight across the screen every time the ship crossed a wrap seam.
- Orbs, ship, bullets, hunters and pickups popped in and out at the screen edges — they
now render wrap-aware duplicates, matching the wrap-aware collision that was already there.
- Rubber orbs spawned off-screen and got snapped to the border on frame one.
- `if(!byChain) G.combo++; G.comboT = 70;` — a brace-less `if` that made the second
statement unconditional.
- Wave-clear could fire before the guard cleared, and `toast`/wave-banner state leaked
across restarts.
- Orb velocity had no ceiling, so wells + shockwaves + rubber boosts could stack an orb
past collision resolution.
- `nova` had a hand-written sound effect but no orb type to trigger it.
**Robustness**
- Auto-pause on tab blur / visibility change.
- A voice budget on the synth so a 20-orb chain reaction can't detonate the mixer.
- Adaptive particle cap and **dynamic resolution scaling** — the game is fill-rate bound,
so instead of dropping frames on weak hardware it scales the backing store and holds 60fps.
- `prefers-reduced-motion` respected; shake/flash toggleable and persisted.
**Content added**
Three new orb types (NOVA, SENTINEL, WARP), elder/boss orbs, five powerups, pulse bomb,
hyperspace, shockwave physics, turret enemies, end-of-run stats (orbs broken, best chain,
accuracy, time survived), persistent best score + furthest wave, pause menu with the
bestiary, touch and gamepad support.
## Tests
```bash
npm test # headless: 57 assertions
npm run test:browsers # Chromium + Firefox + WebKit + emulated-phone touch
npm run test:headed # watch it run, writes screenshots to test/shots/
npm run profile # per-phase render timings + sustained fps
```
The suite drives the real game in Chromium: it detonates **every orb type at every tier**
(including elders), runs a 24-orb chain cascade, exercises pulse/hyperspace/powerups,
pause, wave progression, death, respawn, game over, restart, and resize — and fails on any
console error or uncaught exception. The framerate assertion measures the environment's
own rAF ceiling first, because headed Playwright is vsync-capped at 30fps and headless
uses a software rasteriser; a hardcoded fps number would be meaningless in both.
`test/browsers.mjs` boots the real game in all three engines — Blink, Gecko and
WebKit — plays it, detonates all 13 behaviours in each, checks the WebAudio graph
and `localStorage` path, then runs a touch pass on an emulated iPhone: virtual
stick steers and thrusts, right side fires, the on-screen PULSE button works, and
the thumb controls stay hidden for mouse users. All green in every engine.
Playwright is resolved from the global install via symlinks in `node_modules/`
(gitignored); `npm i -D playwright` also works.