STUGPlay Free

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:

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:

  1. Calls server.updateGamemode(gm) to set the active gamemode.
  2. Picks a map from the gamemode’s mapRotation array.
  3. 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:

3. Round Start

When the lobby countdown expires, server.startRound() fires:

  1. roundLength is used to set the round end timer.
  2. If this is the first round (rounds === 0): onGameStarted() fires.
  3. If this is the first round and not a continuation: init() fires (no gamemodes currently use this).
  4. onRoundStarted() fires (can be async — it’s awaited).
  5. 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.

4. Gameplay Loop (Per Tick)

Every server tick (the main game loop frame), after entity updates and AI processing:

  1. tick() fires — used for ongoing game logic like conquest point scoring, closing rings, nuke timers, etc.
  2. For each dead player waiting to respawn:
    • allowRespawns is checked. If false, dead players stay dead.
    • calculateRespawnTime(client) is called if defined, otherwise respawnTime is used (default 10s).

5. Player Spawning

When a player spawns (initial spawn or respawn):

  1. onSpawnPlayer(client, playerNetData) fires before the vehicle is created.
    • Gamemodes can override the vehicle via client.overrideSlotTank = 'vehicle_key'.
    • playerNetData is mutable — gamemodes can set flags like isObjective, disableRepairing, etc.
  2. The vehicle entity is created with spawnProtectionLifetime duration of invulnerability.
  3. onPlayerVehicleCreated(client, vehicle) fires after creation.

6. Combat Events

When damage and kills occur:

  1. onPlayerDamaged(vehicle, amount, isKill, additionalXpGains) fires when a vehicle takes damage.
    • additionalXpGains is a mutable array — push extra XP gain objects for bonus rewards.
  2. 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().

7. Scoring

The server provides server.recordScore(team, amount, setScore?) for team-based scoring:

8. Round End

When server.endRound() is called (or the timer expires):

  1. server.gamemode.rounds is incremented.
  2. onRoundEnded() fires.
    • Gamemodes typically set server.gamemode.lastRoundWinner here and call server.recordScore() for the winning team.
  3. The server checks maxRounds — if rounds >= maxRounds, the game ends.
  4. 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):

  1. 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).
  2. If disableRecordingStats is false, player stats and game results are sent to the master server.
  3. xpMultiplier is applied to final awards.

10. Player Disconnect

When a player disconnects:

  1. If allowRespawns is false, the player’s vehicle is killed (not just removed).
  2. onClientDisconnected(socket) fires after disconnect processing.

11. Chat

When a player sends a chat message:

  1. onClientChat(socket, message) fires.
  2. 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()

onGameStarted()

onRoundStarted()

onRoundEnded()

onMapLoaded()

onBeforeMapObjectLoaded(object)

onClientConnected(socket)

onClientDisconnected(socket)

onGamemodeAction(socket, data)

onClientChat(socket, message) → boolean

onSpawnPlayer(client, playerNetData)

createPlayerEntity(client, spawn, playerNetData) → entity|null

onPlayerVehicleCreated(client, vehicle)

onPlayerKilled(attacker, victim)

onPlayerDamaged(vehicle, amount, isKill, additionalXpGains)

tick()

calculateRespawnTime(client) → number

getWinningTeam(teams) → number|null

getKillstreakSlots(client) → array|null

autoBalancePlayers()

balanceTeamsWithBots()

getAdditionalAITargets(bot) → array

onScoreboardDataCreate(data)


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)

onClientUITick(gui, scoreboard)

onPlayerUpdate(player)


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:

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.