fahad.ibrahim
← All Case Studies
Case Study · MIT · Public repo

EMBERFALL — a zero-build arcade shooter where grazing bullets builds your score

A bullet-hell that inverts the usual risk model: the closer you fly to incoming fire, the more you score. Eight worlds, five named bosses with multi-phase attack patterns, procedural audio, and an offline PWA — shipped without a bundler and without a single runtime dependency.

Vanilla JS Canvas 2D WebAudio 0 runtime deps Offline PWA MIT
EMBERFALL gameplay showing a bullet curtain with the graze combo meter active
8
Worlds
5
Named Bosses
16
E2E Specs
46
CI Steps
⚡ Key Decisions

Graze instead of dodge: the core loop rewards proximity, so the player's instinct is inverted and the skill ceiling rises. Deterministic simulation core: combat math is extracted into a pure module pinned by golden vectors, so balance changes are verifiable. No bundler: the deployed artifact is the source, which removes a whole class of build-drift bugs. Offline-first: a service worker precaches the whole shell so the game runs with no network at all. Honest scope: leaderboards and a global mirror are built but not promoted until the backend is live.

01 The challenge

Bullet hells are usually about not getting hit. I wanted a game where the safest play is the worst play.

EMBERFALL is a graze-to-score shooter. Enemy fire that passes close to your ship without connecting registers a graze, and grazes feed a heat meter that multiplies your score. Flying into the bullet curtain is the correct strategy, which means the player's real skill is judging distance under pressure rather than memorising a safe lane.

The second constraint was shipping. A browser game that needs a build step, a package tree, and a deploy pipeline can still be zero-dependency, but it introduces a failure surface that has nothing to do with the game. The requirement was that the shipped file be the source, with no compilation between writing code and playing it.

02 The graze mechanic

Two meters, one clock, and a decay curve that punishes camping.

Why decay is separate

Heat decaying inside the combo window meant the multiplier froze whenever the window closed, and every late-run graze overpaid. Moving decay onto its own timer fixed a whole class of scoring edge cases, and the behaviour is pinned by a unit test rather than left to a playtester's memory.

03 Eight worlds, five bosses

Difficulty ramps on a fixed wave schedule, with bosses arriving on a director cycle rather than a hand-placed trigger.

ElementDesign
WorldsEight, entered on a fixed wave progression (4, 9, 14, 20, 26, 33, 40, 48) so the difficulty curve is the same for every run
BossesFive named encounters, each with its own art, hitbox, health pool, and score value
PhasesBosses gain phases as they lose health; later phases add attack patterns rather than only raising damage
PatternsRotating spiral curtains, sweeping cross-beams, and node-based turrets on multi-part bosses
DirectorA wave director owns spawn timing and composition, so encounters are composed rather than hand-scripted

Boss order and stat relationships are asserted directly in the test suite, including a check that the final boss outstats its sibling. Balance is a contract, not a hope.

04 Deterministic simulation core

The part of the game most likely to break is the part that decides whether you lived. So it was extracted and made pure.

Canvas + WebAudio
Input / Render
Presentation
→
packages/sim
Pure combat math
Seeded RNG
→
CI Gates
Golden vectors
Headless tests

EMBERFALL: presentation → pure simulation → verified core

05 The CI gate chain

Forty-six steps, and none of them exist to prove the build works. They exist to stop the build from lying.

The workflow runs as a series of independent gates, each of which can fail the run on its own. A game that only ships when every gate is green is a game whose balance, economy, and copy are all verifiable at merge time.

Why the gates are separate

One big test job tells you something broke. A chain of narrow gates tells you what broke, in minutes instead of an hour, and each gate names the class of mistake it exists to catch. The copy guard is the one I would keep if I had to delete everything else.

06 Zero build step, offline PWA, live boards

No bundler and no package tree. The game needs no network to play; the leaderboard is the one optional online feature.

Live global leaderboard

Global and daily leaderboards are live, with player accounts on Supabase. Players sign in with an email magic link and pick a callsign. Scores are written only through two database functions (submit_score and set_callsign), with row-level security on, score bounds checks and per-player rate limits; the public boards are read-only views. An earlier plan for a self-hosted game server was dropped before it was ever deployed. Scores are bounds-checked, not replayed on a server, so the boards are not claimed to be cheat-proof. Sign in with X is in progress and not live yet.

07 What I learned

Four lessons I carried into every project after this one.

Play EMBERFALL Source on GitHub All Case Studies