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_inputcommands (sequence number,dt, move axes, yaw, pitch, jump, sprint, block). The client never sends a position. - Combat is
attack,draw_start/draw_release, andcast_spellwith 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.