Skip to content

UI Buttons

Add custom buttons to the game interface.

Overview

This guide shows how to create a "Donate" button that lets players transfer troops to teammates.

Step 1: Create the Button

Create ui/buttons/donate-button.ts:

import { ModActionButton, ModButtonContext } from '@lands.io/mod-sdk';

export class DonateButton extends ModActionButton {
  get icon(): string {
    return '/assets/icons/donate.svg';
  }

  get label(): string {
    return 'Donate';
  }

  get intentName(): string {
    return 'donate';
  }

  // Blue styling
  get fillColor(): string {
    return '#3b82f6';
  }

  get hoverFillColor(): string {
    return '#2563eb';
  }

  get strokeColor(): string {
    return '#1d4ed8';
  }

  shouldShow(context: ModButtonContext): boolean {
    // Must be alive
    if (!context.myPlayer?.isAlive()) return false;

    // Must click on a cell
    if (!context.clickedCell) return false;

    // Must click on a teammate (implement your logic)
    return this.isClickedOnTeammate(context);
  }

  isDisabled(context: ModButtonContext): boolean {
    // Disable if not enough troops
    return (context.myPlayer?.troops() ?? 0) < 100;
  }

  getPayload(context: ModButtonContext) {
    const clickedPlayer = this.getClickedPlayer(context);
    return {
      recipientId: clickedPlayer?.id(),
      percentage: 10, // Donate 10% of troops
    };
  }

  private isClickedOnTeammate(context: ModButtonContext): boolean {
    // Implement based on your team logic
    const clickedPlayer = this.getClickedPlayer(context);
    if (!clickedPlayer) return false;

    // Access mod state from game
    // const teams = context.game.modState?.get('teams');
    // return isTeammate(teams, context.myPlayer.id(), clickedPlayer.id());

    return true; // Simplified for example
  }

  private getClickedPlayer(context: ModButtonContext) {
    if (!context.clickedCell) return null;
    return context.game.playerAt(context.clickedCell.x, context.clickedCell.y);
  }
}

Step 2: Create the Execution

Create executions/donate.ts:

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

export class DonateExecution extends Execution {
  private static readonly COOLDOWN_TICKS = 50;
  private static readonly MIN_TROOPS = 100;

  constructor(
    private donorId: string,
    private recipientId: string,
    private percentage: number,
    private modState: ModState
  ) {
    super();
  }

  execute(game: Game): void {
    // Validate players
    const donor = game.player(this.donorId);
    const recipient = game.player(this.recipientId);

    if (!donor || !recipient) return;
    if (!donor.isAlive() || !recipient.isAlive()) return;

    // Check cooldown
    const cooldowns = this.modState.get('donateCooldowns') || {};
    const lastDonate = cooldowns[this.donorId] || 0;

    if (game.tick() - lastDonate < DonateExecution.COOLDOWN_TICKS) {
      return; // Still on cooldown
    }

    // Validate percentage
    if (this.percentage <= 0 || this.percentage > 50) return;

    // Calculate amount
    const amount = Math.floor(donor.troops() * (this.percentage / 100));
    if (amount < DonateExecution.MIN_TROOPS) return;

    // Execute transfer
    donor.removeTroops(amount);
    recipient.addTroops(amount);

    // Set cooldown
    cooldowns[this.donorId] = game.tick();
    this.modState.set('donateCooldowns', cooldowns);

    console.log(
      `${donor.name()} donated ${amount} troops to ${recipient.name()}`
    );
  }
}

Step 3: Register the Intent Handler

import { NoOpExecution } from '@lands.io/mod-sdk';
import { DonateExecution } from './executions/donate';

export default class TeamMod extends Mod {
  registerIntentHandlers(executor: Executor): void {
    executor.registerModIntentHandler(executor.modId, 'donate', (intent) => {
      const payload = intent.payload as {
        recipientId?: string;
        percentage?: number;
      } | null;

      // Validate payload
      if (!payload) return new NoOpExecution();

      const { recipientId, percentage } = payload;

      if (!recipientId || typeof percentage !== 'number') {
        return new NoOpExecution();
      }

      if (percentage <= 0 || percentage > 50) {
        return new NoOpExecution();
      }

      return new DonateExecution(
        intent.playerID,
        recipientId,
        percentage,
        this.modState
      );
    });
  }
}

Button Styling Reference

Property Description
fillColor Background color
strokeColor Border color
hoverFillColor Background on hover
hoverStrokeColor Border on hover
disabledFillColor Background when disabled
disabledStrokeColor Border when disabled
cost Number to display as cost

Best Practices

Validation

Always validate in both shouldShow and the execution. UI state might be stale.

Cooldowns

Implement cooldowns to prevent spam and exploitation.

Feedback

Consider adding visual feedback when actions succeed or fail.