Creating a discord bot for DnD

When the COVID shutdown hit, people were isolated in their homes which they only hoped would be a short period of time. But unfortunately, this lasted a lot longer that most would have anticipated. During this time, I was looking for a way to stay sane, binge watching tv shows and movies was simply not scratching the itch. A group of co-workers just prior to COVID actually introduced me to Dungeons and Dragons and I was intrigued. With in person meetups nixed the campaign we just delved into was put on hold as the DM was rather new and was a bit intimidated of running a game online.

Well, luckily not long after another individual in the gaming group decided to spin up a West Marches style campaign inviting everyone they knew interested in Dungeons and Dragons. This worked exceptionally well, with a handful of DMs, no set party, and somewhat self organizing parties. It was an awesome way to disconnect a bit from the stressors at home and jump into another world.

The group utilized Discord for organizing and mainly running the games as it was a free platform that, had chat channels, file sharing, voice communications etc. But as more and more individuals came to join in on the fun additional DMs were required to keep up with the demand requested by the players. And as more and more DMs came on board it became hard keeping certain things relatively constant such as shops and what happened between games to keep the storyline going.

Que Discord Bots

The group was already using Discord bots at the time for integrating with DnD Beyond or rolling dice in so everyone could see. (Much of this was prior to the amazing support that DnD Beyond now has today). So I thought to myself, these bots already handle a sizeable chunk of the workload for us what would it take for us to create something custom to our use case?

Actually as I came to find out, it was relatively easy to create a bot for Discord as it could be written in Node which was something I was already familiar with at the time. So I said, what the heck, and decided to explore what I could do with minimal effort the help the group and myself, who decided to brave the fair waters of running my own games as well.

Shops

First things first, I decided to build a simple shop in which players could buy the essentials. The DMs introduced a few different shops in the game, one of which I will specifically call out being the potion shop. This shop had a rotating supply that would replenish every Sunday or on demand by the DMs.

The potion shop carried 1d6 of uncommon potions and 1 rare potion each week. And the DMs used to roll which ones each week to determine the type and number of each uncommon and which rare potion was available. So instead the bot would manage this simple roll for them as well as the inventory so players could purchase outside of the session so it was faster to jump into the storyline rather than spend 30+ minutes in the shop every session.

I decided to use firebase as a simple object store for managing current stock, price, and what potions were available during the restocking sequence.

The store was simple create a “collection” of “shop” with each of the shop keepers name for the document. That document then contained data about their current stock, prices, and what was potentially available. Obviously, there are many ways to go about this but I found this was simple enough for my use case.

For the bot itself, I decided to use discord.js to manage the discord interaction, node-schedule to manage things like restocking that needed to happen on a schedule, and lastly, rpg-dice-roller for all of those important rolls I needed to make.

I created a simple Client file that would initialize my firebase connection, set the cron jobs / timezone, and load the commands to watch for into the discord client. I decided it would be easy to expand upon if all my commands followed the same interface and were in a commands folder with each command being its own file. You can see how they were loaded below:

// Create an instance of a Discord client
const client = new Client();
client.login(token);

client.commands = new Collection();
const commandFiles = fs.readdirSync('./src/commands').filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
  const command = require(`./commands/${file}`);
  client.commands.set(command.name, command);
}

As long as each file in the commands folder exported the below structure they could easily be loaded this way:

module.exports = { 
  args: false,
  name: 'shop',
  cooldown: 10,
  description: 'buy items from the shop',
  execute(message, args) {
    shop(message, args);
  }
 };

The shop portion of the bot worked in Discord being part of a channel “shop” and a user interacting with it using %shop. That would prompt a menu and allow the user to make a selection.

Although, prices were listed, we decided to just leave it to the players to deduct and only purchase what they could afford rather than manage money in multiple places. All in all the players loved having a place they could go and purchase items as well as the randomness it added every restocking period.

Pre and Post Game reports

As mentioned above, the game continued to grow and more and more DMs joined the queue to help drive the story forward. DMs were players and players were DMs alternating between their roll within the campaign depending on the day. Well, as that number increased it became increasingly more difficult to keep all the DMs aligned on the storyline. As or story was West Marches but we would revisit or drop major storylines in certain instances.

For this I created two commands that were locked down to specific individuals with the DM role in Discord. Those commands were %pregame and %postgame these were simple commands that would direct message the individual asking a series of questions about the game they were about to run or just ran and then post it to the appropriate text channel for the other DMs to read and discuss.

