返回 Skill 列表
extension
分类: 开发与工程无需 API Key

fvtt-dice-rolls

此技能应在实现掷骰子、创建掷骰公式、使用toMessage将掷骰结果发送到聊天、准备getRollData、创建自定义骰子类型或处理如优势/劣势等掷骰修正时使用。涵盖Roll类、评估及常见模式。

person作者: jakexiaohubgithub

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 substitution
  • options: 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 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


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