@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¶
- getWinners
- onGameEnd
- onGameInit
- onPlayerAdded
- onPlayerEliminated
- onScenarioSetup
- onTerritoryAbsorbed
- onTick
- registerActions
- registerIntentHandlers
Constructors¶
constructor¶
• new Mod(): Mod
Returns¶
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
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;
}