game.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. // Create an instance of the logging tool
  19. this.log = new Log()
  20. // Check graphicsmagick is installed
  21. if (!await commandExists('gm')) {
  22. this.log.warning('GraphicsMagick not found - please install it')
  23. }
  24. // Load the config file
  25. await config.load()
  26. // Connect to the database (nedb or mongodb)
  27. await camo.connect(config.get('database_uri'))
  28. // Create the bot pool
  29. this.clientPool = await Promise.all(config.get('discord_bot_tokens').map(async token => {
  30. const client = new discord.Client()
  31. try {
  32. await client.login(token)
  33. } catch (err) {
  34. this.log.critical('Bot login failure (is the token correct?)')
  35. return null
  36. }
  37. // Check the bot is actually in all the guilds (servers) we will be using
  38. let inAllGuilds = true
  39. const guildIDs = [
  40. config.get('discord_server_id'),
  41. ...config.get('discord_emote_store_server_ids'),
  42. ]
  43. for (const guildID of guildIDs) {
  44. if (!client.guilds.get(guildID)) {
  45. inAllGuilds = false
  46. }
  47. }
  48. if (inAllGuilds) {
  49. this.log.ok(`Bot ${chalk.blue(client.user.tag)} logged in successfully`)
  50. return client
  51. } else {
  52. const url = `https://discordapp.com/oauth2/authorize?&client_id=${client.user.id}&scope=bot&permissions=${0x00000008}&response_type=code`
  53. this.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)}`)
  54. return null
  55. }
  56. })).then(pool => pool.filter(client => client !== null))
  57. if (this.clientPool.length === 0) {
  58. throw 'No bots connected, cannot start'
  59. }
  60. // Setup stores
  61. this.emoteStore = new EmoteStore(this, config.get('discord_emote_store_server_ids'))
  62. // Cleanup temporary channels (incase of crash last time)
  63. await this.emoteStore.cleanUp()
  64. await Promise.all(this.guild.channels
  65. .filter(chnl => chnl.name === 'battle')
  66. .map(chnl => chnl.delete()))
  67. // Clean dead roles
  68. await Party.cleanDeadRoles(this.guild, this.log)
  69. // TEMP: Create a couple moves to give to new characters.
  70. const pokeMove = await Move.upsert('👉', 'Poke', 'Deals a tiny amount of damage to a single enemy.', {
  71. target: 'enemy',
  72. actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
  73. })
  74. const kissMove = await Move.upsert('💋', 'Kiss', 'Heals a party member by a tiny amount.', {
  75. target: 'party',
  76. actions: [{type: 'heal', data: {amount: 3}, to: 'target'}],
  77. basePrepareTicks: 1,
  78. })
  79. const multipokeMove = await Move.upsert('👏', 'Multipoke', 'Deals a tiny amount of damage to up to three enemies at once.', {
  80. target: Move.TargetDesc.of('enemy', 3),
  81. actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
  82. baseCooldownTicks: 2,
  83. })
  84. // Add all players to the database (if they don't exist already)
  85. // This could take a while on large servers
  86. //
  87. // TODO: add new users (memberadd event) as characters when they join if the
  88. // server is already running
  89. await Promise.all(this.guild.members.filter(m => !m.user.bot).map(async member => {
  90. let char = await Character.findOne({discordID: member.id})
  91. if (!char) {
  92. char = await Character.create({
  93. discordID: member.id,
  94. battleAI: 'DiscordAI',
  95. moveIDs: [pokeMove._id, multipokeMove._id, kissMove._id],
  96. }).save()
  97. this.log.info(`Created player data for new user ${chalk.blue(member.user.tag)}`)
  98. await char.healthUpdate(this.guild)
  99. }
  100. }))
  101. // TEMP
  102. this.clientPool[0].on('message', async msg => {
  103. if (msg.guild.id !== config.get('discord_server_id')) return
  104. if (msg.author.bot) return
  105. const self = await Character.findOne({discordID: msg.author.id})
  106. // Usage: .fight @opponent#0001 @opponent#0002 [...]
  107. // Initiates a fight between your party and the parties of the specified
  108. // opponents.
  109. if (msg.content.startsWith('.fight ')) {
  110. const battle = new Battle(this)
  111. // FIXME: this crashes if the battle starts off completed (eg. one team
  112. // is comprised of only dead players).
  113. const characters = await Promise.all(msg.mentions.users.map(user => Character.findOne({discordID: user.id})))
  114. const parties = _.uniq([
  115. await self.getParty(), ...await Promise.all(characters.map(character => character && character.getParty()))
  116. ])
  117. for (const party of parties) {
  118. await battle.addTeam(party)
  119. }
  120. while (await battle.tick()) {
  121. // Use this to mess with the battle as it runs.
  122. }
  123. }
  124. // Usage: .revive
  125. if (msg.content === '.revive') {
  126. self.health = self.maxHealth
  127. await self.healthUpdate(this.guild)
  128. }
  129. // Usage: .suicide
  130. if (msg.content === '.suicide') {
  131. self.health = 0
  132. await self.healthUpdate(this.guild)
  133. }
  134. // Usage: .emote @user#0001 @user#0002 [...]
  135. if (msg.content.startsWith('.emote ')) {
  136. for (const [ userID, user ] of msg.mentions.users) {
  137. const char = await Character.findOne({discordID: userID})
  138. const emote = await char.getEmote(this)
  139. await msg.channel.send(`${emote}`)
  140. }
  141. }
  142. })
  143. }
  144. get guild() {
  145. return _.sample(this.clientPool).guilds.get(config.get('discord_server_id'))
  146. }
  147. }
  148. // Let's go!!
  149. const game = new Game()
  150. game.boot()
  151. .then(() => game.log.ok('Game started'))
  152. .catch(err => {
  153. // :(
  154. game.log.critical('Unhandled error during boot:\n' + (err.message || err))
  155. process.exit(1)
  156. })