Skip to content

Modifiers

Modifiers transform dice results after rolling. They can drop dice, reroll values, cap ranges, make dice explode, and more. Modifiers are applied in a fixed priority order to ensure consistent, predictable results.

Modifiers are applied in priority order (lower number = earlier execution):

PriorityModifierNotationOptions key
10CapC{…}cap
20DropH, L, D{…}drop
21KeepK, klkeep
30ReplaceV{…}replace
40RerollR{…}reroll
50Explode!explode
51Compound!!compound
52Penetrate!ppenetrate
60UniqueUunique
85Pre-arithmetic multiply*Nmultiply
90Plus+Nplus
91Minus-Nminus
95Count successesS{…}countSuccesses
100Total multiply**NmultiplyTotal

This means dice values are capped/constrained first, then pool size is adjusted (drop/keep), then values are replaced or rerolled, then explosive mechanics fire, and finally arithmetic is applied.

interface ModifierOptions {
cap?: ComparisonOptions
drop?: DropOptions
keep?: KeepOptions
replace?: ReplaceOptions | ReplaceOptions[]
reroll?: RerollOptions
unique?: boolean | UniqueOptions
explode?: boolean | number
compound?: boolean | number
penetrate?: boolean | number
countSuccesses?: SuccessCountOptions
multiply?: number
plus?: number
minus?: number
multiplyTotal?: number
}

Limit roll values to a specific range. Values outside the range are clamped to the boundary.

Options key: cap

// Notation
roll('4d20C{>18}') // Cap over 18 to 18
roll('4d20C{<3}') // Cap under 3 to 3
roll('4d20C{<2,>19}') // Cap both ends
// Options object
roll({
sides: 20,
quantity: 4,
modifiers: {
cap: { greaterThan: 18 }
}
})
roll({
sides: 20,
quantity: 4,
modifiers: {
cap: { greaterThan: 19, lessThan: 2 }
}
})

ComparisonOptions:

interface ComparisonOptions {
greaterThan?: number
lessThan?: number
}

Remove dice from the result pool.

Options key: drop

// Drop lowest / highest
roll('4d6L') // Drop lowest 1
roll('4d6L2') // Drop lowest 2
roll('4d6H') // Drop highest 1
roll('4d6H2') // Drop highest 2
// Drop by value
roll('4d20D{>17}') // Drop results over 17
roll('4d20D{<5}') // Drop results under 5
roll('4d20D{8,12}') // Drop exact values
// Options object
roll({
sides: 6,
quantity: 4,
modifiers: {
drop: { lowest: 1 }
}
})
roll({
sides: 20,
quantity: 4,
modifiers: {
drop: { greaterThan: 17 }
}
})

DropOptions:

interface DropOptions extends ComparisonOptions {
highest?: number
lowest?: number
exact?: number[]
}

Keep specific dice from the result (complement of drop).

Options key: keep

// Keep highest
roll('4d6K3') // Keep highest 3
roll('4d6K') // Keep highest 1
// Keep lowest
roll('4d6kl2') // Keep lowest 2
roll('4d6kl') // Keep lowest 1
// Options object
roll({
sides: 6,
quantity: 4,
modifiers: {
keep: { highest: 3 }
}
})

KeepOptions:

interface KeepOptions {
highest?: number
lowest?: number
}

Replace specific results with new values.

Options key: replace

// Replace exact values
roll('4d20V{8=12}') // Replace 8s with 12s
// Replace by comparison
roll('4d20V{>17=20}') // Replace results over 17 with 20
roll('4d20V{<5=1}') // Replace results under 5 with 1
// Options object
roll({
sides: 20,
quantity: 4,
modifiers: {
replace: { from: 8, to: 12 }
}
})
roll({
sides: 20,
quantity: 4,
modifiers: {
replace: { from: { greaterThan: 17 }, to: 20 }
}
})

ReplaceOptions:

interface ReplaceOptions {
from: number | ComparisonOptions
to: number
}

You can pass an array of ReplaceOptions to perform multiple replacements.

Reroll dice matching certain conditions.

Options key: reroll

