Skip to content

Team Mechanics

Implement team-based gameplay in your mod.

Overview

This guide shows how to create a two-team competitive mode with:

  • Automatic team assignment
  • Team colors
  • Friendly fire prevention
  • Team-based win conditions

Step 1: Define Team Types

Create utils/shared.ts:

export type TeamName = 'blue' | 'red';

export interface Teams {
  blue: string[];
  red: string[];
}

export const TEAM_COLORS = {
  blue: { fill: '#3b82f6', border: '#1d4ed8' },
  red: { fill: '#ef4444', border: '#dc2626' },
};

export function getPlayerTeam(teams: Teams, oderId: string): TeamName | null {
  if (teams.blue.includes(playerId)) return 'blue';
  if (teams.red.includes(playerId)) return 'red';
  return null;
}

export function isTeammate(
  teams: Teams,
  player1Id: string,
  player2Id: string
): boolean {
  const team1 = getPlayerTeam(teams, player1Id);
  const team2 = getPlayerTeam(teams, player2Id);
  return team1 !== null && team1 === team2;
}

Step 2: Initialize Teams

import { Mod, Game, Executor } from '@lands.io/mod-sdk';
import { Teams, TEAM_COLORS } from './utils/shared';

export default class TeamMod extends Mod {
  onGameInit(game: Game, executor: Executor): void {
    // Initialize empty teams
    this.modState.set('teams', { blue: [], red: [] } as Teams);
  }
}

Step 3: Assign Players to Teams

onPlayerAdded(player: Player): void {
  const teams = this.modState.get('teams') as Teams;

  // Assign to smaller team for balance
  const team: TeamName = teams.blue.length <= teams.red.length ? 'blue' : 'red';

  // Add to team
  teams[team].push(player.id());
  this.modState.set('teams', teams);

  // Set team color
  this.modState.setPlayerColor(player.id(), TEAM_COLORS[team]);

  console.log(`${player.name()} joined team ${team.toUpperCase()}`);
}

Step 4: Prevent Friendly Fire

import { isTeammate } from './utils/shared';

registerActions(executor: Executor): void {
  const blockTeammateAttacks = (
    ctx: ActionContext,
    target: Player,
    valid: boolean
  ): boolean => {
    if (!valid) return false;

    const teams = this.modState.get('teams') as Teams;
    return !isTeammate(teams, ctx.player.id(), target.id());
  };

  executor.registerActionHooks
    .attack({ isValidTarget: blockTeammateAttacks })
    .boat({ isValidTarget: blockTeammateAttacks })
    .breakthrough({ isValidTarget: blockTeammateAttacks });
}

Step 5: Team-Based Win Condition

async onGameEnd(game: Game, winnerId: string, stats: unknown): Promise<void> {
  const teams = this.modState.get('teams') as Teams;

  // Determine winning team
  const winningTeam = teams.blue.includes(winnerId) ? 'blue' : 'red';
  const losingTeam = winningTeam === 'blue' ? 'red' : 'blue';

  console.log(`🏆 Team ${winningTeam.toUpperCase()} wins!`);

  // Award ELO to winning team
  for (const oderId of teams[winningTeam]) {
    await this.persistentStorage.increment('players', oderId, 'elo', 25);
  }

  // Deduct ELO from losing team
  for (const oderId of teams[losingTeam]) {
    await this.persistentStorage.increment('players', oderId, 'elo', -15);
  }
}

Advanced: Clan-Aware Team Assignment

Keep clan members together:

onPlayerAdded(player: Player): void {
  const teams = this.modState.get('teams') as Teams;
  const clanTeam = this.modState.get('clanTeam') as Record<string, TeamName>;

  let team: TeamName;

  // Check if player's clan is already assigned
  const clanId = player.clanId?.();
  if (clanId && clanTeam[clanId]) {
    team = clanTeam[clanId];
  } else {
    // Assign to smaller team
    team = teams.blue.length <= teams.red.length ? 'blue' : 'red';

    // Remember clan assignment
    if (clanId) {
      clanTeam[clanId] = team;
      this.modState.set('clanTeam', clanTeam);
    }
  }

  teams[team].push(player.id());
  this.modState.set('teams', teams);
  this.modState.setPlayerColor(player.id(), TEAM_COLORS[team]);
}

Complete Example

See the full Team Mode example for a complete implementation including troop donations between teammates.