Here for example was what the post game command looked like:

async function postgame(message, args) {
  // Checks to make sure this is only used in the #shop or #dm-bot-playground channels
  if(message.channel.id !== '123456') {
    if(message.channel.id !== '09876543'){
      message.delete();
      sendMessage(message.author, 'Sorry you cannot use the "postgame" command in that channel.');
      return;
    }
  }
  // const snapshot = await db.collection('postgame').get();

  const filter = m => message.author.id === m.author.id;

  try {
    let instructionMsg = await sendMessage(message.channel, `${message.author} DMing you with further instructions`);
    sendMessage(message.author, "--- Game Master Postgame report ---");
    let adventurersQuestion = "Who are all the adventurers that attended this session?"
    let rumorQuestion = "Which rumor was run?";
    let monstersQuestion = "What monsters did you end up using in this adventure? (Please provide a link to Homebrew Creatures)";
    let extendedMonstersQuestion = "Which additional Monsters did you use, or what additions did you make besides normal things? (Adding, taking away HP, AC, etc)";
    let areasQuestion = "Which areas of West Marches were explored in your session? What are some landmarks?";
    let newAreasQuestion = "What NEW areas were explored? Or what did you add to an already established area?"
    let rewardsQuestion = "What are your planned rewards? (rough xp/player and loot. Please provide links to all Homebrew loot)";
    let newRewardsQuestion = "What NEW Rewards were given? Any extra loot or interesting items from monsters?"
    let outcomesQuestion = "Outcomes? Anything come up? (rule questions, new rumors, character arc ideas, etc. that DM's should know about)?";

    let adventurers = await prompt(message.author, filter, adventurersQuestion, 120000);
    let rumor = await prompt(message.author, filter, rumorQuestion, 120000);
    let monsters = await prompt(message.author, filter, monstersQuestion, 120000);
    let extendedMonsters = await prompt(message.author, filter, extendedMonstersQuestion, 120000);
    let areas = await prompt(message.author, filter, areasQuestion, 120000);
    let newAreas = await prompt(message.author, filter, newAreasQuestion, 120000);
    let rewards = await prompt(message.author, filter, rewardsQuestion, 120000);
    let newRewards = await prompt(message.author, filter, newRewardsQuestion, 120000);
    let outcomes = await prompt(message.author, filter, outcomesQuestion, 120000);

    sendMessage(message.author, "Thank you, I have posted your postgame report.")

    const postgame = new MessageEmbed()
      .setColor('#0099ff')
      .setTitle('Postgame report')
      .setAuthor(message.author.username)
      .setDescription('These reports are to provide a quick review of what actually took place in your session for those that were unable to make it.')
      .setThumbnail('https://pbs.twimg.com/profile_images/768147164753125376/boOkxhnZ_400x400.jpg')
      .addFields(
        { name: adventurersQuestion, value: adventurers },
        { name: rumorQuestion, value: rumor },
        { name: monstersQuestion, value: monsters },
        { name: extendedMonstersQuestion, value: extendedMonsters }, 
        { name: areasQuestion, value: areas }, 
        { name: newAreasQuestion, value: newAreas }, 
        { name: rewardsQuestion, value: rewards }, 
        { name: newRewardsQuestion, value: newRewards }, 
        { name: outcomesQuestion, value: outcomes }
      )
      .setTimestamp()
      .setFooter('Please feel free to start discussion in dm-table-talk', 'https://pbs.twimg.com/profile_images/768147164753125376/boOkxhnZ_400x400.jpg');

    message.channel.send(postgame);

    instructionMsg.delete();

    instructionMsg.delete();

  } catch (err) {
    console.log(err);
    sendMessage(message.channel, 'You did not respond. Preview post cancelled.'); 
  }
}

module.exports = { 
  args: false,
  name: 'postgame',
  cooldown: 10,
  description: 'give a postgame summary before you DM',
  adminOnly: true,
  execute(message, args) {
    postgame(message, args);
  }
 };

Conclusion

As you can see, small simplifications really improved the lives of the individuals running the games. The code and implementation was extremely quick and dirty as it was never meant to be distributed but just created to aid and assist us in our game. I encourage others to be open to using code to solve their common everyday problems, because with a few hours of work it may not be the cleanest solution but it will be functional and get the job done.

Leave a Reply