smithery/impropersubset

fvtt-combat

This skill should be used when working with the Combat tracker, Combatant documents, initiative rolling and sorting, turn/round management, or implementing combat hooks like combatStart, combatTurn, and combatRound.

Installation

$ npx skills add smithery/impropersubset --skill fvtt-combat

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/impropersubset.

npx skills add smithery/impropersubset

Browse all from smithery/impropersubset

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,340 B
  • docs SUMMARY.md 234 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Foundry VTT Combat System

Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05

Overview

Foundry's combat system manages turn-based encounters through Combat and Combatant documents. Understanding the combat lifecycle is essential for game system development.

When to Use This Skill

  • Implementing initiative rolling formulas
  • Managing turn order and round progression
  • Hooking into combat events
  • Extending combat behavior for game systems
  • Creating combat-related UI features

Combat Document

Core Properties

game.combat            // Active combat encounter
game.combats           // All combat encounters
game.combats.active    // Currently active combat

// Combat properties
combat.round           // Current round number
combat.turn            // Current turn index
combat.active          // Is combat started?
combat.combatants      // EmbeddedCollection of Combatants
combat.combatant       // Current turn's Combatant
combat.scene           // Linked Scene

Combat Methods

// Start/end combat
await combat.startCombat();
await combat.endCombat();

// Navigation
await combat.nextTurn();
await combat.previousTurn();
await combat.nextRound();
await combat.previousRound();

// Initiative
await combat.rollInitiative(["combatantId"]);
await combat.rollAll();    // Roll for all
await combat.rollNPC();    // Roll for NPCs only
await combat.resetAll();   // Clear all initiative

Combatant Document

Core Properties

combatant.actor        // Associated Actor
combatant.token        // Token data (not Token instance)
combatant.initiative   // Initiative value
combatant.active       // Is current turn?
combatant.defeated     // Defeated status
combatant.hidden       // Hidden from players?
combatant.players      // Owning players

Combatant Methods

// Roll initiative for this combatant
await combatant.rollInitiative();

// Get Roll without rolling
const roll = combatant.getInitiativeRoll();
const customRoll = combatant.getInitiativeRoll("1d20+5");

Initiative

Roll Initiative

// Single combatant
await combat.rollInitiative("combatantId");

// Multiple combatants
await combat.rollInitiative(["id1", "id2"]);

// With options
await combat.rollInitiative(["id1"], {
  formula: "1d20 + @abilities.dex.mod",
  messageOptions: { flavor: "Initiative" },
  updateTurn: true
});

Custom Initiative Formula

Override in your system's Combatant class:

class MyCombatant extends Combatant {
  _getInitiativeFormula() {
    const actor = this.actor;
    if (!actor) return "1d20";

    // System-specific formula
    return `1d20 + @abilities.dex.mod + @initiative.bonus`;
  }
}

// Register
CONFIG.Combatant.documentClass = MyCombatant;

Custom Sort Order

class MyCombat extends Combat {
  _sortCombatants(a, b) {
    // Higher initiative first
    const initA = a.initiative ?? -Infinity;
    const initB = b.initiative ?? -Infinity;
    if (initA !== initB) return initB - initA;

    // Tie-breaker: alphabetical
    return a.name.localeCompare(b.name);
  }
}

CONFIG.Combat.documentClass = MyCombat;

Combat Hooks

combatStart

Hooks.on("combatStart", (combat, updateData) => {
  console.log(`Combat started: Round ${updateData.round}`);
});

combatTurn

Hooks.on("combatTurn", (combat, updateData, updateOptions) => {
  const combatant = combat.combatants.contents[updateData.turn];
  console.log(`${combatant.name}'s turn`);

  // updateOptions.direction: 1 (forward) or -1 (backward)
});

combatRound

Hooks.on("combatRound", (combat, updateData, updateOptions) => {
  console.log(`Round ${updateData.round} started`);
});

updateCombat

Hooks.on("updateCombat", (combat, changes, options, userId) => {
  if ("turn" in changes) {
    console.log("Turn changed");
  }
  if ("round" in changes) {
    console.log("Round changed");
  }
});

Extending Combat

Workflow Methods

Override these for system-specific behavior:

