Integrating dddice into the "HP Tracker" extension for Owlbear Rodeo
— ♥ Celeste Bloodreign
While DMing with Owlbear I realized I needed more functionality for running my own games, so I created HP Tracker. Starting out as a simple tool for DMs to track Token HP with per player visibility, it now features a wide array of functionality, including creature statblocks. While dddice existed as a standalone extension in Owlbear, I really wanted to give my users the ability to roll damage directly from the HP Tracker statblocks while also supporting outside rolls from D&D Beyond and if possible others.
1️⃣ Phase 1 — Research
As with all software development projects I started out researching the possibilities.
There was:
- Adding a fork of the Owlbear dice roller to HP Tracker
- Doing everything from scratch
- Use one of the existing dice rollers
- Find a dice roller with an SDK or API
Pretty quickly, it became clear that there was one solution that stood out — dddice! dddice offers a well documented SKD and if that wasn't enough an API. Also, after a quick chat with nullfish, he gave me some pointers on where to start. So I made my decision that HP Tracker should get a full integration with dddice.
2️⃣ Phase 2 — Getting Started
Before starting with the implementation, I thought about all the features I wanted to implement. Because of the nature of Owlbear extensions, HP Tracker is basically a bunch of websites (token overview, statblocks, ...) that are connected to Owlbear via the OBR SDK. The main extension window and the stat block popover are each their own “website”. I needed a React Component to handle the dddice stack, but also to limit the roll to only one of the opened “websites”. In other words, I didn't want someone to click on the main extension window and then for the dice to roll atop every popup my extension made.
So the basic structure was:
- Each HP Tracker site gets the dddice-component which can be used to log in, change settings and roll dice
- A new transparent overlay is added where all dice rolls will be rendered
- A roll log is shown every time the overlay finishes a roll
Sounds simple enough! Let's go!
3️⃣ Phase 3 — But Wait! What about Edge Cases?
A thing I needed to keep in mind was, some Owlbear users were maybe already using the dddice extension. So a renderless mode needed to be added to HP Tracker. Because of dddice's client-server-model rolling in one extension and rendering in a second is easy. Synching rolls across apps is one of its basic features! The solution for HP Tracker is adding a toggle button to switch the rendering on or off.
Turning off dice rendering in HP Tracker led to the roll log no longer being displayed. Using dddice's event handlers made this easy to solve. I was able to hook directly into the roll events using the interface exposed by the ThreeDdiceAPI
object in the dddice SDK.
All other edge cases that popped up during development were quick to solve too, either by reading the SDK documentation or asking the very helpful dddice developers for help (Thank You Celest Bloodreign!).
4️⃣ Phase 4 — Gold Plating
Initially, my plan was to just add roll buttons to HP Trackers integrated Statblocks. However, using the dddice integration sped up the whole process so much I was able to add more functions for my users. I added: CustomRoll-Buttons, QuickRoll-Buttons, hidden rolls and more! All in all, integrating dddice into HP Tracker took about two weeks of programming in the evenings. Beating my initial estimation of two months by a lot.
🔍 Developer Findings
As I was using typescript for my implementation, I found that some operations were necessary when interacting with the dddice SDK. And as I found myself repeating them more than once I decided to wrap them into function (as developers tend to do). Here is a collection of some functions that others might also find useful.
🧑 Get the Participant
A lot of the times you want to know who exactly something belongs to. Whether it's a die roll or just look if a user is already part of a room. You end up getting the user
from the SDK/API then getting the room
and then looking if the user
is registered as a participant
in the room.
You can use this information to add the user
to a room
(if the participant
is not found). Or change the participant
s username so you can later use it, e.g., in the roll log (dddice uses this to display the D&D Beyond Character names in their rolls).
My solution for this are the following two function:
export const getDiceUser = async (rollerApi: ThreeDDiceAPI) => {
return (await rollerApi?.user.get())?.data;
};
export const getDiceParticipant = async (rollerApi: ThreeDDiceAPI, roomSlug: string | undefined, user?: IUser) => {
let diceUser: IUser | undefined = user;
if (diceUser === undefined) {
diceUser = await getDiceUser(rollerApi);
}
if (diceUser && roomSlug) {
const diceRoom = (await rollerApi.room.get(roomSlug))?.data;
// @ts-ignore we test diceUser in the if above it will not be undefined
return diceRoom?.participants.find((p) => p.user.uuid === diceUser.uuid);
}
return undefined;
};
An example where I use this is this function
export const prepareRoomUser = async (diceRoom: IRoom, rollerApi: ThreeDDiceAPI) => {
const participant = await getDiceParticipant(rollerApi, diceRoom.slug);
const name = await OBR.player.getName();
if (participant && participant.username !== name) {
await rollerApi.room.updateParticipant(diceRoom.slug, participant.id, {
username: name,
});
}
};
The purpose is getting the current participant and making sure that when they roll a die, the participant username is set to their Owlbear username. So you can see at first glance which of the Owlbear user rolled the dice.
🔌 Initialize and connect to dddice
Each time HP Tracker is started the user needs to be connected to dddice, so I have a function – with some wrapper code I don't include here – which is basically the auth flow I'm using (roller being an instance of ThreeDDice
). I also removed some functions and replaced them with their internal logic to make the code-flow more readable:
roller.initialize(canvas, await getApiKey(room), { autoClear: 3 }, `HP Tracker`);
if (roller.api) {
const diceRoom = await getDiceRoom(roller.api, room);
if (diceRoom) {
const user = (await roller.api?.user.get())?.data;
if (user) {
const participant = diceRoom.participants.find((p) => p.user.uuid === user.uuid);
if (participant) {
await prepareRoomUser(diceRoom, roller.api);
} else {
try {
const userDiceRoom = (await roller?.api?.room.join(diceRoom.slug, diceRoom.passcode))?.data;
if (userDiceRoom) {
await prepareRoomUser(userDiceRoom, roller.api);
}
} catch {
/**
* if we already joined. We already check that when
* looking if the user is a participant in the room,
* but better be safe than sorry
*/
}
}
roller.connect(diceRoom.slug, diceRoom.passcode, user.uuid);
}
}
return true;
}
What this does:
- Initialize the dddice API and let it know which application is connected
- Get the room that is used for this session (there is some internal logic there to make sure we get the right room)
- Get the dddice user
- Check if the user is already a room participant
a. User is already a participant, make sure the correct username is set
b. User is not a participant, join the room and then make sure the username is correct - Connect the ThreeDDice instance to the room with the current user
And the rest is beautifully rendered 3D dice 🎲
🎉 Conclusion
I loved every step of the way. Each little progress got me excited, and the dice rendering is one of the best I've seen across all digital dice rollers. If anybody is still on the fence of using dddice for their project, don't be! It's one of the best developer experiences I had in a long time.
This is a guest post written by Josh, the developer of HP Tracker. You can check out HP Tracker in the Owlbear Rodeo extension library or find his other work (and blog posts) at bitperfect.at.