123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- #!/usr/bin/env node
- const camo = require('camo')
- const discord = require('discord.js')
- const _ = require('lodash')
- const config = require('./config')
- const log = require('./util/log')
- const chalk = require('chalk')
- const commandExists = require('command-exists')
- const EmoteStore = require('./stores/emote')
- const Character = require('./character')
- const Battle = require('./battle/battle')
- const Move = require('./battle/move')
- const Party = require('./character/party')
- const { DiscordAI } = require('./battle/ai')
- class Game {
- // Starts the game. Should only run once per process!
- async boot() {
- // Check graphicsmagick is installed
- if (!await commandExists('gm')) {
- log.warning('GraphicsMagick not found - please install it')
- }
- // Load the config file
- await config.load()
- // Connect to the database (nedb or mongodb)
- await camo.connect(config.get('database_uri'))
- // Create the bot pool
- this.clientPool = await Promise.all(config.get('discord_bot_tokens').map(async token => {
- const client = new discord.Client()
- try {
- await client.login(token)
- } catch (err) {
- log.critical('Bot login failure (is the token correct?)')
- return null
- }
- // Check the bot is actually in all the guilds (servers) we will be using
- let inAllGuilds = true
- const guildIDs = [
- config.get('discord_server_id'),
- ...config.get('discord_emote_store_server_ids'),
- ]
- for (const guildID of guildIDs) {
- if (!client.guilds.get(guildID)) {
- inAllGuilds = false
- }
- }
- if (inAllGuilds) {
- log.ok(`Bot ${chalk.blue(client.user.tag)} logged in successfully`)
- return client
- } else {
- const url = `https://discordapp.com/oauth2/authorize?&client_id=${client.user.id}&scope=bot&permissions=${0x00000008}&response_type=code`
- log.warning(`Bot ${chalk.blue(client.user.tag)} not connected to configured Discord server(s) - add it using the following URL:\n${chalk.underline(url)}`)
- return null
- }
- })).then(pool => pool.filter(client => client !== null))
- if (this.clientPool.length === 0) {
- throw 'No bots connected, cannot start'
- }
- // Setup stores
- this.emoteStore = new EmoteStore(this, config.get('discord_emote_store_server_ids'))
- if (config.get('skip_cleanup')) {
- log.warning('Cleanup skipped')
- } else {
- // Delete everything that isn't in config.json's protected_ids
- log.info(chalk.red('Cleaning up...'))
- await Promise.all([
- ...this.guild.channels
- .filter(chnl =>
- chnl.id !== this.guild.id &&
- !config.get('protected_ids').includes(chnl.id))
- .map(chnl => {
- log.info(`Deleted channel ${chalk.red(`#${chnl.name}`)}`)
- return chnl.delete()
- }),
- ...this.guild.roles
- .filter(role =>
- role.id !== this.guild.id &&
- !config.get('protected_ids').includes(role.id))
- .map(role => {
- log.info(`Deleted role ${chalk.red(role.name)}`)
- return role.delete()
- })
- ])
- log.info(chalk.red('Cleanup complete.'))
- }
- // TEMP: Create a couple moves to give to new characters.
- const pokeMove = await Move.upsert('👉', 'Poke', 'Deals a tiny amount of damage to a single enemy.', {
- target: 'enemy',
- actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
- })
- const kissMove = await Move.upsert('💋', 'Kiss', 'Heals a party member by a tiny amount.', {
- target: 'party',
- actions: [{type: 'heal', data: {amount: 3}, to: 'target'}],
- basePrepareTicks: 1,
- })
- const multipokeMove = await Move.upsert('👏', 'Multipoke', 'Deals a tiny amount of damage to up to three enemies at once.', {
- target: Move.TargetDesc.of('enemy', 3),
- actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
- baseCooldownTicks: 2,
- })
- // Add all players to the database (if they don't exist already)
- // This could take a while on large servers
- //
- // TODO: add new users (memberadd event) as characters when they join if the
- // server is already running
- await Promise.all(this.guild.members.filter(m => !m.user.bot).map(async member => {
- let char = await Character.findOne({discordID: member.id})
- if (!char) {
- char = await Character.create({
- discordID: member.id,
- battleAI: 'DiscordAI',
- moveIDs: [pokeMove._id, multipokeMove._id, kissMove._id],
- }).save()
- log.info(`Created player data for new user ${chalk.blue(member.user.tag)}`)
- await char.healthUpdate(this.guild)
- }
- }))
- // TEMP
- this.clientPool[0].on('message', async msg => {
- if (msg.guild.id !== config.get('discord_server_id')) return
- if (msg.author.bot) return
- const self = await Character.findOne({discordID: msg.author.id})
- // Usage: .fight @opponent#0001 @opponent#0002 [...]
- // Initiates a fight between your party and the parties of the specified
- // opponents.
- if (msg.content.startsWith('.fight ')) {
- const battle = new Battle(this)
- // FIXME: this crashes if the battle starts off completed (eg. one team
- // is comprised of only dead players).
- const characters = await Promise.all(msg.mentions.users.map(user => Character.findOne({discordID: user.id})))
- const parties = _.uniq([
- await self.getParty(), ...await Promise.all(characters.map(character => character && character.getParty()))
- ])
- for (const party of parties) {
- await battle.addTeam(party)
- }
- while (await battle.tick()) {
- // Use this to mess with the battle as it runs.
- }
- }
- // Usage: .revive
- if (msg.content === '.revive') {
- self.health = self.maxHealth
- await self.healthUpdate(this.guild)
- }
- // Usage: .suicide
- if (msg.content === '.suicide') {
- self.health = 0
- await self.healthUpdate(this.guild)
- }
- // Usage: .emote @user#0001 @user#0002 [...]
- if (msg.content.startsWith('.emote ')) {
- for (const [ userID, user ] of msg.mentions.users) {
- const char = await Character.findOne({discordID: userID})
- const emote = await char.getEmote(this)
- await msg.channel.send(`${emote}`)
- }
- }
- // Usage: .id @role @user#0001 #channel
- // Returns the Discord ID of anything. Useful for working out what to put
- // in config.json's protected_ids field - the Discord app doesn't provide
- // a way to copy the ID of a role, for example!
- if (msg.content.startsWith('.id ')) {
- const mentions = [
- ...msg.mentions.channels,
- ...msg.mentions.roles,
- ...msg.mentions.users,
- ]
- for (const [ id, mentioned ] of mentions) {
- const name = mentioned.name || mentioned.username
- await msg.channel.send(`**${name}**: \`${id}\``)
- }
- }
- })
- }
- get guild() {
- return _.sample(this.clientPool).guilds.get(config.get('discord_server_id'))
- }
- }
- // Let's go!!
- const game = new Game()
- game.boot()
- .then(() => log.ok('Game started'))
- .catch(err => {
- // :(
- if (typeof err === 'string') {
- // Human-readable
- log.critical('Error! ' + err)
- } else {
- // Unexpected
- log.critical(err.stack)
- }
- process.exit(1)
- })
|