Skip to content

Configuration

Override game mechanics by implementing a partial Config class.

Overview

Mods can customize game behavior by providing a Config class:

import { Config, Player } from '@lands.io/mod-sdk';

export class MyConfig implements Partial<Config> {
  // Only override what you need
  attackAmount(attacker: Player, defender: Player): number {
    return attacker.troops() / 10; // Reduced attack power
  }
}

Attach it to your mod:

export default class MyMod extends Mod {
  static override Config = MyConfig;
}

DefaultConfig Reference

The DefaultConfig class provides the base implementation for all game mechanics. Your config only needs to override what you want to change:

/**
 * Default game configuration used as the base.
 * Mods don't need to extend this - instead implement Partial<Config>
 * and attach it to your Mod class via the static Config property.
 *
 * @example
 * ```typescript
 * import { Mod, Config } from '@lands.io/mod-sdk';
 *
 * class MyConfig implements Partial<Config> {
 *   // Override only what you need - rest falls back to DefaultConfig
 *   attackAmount(attacker, defender) {
 *     return attacker.troops() / 10;
 *   }
 * }
 *
 * export default class MyMod extends Mod {
 *   static Config = MyConfig;
 *   // ... lifecycle hooks
 * }
 * ```
 */
export class DefaultConfig implements Config {
  constructor(protected _gameConfig: GameConfig) {}

  // ============================================================
  // Game configuration (override to customize)
  // ============================================================

  gameConfig(): GameConfig {
    return this._gameConfig;
  }

  spawnImmunityDuration(): Tick {
    return 5 * 10;
  }

  numSpawnPhaseTurns(): number {
    return this._gameConfig.gameType === GameType.Singleplayer ? 70 : 150;
  }

  percentageTilesOwnedToWin(): number {
    return 80;
  }

  bots(): number {
    return this._gameConfig.bots;
  }

  numBots(): number {
    return this.bots();
  }

  fakeHumans(): number {
    return this._gameConfig.fakeHumans || 0;
  }

  infiniteGold(): boolean {
    return this._gameConfig.infiniteGold;
  }

  infiniteTroops(): boolean {
    return this._gameConfig.infiniteTroops;
  }

  // ============================================================
  // Player settings (override to customize)
  // ============================================================

  startManpower(player: Player): number {
    const maxPop = this.maxPopulation(player);
    const ratio = player.type() === PlayerType.Bot ? 0.5 : 0.75;
    return maxPop * ratio;
  }

  maxPopulation(player: Player): number {
    const maxPop = this.infiniteTroops()
      ? 1_000_000_000
      : Math.pow(player.numTilesOwned(), 0.6) * 100;
    return maxPop;
  }

  populationIncreaseRate(player: Player): number {
    const max = this.maxPopulation(player);
    const toAdd = max * 0.0007;
    return Math.min(player.population() + toAdd, max) - player.population();
  }

  goldAdditionRate(player: Player): number {
    return Math.sqrt(player.workers() * player.numTilesOwned()) / 200;
  }

  troopAdjustmentRate(player: Player): number {
    const maxDiff = this.maxPopulation(player) / 1000;
    const target = player.population() * player.targetTroopRatio();
    const diff = target - player.troops();
    if (Math.abs(diff) < maxDiff) {
      return diff;
    }
    const adjustment = maxDiff * Math.sign(diff);
    return adjustment < 0 ? adjustment * 5 : adjustment;
  }

  // ============================================================
  // Combat settings (override to customize)
  // ============================================================

  attackAmount(attacker: Player, defender: Player | TerraNullius): number {
    if (attacker.type() === PlayerType.Bot) {
      return attacker.troops() / 20;
    }
    return attacker.troops() / 5;
  }