// Reroll by comparison
roll('4d20R{>17}') // Reroll results over 17
roll('4d20R{<5}') // Reroll results under 5
// Reroll exact values
roll('4d20R{8,12}') // Reroll 8s and 12s
// With max attempts
roll('4d20R{<5}3') // Reroll under 5, max 3 attempts
// Options object
roll({
sides: 20,
quantity: 4,
modifiers: {
reroll: { lessThan: 5, max: 3 }
}
})

RerollOptions:

interface RerollOptions extends ComparisonOptions {
exact?: number[]
max?: number
}

Roll additional dice when a die shows its maximum value.

Options key: explode

roll('4d20!') // Explode once per die
roll('3d6!5') // Max depth of 5
roll('3d6!0') // Unlimited (capped at 100)
// Options object
roll({
sides: 20,
quantity: 4,
modifiers: { explode: true } // Once per die
})
roll({
sides: 6,
quantity: 3,
modifiers: { explode: 5 } // Max depth 5
})

When a die shows its maximum value, a new die is rolled and added as a separate result. Each new maximum continues the chain.

Compounding exploding dice add to the triggering die instead of creating new dice.

Options key: compound

roll('3d6!!') // Compound once
roll('3d6!!5') // Max depth 5
roll('3d6!!0') // Unlimited (capped at 100)
// Options object
roll({
sides: 6,
quantity: 3,
modifiers: { compound: true }
})

Unlike regular exploding, compound does not create new dice — it modifies the existing die value.

Penetrating exploding dice subtract 1 from each subsequent explosion (Hackmaster-style).

Options key: penetrate

roll('3d6!p') // Penetrate once
roll('3d6!p5') // Max depth 5
roll('3d6!p0') // Unlimited (capped at 100)
// Options object
roll({
sides: 6,
quantity: 3,
modifiers: { penetrate: true }
})

Each subsequent penetration subtracts 1 from the result before adding, creating diminishing returns.

Force all dice in a pool to show different values.

Options key: unique

roll('4d20U') // All must be unique
roll('4d20U{5,10}') // Unique except 5s and 10s
// Options object
roll({
sides: 20,
quantity: 4,
modifiers: { unique: true }
})
roll({
sides: 20,
quantity: 4,
modifiers: {
unique: { notUnique: [5, 10] }
}
})

UniqueOptions:

interface UniqueOptions {
notUnique: number[]
}

Multiply the dice sum before +/- arithmetic.

Options key: multiply

roll('2d6*2+3') // (dice sum * 2) + 3
// Options object
roll({
sides: 6,
quantity: 2,
modifiers: { multiply: 2, plus: 3 }
})

Add or subtract a fixed value from the total.

Options keys: plus, minus

roll('4d6+5') // Add 5
roll('2d8-2') // Subtract 2
// Options object
roll({
sides: 6,
quantity: 4,
modifiers: { plus: 5 }
})

Count dice meeting a threshold instead of summing. Used in dice pool systems (World of Darkness, Shadowrun).

Options key: countSuccesses

roll('5d10S{7}') // Count dice >= 7
roll('5d10S{7,1}') // Successes >= 7, minus botches <= 1
// Options object
roll({
sides: 10,
quantity: 5,
modifiers: {
countSuccesses: { threshold: 7, botchThreshold: 1 }
}
})

SuccessCountOptions:

interface SuccessCountOptions {
threshold: number
botchThreshold?: number
}

Multiply the entire final total after all other modifiers.

Options key: multiplyTotal

roll('2d6+3**2') // (dice + 3) * 2
roll('4d6L+2**3') // ((drop lowest) + 2) * 3
// Options object
roll({
sides: 6,
quantity: 2,
modifiers: { plus: 3, multiplyTotal: 2 }
})

The full calculation order is:

  1. Roll all dice
  2. Apply cap (clamp values)
  3. Apply drop/keep (adjust pool)
  4. Apply replace (swap values)
  5. Apply reroll (re-roll matching dice)
  6. Apply explode/compound/penetrate (add dice or modify values)
  7. Apply unique (ensure no duplicates)
  8. Sum remaining dice
  9. Apply pre-arithmetic multiply (*)
  10. Apply plus/minus (+/-)
  11. Count successes if applicable (S{…})
  12. Apply total multiply (**)
// Full example: ((sum of kept dice) * 2) + 3) * 2
roll('4d6K3!*2+3**2')