Inspiration

Arcade shooters were legible in a way modern mobile games often aren't — you could read the whole screen at a glance. I wanted that legibility on a phone, but without the twitch execution, so it works on a commute with one hand free.

What it does

You command a single space station at the centre of the screen. Waves converge from every direction and your weapons fire automatically. You never aim. What you do is allocate: which of five weapons fill your two equipment slots, which of six upgrade tracks to buy with salvage earned from kills, and when to spend a charged Overdrive burst.

Each enemy archetype is a question your loadout answers. Armoured units want missiles, swarms want chain lightning, and fast tangential orbiters punish weapons with poor lead-aim. The loop is: survive the wave, bank salvage, re-spec, meet a harder one.

Automatic fire is the deliberate design choice. It moves your attention off execution and onto reading the wave and committing resources — which is what makes it playable one-handed, and what makes the idle framing honest rather than a euphemism for waiting.

How I built it

Prompt-built with Claude across every session: describe the behaviour or report the bug, let the model write the patch, run it, report back precisely what changed. Three.js r128 for flat orthographic sprite rendering, vanilla JavaScript, and a fully procedural Web Audio layer — no audio files anywhere, every sound synthesised at runtime. The whole game lived in a single HTML file until packaging.

Challenges I ran into

The orbit enemy fought me across two sessions. It wouldn't visually reverse direction despite the direction flag flipping correctly — the cause turned out to be a line downstream of the movement block overwriting sprite rotation to always face inward, so both directions looked identical. Then it turned out to be unhittable, because predictPos() was a stub returning the enemy's current position with no lead, so bullets always landed behind a fast tangential target.

The worst was a wave-lock infinite loop, traced to atan2(0,0) returning 0 for an enemy sitting exactly at the origin — which is precisely where an enemy is when it has just touched the bastion. It got flung to a fixed point where the weapons fired at it forever and the wave never advanced.

Accomplishments that I'm proud of

A complete session rather than a vertical slice: nine enemy archetypes with distinct movement, five weapons under a two-slot constraint, six upgrade tracks, a boss cadence, three lives, and a full procedural score — in a 192KB package with zero external requests.

What I learned

Almost every hard bug was later code silently overwriting state set earlier in the same frame. When a value isn't sticking, audit the whole per-frame pipeline instead of staring at the block that looks wrong.

What's next

Persistent meta-progression so a lost run still advances something, a true idle tier that earns while the game is closed, and a wave-modifier choice that trades difficulty for payout — turning escalation into a decision instead of a schedule.

Built With

Share this project:

Updates

Submission history