class MyCombat extends Combat {
  // Called at start of each turn
  async _onStartTurn(combatant) {
    await super._onStartTurn(combatant);

    // Decrement duration effects
    const actor = combatant.actor;
    if (actor) {
      await this._decrementEffects(actor);
    }
  }

  // Called at end of each turn
  async _onEndTurn(combatant) {
    await super._onEndTurn(combatant);

    // Process end-of-turn effects
  }

  // Called at start of each round
  async _onStartRound() {
    await super._onStartRound();

    // Reset per-round resources
    for (const c of this.combatants) {
      await c.actor?.resetRoundResources?.();
    }
  }

  // Called at end of each round
  async _onEndRound() {
    await super._onEndRound();
  }

  async _decrementEffects(actor) {
    for (const effect of actor.effects) {
      if (effect.duration.rounds) {
        const remaining = effect.duration.rounds - 1;
        if (remaining <= 0) {
          await effect.delete();
        } else {
          await effect.update({ "duration.rounds": remaining });
        }
      }
    }
  }
}

CONFIG.Combat.documentClass = MyCombat;

GM-Only Execution

Workflow methods only run for one GM. Handle player-side logic with hooks:

// This runs for all clients
Hooks.on("combatTurn", (combat, update, options) => {
  // Player-safe logic here
});

Common Patterns

Get Current Combatant

function getCurrentCombatant() {
  const combat = game.combat;
  if (!combat?.started) return null;
  return combat.combatant;
}

Check If Actor's Turn

function isActorsTurn(actor) {
  const combat = game.combat;
  if (!combat?.started) return false;
  return combat.combatant?.actor?.id === actor.id;
}

Add Token to Combat

async function addToCombat(token) {
  let combat = game.combat;

  // Create combat if none exists
  if (!combat) {
    combat = await Combat.create({ scene: token.scene.id });
  }

  // Add combatant
  await combat.createEmbeddedDocuments("Combatant", [{
    tokenId: token.id,
    actorId: token.actor?.id,
    sceneId: token.scene.id
  }]);
}

Skip Defeated Combatants

class MyCombat extends Combat {
  async nextTurn() {
    let next = this.turn + 1;
    let round = this.round;

    // Skip defeated
    while (this.combatants.contents[next]?.defeated) {
      next++;
      if (next >= this.combatants.size) {
        next = 0;
        round++;
      }
    }

    return this.update({ turn: next, round });
  }
}

Common Pitfalls

1. Empty Combat Errors

// WRONG - errors with no combatants
const combat = await Combat.create({ scene: sceneId });
await combat.startCombat();  // Error!

// CORRECT - add combatants first
const combat = await Combat.create({
  scene: sceneId,
  combatants: [{ tokenId: token1.id }]
});
await combat.startCombat();

2. Token vs TokenDocument

// combatant.token is TokenDocument data, not Token
const tokenData = combatant.token;  // Plain object

// To get actual Token instance:
const token = canvas.tokens.get(combatant.tokenId);

3. Null Initiative

// Initiative can be null before rolling
const init = combatant.initiative;  // Could be null

// Handle null in comparisons
const sortedInit = init ?? -Infinity;

4. Workflow Method Timing

// _onStartTurn runs AFTER the turn changes
// Use hooks if you need to act BEFORE
Hooks.on("preUpdateCombat", (combat, changes) => {
  // Runs before any combat update
});

5. Linked vs Unlinked Tokens

// Be aware of token linking
if (combatant.token.actorLink) {
  // Changes affect the base Actor
} else {
  // Changes only affect this token's synthetic actor
}

6. Multiple GM Execution

// Workflow methods only run for one GM
// Don't rely on them for all-client logic

// Use hooks for all-client execution
Hooks.on("combatTurn", () => {
  // Runs on all clients
});

Implementation Checklist

  • Register custom Combat/Combatant classes in CONFIG
  • Override _getInitiativeFormula() for system formula
  • Implement _sortCombatants() for custom ordering
  • Use workflow methods for GM-only automation
  • Use hooks for all-client logic
  • Handle null initiative values
  • Check combat.started before accessing turn data
  • Consider defeated/hidden combatants
  • Test with multiple GMs connected

References


Last Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset