SHOOTER.PRG
WHAT IT IS.
A small Phaser 3 shooter I built to learn Phaser by reverse-engineering a single, very small idea: how does Galaga run so many waves with so little code? The answer turned out to be data, not code — a wave is a small set of curves, the curves are reused across stages, the enemies only need to know their position on a path and how long to hold it. The shooter's source is mostly that finding: level.js and paths.js carry the wave data and the path geometry, and gameEvents.js is the part that fires actions at scheduled times. The game itself is a 240×300 canvas (Phaser.Scale.FIT, width: 240, height: 300) — the small viewport is part of the study; it keeps the GPU budget honest so the wave logic has nowhere to hide.
I picked Galaga rather than Space Invaders because Galaga is the game where the wave is the design — the iconic enemy dives, the bonus-flag ship that splits off and rejoins, the formation-easing curves are the game. The shooter copies none of those directly. What I wanted to study was the engine underneath them: how few sprites you really need, how a path system can carry a hundred wave variations from a handful of Bezier curves, how the action scheduler that times enemy arrivals has to be its own module rather than buried in the scene. Everything else in the build — the parallax title screen, the joystix pixel font, the high-score strip — is scaffolding I added while I was in there.
THE ARC.
Two months of focused build, then three returns across three years. initial commit lands 2021-02-14 with one scene and a single enemy type. Two days of shot firing, refined collider, the alien-killing bullet, and then enemy ships moving around (with the first explosion.png). Working on paths (April 10, 2021) is the commit I went looking for: three docs/path_*.pdf files are checked in as the wave-geometry scratchwork — paths I drew by hand to convince myself that the same four curves could carry most of a stage. The following commits (paths, refactored, Single ship wave, Menu and gameover, Basic game working) turn that geometry into running code, with formation.js extracting the 48-enemy 6×8 grid setup. By April 11, 2021 the game plays end-to-end. The remaining 2021 commits are visual polish: parallax backdrop, aseprite-source art, the joystix monospace pixel font for the score strip.
Seventeen months of silence. December 2022 (Moved to parcel) is the first return: webpack, Babel and Jest all gone, art in art/.aseprite, the canvas resolution halved. January 2023 (Removed React, Updated to latest NPMs, Replaced sprites) is the second: the canvas game didn't need a UI tree, the sprite set is rotated to a Galaga-styled palette (art/galaga-.aseprite), and level.js becomes the file that owns the wave definitions. June 2024 is the third: Replaced parcel with esbuild is the bundler swap, then Refactoring shots and collision lifts paths.js out as its own module (163 lines), Accounting player lives adds the 252-line player-lives state, and the commit pattern that follows — a 177-line accounting commit, then a 3-line Fixing lives count — is the engineering move I came to trust: large rewrite followed by surgical delta. The closing commits (Refactored into distinct files, refactoring) split game.js into gameSetup.js, gameUpdate.js, gameEvents.js, gameScene.js, with coordinator.js as a reusable action scheduler (add_action(time, action) pushes [id, accumulated_ms, action], poll(delta) fires expired ones). 32 commits total, 9 of them on the wave/path layer.
Stack chips: Phaser 3.55 · esbuild · esbuild-copy-static-files · Parcel (past) · Webpack/Babel (past) · joystix monospace pixel font · aseprite source art.
WHAT I LEARNED.
A small canvas is a feature. Running at 240×300 with
Phaser.Scale.FITkeeps the wave code honest — anything expensive shows up on screen at this resolution, and you can't hide bad logic behind a fast GPU.The wave is data, not code. A small set of Bezier curves reused across stages carries more variation than a big set of scripts, and
paths.jsis the only file that has to grow as more waves are added. Everything else stays the same.A scheduler for timed actions is its own module. The first version had enemy arrival buried in scene code; lifting it into
coordinator.js(orgameEvents.jsin the second iteration) made the wave timing legible in one place. The cost was one extra file; the payoff was the bug class where two timed events overlap going away.Build systems are interchangeable; the game outlives them. Each of
webpack → parcel → esbuildwas a return-day refactor, not a rewrite — the gameplay state survived all three.Returning to old code is cheap if the dependency surface stayed small. The whole build pipeline is now a 21-line
esbuild-build.js, and the game's state lives in one Phaser scene. That combination is why the third return (June 2024) ended in a refactor rather than a re-implementation.