Claude-skill-registry fvtt-dice-rolls
This skill should be used when implementing dice rolling, creating Roll formulas, sending rolls to chat with toMessage, preparing getRollData, creating custom dice types, or handling roll modifiers like advantage/disadvantage. Covers Roll class, evaluation, and common patterns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fvtt-dice-rolls" ~/.claude/skills/majiayu000-claude-skill-registry-fvtt-dice-rolls && rm -rf "$T"
skills/data/fvtt-dice-rolls/SKILL.mdFoundry VTT Dice Rolls
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04
Overview
Foundry VTT provides a powerful dice rolling system built around the
Roll class. Understanding this system is essential for implementing game mechanics.
When to Use This Skill
- Creating roll formulas with variable substitution
- Implementing attack rolls, damage rolls, saving throws
- Sending rolls to chat with proper speaker/flavor
- Preparing actor/item roll data with getRollData()
- Creating custom dice types for specific game systems
- Using roll modifiers (keep, drop, explode, reroll)
Roll Class Basics
Constructor
const roll = new Roll(formula, data, options);
: Dice expression string (e.g., "2d20kh + @prof")formula
: Object for @ variable substitutiondata
: Optional configurationoptions
const roll = new Roll("2d20kh + @prof + @strMod", { prof: 2, strMod: 4 });
Formula Syntax
// Basic dice "1d20" // Roll one d20 "4d6" // Roll four d6 // Variables with @ syntax "1d20 + @abilities.str.mod" "1d20 + @prof" // Nested paths "@classes.barbarian.levels" "@abilities.dex.mod" // Parenthetical (dynamic dice count) "(@level)d6" // Roll [level] d6s // Dice pools "{4d6kh3, 4d6kh3, 4d6kh3}" // Multiple separate rolls
Roll Evaluation
Async evaluate() - REQUIRED
const roll = new Roll("1d20 + 5"); await roll.evaluate(); console.log(roll.result); // "15 + 5" console.log(roll.total); // 20
Critical:
roll.total is undefined until evaluated.
Evaluation Options
await roll.evaluate({ maximize: true, // All dice roll max value minimize: true, // All dice roll min value allowStrings: true // Don't error on string terms });
Sync Evaluation (Deterministic Only)
// Only for maximize/minimize (deterministic) roll.evaluateSync({ strict: true }); // With strict: false, non-deterministic = 0 roll.evaluateSync({ strict: false });
Roll.toMessage()
Sends a roll to chat as a ChatMessage.
Basic Usage
await roll.toMessage();
With Options
await roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: "Attack Roll", user: game.user.id }, { rollMode: game.settings.get("core", "rollMode") });
Roll Modes
| Mode | Command | Visibility |
|---|---|---|
| Public | | Everyone |
| GM | | Roller + GM |
| Blind | | GM only |
| Self | | Roller only |
Always respect user's roll mode:
rollMode: game.settings.get("core", "rollMode")
getRollData()
Prepares data context for roll formulas.
Actor getRollData()
getRollData() { // Always return a COPY const data = foundry.utils.deepClone(this.system); // Add shortcuts data.lvl = data.details.level; // Flatten ability mods for easy access for (const [key, ability] of Object.entries(data.abilities)) { data[key] = ability.mod; // @str, @dex, etc. } return data; }
Item getRollData()
Merge item and actor data:
getRollData() { const data = foundry.utils.deepClone(this.system); if (!this.actor) return data; // Merge actor's roll data return foundry.utils.mergeObject( this.actor.getRollData(), data ); }
Debugging Roll Data
// In console with token selected: console.log(canvas.tokens.controlled[0].actor.getRollData());
Roll Modifiers
Keep/Drop
"4d6kh3" // Keep 3 highest (ability scores) "4d6kl3" // Keep 3 lowest "4d6dh1" // Drop 1 highest "4d6dl1" // Drop 1 lowest "2d20kh" // Advantage (keep highest) "2d20kl" // Disadvantage (keep lowest)
Exploding Dice
"5d10x" // Explode on max (10) "5d10x8" // Explode on 8+ "2d10xo" // Explode once per die
Reroll
"1d20r1" // Reroll 1s (once) "1d20r<3" // Reroll below 3 (once) "1d20rr<3" // Recursive reroll while < 3
Count Successes
"10d6cs>4" // Count successes > 4 "10d6cf<2" // Count failures < 2
Min/Max
"1d20min10" // Minimum result 10 "1d20max15" // Maximum result 15
Common Patterns
Attack Roll
async rollAttack() { const rollData = this.actor.getRollData(); const parts = ["1d20"]; if (this.system.proficient) parts.push("@prof"); if (this.system.ability) parts.push(`@${this.system.ability}.mod`); if (this.system.attackBonus) parts.push(this.system.attackBonus); const formula = parts.join(" + "); const roll = new Roll(formula, rollData); await roll.evaluate(); return roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: `${this.name} - Attack Roll`, rollMode: game.settings.get("core", "rollMode") }); }
Damage Roll (with Critical)
async rollDamage(critical = false) { const rollData = this.actor.getRollData(); let formula = this.system.damage.formula; // Add ability mod if (this.system.damage.ability) { formula += ` + @${this.system.damage.ability}.mod`; } // Double dice on critical if (critical) { formula = formula.replace(/(\d+)d(\d+)/g, (m, num, faces) => { return `${num * 2}d${faces}`; }); } const roll = new Roll(formula, rollData); await roll.evaluate(); return roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: `${this.name} - ${critical ? "Critical " : ""}Damage`, rollMode: game.settings.get("core", "rollMode") }); }
Ability Check with Advantage/Disadvantage
async rollAbility(abilityId, { advantage = false, disadvantage = false } = {}) { const rollData = this.actor.getRollData(); let dieFormula = "1d20"; if (advantage && !disadvantage) dieFormula = "2d20kh"; if (disadvantage && !advantage) dieFormula = "2d20kl"; const formula = `${dieFormula} + @abilities.${abilityId}.mod`; const roll = new Roll(formula, rollData); await roll.evaluate(); return roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: `${CONFIG.abilities[abilityId]} Check`, rollMode: game.settings.get("core", "rollMode") }); }
Sheet Rollable Button
// In activateListeners html.on("click", ".rollable", this._onRoll.bind(this)); async _onRoll(event) { event.preventDefault(); const element = event.currentTarget; const { roll: formula, label } = element.dataset; if (!formula) return; const roll = new Roll(formula, this.actor.getRollData()); await roll.evaluate(); return roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), flavor: label || "Roll", rollMode: game.settings.get("core", "rollMode") }); }
Template:
<a class="rollable" data-roll="1d20 + @str" data-label="Strength Check"> <i class="fas fa-dice-d20"></i> Roll </a>
Custom Dice
Custom Die Class
export class StressDie extends foundry.dice.terms.Die { static DENOMINATION = "s"; // Use as "1ds" async evaluate(options = {}) { await super.evaluate(options); // Custom logic: explode on 6, panic on 1 for (const result of this.results) { if (result.result === 6) result.exploded = true; if (result.result === 1) result.panic = true; } return this; } }
Custom Roll Class
export class CustomRoll extends Roll { static CHAT_TEMPLATE = "systems/mysystem/templates/roll.hbs"; get successes() { return this.dice.reduce((sum, die) => { return sum + die.results.filter(r => r.success).length; }, 0); } }
Registration
Hooks.once("init", () => { CONFIG.Dice.terms.s = StressDie; CONFIG.Dice.rolls.push(CustomRoll); });
Critical: Register custom rolls or they won't reconstruct from chat messages.
Common Pitfalls
1. Using total Before evaluate()
// WRONG - total is undefined const roll = new Roll("1d20"); console.log(roll.total); // undefined! // CORRECT const roll = new Roll("1d20"); await roll.evaluate(); console.log(roll.total); // 15
2. Ignoring Roll Mode
// WRONG - always public roll.toMessage(); // CORRECT - respects user setting roll.toMessage({}, { rollMode: game.settings.get("core", "rollMode") });
3. Modifying getRollData() Return
// WRONG - modifies document data getRollData() { return this.system; // Direct reference! } // CORRECT - return a copy getRollData() { return foundry.utils.deepClone(this.system); }
4. Stale Roll Data
// WRONG - data captured once const rollData = this.actor.getRollData(); // ...actor updates... new Roll("1d20 + @prof", rollData); // Stale! // CORRECT - get fresh data new Roll("1d20 + @prof", this.actor.getRollData());
5. Unvalidated User Input
// UNSAFE const roll = new Roll(userInput); // SAFER - validate first if (!Roll.validate(userInput)) { ui.notifications.error("Invalid roll formula"); return; } const roll = new Roll(userInput, rollData);
6. Forgetting to Register Custom Rolls
// WRONG - rolls break on reload class MyRoll extends Roll {} // CORRECT - register with CONFIG class MyRoll extends Roll {} CONFIG.Dice.rolls.push(MyRoll);
7. Async in preCreate Hooks
// PROBLEMATIC - hooks can't reliably await Hooks.on("preCreateItem", async (doc, data) => { const roll = new Roll("1d20"); await roll.evaluate(); // May fail! }); // BETTER - use onCreate Hooks.on("createItem", async (doc, options, userId) => { if (userId !== game.user.id) return; const roll = new Roll("1d20"); await roll.evaluate(); // Safe });
Implementation Checklist
- Always
before accessingawait roll.evaluate()roll.total - Use
returning a deep clonegetRollData() - Pass
to toMessagerollMode: game.settings.get("core", "rollMode") - Use
for proper speakerChatMessage.getSpeaker({ actor }) - Validate user-provided formulas with
Roll.validate() - Register custom Roll/Die classes in CONFIG.Dice
- Add flavor text describing the roll
- Use @ syntax for variable substitution in formulas
References
Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset