Skip to content

@lands.io/mod-sdk / Mod

Class: Mod

Base class for Lands.io mods.

Provides: - Lifecycle hooks: onGameInit, onPlayerAdded, onGameEnd - Action registration: registerActions - Intent handling: registerIntentHandlers - State access: modState (in-game), persistentStorage (cross-game) - Config attachment: static Config property for game mechanics overrides

Table of contents

Constructors

Properties

Methods

Constructors

constructor

new Mod(): Mod

Returns

Mod

Properties

modState

modState: ModState

In-game state container for the mod. Changes are automatically synced to clients. Set by the engine before any hooks are called.


persistentStorage

persistentStorage: IModStorage

Persistent storage for cross-game data (ELO, leaderboards, etc.). Set by the engine before any hooks are called.


Config

Static Optional Config: ConfigClass

Optional Config class for game mechanics overrides. Implement Partial to override specific methods. The rest will fall back to DefaultConfig.

Methods

getWinners

getWinners(game): null | Player[]

Optional custom win condition logic. If provided, this completely replaces the default win check. Return an array of winners to end the game, or null/undefined if no winner yet.

Called periodically during gameplay (every 10 ticks by default).

Parameters

Name Type Description
game Game The game instance

Returns

null | Player[]

Array of winning players, or null if game should continue

Example

// Team mode: Team wins when controlling >80% of territory
getWinners(game) {
  const teams = this.modState.get('teams');
  const territoryByTeam = { blue: 0, red: 0 };

  for (const player of game.players()) {
    const team = getPlayerTeam(game, player.id());
    if (team) territoryByTeam[team] += player.numTilesOwned();
  }

  const totalTiles = game.numLandTiles();

  for (const [teamName, territory] of Object.entries(territoryByTeam)) {
    if ((territory / totalTiles) * 100 > 80) {
      // Return all players on winning team
      return teams[teamName].map(id => game.player(id));
    }
  }

  return null; // No winner yet
}

onGameEnd

onGameEnd(game, winnerIds, stats): void

Called when the game ends (winner determined). Use this for post-game processing, ELO calculations, etc.

Parameters

Name Type Description
game Game The game instance
winnerIds string[] Array of PlayerIDs of the winners
stats unknown Game statistics for all players

Returns

void


onGameInit

onGameInit(game, executor): void

Called after game initialization, before gameplay starts. Use this to set up teams, initial state, etc.

Parameters

Name Type Description
game Game The game instance
executor Executor The executor for adding executions

Returns

void


onPlayerAdded

onPlayerAdded(player): void

Called when a player spawns. Use this to assign teams, set up per-player state, etc.

Parameters

Name Type Description
player Player The player that was added

Returns

void


onPlayerEliminated

onPlayerEliminated(game, event): void

Called when a player loses their last tile and is eliminated from the game. The eliminator is the player who conquered the final tile, or null if the elimination was caused by a non-combat mechanic (e.g. tile relinquish).

Parameters

Name Type
game Game
event PlayerEliminatedEvent

Returns

void


onScenarioSetup

onScenarioSetup(scenario, game, executor): void

Called only during fresh initialization (never during snapshot restore).

Use this to deterministically set up a scenario: - Create NPC players - Paint initial territories - Override deterministic human spawn location(s)

This hook runs before any bot/fake-human spawning and before gameplay starts.

IMPORTANT: ScenarioSetup is init-only. Calls after this hook will throw.

Parameters

Name Type
scenario ScenarioSetup
game Game
executor Executor

Returns

void


onTerritoryAbsorbed

onTerritoryAbsorbed(game, event): void

Called when territory is absorbed from one player into another due to cutoff/encircled resolution.

Parameters

Name Type
game Game
event TerritoryAbsorbedEvent

Returns

void


onTick

onTick(game, executor): void

Called once per engine tick.

Determinism rules: - Do not use Date/time, Math.random, or other non-deterministic sources. - Drive behavior from game state + modState only.

Parameters

Name Type
game Game
executor Executor

Returns

void


registerActions

registerActions(executor): void

Register custom actions or override default actions. Called during game initialization.

Parameters

Name Type Description
executor Executor The executor for registering actions

Returns

void

Example

registerActions(executor) {
  // Override default attack action with team-aware version
  executor.registerAction(new TeamAttackAction());
}

registerIntentHandlers

registerIntentHandlers(executor): void

Register custom intent handlers for mod-specific player actions. Called during game initialization.

Parameters

Name Type Description
executor Executor The executor for registering intent handlers

Returns

void

Example

registerIntentHandlers(executor) {
  executor.registerModIntentHandler('my-mod', 'donate', (intent) => {
    return new DonateExecution(intent.playerID, intent.payload);
  });
}

Source Code

View full implementation
/**
 * Base class for Lands.io mods.
 *
 * Provides:
 * - Lifecycle hooks: onGameInit, onPlayerAdded, onGameEnd
 * - Action registration: registerActions
 * - Intent handling: registerIntentHandlers
 * - State access: modState (in-game), persistentStorage (cross-game)
 * - Config attachment: static Config property for game mechanics overrides
 */
