Skip to content
appsbynate
← games

Huss Valley

A top-down free-runner in Phaser 3 with a three-act story — no build step, no npm, and every level defined as a row of data.

Section
games
Year
2026
Role
Solo project
Stack
Phaser 3, JavaScript, ES Modules, Playwright

Summary

A top-down free-runner in Phaser 3 with a three-act story. The camera scrolls up, hussers pour in behind you, and you juke, punch and burn stamina on boosts to reach the far gate — with dialogue interludes between runs. No build step, no npm: Phaser from a CDN plus plain ES modules.

Story

A story game invites you to write a scene per level, and I wanted the whole arc playable without that. So the game is one ordered list of beats in data/story.js — dialogue or run — walked by a StoryDirector that owns the checkpoints. A run row defines its length, pace, obstacle mix, husser waves and spawn points, so a new level is a new row rather than a new file in scenes/. Numbers live in data, behaviour lives in code. The hard part was state that outlives the scene that created it: strangers you pick up carry a betrayChance that can flip them into a boss husser mid-run, which meant dialogue effects living on the choice rather than the line, and every outcome guarded against double-apply because two keydowns can land inside one frame. Phaser had its own traps — scene.start(key) silently reusing the previous run’s init data, create-time clock math comparing against a different base, physics.add.group() clobbering child body settings.

Lessons

  • Every Phaser trap cost a debugging session, then became a rule in CLAUDE.md. Writing them down is the only reason they cost one session each.
  • Scenes that call each other can’t be restarted cleanly. An event bus, one gameState singleton and a director make “restart this run” mean something.
  • With no unit tests, Playwright driving the real game against exposed scene state is a real substitute — two journeys stay green.