game.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/usr/bin/env node
  2. const camo = require('camo')
  3. const discord = require('discord.js')
  4. const _ = require('lodash')
  5. const config = require('./config')
  6. const log = require('./util/log')
  7. const chalk = require('chalk')
  8. const commandExists = require('command-exists')
  9. const EmoteStore = require('./stores/emote')
  10. const Character = require('./character')
  11. const Battle = require('./battle/battle')
  12. const Move = require('./battle/move')
  13. const Party = require('./character/party')
  14. const { DiscordAI } = require('./battle/ai')
  15. class Game {
  16. // Starts the game. Should only run once per process!
  17. async boot() {
  18. // Check graphicsmagick is installed
  19. if (!await commandExists('gm')) {
  20. log.warning('GraphicsMagick not found - please install it')
  21. }
  22. // Load the config file
  23. await config.load()
  24. // Connect to the database (nedb or mongodb)
  25. await camo.connect(config.get('database_uri'))
  26. // Create the bot pool
  27. this.clientPool = await Promise.all(config.get('discord_bot_tokens').map(async token => {
  28. const client = new discord.Client()
  29. try {
  30. await client.login(token)
  31. } catch (err) {
  32. log.critical('Bot login failure (is the token correct?)')
  33. return null
  34. }
  35. // Check the bot is actually in all the guilds (servers) we will be using
  36. let inAllGuilds = true
  37. const guildIDs = [
  38. config.get('discord_server_id'),
  39. ...config.get('discord_emote_store_server_ids'),
  40. ]
  41. for (const guildID of guildIDs) {
  42. if (!client.guilds.get(guildID)) {
  43. inAllGuilds = false
  44. }
  45. }
  46. if (inAllGuilds) {
  47. log.ok(`Bot ${chalk.blue(client.user.tag)} logged in successfully`)
  48. return client
  49. } else {
  50. const url = `https://discordapp.com/oauth2/authorize?&client_id=${client.user.id}&scope=bot&permissions=${0x00000008}&response_type=code`
  51. 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)}`)
  52. return null
  53. }
  54. })).then(pool => pool.filter(client => client !== null))
  55. if (this.clientPool.length === 0) {
  56. throw 'No bots connected, cannot start'
  57. }
  58. // Setup stores
  59. this.emoteStore = new EmoteStore(this, config.get('discord_emote_store_server_ids'))
  60. if (config.get('skip_cleanup')) {
  61. log.warning('Cleanup skipped')
  62. } else {
  63. // Delete everything that isn't in config.json's protected_ids
  64. log.info(chalk.red('Cleaning up...'))
  65. await Promise.all([
  66. ...this.guild.channels
  67. .filter(chnl =>
  68. chnl.id !== this.guild.id &&
  69. !config.get('protected_ids').includes(chnl.id))
  70. .map(chnl => {
  71. log.info(`Deleted channel ${chalk.red(`#${chnl.name}`)}`)
  72. return chnl.delete()
  73. }),
  74. ...this.guild.roles
  75. .filter(role =>
  76. role.id !== this.guild.id &&
  77. !config.get('protected_ids').includes(role.id))
  78. .map(role => {
  79. log.info(`Deleted role ${chalk.red(role.name)}`)
  80. return role.delete()
  81. })
  82. ])
  83. log.info(chalk.red('Cleanup complete.'))
  84. }
  85. // TEMP: Create a couple moves to give to new characters.
  86. const pokeMove = await Move.upsert('👉', 'Poke', 'Deals a tiny amount of damage to a single enemy.', {
  87. target: 'enemy',
  88. actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
  89. })
  90. const kissMove = await Move.upsert('💋', 'Kiss', 'Heals a party member by a tiny amount.', {
  91. target: 'party',
  92. actions: [{type: 'heal', data: {amount: 3}, to: 'target'}],
  93. basePrepareTicks: 1,
  94. })
  95. const multipokeMove = await Move.upsert('👏', 'Multipoke', 'Deals a tiny amount of damage to up to three enemies at once.', {
  96. target: Move.TargetDesc.of('enemy', 3),
  97. actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
  98. baseCooldownTicks: 2,
  99. })
  100. // Add all players to the database (if they don't exist already)
  101. // This could take a while on large servers
  102. //
  103. // TODO: add new users (memberadd event) as characters when they join if the
  104. // server is already running
  105. await Promise.all(this.guild.members.filter(m => !m.user.bot).map(async member => {
  106. let char = await Character.findOne({discordID: member.id})
  107. if (!char) {
  108. char = await Character.create({
  109. discordID: member.id,
  110. battleAI: 'DiscordAI',
  111. moveIDs: [pokeMove._id, multipokeMove._id, kissMove._id],
  112. }).save()
  113. log.info(`Created player data for new user ${chalk.blue(member.user.tag)}`)
  114. await char.healthUpdate(this.guild)
  115. }
  116. }))
  117. // TEMP
  118. this.clientPool[0].on('message', async msg => {
  119. if (msg.guild.id !== config.get('discord_server_id')) return
  120. if (msg.author.bot) return
  121. const self = await Character.findOne({discordID: msg.author.id})
  122. // Usage: .fight @opponent#0001 @opponent#0002 [...]
  123. // Initiates a fight between your party and the parties of the specified
  124. // opponents.
  125. if (msg.content.startsWith('.fight ')) {
  126. const battle = new Battle(this)
  127. // FIXME: this crashes if the battle starts off completed (eg. one team
  128. // is comprised of only dead players).
  129. const characters = await Promise.all(msg.mentions.users.map(user => Character.findOne({discordID: user.id})))
  130. const parties = _.uniq([
  131. await self.getParty(), ...await Promise.all(characters.map(character => character && character.getParty()))
  132. ])
  133. for (const party of parties) {
  134. await battle.addTeam(party)
  135. }
  136. while (await battle.tick()) {
  137. // Use this to mess with the battle as it runs.
  138. }
  139. }
  140. // Usage: .revive
  141. if (msg.content === '.revive') {
  142. self.health = self.maxHealth
  143. await self.healthUpdate(this.guild)
  144. }
  145. // Usage: .suicide
  146. if (msg.content === '.suicide') {
  147. self.health = 0
  148. await self.healthUpdate(this.guild)
  149. }
  150. // Usage: .emote @user#0001 @user#0002 [...]
  151. if (msg.content.startsWith('.emote ')) {
  152. for (const [ userID, user ] of msg.mentions.users) {
  153. const char = await Character.findOne({discordID: userID})
  154. const emote = await char.getEmote(this)
  155. await msg.channel.send(`${emote}`)
  156. }
  157. }
  158. // Usage: .id @role @user#0001 #channel
  159. // Returns the Discord ID of anything. Useful for working out what to put
  160. // in config.json's protected_ids field - the Discord app doesn't provide
  161. // a way to copy the ID of a role, for example!
  162. if (msg.content.startsWith('.id ')) {
  163. const mentions = [
  164. ...msg.mentions.channels,
  165. ...msg.mentions.roles,
  166. ...msg.mentions.users,
  167. ]
  168. for (const [ id, mentioned ] of mentions) {
  169. const name = mentioned.name || mentioned.username
  170. await msg.channel.send(`**${name}**: \`${id}\``)
  171. }
  172. }
  173. })
  174. }
  175. get guild() {
  176. return _.sample(this.clientPool).guilds.get(config.get('discord_server_id'))
  177. }
  178. }
  179. // Let's go!!
  180. const game = new Game()
  181. game.boot()
  182. .then(() => log.ok('Game started'))
  183. .catch(err => {
  184. // :(
  185. if (typeof err === 'string') {
  186. // Human-readable
  187. log.critical('Error! ' + err)
  188. } else {
  189. // Unexpected
  190. log.critical(err.stack)
  191. }
  192. process.exit(1)
  193. })