Skip to content

Project · 2026

Towerpower

A browser-based tower defense game with a campaign mode, a roguelite survival mode, and a Fastify API backing auth and persistent progression.

Role: Creator
  • TypeScript
  • Phaser 3
  • Vite
  • Fastify
  • Drizzle ORM
  • SQLite
  • Zod
  • Vitest
  • Playwright
  • pnpm
  • Turborepo

Project snapshot

Tower Power title screen with campaign, roguelite, squad, and leaderboard menu options.
Live deployment screenshot.
Status
Live and actively developed toward a Chapter 1 beta slice.
Modes
Server-authoritative campaign progression plus a local roguelite survival loop.
Backend
Fastify API with Drizzle-managed SQLite, guest/full-account auth, Google OAuth, leaderboards, and stage sessions.
Validation
Shared Zod schemas validate level configs, boot flow, and run-completion submissions across client and server.

Technical proof

  • Phaser 3 client and Fastify API live in a pnpm/Turborepo monorepo with shared contracts.
  • Campaign V2 combines authored grid maps, terrain contracts, operator roles, and schema-validated JSON content.
  • Test and tooling coverage includes Vitest, Playwright, content validation, art sync, determinism checks, and boundary enforcement.

In the app

Operation prep screen showing the stage grid, lane routes, wave preview, and operator squad.
Operation prep — grid map with lane routes, wave preview, and the deployed squad.
Mid-mission gameplay on a night-cycle map with campfires and the operator bar ready to deploy.
Mid-mission — night cycle in effect, wave 2 of 6, operators ready to deploy.

What it is

Towerpower is a browser-based tower defense game built around two distinct play modes. The campaign mode runs fixed-grid maps where the player deploys a squad of named operators to hold lanes against wave-based enemy spawns. Progress is server-authoritative for authenticated users — gold, unlocked operators, stars, and high scores are stored server-side, with client and server scores merged by best value. The roguelite mode is a self-contained survivor loop with player movement, auto-fire, XP drops, level-up card offers, run-focus mechanics, and local meta-progression tracked in localStorage.

Architecture tour

Monorepo shape

The project is a pnpm monorepo run through Turborepo. @towerpower/web is the Phaser 3 client, built with Vite, under apps/web/src, split into game/systems, game/scenes, game/data, game/roguelite, and game/operatorSkills among other domains. @towerpower/api is a Fastify 5 server with SQLite persistence through Drizzle ORM. @towerpower/shared holds Zod schemas, level configs, and validation helpers consumed by both sides, so the client’s boot-time content checks and the API’s run-completion checks are validating against the same contract. If the two drift, a completion gets rejected at the boundary instead of silently accepted.

The API handles guest and full-account auth, Google OAuth linkage, rotating refresh tokens, a leaderboard, Campaign V2 squad and operator state, stage session tracking for anti-cheat checkpointing, and an admin interface. The Fastify server also serves the built web client in production.

Campaign design

Campaign V2 uses authored grid maps with terrain overlays (campaignV2TerrainOverlays.ts, campaignV2ForestWholeTilePackage.ts) and typed ground/flying deployment surfaces, plus an operator taxonomy defined by OperatorClassId in the shared package. Operator rigs, skills, and roster UI each get their own module cluster (operatorSkills, data/operatorRigs, systems/operatorAnimation, five separate operatorRoster* scene files for panels, filters, and detail tabs), which is the roguelite mode’s job too: roguelite/archetypes.ts defines a separate stat-and-passive profile per playable archetype for the survivor loop, distinct from the campaign roster.

Determinism and tests

Campaign runs are checked for determinism directly: scripts/tools/check-campaign-v2-determinism.mjs builds the web app with an E2E capture bridge enabled, spins up a temporary preview server, and replays named scenarios against it, by default running each scenario twice and diffing the result. It is meant as a manual regression check when content or runtime timing changes, and reports land in reports/campaign-v2-scenarios (dozens of named scenario runs already recorded there, from forest-caster builds to phase-shift and unique-operator retuning passes). Randomness itself is centralized behind a DeterministicRandomController in the shared package rather than scattered Math.random() calls, with its own unit test (deterministicRandom.test.ts).

On top of that there are 250 Vitest test files across web and api, an eight-file Playwright E2E suite including a dedicated capture-determinism.spec.ts, and tooling scripts for content validation, art sync, and boundary enforcement. The project is actively developed toward a Chapter 1 beta slice.