Skip to content

Mod Class

The Mod class is the foundation of every Lands.io mod.

Overview

Every mod exports a default class that extends Mod:

import { Mod } from '@lands.io/mod-sdk';

export default class MyMod extends Mod {
  // Your mod implementation
}

Full Class Definition

The complete Mod class (auto-generated from source):

/**
 * 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;
}

Lifecycle Hooks

onGameInit

Called after game initialization, before gameplay starts.

/**
   * 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;

!!! tip "Use Cases" - Initialize team structures - Set up game-wide state - Log game configuration

onPlayerAdded

Called when a player spawns into the game.

/**
   * 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;

onGameEnd

Called when the game ends (winner determined).

/**
   * 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;

Registration Hooks

registerActions

Register custom actions or modify existing ones.

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

registerIntentHandlers

Handle custom intents from UI buttons.

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

State Access

Every mod has access to two state containers:

modState (In-Game)

Temporary state that lives for one game session. Automatically synced to clients.

Usage example:

// Set state
this.modState.set('teams', { blue: [], red: [] });

// Get state
const teams = this.modState.get('teams');

// Update nested values
const teams = this.modState.get('teams');
teams.blue.push(playerId);
this.modState.set('teams', teams);

persistentStorage (Cross-Game)

Permanent storage for leaderboards, ELO, and lifetime stats.

Usage example:

// Increment a value
await this.persistentStorage.increment('players', oderId, 'wins', 1);

// Get top players
const leaders = await this.persistentStorage.getOrderedDesc(
  'players',
  'elo',
  10
);

Configuration Attachment

Attach a config class to override game mechanics:

/**
   * Optional Config class for game mechanics overrides.
   * Implement Partial<Config> to override specific methods.
   * The rest will fall back to DefaultConfig.
   */
  static Config?: ConfigClass;

See Configuration for details.