xiaohongshu
xiaohongshu
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.
Full skill instructions
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04
Foundry VTT provides a powerful dice rolling system built around the Roll class. Understanding this system is essential for implementing game mechanics.
const roll = new Roll(formula, data, options);
formula: Dice expression string (e.g., "2d20kh + @prof")data: Object for @ variable substitutionoptions: Optional configurationconst roll = new Roll("2d20kh + @prof + @strMod", {
prof: 2,
strMod: 4
});
// 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
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.
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
});
// Only for maximize/minimize (deterministic)
roll.evaluateSync({ strict: true });
// With strict: false, non-deterministic = 0
roll.evaluateSync({ strict: false });
Sends a roll to chat as a ChatMessage.
await roll.toMessage();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: "Attack Roll",
user: game.user.id
}, {
rollMode: game.settings.get("core", "rollMode")
});
| 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")
Prepares data context for roll formulas.
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;
}
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
);
}
// In console with token selected:
console.log(canvas.tokens.controlled[0].actor.getRollData());
"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)
"5d10x" // Explode on max (10)
"5d10x8" // Explode on 8+
"2d10xo" // Explode once per die
"1d20r1" // Reroll 1s (once)
"1d20r<3" // Reroll below 3 (once)
"1d20rr<3" // Recursive reroll while < 3
"10d6cs>4" // Count successes > 4
"10d6cf<2" // Count failures < 2
"1d20min10" // Minimum result 10
"1d20max15" // Maximum result 15
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")
});
}
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")
});
}
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")
});
}
// 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>
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;
}
}
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);
}
}
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.
// 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
// WRONG - always public
roll.toMessage();
// CORRECT - respects user setting
roll.toMessage({}, {
rollMode: game.settings.get("core", "rollMode")
});
// WRONG - modifies document data
getRollData() {
return this.system; // Direct reference!
}
// CORRECT - return a copy
getRollData() {
return foundry.utils.deepClone(this.system);
}
// 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());
// 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);
// WRONG - rolls break on reload
class MyRoll extends Roll {}
// CORRECT - register with CONFIG
class MyRoll extends Roll {}
CONFIG.Dice.rolls.push(MyRoll);
// 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
});
await roll.evaluate() before accessing roll.totalgetRollData() returning a deep clonerollMode: game.settings.get("core", "rollMode") to toMessageChatMessage.getSpeaker({ actor }) for proper speakerRoll.validate()Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience