Skip to content

Commit c885cf4

Browse files
committed
Merge branch 'develop'
2 parents bd5eb19 + 5e8a440 commit c885cf4

File tree

11 files changed

+631
-69
lines changed

11 files changed

+631
-69
lines changed

README.md

+81-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
1-
# Elbie-Mk.-III
2-
Elbeanor rewritten from scratch for the third time, this time utilizing node.js to run her as a real "server-side" application, now deployed via Heroku.
1+
# Elbie (Landon Bot)
2+
An extensible [Discord](https://discordapp.com/) bot designed to facilitate running rules-lite [TTRPG](https://en.wikipedia.org/wiki/Tabletop_role-playing_game) systems, including [Dungeon World](http://dungeon-world.com/), [Fiasco](http://bullypulpitgames.com/games/fiasco/), and others.
3+
What makes Elbie distinct from many of the conventionally available Discord bots is her tight RPG integrations, capable of handling skills, inventory, dice rolls, and creating character summaries on the fly. She also has rudimentary (and in-progress) audio capabiities, with built-in support for character theme songs.
4+
5+
Elbie's purpose is to provide a wide array of tools and accessories and then get out of the way, letting the players and DM be able to act freely without having to constantly be referring to a character sheet. This enables better role-playing and more mentally present players.
6+
## Installation
7+
### Adding to a server
8+
Elbie lives in Heroku at present to make her available 24/7. To add Elbie to a Discord server, visit [this link](https://discordapp.com/oauth2/authorize?client_id=255390394219626496&scope=bot&permissions=1) and select a server in which you have the Manage Server permission.
9+
### Installing Locally
10+
To run Elbie locally, fork this repository and clone it to your local machine. You'll need to register a Discord app to get a login token, and if you want her to report version information, you'll need a Github token as well. You'll need to add her to a server as well, which you can do while registering her as a Discord app.
11+
## Usage
12+
All of Elbie's commands start with a **prefix**, letting her know that you're talking to her. By default, this prefix is set to `+`, such that asking her to flip a coin is simply `+flip`.
13+
14+
Elbie is broken up into **modules**, each of which encapsulates several related commands. The "master" version of Elbie has all modules I've written installed, but writing one's own is fairly simple and lets Elbie be easily extensible. As it stands, the modules available in the master version are:
15+
* bot
16+
* `+ping`: Returns "pong!" as proof-of-life.
17+
* `+echo (String)`: Echoes back `(String)` as proof-of-life
18+
* `+flip`: Returns either "heads" or "tails"
19+
* rpg
20+
* `+r (Integer? modifier)`: Makes a single default roll as defined by whatever TTRPG system the channel is currently bound to (2d6 for PBtA games, 1d20 for most D&D/Pathfinder games, etc.) If `modifier` is provided, it will be added as a modifier
21+
* `+bind (String shortname) (String name)`: Binds the current channel to a new campaign under the name `name` (which may contain spaces) and with abbreviated name `shortname` (which must not contain spaces) and assigns the player who issued the command as DM. Setup will need to be finished on the web interface, which is not, at present, publically available. (If you want to use Elbie in a campaign of yours, send me a message and I will manually finish setup.)
22+
* `+who (String? name)`: Generates a character summary of either the current player if no name is provided, or if `name` is a player name (like "Sam"), a character name (like "Lurreka Al-Petarra"), or a nickname (like "Lurreka"), it will generate a summary of that character or the character assigned to that player. (For reference, if for some reason you have a player named "Sam" and a different player has a character named "Sam", `+who Sam` will refer to the player. If you are doing this, why?)
23+
* `+listChar`: Lists all the characters in the current campaign with their player's name (e.g. "Amateotl Maikali, played by Sam")
24+
* `+roll`: Accepts a comma-separated list of rolls and modifiers to perform in `xdy+z` format, e.g. `+roll 1d20+2d4+1d6+3,2d6+-2`. Note that it is crucial to preface any negative modifiers with a `+`, e.g. (`2d4+-1`).
25+
* `+hp (Integer? amount)`: Adjusts the current character's HP by the given `amount`. Restricts to values between 0 and the character's maximum health. Not providing `amount` returns the character's current HP.
26+
* `+mark (Integer? amount)`: Increases the current character's XP by `amount` if provided. Defaults to 1.
27+
* `+s (String skill) (Integer? modifier)`: Rolls a "skill roll" with the appropriate modifier depending on the character's skill in that attribute. If `modifier` is provided, it will modify the result. `skill` must be an abbreviated skill name ("str", "con", etc.)
28+
* `+levelup`: Increases the character's level by 1, if the character has enough XP. If not, fails.
29+
* (`+theme`:) Immediately plays the current character's theme. (This command is not currently available in the master build.)
30+
* (audio)
31+
* This module is not currently available in the master build. If you want to use this module, you must fork Elbie and compile her from the `audio` branch. Use at your own risk, as it is still actively in development and may change without warning.
32+
33+
## Extending Elbie
34+
Elbie is written to be fully extensible. All you need to do to write your own module is create a folder inside of `bot_modules` titled for your new module, and inside that create `index.js`. You may want to import `common.js` from `bot_modules`, which provides several common functions required by modules. Then you'll need four components in `index.js`:
35+
36+
```javascript
37+
const name = "your-module-name"
38+
const desc = "My new module"
39+
const functions = {
40+
foo: (Command)=>{
41+
Command.channel.send("Hello World")
42+
}
43+
}
44+
const commands = {
45+
'hello': functions.foo
46+
}
47+
module.exports = {name, desc, functions, commands}
48+
```
49+
To break this pattern down, `name` and `desc` are self-explanatory. `functions` is where all of your functions live, and their names may not be correlated with their command. That's why `commands` takes in a string (i.e. the command Elbie received) and returns a function, which she then executes by passing it the Command object she received. You are free to "alias" functions by having several keys in `commands` with the same function value.
50+
51+
Elbie automatically includes any modules in the bot_modules folder, so they should be available as soon as she starts.
52+
53+
If you've done everything right, when you start up Elbie, she should log the modules she has installed in the console and you should see your module's name and description, as well as its commands, which you should be able to use.
54+
55+
## Future Enhancements
56+
### Audio
57+
Audio is her next big step -- having her able to smoothly play music from youtube and possibly other sources would help her be a bot that can be used in a variety of circumstances.
58+
### Web Interface
59+
There is a lot you can do with a text interface, but for our purposes, setting up a web interface for instantiating a campaign, adjusting character attributes, or making administrative changes to your TTRPG sessions is going to be easier for everyone.
60+
### Grid-based Movement
61+
Many TTRPGs use a game-board like grid for character positioning and line-of-sight determination. At present, there are no plans to include such a feature in Elbie. Get back to role-playing!! ;-)
62+
63+
## Thanks & Tech
64+
* [discord-js](https://discord.js.org)
65+
* node
66+
* express
67+
* yarn
68+
* Mongoose
69+
* MongoDB
70+
* Heroku
71+
* ytdl
72+
_____
73+
* [Friends at the Table](http://friendsatthetable.net/)
74+
* [The Adventure Zone](http://www.maximumfun.org/shows/adventure-zone)
75+
* [Dungeon World](http://dungeon-world.com/)
76+
* The Pilgrims of Her Blue Flame
77+
78+
## One Last Thing
79+
If you're a traditional D&D or Pathfinder player who has grown a little tired of the standard formula of "go into dungeon, kill goblin, walk out with treasure, repeat until dead", I urge you to check out any of the "Powered By the Apocalypse" games. Dungeon World is their D&D analogue, and since our group switched, we've seen better stories, more realistic role-playing, less focus on stats and skills and more focus on interpersonal connection.
80+
81+
If you've never played a TTRPG before, Dungeon World is a great place to start. There are few rules to memorize, lots of things you can do, and lots of ways to be an extraordinary person in a fantasy world. I'm blessed to work with the world's greatest Game Master, but there are lots of great published adventures that your group can run that don't involve writing a novel's worth of backstory.

api/discordauth.js

-32
This file was deleted.

bot_modules/audio/index.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// dependencies
2+
const ytdl = require('ytdl-core')
3+
4+
// module information
5+
const name = 'audio'
6+
const desc = 'functions to allow Elbie to stream audio thru a voice channel'
7+
const audio = {
8+
passes: 3,
9+
volume: 0.5,
10+
play: (Command) => {
11+
if (!Command.server) {
12+
Command.channel.send('You have to be in a server to use voice!')
13+
} else {
14+
if (Command.member.voiceChannel) {
15+
console.log(`Connecting to voice in ${Command.server.name} in channel ${Command.channel.name}`)
16+
Command.member.voiceChannel.join()
17+
.then(connection => {
18+
console.log('Successfully connected to voice channel.')
19+
let stream = ytdl(Command.argument)
20+
ytdl.getInfo(Command.argument, (err, info) => {
21+
if (err) console.log(err)
22+
else {
23+
Command.channel.send(`Playing ${info.title}`)
24+
const dispatcher = connection.playStream(stream, {audioonly: true})
25+
dispatcher.on('end', () => {
26+
Command.member.voiceChannel.leave()
27+
})
28+
}
29+
})
30+
})
31+
}
32+
}
33+
},
34+
stop: (Command) => {
35+
if (Command.member.voiceChannel) {
36+
try {
37+
Command.member.voiceChannel.leave()
38+
} catch (err) {
39+
console.log(err)
40+
Command.channel.send("I can't stop if I'm not playing anything")
41+
}
42+
}
43+
}
44+
}
45+
46+
const commands = {
47+
play: audio.play,
48+
stop: audio.stop
49+
}
50+
module.exports = { audio, commands, name, desc }

bot_modules/bot.js bot_modules/bot/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// dependencies
2-
const common = require('./common.js')
2+
const common = require('../common.js')
33
// basic bot commands
44
const name = 'bot'
55
const desc = 'basic bot commands'

bot_modules/common.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const name = 'common'
2+
const desc = 'Common functions for use in other modules'
13
// common commands for use in other elbie modules
24
const common = {
35
randInt (a, b) {
@@ -16,7 +18,7 @@ const common = {
1618
typed (arg) {
1719
if (isNaN(parseFloat(arg))) {
1820
return arg
19-
} else if (Math.floor(parseFloat(arg)) == parseFloat(arg)) {
21+
} else if (Math.floor(parseFloat(arg)) === parseFloat(arg)) {
2022
return parseInt(arg)
2123
} else return parseFloat(arg)
2224
},
@@ -27,4 +29,5 @@ const common = {
2729
return string.charAt(0).toUpperCase() + string.slice(1)
2830
}
2931
}
30-
module.exports = common
32+
const commands = {}
33+
module.exports = {name, desc, common, commands}

bot_modules/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const fs = require('fs')
2+
let modules = []
3+
fs
4+
.readdirSync(__dirname)
5+
.filter(file => file !== 'index.js')
6+
.forEach(file => {
7+
modules.push(require(`./${file}`))
8+
})
9+
module.exports = modules

bot_modules/rpg/character.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ const common = require('../common.js')
33
const gameList = {
44
'Dungeon World': require('./dungeon-world.js')
55
}
6+
const audio = require('../audio') || false
67

78
const DB_URI = process.env.MONGODB_URI
89
const mongoose = require('mongoose')
910
const db = require('../../models/schema.js')
10-
mongoose.connect(DB_URI).then(
11+
mongoose.connect(DB_URI, {useNewUrlParser: true}).then(
1112
() => {
1213
console.log('DB connection ready')
1314
},
@@ -79,8 +80,7 @@ var Character = {
7980
Character.getChar(Command, (char) => {
8081
var attr = Command.args[0]
8182
try {
82-
if (!char[attr] & char[attr] !== 0) cb(char[attr])
83-
else Command.channel.send(`Could not fetch attribute ${attr} -- query returned undefined.`)
83+
if (!char[attr] & char[attr] !== 0) { console.log(char[attr]); cb(char[attr]) } else Command.channel.send(`Could not fetch attribute ${attr} -- query returned undefined.`)
8484
} catch (err) {
8585
console.error(err)
8686
Command.channel.send('Could not fetch attribute ' + attr)
@@ -114,6 +114,20 @@ var Character = {
114114
} else Command.channel.send('Could not level up. Check EXP.')
115115
})
116116
})
117+
},
118+
theme: function (Command) {
119+
if (audio) {
120+
Character.getChar(Command, function (char) {
121+
Command.argument = `https://www.youtube.com/watch?v=${char.get('theme')}`
122+
console.log(char)
123+
console.log(Object.keys(char))
124+
console.log(char.get('theme'))
125+
console.log(Command.argument)
126+
audio.audio.play(Command)
127+
})
128+
} else {
129+
Command.channel.send('audio module not installed or not working.')
130+
}
117131
}
118132
}
119133
module.exports = {Character, gameList, db}

bot_modules/rpg.js bot_modules/rpg/index.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// dependencies
2-
const common = require('./common.js')
2+
const common = require('../common.js').common
33
const Discord = require('discord.js')
4-
const cInfo = require('./rpg/character.js')
4+
const cInfo = require('./character.js')
55
const Character = cInfo.Character
66
const db = cInfo.db
77

@@ -190,7 +190,8 @@ const commands = {
190190
hp: rpg.cast,
191191
mark: rpg.cast,
192192
s: rpg.statRoll,
193-
levelup: Character.levelup
193+
levelup: Character.levelup,
194+
theme: Character.theme
194195
}
195196
// object to turn game strings into game objects
196197
module.exports = {rpg, commands, name, desc}

elbie.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ require('dotenv').config()
33
const Discord = require('discord.js')
44
const client = new Discord.Client()
55
const token = process.env.DISCORD_TOKEN
6-
const bot = require('./bot_modules/bot.js')
7-
const rpg = require('./bot_modules/rpg.js')
6+
const modules = require('./bot_modules')
87
const axios = require('axios')
98
const prefix = '+'
9+
const env = process.env.NODE_ENV || 'dev'
1010

1111
// adding commands
12-
var modules = [bot, rpg]
1312
var commandList = {}
1413
modules.forEach(module => {
1514
Object.assign(commandList, module.commands)
16-
console.log('Loaded commands for module ' + module.name)
17-
console.log('>' + module.desc)
15+
console.log('Loaded commands for module ' + module['name'])
16+
console.log('>' + module['desc'])
1817
})
1918
console.log(commandList)
2019

@@ -59,7 +58,7 @@ client.on('ready', function () {
5958
'status': 'online',
6059
'afk': false,
6160
'game': {
62-
name: version,
61+
name: env !== 'dev' ? `${version}` : `DEV -- ${version}`,
6362
type: 'PLAYING'
6463
}
6564
})
@@ -100,9 +99,11 @@ client.on('message', function (message) {
10099
var Command = {
101100
channel: clean(message.channel),
102101
auth: clean(message.author),
102+
member: message.member,
103103
command: content[0],
104104
argument: content.slice(1).join(' '),
105-
args: content.slice(1)
105+
args: content.slice(1),
106+
server: message.guild
106107
}
107108
// commands
108109
if (!Object.keys(commandList).includes(Command.command)) {

package.json

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
{
22
"name": "elbie",
3-
"version": "3.3.2",
3+
"version": "3.5.0",
44
"description": "discord bot designed to handle dungeon world and a few other ttrpgs",
55
"main": "elbie.js",
66
"dependencies": {
77
"axios": "^0.18.0",
8-
"discord.js": "^11.3.2",
8+
"bufferutil": "^3.0.3",
9+
"discord.js": "^11.4.2",
910
"dotenv": "^5.0.1",
1011
"ejs": "^2.6.1",
12+
"erlpack": "discordapp/erlpack",
1113
"eslint": "^4.19.1",
1214
"express": "^4.16.3",
1315
"ffmpeg-binaries": "^3.2.2",
16+
"fluent-ffmpeg": "^2.1.2",
1417
"fs": "0.0.1-security",
18+
"libsodium-wrappers": "^0.7.3",
1519
"mongoose": "^5.0.17",
16-
"node-fetch": "^2.1.2"
20+
"node-fetch": "^2.1.2",
21+
"node-opus": "^0.3.0",
22+
"save": "^2.3.2",
23+
"ytdl-core": "^0.20.4"
1724
},
1825
"devDependencies": {
1926
"eslint-config-standard": "^11.0.0",
@@ -23,7 +30,7 @@
2330
"eslint-plugin-standard": "^3.1.0"
2431
},
2532
"scripts": {
26-
"start": "node index.js",
33+
"start": "node elbie.js",
2734
"test": "echo \"Error: no test specified\" && exit 1"
2835
},
2936
"repository": {

0 commit comments

Comments
 (0)