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.