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
- claude
- html5
- javascript
- three.js
- web-audio-api
- webgl
Log in or sign up for Devpost to join the conversation.