

Dice+
Missing Link Dev

Dice+
Support Development
If you enjoy Dice+ and my other extensions, please consider supporting development on Patreon! Your support helps me continue creating and maintaining quality extensions for the Owlbear Rodeo community.
A feature-rich 3D dice roller extension for Owlbear Rodeo with deterministic physics, customizable dice, and advanced dice rolling capabilities. Roll your dice in beautiful 3D with realistic physics simulation while maintaining complete control over your dice collection and rolling options.
Table of Contents
Features
- 3D Physics-Based Dice Rolling: Realistic dice rolling with deterministic physics using Three.js and Rapier physics engine
- Customizable Dice: Create custom dice with unique colors, materials, sizes, and shapes
- Quick Roll Interface: Fast dice selection with intuitive left/right click controls
- Advanced Dice Notation: Supports complex dice notation including advantage/disadvantage, exploding dice, re-rolls, min/max values, and more
- Dice Bag Management: Organize and manage your dice collection with filters, active/jailed states, and custom configurations
- Material System: Choose from Standard and Galaxy materials with extensive customization options
- Statistics Tracking: View detailed roll history and statistics for your dice
- Developer Integration: Broadcast channels for sending roll requests and receiving results from other extensions
Adding Dice
There are two primary ways to add dice to your roll:
Quick Roll Buttons
The quick roll interface provides fast, visual dice selection with modifier controls:
-
Add/Subtract Dice
- Left click on a dice button to add one die of that type
- Right click on a dice button to subtract one die
-
Advantage/Disadvantage
- Left click the A/D button to add advantage (roll an extra die and keep the highest)
- Right click the A/D button to add disadvantage (roll an extra die and keep the lowest)
- Visual badges show the number of advantage or disadvantage dice applied
-
Modifiers
- Use the + and - buttons to adjust the modifier value (adds or subtracts from the final total)
- Modifiers range from -99 to +99
-
Exploding Dice
- Left click the flame icon to enable exploding dice (when a die rolls its maximum value, roll it again and add the result)
- Right click the flame icon to remove the exploding modifier
- Badge shows the maximum number of explosions allowed (up to 10)
Dice Notation Input
For more complex rolls, use the dice notation input field. This supports the full range of dice notation features (see Dice Notation Reference below).
Examples:
2d20kh1+5- Roll 2d20, keep highest, add 54d6dl1 # Ability Score- Roll 4d6, drop lowest, with label3d6{Fear} # Fire + 2d6{Hope} # Ice- Use specific dice models with descriptions
Dice Bag
The Dice Bag is your dice collection management system where you can store, organize, and customize your dice.
Filters
Filter your dice collection by type:
- Filter chips show D4, D6, D8, D10/D100, D12, and D20
- Click a filter chip to show only that dice type
- Click again to remove the filter and show all dice
- Multiple filters can be active simultaneously
Active and Jailed Dice
Dice in your bag can be in two states:
- Active Dice: Available for rolling and rolls will cycle through your active dice as they are rolled
- Tip: For daggerheart create two d12s (Hope/Fear) and only have those two active, that way every time you roll it will roll those two and be easy to tell what your fear and hope score is.
- Jailed Dice: Stored but not currently available for rolling
- Dice not rolling great jail it
- Create sets for different characters and activate/jail dice to have the right dice for your session
The dice bag enforces a maximum capacity of 50 total dice (active + jailed combined). Unlimited Dice capacity for Premium users (see Patreon for upgrade)
Creating and Editing Dice
Click the "Add Dice" button to create new dice with custom properties:
Dice Type Selection
Choose the dice type Set (creates all dice type with same settings), D4, D6, D8, D10, D12, D20.
Name
Give your dice a custom name to easily identify them in your collection.
Preview
Real-time 3D preview of your dice with the selected shape, material, and settings.
Shape Selection
Choose the physical shape for your die:
- Each dice type supports multiple geometric shapes (e.g., D6 can be a cube or alternative shapes)
- The shape affects the visual appearance and rolling physics
Size Selection
Adjust the dice size from 25% to 300% of the standard size
Material Selection
Choose from two material types with extensive customization (more coming soon):
Standard Material
A physically-based material with realistic lighting and surface properties:
- Color: Base color of the dice body
- Font Color: Color of the numbers on the dice
- Number Depth (0.0-1.0): How deeply the numbers are engraved into the surface
- Roughness (0.0-1.0): Surface roughness (0 = mirror-like, 1 = matte)
- Metalness (0.0-1.0): Metallic appearance (0 = non-metallic, 1 = fully metallic)
- Sheen (0.0-1.0): Soft sheen/glow effect on the surface
Galaxy Material (Premium Only)
A stunning cosmic material with animated star fields:
- Font Color: Color of the numbers
- Number Depth (0.0-1.0): How deeply the numbers are engraved
- Primary Color: Primary color of the galaxy nebula
- Secondary Color: Secondary color blended into the galaxy
- Star Density (0.5-1.5): How many stars appear in the galaxy effect
- Brightness (0.0-2.0): Overall brightness of the galaxy effect
Dice Stats and Log
View detailed statistics and history for your dice:
- Roll history showing all past rolls
- Statistical analysis of roll outcomes
- Distribution charts showing roll frequencies
- Individual die performance tracking
Dice Notation Reference
Dice+ uses a dice notation system compatible with Roll20 and RPG Dice Roller, with some extensions and variations.
Basic Rolls
d20- Roll a single d202d6- Roll two six-sided dice1d8+5- Roll a d8 and add 53d6-2- Roll three d6 and subtract 2
Modifiers
Advantage/Disadvantage (Keep/Drop High/Low)
Keep Operators - Keep specified dice, drop the rest:
2d20kh1- Roll 2d20, keep highest 1 (advantage)2d20kl1- Roll 2d20, keep lowest 1 (disadvantage)4d6kh3- Roll 4d6, keep highest 3 (common for ability score generation)
Drop Operators - Drop specified dice, keep the rest:
2d20dl1- Roll 2d20, drop lowest 1 (same as keep highest - advantage)2d20dh1- Roll 2d20, drop highest 1 (same as keep lowest - disadvantage)4d6dl1- Roll 4d6, drop lowest 1 (keep highest 3)
Min/Max
1d20min10- Result cannot be less than 101d20max15- Result cannot be more than 15
Exploding
Exploding dice re-roll when they hit their maximum value and add the new roll to the total:
Basic Exploding:
1d6!or1d6e- Explode on 6 (implicit max value)1d6!6- Explode on 6 (explicit single value)1d6!6:3- Explode on 6, maximum 3 explosions1d6!>4- Explode on any value greater than 4
Multiple Values (Comma-Separated):
1d6!1,6- Explode on 1 or 61d6!1,6:2- Explode on 1 or 6, maximum 2 explosions
Re-Roll
Re-roll dice that meet certain conditions:
Basic Re-Roll:
1d20ror1d20rr- Re-roll on 1 (implicit minimum, infinite)1d20ro- Re-roll once on 11d20r1- Re-roll on 1 (explicit single value)1d20r<5- Re-roll on any value less than 51d6r6:3- Re-roll on 6, maximum 3 re-rolls
Multiple Values (Comma-Separated):
1d20r1,2- Re-roll on 1 or 21d8r1,2,7,8- Re-roll on extreme values
Unique (Coming Soon)
4d6u- Roll 4d6, all results must be unique4d6uo- Roll 4d6 unique, unlimited re-rolls to ensure uniqueness
Advanced Operators
Dice Model Selection
Assign specific dice from your dice bag by name:
2d6{Red}- Use dice named "Red" from your d6 dice bag1d12{Fear}+1d12{Hope}- Use "Fear" and "Hope" dice- If the named die doesn't exist, falls back to normal cycling through active dice
Roll Descriptions/Labels
Add descriptive labels to your rolls using the # symbol:
3d6 # Fire damage- Add a description/label to the roll3d6 # Fire + 2d6 # Ice- Multiple rolls with different descriptions- Descriptions stop at math operators, allowing complex expressions
Combined Features
All features can be combined for powerful roll expressions:
3d6{Red} # Fire damage + 2d6{White} # Frost damage- Models and descriptions2d20dl1{Red} # Attack with advantage- Drop lowest with model and description4d6dh1 # Ability Score- Drop highest with description
Complex Expressions
Dice notation supports complex mathematical expressions:
2d20 + 1d6 + 5- Multiple dice types in one roll2d20kh1 + 1d4- Combine advantage with additional dice-1d4- Negative dice (subtract the result)3d6!1,6 # Wild Magic- Explode on 1 or 6 with description2d20kh1 + 1d6!5,6 + 3- Advantage with exploding damage dice
Extension Integration
Dice+ provides broadcast channels for integration with other Owlbear Rodeo extensions using the OBR Broadcast API.
Ready Check Channel
Before sending roll requests, external extensions can check if Dice+ is loaded and ready to accept requests.
Channel: dice-plus/isReady
How It Works:
- Your extension sends a ready check request on the
dice-plus/isReadychannel - Dice+ automatically responds on the same channel when it's ready
- Your extension listens for the response to confirm Dice+ is available
Request Message Structure:
{
requestId: string; // Unique request identifier (to match responses)
timestamp: number; // Request timestamp
}
Response Message Structure:
{
requestId: string; // Matches the request requestId
ready: true; // Always true when Dice+ responds
timestamp: number; // Response timestamp
}
Example:
import OBR from "@owlbear-rodeo/sdk";
async function checkDicePlusReady(): Promise<boolean> {
const requestId = crypto.randomUUID();
return new Promise((resolve) => {
const unsubscribe = OBR.broadcast.onMessage("dice-plus/isReady", (event) => {
const data = event.data;
// Check if this is a response (not a request)
if ('ready' in data && data.requestId === requestId) {
unsubscribe();
resolve(true);
}
});
// Send ready check request
OBR.broadcast.sendMessage("dice-plus/isReady", {
requestId,
timestamp: Date.now()
}, { destination: 'ALL' });
// Timeout after 1 second if no response
setTimeout(() => {
unsubscribe();
resolve(false);
}, 1000);
});
}
// Use it before sending roll requests
const isDicePlusReady = await checkDicePlusReady();
if (!isDicePlusReady) {
console.error("Dice Plus extension not found!");
return;
}
Roll Request Channel
Other extensions can send dice roll requests to Dice+ using the broadcast channel.
Channel: dice-plus/roll-request
Message Structure:
{
rollId: string; // Unique roll identifier (save this to match results)
playerId: string; // OBR player ID
playerName: string; // Player display name
rollTarget: 'everyone' | 'self' | 'dm'; // Who sees the roll
diceNotation: string; // Standard dice notation (e.g., "2d20kh1+5")
showResults: boolean; // Show default popup (false = handle in your UI)
timestamp: number; // Request timestamp
source: string; // Your extension identifier (e.g., "my-extension-id")
}
Important Notes:
- source field: Must be your extension's unique identifier. This is used to create dedicated result and error channels for your extension.
- Result channels: Results are sent to your dedicate channel:
{source}/roll-result- Your extension's dedicated channel
- Error channel: Errors will be sent to
{source}/roll-error - Stats & History: ALL rolls (internal and external) are automatically logged to Dice+ stats and history to be viewed in dice bag stats panel no matter the source of the roll and to track dice statistics
- Popup behavior:
- Internal rolls always show the Dice+ popup
- External rolls show popup only if
showResults: true
Example:
import OBR from "@owlbear-rodeo/sdk";
const MY_EXTENSION_ID = "my-extension-id"; // Use your extension's unique identifier
const rollId = `roll_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
// Send roll request
await OBR.broadcast.sendMessage("dice-plus/roll-request", {
rollId,
playerId: await OBR.player.getId(),
playerName: await OBR.player.getName(),
rollTarget: 'everyone',
diceNotation: '2d20kh1+5',
showResults: false, // Handle results in your own UI
timestamp: Date.now(),
source: MY_EXTENSION_ID // Use your extension ID
}, { destination: 'ALL' });
// Listen for results on YOUR extension's channel
OBR.broadcast.onMessage(`${MY_EXTENSION_ID}/roll-result`, (event) => {
const result = event.data;
// Handle your roll result
if (result.rollId === rollId) {
console.log("Roll complete:", result.result.totalValue);
}
});
// Listen for errors on YOUR extension's channel
OBR.broadcast.onMessage(`${MY_EXTENSION_ID}/roll-error`, (event) => {
const error = event.data;
if (error.rollId === rollId) {
console.error("Roll failed:", error.error);
}
});
Roll Result Channel
Dice+ sends roll results to source-specific broadcast channels.
Channel: {source}/roll-result (where {source} is the value you provided in the roll request)
Message Structure:
{
rollId: string; // Matches the request rollId
playerId: string; // Who rolled
playerName: string; // Display name
rollTarget: 'everyone' | 'self' | 'dm'; // Roll target
timestamp: number; // Original timestamp
result: {
rollId: string; // Roll identifier
diceNotation: string; // Original notation
totalValue: number; // Final sum
rollSummary: string; // E.g., "2d6+3 = 11"
groups: DiceGroup[]; // Grouped results by notation order
};
}
// DiceGroup structure
interface DiceGroup {
description?: string; // E.g., "Fire" from "# Fire"
diceModel?: string; // E.g., "Red" from "{Red}"
diceType: string; // E.g., "d6", "d20"
dice: DiceResult[]; // Individual dice in this group
total: number; // Total of kept dice in this group
isNegative?: boolean; // Whether this group is subtracted
}
Complete Example with Data:
For a roll request of 2d20kh1-1d4 (advantage on d20, subtract 1d4):
const MY_EXTENSION_ID = "my-extension-id";
// Request sent
const rollId = 'roll_1760043220783_abc123';
await OBR.broadcast.sendMessage("dice-plus/roll-request", {
rollId,
playerId: '81e7413f-920c-4639-bd9d-1895ac156cf1',
playerName: 'Gandalf',
rollTarget: 'everyone',
diceNotation: '2d20kh1-1d4',
showResults: false,
timestamp: 1760043220783,
source: MY_EXTENSION_ID // Your extension ID
}, { destination: 'ALL' });
// Result received on channel: my-extension-id/roll-result
{
rollId: 'roll_1760043220783_abc123',
playerId: '81e7413f-920c-4639-bd9d-1895ac156cf1',
playerName: 'Gandalf',
rollTarget: 'everyone',
timestamp: 1760043220783,
result: {
rollId: 'roll_1760043220783_abc123',
diceNotation: '2d20kh1-1d4',
totalValue: 15, // Final result: 18 - 3 = 15
rollSummary: '[18] 18 - 3 = 15', // Human-readable summary
groups: [
{
diceType: "d20",
dice: [
{ diceId: 'dice_001', rollId: 'roll_abc123', diceType: 'd20', value: 18, kept: true },
{ diceId: 'dice_002', rollId: 'roll_abc123', diceType: 'd20', value: 7, kept: false }
],
total: 18, // Total of kept dice only (18)
isNegative: false
},
{
diceType: "d4",
dice: [
{ diceId: 'dice_003', rollId: 'roll_abc123', diceType: 'd4', value: 3, kept: true }
],
total: 3,
isNegative: true // This group is subtracted
}
]
}
}
Understanding the Results:
groupsorganizes all dice into their notation groups - even single dice get their own group- Each group includes ALL dice rolled for that group (both kept and dropped dice)
- Each group's
dicearray contains all dice with akeptflag indicating if they count toward the total - Each group's
totalfield is the sum of only the kept dice in that group totalValueis the final calculated result after all modifiers- For
2d20kh1-1d4: rolled 18 and 7 on the d20s (kept 18), rolled 3 on the d4, final: 18 - 3 = 15
Example with Descriptions and Dice Models:
For a roll request of 3d6{Red} # Fire + 2d6{White} # Ice:
// Result received on channel: my-extension-id/roll-result
{
rollId: 'roll_1760043220783_xyz789',
playerId: '81e7413f-920c-4639-bd9d-1895ac156cf1',
playerName: 'Gandalf',
rollTarget: 'everyone',
timestamp: 1760043220783,
result: {
rollId: 'roll_1760043220783_xyz789',
diceNotation: '3d6{Red} # Fire + 2d6{White} # Ice',
totalValue: 19,
rollSummary: '[1, 3, 6] 10 Fire + [2, 3, 4] 9 Ice = 19',
groups: [
{
description: "Fire",
diceModel: "Red",
diceType: "d6",
dice: [
{ diceId: 'dice_001', rollId: 'roll_xyz789', diceType: 'd6', value: 1, kept: true },
{ diceId: 'dice_002', rollId: 'roll_xyz789', diceType: 'd6', value: 3, kept: true },
{ diceId: 'dice_003', rollId: 'roll_xyz789', diceType: 'd6', value: 6, kept: true }
],
total: 10,
isNegative: false
},
{
description: "Ice",
diceModel: "White",
diceType: "d6",
dice: [
{ diceId: 'dice_004', rollId: 'roll_xyz789', diceType: 'd6', value: 2, kept: true },
{ diceId: 'dice_005', rollId: 'roll_xyz789', diceType: 'd6', value: 3, kept: true },
{ diceId: 'dice_006', rollId: 'roll_xyz789', diceType: 'd6', value: 4, kept: true }
],
total: 9,
isNegative: false
}
]
}
}
The groups field makes it easy to:
- Access dice results organized by their notation order
- Get the description and dice model name for each group
- Calculate group totals without parsing individual dice
- Build custom UI displays that match the roll notation
Roll Error Channel
Dice+ sends error messages to source-specific channels when a roll fails.
Channel: {source}/roll-error (where {source} is the value you provided in the roll request)
Message Structure:
{
rollId: string; // Matches the request rollId
error: string; // Error message
notation: string; // Notation that failed
}
Example Listener:
const MY_EXTENSION_ID = "my-extension-id";
OBR.broadcast.onMessage(`${MY_EXTENSION_ID}/roll-error`, (event) => {
const error = event.data;
// Match by rollId if needed
if (error.rollId === rollId) {
console.error(`Roll failed: ${error.error}`);
}
});
Made with ❤️ for the Owlbear Rodeo community