Skip to content

Architecture Overview

Status: Current Last reviewed: 2026-07-07

Fight the Frontier is a TypeScript monorepo with three npm workspaces. The guiding rule is simple: the client sends intent, never results, and the server owns the truth.

Layout

graph TD
  subgraph shared["shared/ โ€” imported by both sides"]
    S1[types + protocol]
    S2[movement sim]
    S3[zone data + buildZoneRuntime]
    S4[prefabs registry]
    S5[quests + ember data]
    S6[combat defs + constants]
  end
  subgraph server["server/ โ€” authoritative Node"]
    SV1[Zone simulation]
    SV2[goblin AI]
    SV3[QuestSystem]
    SV4[EmberSystem]
    SV5[ZoneStore + persistence]
  end
  subgraph client["client/ โ€” Three.js"]
    C1[prediction + reconciliation]
    C2[interpolation]
    C3[renderer + effects]
    C4[HUD]
    C5[admin editor]
  end
  shared --> server
  shared --> client
  server <-->|WebSocket JSON| client
Workspace Responsibility
shared Types, the network protocol, the movement simulation (run identically on both sides), zone data and its runtime derivation, the prefab registry, quest and ember data, weapon and spell definitions, tunable constants.
server The authoritative simulation: a fixed-tick Zone, goblin AI, projectile physics, the quest engine, the Emberwake system, zone persistence, and player persistence.
client Rendering with Three.js, local movement prediction and reconciliation, interpolation of remote entities, HUD, audio, and effects. client/src/admin is a separate editor app.

Server Authority

The client only ever tells the server what the player is trying to do:

  • Movement is a stream of player_input commands (sequence number, dt, move axes, yaw, pitch, jump, sprint, block). The client never sends a position.
  • Combat is attack, draw_start/draw_release, and cast_spell with an aim direction. The server decides windup timing, hit validation, damage, stamina and mana cost, cooldowns, and skill gain.

The server validates and clamps everything: dt is capped (MAX_INPUT_DT), the input queue is bounded, horizontal speed is clamped in the shared movement code, and stamina/mana/cooldown/draw checks all run server-side. See Networking.

Tick Loop

server/src/index.ts runs a fixed-step loop with catch-up so simulation time tracks wall time even if the event loop stalls:

sequenceDiagram
  participant Wall as Wall clock
  participant Loop as setInterval (120 Hz poll)
  participant Zone as Zone.update
  participant Net as Client
  Wall->>Loop: time passes
  Loop->>Loop: accumulate elapsed, clamp to 0.5s
  loop while accumulator >= TICK_DT (30 Hz)
    Loop->>Zone: update(TICK_DT)
    Zone->>Zone: players, goblins, projectiles, respawns, ember
    alt every 2nd tick (15 Hz)
      Zone->>Net: server_snapshot
    end
  end

Constants: TICK_RATE 30, SNAPSHOT_EVERY 2 (so 15 Hz snapshots), INTERP_DELAY_MS 140.

Entity System

Entities are plain server classes in server/src/game/entities.ts: PlayerEntity, GoblinEntity, ProjectileEntity, LootBagEntity, NpcEntity. Each exposes spawnData() (the full description sent when it enters view) and snap() (the small per-tick state included in snapshots). Ids are prefix_base36, for example pl_1, gob_a, prj_3. See Entities.

Zone System

A zone is data. A ZoneData document lists every placed object as a ZoneObjectInstance. On boot, ZoneStore loads server/data/zones/<id>.json (generating and writing the shipped default the first time), and buildZoneRuntime() in shared/src/world.ts derives everything the simulation needs from it: colliders, NPC definitions, mob spawns, quest triggers, target props, the safe zone, and the ember relays. The client derives its visuals from the same document, so the two agree without shipping geometry. See Level Editor for the schema.

Networking Messages

The protocol is JSON over one WebSocket per client, versioned by PROTOCOL_VERSION (currently 2). Messages are discriminated by a t field. The full list and the snapshot shape are in Networking.

Persistence Adapters

server/src/persistence/persistence.ts defines a PersistenceAdapter interface (loadPlayer, savePlayer, flush). The prototype ships JsonFilePersistence, backed by server/data/players.json. Player inventory, skills, skill-use accumulators, and quest log survive reconnects. A real deployment swaps in a database implementation behind the same interface without touching game code.

Zone content has its own store, ZoneStore, which owns the zone JSON files and their timestamped backups.

Admin / Editor Architecture

The editor is a client that authenticates with an admin_login message and then issues admin_cmd messages (create/update/delete object, save/reload/import zone, spawn/despawn, reset camp/quests, set ember, god mode, teleport, save quests). The server validates each command against conn.isAdmin, applies it to the live ZoneData, and broadcasts zone_object_update so any connected game clients rebuild the changed objects immediately. Normal players cannot issue these commands. See Level Editor.