Skip to content

Custom Actions

Add new player abilities to your mod.

Overview

Actions are abilities that players (and AI) can use. This guide shows how to create a "Rally" action that boosts nearby troops.

Step 1: Define the Action

Create actions/rally-action.ts:

import {
  BaseAction,
  ActionContext,
  ActionTarget,
  Execution,
} from '@lands.io/mod-sdk';
import { RallyExecution } from '../executions/rally';

export class RallyAction extends BaseAction<ActionTarget> {
  readonly id = 'rally';
  readonly label = 'Rally Troops';

  // Can only rally with enough troops
  canActivate(context: ActionContext): boolean {
    return context.player.troops() >= 5000;
  }

  // AI targeting - rally when we have lots of troops
  chooseTarget(context: ActionContext): ActionTarget | null {
    if (context.player.troops() >= 10000) {
      return { type: 'self' };
    }
    return null;
  }

  // Create the execution
  createExecution(context: ActionContext, target: ActionTarget): Execution {
    return new RallyExecution(context.player.id());
  }

  // Optional: Show cost on UI
  getCost(context: ActionContext): number {
    return 1000; // Costs 1000 gold
  }
}

Step 2: Create the Execution

Create executions/rally.ts:

import { Execution, Game } from '@lands.io/mod-sdk';

export class RallyExecution extends Execution {
  private static readonly BOOST_PERCENT = 10;
  private static readonly GOLD_COST = 1000;

  constructor(private playerId: string) {
    super();
  }

  execute(game: Game): void {
    const player = game.player(this.playerId);
    if (!player || !player.isAlive()) return;

    // Check gold cost
    if (player.gold() < RallyExecution.GOLD_COST) return;

    // Deduct gold
    player.removeGold(RallyExecution.GOLD_COST);

    // Boost troops by 10%
    const boost = Math.floor(
      player.troops() * (RallyExecution.BOOST_PERCENT / 100)
    );
    player.addTroops(boost);

    console.log(`${player.name()} rallied troops! +${boost} troops`);
  }
}

Step 3: Register the Action

In your mod's registerActions:

import { RallyAction } from './actions/rally-action';

export default class MyMod extends Mod {
  registerActions(executor: Executor): void {
    executor.registerAction(new RallyAction());
  }
}

Modifying Existing Actions

Use action hooks to modify built-in actions:

registerActions(executor: Executor): void {
  executor.registerActionHooks
    // Modify attack targeting
    .attack({
      isValidTarget: (ctx, target, currentValid) => {
        // Don't allow attacking players with < 1000 troops
        if ('troops' in target && target.troops() < 1000) {
          return false;
        }
        return currentValid;
      }
    })
    // Modify boat action
    .boat({
      isValidTarget: (ctx, target, currentValid) => {
        // Custom boat logic
        return currentValid;
      }
    });
}

Action Types

Self-Target Actions

Actions that affect only the acting player:

chooseTarget(context: ActionContext): ActionTarget | null {
  return { type: 'self' };
}

Player-Target Actions

Actions that target another player:

chooseTarget(context: ActionContext): Player | null {
  return context.game.players()
    .filter(p => p.isAlive() && p.id() !== context.player.id())
    .sort((a, b) => a.troops() - b.troops())[0] || null;
}

Tile-Target Actions

Actions that target a specific tile:

chooseTarget(context: ActionContext): { x: number; y: number } | null {
  // Find a strategic tile
  return { x: 100, y: 100 };
}

Best Practices

Balance

Test your actions thoroughly. Overpowered abilities ruin gameplay.

AI Support

Always implement chooseTarget so AI bots can use your actions.

Feedback

Log action results so players understand what happened.