| name | fvtt-dice-rolls |
| description | 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. |
Foundry 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);
formula: Dice expression string (e.g., "2d20kh + @prof")data: Object for @ variable substitutionoptions: Optional configuration
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 | /roll |
Everyone |
| GM | /gmroll |
Roller + GM |
| Blind | /blindroll |
GM only |
| Self | /selfroll |
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
await roll.evaluate()before accessingroll.total - Use
getRollData()returning a deep clone - Pass
rollMode: game.settings.get("core", "rollMode")to toMessage - Use
ChatMessage.getSpeaker({ actor })for proper speaker - 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