Gamemodes System
Overview
The gamemode system is a flat, duck-typed plugin architecture with no base class or inheritance. Each gamemode is a plain JavaScript object returned from a factory function that closes over a server reference. The server checks for the existence of optional callback methods before calling them — gamemodes only need to define the properties and methods they care about.
How It Works
Factory Pattern
Every gamemode file exports a factory function:
module.exports = function(server) {
// Private state via closure
let someState = null;
return {
title: 'My Gamemode',
roundLength: 1000 * 60 * 5, // 5 minutes
maxPlayers: 16,
onPlayerKilled: (attacker, victim) => {
// Game logic using `server` from closure
},
};
};
The server parameter is the live game server instance, giving gamemodes direct access to all server APIs. Private state is maintained via closure variables declared before the return.
Registry
src-game/gamemodes.js is a factory that instantiates all gamemodes and returns a map of string -> gamemode object:
module.exports = function(server) {
return {
conquest: require('./gamemodes/conquest')(server),
battle: require('./gamemodes/battle')(server),
// ... 17 more
};
};
The registry is instantiated in two contexts:
- Server constructor (
server.js): Called with the liveserverfor actual gameplay. Stored asserver.gamemodeList. - Game server init (
game_server_init.js): Called withnullfor metadata-only access (map rotations, titles) exposed to the master server and client.
Active Gamemode State
server.gamemode is a runtime state object (not the definition). It holds:
| Field | Type | Description |
|---|---|---|
type |
string | Key into gamemodeList (e.g. 'conquest') |
rounds |
number | Incremented each time a round ends |
lastRoundWinner |
number/null | Set by gamemodes during onRoundEnded |
To access the active gamemode’s definition: server.gamemodeList[server.gamemode.type].
Gamemode Selection
server.updateGamemode(gm) sets the active gamemode:
server.updateGamemode = (gm) => {
if (config.gamemodeOverride) { gm = config.gamemodeOverride; }
if (!server.gamemodeList[gm]) { gm = 'conquest'; } // fallback
server.gamemode = { type: gm, rounds: 0 };
};
Game Lifecycle
Understanding the lifecycle is essential for knowing when each gamemode hook fires and what systems are in play.
1. Gamemode Selection & Map Loading
When a game starts (or a new map rotation occurs), the server:
- Calls
server.updateGamemode(gm)to set the active gamemode. - Picks a map from the gamemode’s
mapRotationarray. - Loads the map. During loading:
onBeforeMapObjectLoaded(object)fires for each map object (before it’s processed).onMapLoaded()fires after all entities are created.
2. Lobby Phase
After the map loads, the server enters a lobby/waiting state:
- The
lobbyWaitTimeproperty controls how long the lobby countdown lasts (defaults to the server’s standard if not set). teamCountdetermines how many teams are created (default 2). Set to 8 for FFA-style modes, 1 for cooperative modes like zombies.- Players connect during this phase, triggering
onClientConnected(socket). disableTeamJoiningprevents players from manually choosing teams.disableVehicleSelectionhides the vehicle picker in the lobby UI.
3. Round Start
When the lobby countdown expires, server.startRound() fires:
roundLengthis used to set the round end timer.- If this is the first round (
rounds === 0):onGameStarted()fires. - If this is the first round and not a continuation:
init()fires (no gamemodes currently use this). onRoundStarted()fires (can be async — it’sawaited).- Team auto-balancing runs:
- If
autoBalancePlayers()is defined on the gamemode, it replaces the default balancing logic entirely. - Similarly,
balanceTeamsWithBots()replaces the default bot balancing if defined.
- If
4. Gameplay Loop (Per Tick)
Every server tick (the main game loop frame), after entity updates and AI processing:
tick()fires — used for ongoing game logic like conquest point scoring, closing rings, nuke timers, etc.- For each dead player waiting to respawn:
allowRespawnsis checked. If false, dead players stay dead.calculateRespawnTime(client)is called if defined, otherwiserespawnTimeis used (default 10s).
5. Player Spawning
When a player spawns (initial spawn or respawn):
onSpawnPlayer(client, playerNetData)fires before the vehicle is created.- Gamemodes can override the vehicle via
client.overrideSlotTank = 'vehicle_key'. playerNetDatais mutable — gamemodes can set flags likeisObjective,disableRepairing, etc.
- Gamemodes can override the vehicle via
- The vehicle entity is created with
spawnProtectionLifetimeduration of invulnerability. onPlayerVehicleCreated(client, vehicle)fires after creation.
6. Combat Events
When damage and kills occur:
onPlayerDamaged(vehicle, amount, isKill, additionalXpGains)fires when a vehicle takes damage.additionalXpGainsis a mutable array — push extra XP gain objects for bonus rewards.
onPlayerKilled(attacker, victim)fires after kill/score recording.- This is the most common hook. Gamemodes use it to track scores, check win conditions, and trigger
server.endRound().
- This is the most common hook. Gamemodes use it to track scores, check win conditions, and trigger
7. Scoring
The server provides server.recordScore(team, amount, setScore?) for team-based scoring:
maxScoredefines the score cap. When a team reaches it,server.endRound()is automatically triggered.xpMultiplierscales all XP gains for the gamemode (e.g. 2x for battle, 0.6 for tank_ball).disableRecordingStatsprevents player stats from being saved to the database.disableKillstreaksprevents killstreak progression.
8. Round End
When server.endRound() is called (or the timer expires):
server.gamemode.roundsis incremented.onRoundEnded()fires.- Gamemodes typically set
server.gamemode.lastRoundWinnerhere and callserver.recordScore()for the winning team.
- Gamemodes typically set
- The server checks
maxRounds— ifrounds >= maxRounds, the game ends. - Otherwise, a new round starts (back to step 3 of the lifecycle).
9. Game End
When the game ends (max rounds reached, max score reached, or other condition):
getWinningTeam(teams)is called if defined, otherwise the server determines the winner by score.- Returns a team ID (number), null for a draw, or -1 for a loss (used by zombies).
- If
disableRecordingStatsis false, player stats and game results are sent to the master server. xpMultiplieris applied to final awards.
10. Player Disconnect
When a player disconnects:
- If
allowRespawnsis false, the player’s vehicle is killed (not just removed). onClientDisconnected(socket)fires after disconnect processing.
11. Chat
When a player sends a chat message:
onClientChat(socket, message)fires.- If it returns
true, the normal chat broadcast is suppressed.
Configuration Properties
Static properties set on the gamemode object that control gameplay behavior. All are optional with implicit defaults.
Core Settings
| Property | Type | Default | Description |
|---|---|---|---|
title |
string | 'Unknown Gamemode' |
Display name shown to players |
alias |
string | — | Short alternative name (e.g. 'CTF', 'Deathmatch') |
text |
string | — | Description text shown in lobby/announcements |
mapRotation |
string[] | — | Available map keys for this gamemode |
guiType |
string | 'gui_battle' |
Client UI template: 'gui_conquest', 'percentage_only', 'none' |
Players & Teams
| Property | Type | Default | Description |
|---|---|---|---|
maxPlayers |
number | 16 |
Max total players in the game |
maxPlayersPerTeam |
number | 10 |
Max players per team |
teamCount |
number | 2 |
Number of teams (e.g. 8 for FFA, 1 for co-op) |
botFillInAmount |
number | 0 |
Minimum player count bots will fill up to |
disableTeamJoining |
boolean | false |
Prevent players from choosing teams |
disableVehicleSelection |
boolean | false |
Hide vehicle selection UI |
forceAllowVehicleSelection |
boolean | false |
Override vehicle selection hiding for low-XP players |
Round Timing
| Property | Type | Default | Description |
|---|---|---|---|
roundLength |
number (ms) | — | Duration of each round |
maxScore |
number | — | Score cap per team; triggers endRound() when reached |
maxRounds |
number | — | Game ends when rounds >= maxRounds |
lobbyWaitTime |
number (ms) | Server default | Custom lobby countdown duration |
Spawning & Combat
| Property | Type | Default | Description |
|---|---|---|---|
allowRespawns |
boolean | falsy | Whether dead players respawn |
respawnTime |
number (ms) | 10000 |
Fixed respawn delay (overridden by calculateRespawnTime if defined) |
spawnProtectionLifetime |
number (ms) | 12000 |
Duration of spawn invulnerability |
useRandomSpawns |
boolean | false |
Use random spawn points instead of team spawns |
disableSpawnFlipping |
boolean | false |
Prevent spawn-side rotation between rounds |
disableVision |
boolean | false |
Turn off fog of war for this gamemode: every client receives every entity (see Vision / Fog of War) |
vision |
object | — | Fog of war settings for this gamemode: any of enabled, wallsBlock, teamShared, teammatesVisible, range. Keys it sets win over map root properties; admin /vision overrides still win over it |
Scoring & Progression
| Property | Type | Default | Description |
|---|---|---|---|
xpMultiplier |
number | 1 |
Multiplier on all XP gains |
disableRecordingStats |
boolean | false |
Prevent recording player stats to database |
disableKillstreaks |
boolean | false |
Prevent killstreak progression |
Lifecycle Callbacks
Server-side methods called at specific points in the game lifecycle. All are optional — the server checks for existence before calling.
init()
- When: First round starts (round 0, non-continuing), after the gamemode announcement.
- Params: none
- Notes: One-time initialization hook. No gamemodes currently implement this.
onGameStarted()
- When: Round 0 starts (first round of the game), before
onRoundStarted. - Params: none
- Use for: One-time game setup, initializing custom state, configuring bots.
onRoundStarted()
- When: Every round starts, after
onGameStarted(if round 0). - Params: none
- Returns: Can return a Promise (is
awaited). - Use for: Per-round setup, resetting state, spawning round-specific entities.
onRoundEnded()
- When: Round ends, after
roundsis incremented. - Params: none
- Use for: Setting
server.gamemode.lastRoundWinner, callingserver.recordScore(), cleanup.
onMapLoaded()
- When: After map is fully loaded and all entities are created.
- Params: none
- Common pattern: Remove turrets and spawn protection objects:
onMapLoaded: () => { for (let i = 0; i < server.entities.length; i++) { let entity = server.entities[i]; if ((entity.subtype === 'tank' && entity.netData.tankType === 'turret') || (entity.type === 'static_object' && entity.subtype === 'spawn_protection')) { entity.remove(); } } }
onBeforeMapObjectLoaded(object)
- When: During map loading, before each map object is processed.
- Params:
object— the map object definition with mutable.properties. - Use for: Modifying map objects before they’re created (e.g. swapping team sides).
onClientConnected(socket)
- When: After a client connects and is fully set up.
- Params:
socket— the client socket.
onClientDisconnected(socket)
- When: After client disconnect processing (player killed/removed, team handling done).
- Params:
socket— the disconnecting client socket.
onGamemodeAction(socket, data)
- When: Player sends a
gamemodeActionsocket event. - Params:
socket(client),data(object sent from client). - Use for: Handling arbitrary client actions without modifying server.js. Pairs with client-side
socket.emit('gamemodeAction', data)andgame.onGamemodeDatafor server→client communication viasocket.emit('gamemodeData', data).
onClientChat(socket, message) → boolean
- When: Player sends a chat message.
- Params:
socket(client),message(string). - Returns:
trueto suppress normal chat broadcast, falsy to allow it. - Use for: Custom chat commands (e.g.
/vehicle,/spawn).
onSpawnPlayer(client, playerNetData)
- When: Before a player vehicle is created.
- Params:
client(socket),playerNetData(mutable object passed to vehicle creation). - Modifiable:
client.overrideSlotTank(force a vehicle key),playerNetData.isObjective(also makes the vehicle visible to every team through fog of war),playerNetData.disableRepairing, etc.
createPlayerEntity(client, spawn, playerNetData) → entity|null
- When: Instead of creating the player’s tank, after
onSpawnPlayer. - Params:
client(socket),spawn({ x, y, rotation }, rotation in degrees as a tank heading, 0 = facing up),playerNetData(mutable). - Returns: The entity the player controls. Return null/undefined to fall back to the normal tank.
- Use for: Non-tank players.
infantry_deathmatchreturnsserver.createActor('human', ...), a top-down soldier (entities/actor.js) with health, hold-to-fire rifle, killstreaks (smoke,grenade,airstrike) and bot support.
onPlayerVehicleCreated(client, vehicle)
- When: After the player vehicle entity is created, before
switchPlayer. - Params:
client(socket),vehicle(the created vehicle entity). - Use for: Modifying the vehicle after creation (e.g. replacing fire function, tracking state).
onPlayerKilled(attacker, victim)
- When: A player is killed, after kill/score recording.
- Params:
attacker(vehicle entity or null),victim(vehicle entity or null). - Use for: Score tracking, checking win conditions (last team standing, score cap), calling
server.endRound().
onPlayerDamaged(vehicle, amount, isKill, additionalXpGains)
- When: A player vehicle takes damage, after damage calculation.
- Params:
vehicle(damaged entity),amount(damage number),isKill(boolean),additionalXpGains(mutable array — push{ type, amount }objects for bonus XP).
tick()
- When: Every server tick (game loop frame), after entity ticking and AI.
- Params: none
- Use for: Ongoing game logic — point scoring, closing rings, nuke timers, periodic checks.
calculateRespawnTime(client) → number
- When: Checking if a dead player should respawn.
- Params:
client(socket). - Returns: Respawn time in milliseconds.
- Use for: Dynamic respawn times (e.g. faster respawn for losing team).
getWinningTeam(teams) → number|null
- When: Determining the winner at game end.
- Params:
teams— array of team keys (Object.keys(server.teamData)). - Returns: Winning team ID (number), null for draw, or -1 for loss.
getKillstreakSlots(client) → array|null
- When: Resetting killstreak slots for a client.
- Params:
client(socket). - Returns: Array of
{ type: string, active: boolean }objects, or null for defaults.
autoBalancePlayers()
- When: Called at round start during team balancing.
- Params: none
- Returns: If defined, replaces the default team balancing logic entirely.
balanceTeamsWithBots()
- When: Called during bot balancing.
- Params: none
- Returns: If defined, replaces the default bot balancing logic entirely.
getAdditionalAITargets(bot) → array
- When: During AI target scanning, after the normal
server.clientsscan. - Params:
bot— the bot AI object (has.team,.entity, etc.). - Returns: Array of additional entities for bots to consider as targets. Each entity is evaluated with the same distance/LOS checks as player targets.
- Use for: Making bots target non-player entities like player-built turrets. Frontline uses this to return enemy deployed turrets.
onScoreboardDataCreate(data)
- When: Building scoreboard data to send to clients.
- Params:
data— the scoreboard data object (mutable). - Use for: Adding custom gamemode-specific data to the scoreboard.
Client-Side Hooks
These methods execute on the client (via the bundled public/serverBuilt.js for singleplayer, or through window.gameServer.gamemodeList). They reference client-side globals like game, PIXI, etc.
onClientUICreate(gui, scoreboard)
- When: Round indicators are created/updated on the client. Called multiple times during gameplay (on scoreboard updates, player alive events, and UI refreshes) — not just once.
- Params:
gui(PIXI.Container — a fresh container each call),scoreboard(scoreboard data). - Use for: Creating custom HUD elements.
- Important: Must be idempotent. Create PIXI objects once using a closure guard (e.g.
if (!panel) { ... }), then re-add them toguieach call. Callbacks likegame.onSpecialTriggeredandgame.onGamemodeDatashould be set every call since they may reference stale closure state. - Pattern:
let panel = null; // closure var // ... onClientUICreate: (gui, scoreboard) => { if (!panel) { panel = new PIXI.Container(); // ... create child objects once ... } gui.addChild(panel); },
onClientUITick(gui, scoreboard)
- When: Every client render tick when a custom UI container exists.
- Params:
gui(PIXI.Container),scoreboard(scoreboard data). - Use for: Updating custom HUD elements (timers, animations, etc.).
onPlayerUpdate(player)
- When: Client-side player entity update.
- Params:
player— the client-side player entity. - Use for: Updating client-side visuals based on player state.
Server APIs Available to Gamemodes
Gamemodes access server from their closure. Key APIs:
Entities & World
| API | Description |
|---|---|
server.entities |
Array of all game entities |
server.world |
p2.js physics world (for raycasting, etc.) |
server.MAP_WIDTH / server.MAP_HEIGHT |
Map dimensions |
server.mapData |
Map data including .properties |
server.capturePoints |
Array of capture point entity references |
server.createObject(type, x, y, ...) |
Create game objects (balls, nukes, capture points, etc.) |
server.createVehicle(type, x, y, ...) |
Create vehicle entities |
Players & Teams
| API | Description |
|---|---|
server.clients |
Array of all connected client sockets |
server.teamData |
Object with team info: .clients, .score, .spawns, .specialSpawns, .maxPlayers, .performance, .locked, .dead |
server.setClientTeam(client, team, disableBalance?) |
Move a client to a team |
server.spawnPlayer(client, x?, y?, angle?) |
Spawn a player’s vehicle |
server.getPlayerCount() |
Current player count |
Scoring & Progression
| API | Description |
|---|---|
server.recordScore(team, amount, setScore?) |
Add score to a team (or set absolute if setScore=true) |
server.recordXP(client, gains, damage?) |
Award XP to a client |
server.resetKillstreakSlots(client, forceEnableAll?) |
Reset killstreak state |
Round Control
| API | Description |
|---|---|
server.endRound() |
Trigger round end |
server.hasRoundEnded() |
Check if round has ended |
server.getRoundState() |
Returns { type, endTime, nukeTime?, nukeTeam? } |
server.updateRoundState(state) |
Set and broadcast new round state |
Communication
| API | Description |
|---|---|
server.sendMessage(event, data) |
Broadcast to all clients |
server.updateAmbientSound(sound) |
Change ambient sound |
Bots
| API | Description |
|---|---|
server.enableBots |
Boolean toggle for bot spawning |
server.aiManager.destroyAllBots() |
Remove all bots |
server.aiManager.createBot() |
Create a single bot |
server.aiManager.destroyBot(team) |
Remove a bot from a specific team |
Other
| API | Description |
|---|---|
server.vehicleData |
Vehicle definitions by key |
server.running |
Server running state |
server.isPrivate() |
Check if private game |
server.recording.getCurrentTime() |
Current replay time |
server.recordPotentialBestClip(clipData) |
Submit a best clip candidate |
server.disableNegativeStats |
Boolean toggle (used during nuke sequences) |
Common Patterns
Score-on-Kill (FFA/TDM Style)
onPlayerKilled: (attacker, victim) => {
if (attacker && victim && attacker.team !== null &&
victim.team !== null && server.teamData[attacker.team]) {
server.recordScore(attacker.team, 1);
}
}
Last-Team-Standing (Battle/War Style)
Check alive player count across teams in onPlayerKilled. When only one team has survivors, call server.endRound(). Set server.gamemode.lastRoundWinner in onRoundEnded.
Vehicle Override
Force players into a specific vehicle:
onSpawnPlayer: (client) => {
client.overrideSlotTank = 'racecar';
}
Closing Ring
Create a shrinking ring entity when time is running low (used by battle, war):
let closingRing = null;
// In tick():
if (timeRemaining < 60000 && !closingRing) {
closingRing = server.createObject('closing_ring', ...);
}
Nuke Sequence (Conquest/Frontline)
When one team captures all points, start a 35-second nuke timer via server.updateRoundState(). These two gamemodes share nearly identical tick() implementations.
Custom Server/Client Properties
Gamemodes freely set custom properties:
// On server
server.currentBall = null;
server.zombieWave = 0;
// On client sockets
client.overrideSlotTank = 'tiger';
client.score = 0;
this Binding
Most callbacks use arrow functions (accessing server from closure). If a gamemode needs to call its own methods via this, use function keyword:
return {
findNewQueen: function(client) { /* ... */ },
onClientDisconnected: function(socket) {
this.findNewQueen(socket);
},
};
Vision / Fog of War
Clients only receive entities their own player can see (src-game/vision_system.js). An entity is visible when the client’s alive player is within VISION_RANGE (shared_functions.js, 1250 px by default) and the line between them does not cross a smoke cloud. Teammates are culled like enemies unless config.game.visionTeammatesVisible is true (or /vision teammates on, or the map’s visionTeammatesVisible), in which case they are always sent; your own tank is always sent. With config.game.visionTeamShared (or /vision team on) every alive teammate counts as a seer instead. Vision is inactive in the lobby, in singleplayer, and for gamemodes that set disableVision: true. Admin/dev spectators see everything. Dead players look through their spectate target: the server follows the killer, else any alive player (src-game/spectate_system.js, client.spectateTarget), the client cycles with the spectateNext event, and control never moves because inputs only ever act on client.player.
What gamemodes can rely on:
entity.alwaysVisible—truefor map geometry, capture points, flags, the ball, deployables and every object subtype except thrown/launched ones (grenade,landmine,missile,artillery,airstrike,bomb);falsefor vehicles and actors. Flip it after creation (e.g. inonPlayerVehicleCreated) to exempt an entity; the next snapshot tick reconciles.playerNetData.isObjectiveandnetData.mapProperties.enableArrowPointerimply always visible, since the client draws off-screen arrows for them.- Map root properties
disableVision,visionRange,visionWallsBlock,visionTeamShared,visionTeammatesVisibletune the same flags per map (see MAPPING.md);server.visionSystem.applyMapProperties(props)applies them on map load. The gamemode’svisionobject sits above the map, and/visionadmin overrides sit above both. entity.setBlocksVision(false)— in walls mode, this entity’s body never blocks line of sight (server raycast and client shadow); wreck debris is created this way. Map makers get the same through theblocksVisionobject property. Onlyfalsedoes anything: an entity without a solid body can’t be made to block.- Smoke blocks vision over
smokeVisionRadius(shell param, default 70% ofsmokeRadius); the aim clamp and the visual cloud keep the full radius. server.visionSystem.canClientSee(client, entity)/canTeamSee(team, entity)— last tick’s answer.server.visionSystem.canSee(seer, target)— live check between two entities (distance, smoke, optional walls).entity.getVisionRange()/entity.visionRangeOverride— per-entity range hook.- Entity RPCs (
entity.sendRPC) are still global;server.visionSystem.sendMessageToKnown(entity, event, data)sends only to clients that currently have the entity.
With walls on, worldData.visionBlockers carries the blocking collision polygons (built from the p2 bodies, spawn protection excluded) so the client fog shader can draw wall shadows; crates ship netData.visionPolygons. Admins can tune it live with /vision, /vision on|off, /vision walls on|off (experimental wall occlusion), /vision team on|off (shared team vision), /vision teammates on|off (teammates visible out of range), /vision range <px> and /vision stats. Press F2 in game to draw the vision circles.