  attackTilesPerTick(
    attackTroops: number,
    attacker: Player,
    defender: Player | TerraNullius,
    numAdjacentTilesWithEnemy: number,
  ): number {
    if ('isPlayer' in defender && defender.isPlayer()) {
      return (
        within(((5 * attackTroops) / defender.troops()) * 2, 0.01, 0.5) *
        numAdjacentTilesWithEnemy *
        3
      );
    }
    const troopFactor = Math.min(3, Math.max(1, Math.sqrt(attackTroops / 500) * 3));
    return numAdjacentTilesWithEnemy * 2 * troopFactor;
  }

  // ============================================================
  // Unit costs (override to customize)
  // ============================================================

  unitInfo(type: UnitType): UnitInfo {
    // Default costs - override for custom pricing
    const defaultInfo: UnitInfo = {
      cost: () => 100_000,
      territoryBound: true,
    };
    return defaultInfo;
  }

  // ============================================================
  // Difficulty settings
  // ============================================================

  difficultyModifier(difficulty: Difficulty): number {
    switch (difficulty) {
      case Difficulty.Easy:
        return 1;
      case Difficulty.Medium:
        return 3;
      case Difficulty.Hard:
        return 9;
      case Difficulty.Impossible:
        return 18;
    }
  }
}

Available Overrides

Combat Settings

attackAmount(attacker: Player, defender: Player | TerraNullius): number {
    if (attacker.type() === PlayerType.Bot) {
      return attacker.troops() / 20;
    }
    return attacker.troops() / 5;
  }
attackTilesPerTick(
    attackTroops: number,
    attacker: Player,
    defender: Player | TerraNullius,
    numAdjacentTilesWithEnemy: number,
  ): number {
    if ('isPlayer' in defender && defender.isPlayer()) {
      return (
        within(((5 * attackTroops) / defender.troops()) * 2, 0.01, 0.5) *
        numAdjacentTilesWithEnemy *
        3
      );
    }
    const troopFactor = Math.min(3, Math.max(1, Math.sqrt(attackTroops / 500) * 3));
    return numAdjacentTilesWithEnemy * 2 * troopFactor;
  }

Player Settings

startManpower(player: Player): number {
    const maxPop = this.maxPopulation(player);
    const ratio = player.type() === PlayerType.Bot ? 0.5 : 0.75;
    return maxPop * ratio;
  }
maxPopulation(player);
populationIncreaseRate(player: Player): number {
    const max = this.maxPopulation(player);
    const toAdd = max * 0.0007;
    return Math.min(player.population() + toAdd, max) - player.population();
  }
goldAdditionRate(player: Player): number {
    return Math.sqrt(player.workers() * player.numTilesOwned()) / 200;
  }

Game Settings

spawnImmunityDuration(): Tick {
    return 5 * 10;
  }
numSpawnPhaseTurns(): number {
    return this._gameConfig.gameType === GameType.Singleplayer ? 70 : 150;
  }
percentageTilesOwnedToWin(): number {
    return 80;
  }
numBots(): number {
    return this.bots();
  }

Difficulty Modifiers

difficultyModifier(difficulty: Difficulty): number {
    switch (difficulty) {
      case Difficulty.Easy:
        return 1;
      case Difficulty.Medium:
        return 3;
      case Difficulty.Hard:
        return 9;
      case Difficulty.Impossible:
        return 18;
    }
  }

Example: Team Mode Config

import { Config, Player, TerraNullius, PlayerType } from '@lands.io/mod-sdk';

export class TeamConfig implements Partial<Config> {
  // Boost attack power for humans
  attackAmount(attacker: Player, defender: Player | TerraNullius): number {
    if (attacker.type() === PlayerType.Bot) {
      return attacker.troops() / 20;
    }
    return attacker.troops() / 5;
  }

  // Longer games with higher win threshold
  percentageTilesOwnedToWin(): number {
    return 90;
  }

  // More bots for larger battles
  numBots(): number {
    return 8;
  }
}

How It Works

  1. Your Config class implements only the methods you want to override
  2. The engine creates a proxy that falls back to DefaultConfig for unimplemented methods
  3. All game mechanics use your overrides seamlessly