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.