roll()
Roll dice, return the total, and — unless you say otherwise — show the roll in chat with 3D dice.
Part of the Action API · Function reference
Signature
roll(expression: string): number
roll(expression: string, pipeToChat: boolean): number
roll(expression: string, options: {
label?: string;
text?: string;
silent?: boolean;
pipeToChat?: boolean;
}): number
roll is synchronous. It returns a number, not a promise. await roll(...)
is harmless but does nothing.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
expression |
string |
— | The dice expression. See Expressions below. |
pipeToChat |
boolean |
true |
Shorthand second argument. false rolls silently — no chat row, no 3D dice. |
options.label |
string |
none | A header rendered above the roll, e.g. 'Perception'. Plain text; HTML is escaped. |
options.text |
string |
none | Supplemental text on the same chat row. {result} is replaced with the rendered roll; without the placeholder the roll is appended after your text. Plain text; HTML is escaped. |
options.silent |
boolean |
false |
true rolls silently — no chat row, no 3D dice — even when label/text are given. The readable spelling of the two below (v0.86.0). |
options.pipeToChat |
boolean |
true |
The older spelling: false means the same as silent: true. When both are given, silent wins. |
Returns
number — the total, immediately.
Negative totals are possible (roll('1d4-10')). The value is always a number:
an expression the roller cannot parse returns 0 rather than throwing.
Expressions
An expression is a series of terms joined by + and -. Each term is either a
dice group, a plain number, or a property name.
roll('1d20') // one d20
roll('d20') // the count is optional
roll('2d6+3') // dice plus a flat modifier
roll('1d8+1d6-2') // as many terms as you like
roll('1d20+strength') // a property name from the acting token
Dice groups are NdS — N dice of S sides, with N defaulting to 1.
Any number of sides works, including odd ones like 1d7. A silent roll
(pipeToChat: false) caps a group at 100 dice and returns 0 beyond that;
the chat path has no such cap, so keep groups sane rather than relying on
either behaviour.
Only + and - are supported. There is no *, /, or parentheses, and no
keep-highest / drop-lowest / exploding notation. Do that arithmetic in
JavaScript instead:
const total = roll('2d6', false) * 2;
Property names are resolved against the acting token, case-insensitively,
by property name, alias, or id — the same resolution
getProperty() uses, so str, Strength, and a
system-prefixed id all find the same value. The stored value is substituted:
a Strength of 18 puts 18 into the expression, not a derived modifier. If your
system wants a modifier, store one as its own property and roll that.
A name that resolves to nothing, or to something non-numeric, is left in the
expression as-is — which usually means the term is dropped from the total. A
bare d20-shaped word is always treated as dice, never as a property.
Chat output
| Expression | What appears in chat |
|---|---|
One group of standard dice, e.g. 2d6+3 |
A rich dice row, plus 3D dice tinted with the acting token's chat colour |
Several terms, e.g. 1d20+4+17 |
A plain dice line with the correct total; no 3D animation |
Non-standard dice, e.g. 1d7 |
A plain dice line. The 3D dice only know d4, d6, d8, d10, d12, d20, and d100 |
No dice at all, e.g. strength |
Nothing — the value is returned silently, unless you passed label or text |
The 3D dice are fire-and-forget: they never change or delay the number in chat.
Every message a single action sends — dice rows included — is grouped into one bordered entry in the chat log, so a five-roll action does not spam it.
Examples
A plain roll
const attack = roll('1d20+attackBonus');
A labelled roll
roll('1d20', { label: 'Perception' });
A roll inside a sentence
roll('2d6+3', {
label: 'Longsword',
text: `${token.name} swings and connects for {result}.`,
});
A silent roll you format yourself
const damage = roll('3d6', false);
sendMessage(`<b>${token.name}</b> deals ${damage} points of searing damage.`);
Attack, then damage on a hit
const attack = roll('1d20+attackBonus', { label: 'Attack' });
if (target && attack >= (target.getProperty('ac') ?? 10)) {
target.applyDamage(roll('1d8+2', { label: 'Damage' }), 'slashing');
} else {
sendMessage('A miss.');
}
Notes and gotchas
- Don't put a dice expression in
sendMessage()text if you already rolled it.sendMessage()rolls any dice expression it finds, sosendMessage('You take 2d6')produces a second, unrelated roll. Pass the number you already have, or useroll(expr, { text }). labelandtextare escaped. They are plain text by design. For HTML, roll silently and callsendMessage().- Each call is one roll. Calling
roll()twice gives two different results; store the total in a variable if you need it more than once. - The Test panel really rolls. Only chat output is captured; the numbers are real.
See also
rollDetailed()— the same maths, with the individual dice, and never any chat outputrequestRoll()— ask another player to rollsendMessage()— write your own chat output- Dice — how dice work everywhere else in the app