Sovereign Storm is a pnpm workspace monorepo in TypeScript (strict mode, ES2022). It splits into three packages with a strict one-way dependency flow, and the whole system is server-authoritative: the server is the only thing that simulates a round.

See also: Multiplayer & Netcode · Game Design · Developer Guide.

Package layout

packages/
├── shared/   schema definitions + types + validators (the wire contract)
├── server/   Colyseus game server (the only place a round resolves)
└── client/   Phaser 3 + Vite game client (animates replays; never simulates)
PackageRole
sharedThe single source of legality: the state schema, the message-type constants, gameplay constants, and the validators/sanitizers that both tiers trust. No runtime dependency on client or server.
serverNode + Express + Colyseus. Hosts the lobby and battle rooms and runs the authoritative round resolver.
clientPhaser 3 + Vite. Renders the lobby and the match, plays the resolution replay, and handles all input and visual effects. Talks to the server only over WebSocket.

The dependency flow is one-directional: shared is consumed by both server and client, and the client never reaches into server internals — it communicates exclusively through the shared wire contract.

Server-authoritative by design

The defining architectural choice is that the server simulates the entire round and ships a finished replay to clients, which only animate it. A captain’s client sends intent (planned moves and cannons); it does not compute outcomes. This guarantees that two players can never see a divergent result, because there is only one simulation. The trade-off is bandwidth — each round’s payload carries the full per-slot, per-ship resolution — which is an accepted cost for zero desync. The full flow is described in Multiplayer & Netcode.

The battle room

A single game-room class is the heart of the multiplayer loop. It owns:

  • Lifecycle — room creation (applying presets, overrides, mode strategy, and map selection), players joining and leaving, host assignment, and reconnection grace.
  • Message handlers — the real protocol. Captains send move plans, cannon selections, submissions, settings changes, chat, and more; each handler validates an untrusted payload before mutating authoritative state.
  • Resolution — the round resolver that advances the round counter, applies buffs/repairs/respawns, dispatches into the active mode strategy, walks each move slot, builds the tick replay, and broadcasts the result.

Mode-specific rules plug in through a strategy registry: each of the five modes implements a common interface, and the room dispatches into the active mode at the right points. This keeps the core resolver mode-agnostic — adding or tuning a mode is largely data plus one strategy entry, per the design’s “data, not code branches” principle.

The shared wire contract

The shared package is the contract both tiers obey. It holds the message-type constants, gameplay constants (vessel HP, ranges, shot damage, ram tables, mode metadata), and the validators that sanitize every inbound client message. Because both client and server import the same definitions, the protocol cannot drift between them at compile time.

A practical nuance worth knowing: rather than relying heavily on framework state-replication for every field, the game broadcasts most state through explicit messages, treating those custom messages as the canonical protocol. See Multiplayer & Netcode for why and how.

Hosting model

For multiplayer beyond localhost, the project includes a one-command host script that builds the client bundle, starts the production server (which serves the single-page app and the WebSocket on the same port), and opens a Cloudflare quick tunnel — producing a single shareable URL. This keeps casual co-op sessions zero-config; see Developer Guide.

1 item under this folder.