export abstract class Mod {
  /**
   * Optional Config class for game mechanics overrides.
   * Implement Partial<Config> to override specific methods.
   * The rest will fall back to DefaultConfig.
   */
  static Config?: ConfigClass;

  /**
   * In-game state container for the mod.
   * Changes are automatically synced to clients.
   * Set by the engine before any hooks are called.
   */
  public modState!: ModState;

  /**
   * Persistent storage for cross-game data (ELO, leaderboards, etc.).
   * Set by the engine before any hooks are called.
   */
  public persistentStorage!: IModStorage;

  /**
   * Called by the engine to inject mod state.
   * @internal
   */
  setModState(state: ModState): void {
    this.modState = state;
  }

  /**
   * Called by the engine to inject persistent storage.
   * @internal
   */
  setPersistentStorage(storage: IModStorage): void {
    this.persistentStorage = storage;
  }

  // ═══════════════════════════════════════════════════════════════════════════
  // LIFECYCLE HOOKS - Override these in your mod
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Called only during fresh initialization (never during snapshot restore).
   *
   * Use this to deterministically set up a scenario:
   * - Create NPC players
   * - Paint initial territories
   * - Override deterministic human spawn location(s)
   *
   * This hook runs before any bot/fake-human spawning and before gameplay starts.
   *
   * IMPORTANT: ScenarioSetup is init-only. Calls after this hook will throw.
   */
  onScenarioSetup?(scenario: ScenarioSetup, game: Game, executor: Executor): void;

  /**
   * Called after game initialization, before gameplay starts.
   * Use this to set up teams, initial state, etc.
   *
   * @param game - The game instance
   * @param executor - The executor for adding executions
   */
  onGameInit?(game: Game, executor: Executor): void;

  /**
   * Called once per engine tick.
   *
   * Determinism rules:
   * - Do not use Date/time, Math.random, or other non-deterministic sources.
   * - Drive behavior from game state + modState only.
   */
  onTick?(game: Game, executor: Executor): void;

  /**
   * Called when a player spawns.
   * Use this to assign teams, set up per-player state, etc.
   *
   * @param player - The player that was added
   */
  onPlayerAdded?(player: Player): void;

  /**
   * Called when the game ends (winner determined).
   * Use this for post-game processing, ELO calculations, etc.
   *
   * @param game - The game instance
   * @param winnerIds - Array of PlayerIDs of the winners
   * @param stats - Game statistics for all players
   */
  onGameEnd?(game: Game, winnerIds: PlayerID[], stats: unknown): void;

  /**
   * Called when a player loses their last tile and is eliminated from the game.
   * The eliminator is the player who conquered the final tile, or null if the
   * elimination was caused by a non-combat mechanic (e.g. tile relinquish).
   */
  onPlayerEliminated?(game: Game, event: PlayerEliminatedEvent): void;

  /**
   * Called when territory is absorbed from one player into another due to
   * cutoff/encircled resolution.
   */
  onTerritoryAbsorbed?(game: Game, event: TerritoryAbsorbedEvent): void;

  /**
   * Optional custom win condition logic.
   * If provided, this completely replaces the default win check.
   * Return an array of winners to end the game, or null/undefined if no winner yet.
   *
   * Called periodically during gameplay (every 10 ticks by default).
   *
   * @param game - The game instance
   * @returns Array of winning players, or null if game should continue
   *
   * @example
   * ```typescript
   * // Team mode: Team wins when controlling >80% of territory
   * getWinners(game) {
   *   const teams = this.modState.get('teams');
   *   const territoryByTeam = { blue: 0, red: 0 };
   *
   *   for (const player of game.players()) {
   *     const team = getPlayerTeam(game, player.id());
   *     if (team) territoryByTeam[team] += player.numTilesOwned();
   *   }
   *
   *   const totalTiles = game.numLandTiles();
   *
   *   for (const [teamName, territory] of Object.entries(territoryByTeam)) {
   *     if ((territory / totalTiles) * 100 > 80) {
   *       // Return all players on winning team
   *       return teams[teamName].map(id => game.player(id));
   *     }
   *   }
   *
   *   return null; // No winner yet
   * }
   * ```
   */
  getWinners?(game: Game): Player[] | null;

  // ═══════════════════════════════════════════════════════════════════════════
  // REGISTRATION HOOKS - Override these in your mod
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Register custom actions or override default actions.
   * Called during game initialization.
   *
   * @param executor - The executor for registering actions
   *
   * @example
   * ```typescript
   * registerActions(executor) {
   *   // Override default attack action with team-aware version
   *   executor.registerAction(new TeamAttackAction());
   * }
   * ```
   */
  registerActions?(executor: Executor): void;

  /**
   * Register custom intent handlers for mod-specific player actions.
   * Called during game initialization.
   *
   * @param executor - The executor for registering intent handlers
   *
   * @example
   * ```typescript
   * registerIntentHandlers(executor) {
   *   executor.registerModIntentHandler('my-mod', 'donate', (intent) => {
   *     return new DonateExecution(intent.playerID, intent.payload);
   *   });
   * }
   * ```
   */
  registerIntentHandlers?(executor: Executor): void;
}