Rolls are how Foundry handles randomization and the game math involving them.
Official Documentation
Legend
Roll.simulate() // `.` indicates static method or property
Roll#terms // `#` indicates instance method or property
Rolls are a critical piece of Foundry's infrastructure and useful to every tabletop roleplaying game, but their actual usage is entirely dependent on the system or module.
Code for Roll and its related classes can be found in yourFoundryInstallPath/resources/app/client-esm/dice/
.
Rolls work like normal javascript classes; code will frequently feature lines like const roll = new Roll(formula, data)
, where the resulting roll
variable is an instance of the Roll class. Also unlike other parts of Foundry, there aren't a ton of built-in static methods - most everything that matters is done as part of an instance of the class.
Once a roll instance is constructed, you can call Roll#evaluate
to produce a total for the roll. If you're planning to just send it straight to chat, Roll#toMessage
will evaluate the roll then create the chat message.
Asynchronicity. Calling Roll#evaluate
is an asynchronous operation, which means it must happen within an asynchronous function if you want to be able to use the total
. The exception is if you pass {maximize: true}
or {minimize: true}
, in which case the roll terms can be evaluated synchronously because turns non-deterministic values (e.g. 1d6
) into deterministic ones (e.g. 6
).
Evaluated Once. Once a roll is evaluated, that instance is locked; if you need to reroll, you can use Roll#clone
to create a new instance that can be modified, or perhaps a the helper method Roll#reroll
which clones and then immediately evaluates.
The Roll#terms
property actually determines how the roll is evaluated, and is constructed with Roll.parse
on the formula and data. It's an array of RollTerm
instances, which are handled in-order during evaluation to produce the final total.
The two most important subclasses of RollTerm
are DiceTerm
and NumericTerm
. DiceTerm is non-deterministic, which is to say that the result will change each time it is evaluated. NumericTerm, by contrast, is deterministic and always produces the same results.
The options
argument in the roll constructor baseline only uses a flavor
argument, but any JSON serializable parameter (e.g. Arrays but not Sets) can be passed and will be saved to this.options
.
The purpose of options
is far larger; when a roll is added to a chat message, it is serialized as a JSON string and then reconstituted by field initialization. If you assign this.foo = "bar"
, the serialization will not save that. However, if you assign this.options.foo = "bar"
, that will be saved by the serialization and will be properly available to chat messages.
Examples of things you might want to save into options:
formula
and data
, like advantage/disadvantage in 5eThe Roll
class has a large number of instance and static functions, the use of which may not be entirely obvious.
This simple instance method will multiply or add to each of the dice terms in a formula. This won't cover all possible ways you may want to modify a roll, but is an efficient one for many common use cases, such as adding or subtracting dice from a die pool.
API Reference
Foundry has built-in support for sending rolls to chat; often times just calling roll.toMessage()
is sufficient. The first argument, messageData
, uses the following defaults and then is passed into new ChatMessage
.
{
user: game.user.id,
content: String(this.total),
sound: CONFIG.sounds.dice
}
If this doesn't seem like much, that's correct - the majority of the formatting comes from roll.render
, which references Roll.CHAT_TEMPLATE
and Roll.TOOLTIP_TEMPLATE
to build the actual message HTML. These static methods are the actual best place to alter roll HTML as a system through providing your own handlebars template to be filled in.
The default object passed to the CHAT_TEMPLATE
is as follows
const chatData = {
formula: isPrivate ? "???" : this._formula,
flavor: isPrivate ? null : flavor ?? this.options.flavor,
user: game.user.id,
tooltip: isPrivate ? "" : await this.getTooltip(),
total: isPrivate ? "?" : Math.round(this.total * 100) / 100
}
If you want to change that, you need to override Roll#render
. Calling super
won't be helpful in this instance as it's returning the raw HTML string; instead, you need to fully override the function.
For further context, Roll#render
is called by two functions; ChatMessage#_renderRollHTML
and RollTable#toMessage
. The reason roll rendering is complicated and dynamic is to respect roll privacy settings across different clients, rather than have a static chunk of HTML stored in the message content
.
CONFIG.Dice.Rolls: Both Roll.create
and Roll.defaultImplementation
refer to CONFIG.Dice.Rolls[0]
, an array that by default is just the base Roll
class. These methods are used in a number of places:
/r
and /roll
[[/r 1d6]]
)Combatant#getInitiativeRoll
RollTable#roll
MathTerm
, ParentheticalTerm
, and PoolTerm
In combination with overriding CHAT_TEMPLATE
and TOOLTIP_TEMPLATE
you can do deep alterations of Foundry's default roll display across all possible invocations.
The remainder of the array is used as part of chat message serialization; if you use a roll subclass but don't register it with CONFIG.Dice.Rolls.push
the rolls will fail to be properly reconstructed as part of message initialization.
API Reference
The replaceFormulaData function, a static method of the Roll class, can help users visualize the effect of their roll formulas by providing the "translated" formula.
Here are some more specific examples and implementations involving the Roll class.
The Roll class isn't just useful for literal dice rolls - it's also useful as a well-scoped math engine to process user input. You can use Roll.safeEval
to validate that a user-input formula evaluates to a clean result, including the use of external roll data. This is the type of structure that dnd5e
uses for its custom AC formulas.
// assuming you've defined `actor` and `formula` in some way
const data = actor.getRollData()
const updatedFormula = Roll.replaceFormulaData(formula, data, {missing: '0', warn: true})
let result = null
try {
result = Roll.safeEval(updatedFormula)
}
catch {
ui.notifications.warn("Bad formula!") // this should be more descriptive
// if you do this in a preUpdate operation you could `return false` here
// or you can do some kind of fallback `result = FALLBACK_FORMULA` kinda deal
}
By default, rolls do not fire hooks - that must be handled by the system itself. One common way to do this is with a "preRoll" hook, along the lines of the following.
const roll = new Roll(formula, data)
if (Hooks.call("system.preRoll", roll) === false) return;
This constructs the roll, then creates a hook that modules can respond to with their own Hooks.on("system.preRoll", (roll) => {})
invocation. Systems can any number of additional arguments to the hook - keep in mind that objects passed this way are mutable, which may or may not be desirable.
To actually modify the roll as a module, the simplest option is to modify the terms
property of the roll, then call roll.resetFormula()
. Modifying the terms
can either involve making specific changes (Keeping in mind that the subclass of the RollTerm matters - if you're changing a 1d6
to a 4
or visa versa, you have to swap between DiceTerm
and NumericTerm
. Another method is to to re-run roll.terms = roll.constructor.parse(formula, data)
, if you want to totally rebuild things.
These are some of the common foibles and stumbling blocks when dealing with rolls.
Because Roll#evaluate
is asynchronous, unless you're using the maximize
or minimize
options it cannot be used in strictly synchronous operations. Within Foundry, there's two common cases for this
pre
document operation hooks. (Note that the _pre
document functions available to systems do run asynchronously).For the first, this is an intentional limitation - data preparation should be idempotent, which means re-running it shouldn't create new results. Randomization from a roll would violate this principal.
For the second, this is a major limitation of modules compared to systems; if you find yourself needing to perform an asynchronous preCreate
, preUpdate
, or preDelete
operations, consider the two following two options
create
, update
, or delete
operations - these fire on all clients, so you will need to ensure only one client performs any further operations and you avoid infinite loops.If you require synchronous randomization, for simple scenarios you can build your own.
Given a formula
of the format xdy+z
:
const TWIST = new foundry.dice.MersenneTwister(Date.now());
const roll = new Roll(formula);
roll.evaluateSync({strict: false}); // calculate the constant portion
let total = Array(roll.dice[0].number).fill(roll.dice[0].faces).reduce((acc,f) => acc+Math.ceil(TWIST.random()*f), roll.total);