Integrating dddice into the "HP Tracker" extension for Owlbear Rodeo

Integrating dddice into the "HP Tracker" extension for Owlbear Rodeo
🎲
This article is a guest post by Josh, the developer of the HP Tracker extension for the Owlbear Rodeo Virtual Table Top. He is also a founder and developer at bitperfect a custom software company.
— ♥ 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.

HP Tracker with dddice roll-buttons at the bottom

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!

HP Tracker Owlbear Statblock with roll-buttons (STR hovered)

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!).

🎲
If you too are integrating with dddice, and you want the same help Josh got, come join our discord, we are happy to help
HP Tracker roll log

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.

HP Tracker - customizable roll-buttons

🔍 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 participants 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:

  1. Initialize the dddice API and let it know which application is connected
  2. Get the room that is used for this session (there is some internal logic there to make sure we get the right room)
  3. Get the dddice user
  4. 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
  5. 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.

0:00
/
HP Tracker with dddice integration demo

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.