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.