Skip to content
Back to skills

Fvtt Dice Rolls

ASecurity

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.

  • 666 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added February 8, 2026
developmentjavascriptjavaexpressdebuggingapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill fvtt-dice-rolls --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fvtt Dice Rolls?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fvtt Dice Rolls
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-fvtt-dice-rolls/badge)](https://www.skillsdirectory.com/skills/majiayu000-fvtt-dice-rolls)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

```javascript
const roll = new Roll(formula, data, options);
```

- `formula`: Dice expression string (e.g., "2d20kh + @prof")
- `data`: Object for @ variable substitution
- `options`: Optional configuration

```javascript
const roll = new Roll("2d20kh + @prof + @strMod", {
  prof: 2,
  strMod: 4
});
```

### Formula Syntax

```javascript
// 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

```javascript
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

```javascript
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)

```javascript
// 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

```javascript
await roll.toMessage();
```

### With Options

```javascript
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:**
```javascript
rollMode: game.settings.get("core", "rollMode")
```

## getRollData()

Prepares data context for roll formulas.

### Actor getRollData()

```javascript
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:

```javascript
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

```javascript
// In console with token selected:
console.log(canvas.tokens.controlled[0].actor.getRollData());
```

## Roll Modifiers

### Keep/Drop

```javascript
"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

```javascript
"5d10x"    // Explode on max (10)
"5d10x8"   // Explode on 8+
"2d10xo"   // Explode once per die
```

### Reroll

```javascript
"1d20r1"    // Reroll 1s (once)
"1d20r<3"   // Reroll below 3 (once)
"1d20rr<3"  // Recursive reroll while < 3
```

### Count Successes

```javascript
"10d6cs>4"  // Count successes > 4
"10d6cf<2"  // Count failures < 2
```

### Min/Max

```javascript
"1d20min10"  // Minimum result 10
"1d20max15"  // Maximum result 15
```

## Common Patterns

### Attack Roll

```javascript
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)

```javascript
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

```javascript
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

```javascript
// 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:**
```html
<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

```javascript
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

```javascript
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

```javascript
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()

```javascript
// 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

```javascript
// WRONG - always public
roll.toMessage();

// CORRECT - respects user setting
roll.toMessage({}, {
  rollMode: game.settings.get("core", "rollMode")
});
```

### 3. Modifying getRollData() Return

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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 accessing `roll.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

- [Roll API](https://foundryvtt.com/api/classes/foundry.dice.Roll.html)
- [Roll Wiki](https://foundryvtt.wiki/en/development/api/roll)
- [Dice Modifiers](https://foundryvtt.com/article/dice-modifiers/)
- [Advanced Dice](https://foundryvtt.com/article/dice-advanced/)
- [Dice in V12+](https://foundryvtt.wiki/en/development/guides/dice-in-v12)

---

**Last Updated:** 2026-01-04
**Status:** Production-Ready
**Maintainer:** ImproperSubset

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.