Because client work isn't the only reason to write code

We do this for a living — Laravel apps, product refactors, the usual grown-up stuff. That doesn't mean every line of code has to bill a client.

Galactic Strike is an in-house space shooter we built for the joy of it: Vite + TypeScript + Phaser, lasers, bosses, the works. You can play it here. It started as something fun on the side and turned into a proper excuse to keep our devs fresh — same stack muscles we use day to day, zero deadline pressure.

Doing serious work all the time is how skills go stale. A small game forces you to read unfamiliar APIs, refactor when things get messy, and actually run the build before you ship — without a stakeholder watching the burndown chart.

Galactic Strike title screen

Same habits, different canvas

Even a silly shooter maps cleanly onto how we think about product code:

Day job habit Galactic Strike repo
Routes Scene registration in src/main.ts
Controllers Phaser scenes (GameScene, StartScene, …)
Models / entities src/entities/
Config src/config.ts
Services SpawnController.ts, CollisionService.ts
Public assets public/assets/

It is the same discipline — thin scenes, extracted services, config you can tune without spelunking — just with more explosions than your average CRM.

The runnable app lives in our academy repo (forked from project-space-shooter) so anyone on the team — or anyone learning with us — can clone it, break it, and fix it.

Refactoring because we couldn't leave it ugly

Early GameScene code owned everything — spawn timers, waves, boss triggers, every overlap handler. Classic god controller. Even on a fun project, that gets annoying fast.

So we pulled the same move we use on client code:

SpawnController owns spawn timers, wave counter, boss spawn, and the boss reference. GameScene creates it in create() and calls update() each frame.

CollisionService owns overlap handlers — bullets, shields, ricochet, ship collisions. GameScene delegates to collisionService.update() after enemy updates.

Same lesson as avoiding controller bloat in Laravel. Different runtime. Still satisfying.

Galactic Strike gameplay

Staying sharp without burning out

The point isn't to become game developers. The point is to stay curious — read a real TypeScript repo, extract a service class, write a test that doesn't boot the whole UI.

Vitest covers pure game logic and services without booting Phaser — damage rules, spawn thresholds, config branches. Framework at the edges, logic you can actually test. That reflex carries straight back to client work on Monday morning.

cd php-to-js/16-space-shooter
npm install
npm run dev    # local play
npm test       # Vitest
npm run build  # dist/ for static host

Output lands in dist/; assets under public/ copy into the build. Even side projects deserve a clean deploy.

Galactic Strike in action

Adding a new enemy (when you can't stop tinkering)

Once you're in, it's hard not to add one more thing. The README spells out a repeatable path:

  1. Create a class in src/entities/enemies/ extending BaseEnemy.
  2. Preload the texture in LoadingScene.ts.
  3. Add a spawn path in SpawnController.
  4. Handle score branches in CollisionService.
  5. Add a constant under SCORE_CONFIG in config.ts.
  6. Export from src/entities/enemies/index.ts.

Entity, asset, spawn rule, collision outcome, config, barrel export — a vertical slice, testable at each step. Fun project; serious craft.

Play it, poke around, steal the pattern

We build things like this in-house because learning shouldn't only happen on someone else's invoice — and because blowing up pixel ships is a perfectly valid way to keep your edge.

If you want a studio that ships client work and knows how to stay sharp between projects, book a free systems review or see what we offer.