index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // Note: Battles are *not* database-backed.
  2. const discord = require('discord.js')
  3. const _ = require('lodash')
  4. const time = require('../util/time')
  5. const Party = require('../character/party')
  6. class Battle {
  7. constructor(game) {
  8. this.game = game
  9. this.ticks = 0
  10. this.teams = []
  11. }
  12. // Adds a team (Party) to the fight.
  13. async addTeam(party) {
  14. const everyoneRole = this.game.guild.id
  15. const channel = await this.game.guild.createChannel('battle', 'text', [
  16. // Permissions! Beware; here be demons.
  17. {id: everyoneRole, deny: 3136, allow: 0}, // -rw -react
  18. ...party.members.filter(char => char.discordID).map(char => {
  19. return {id: char.discordID, deny: 0, allow: 3072} // +rw
  20. })
  21. ])
  22. await party.save() // _id is needed later
  23. for (const char of party.members) {
  24. char._battle = {
  25. ai: new char.BattleAI(char, this, {channel, party}),
  26. next: 'choice', // or 'action'
  27. ticksUntil: 0,
  28. moveChosen: undefined,
  29. targetsChosen: undefined,
  30. }
  31. }
  32. this.teams.push({channel, party})
  33. }
  34. // Advances the battle by one in-game second.
  35. async tick() {
  36. if (await this.isComplete()) {
  37. // ??
  38. const e = new Error('Battle complete but tick() called')
  39. e.battle = this
  40. throw e
  41. }
  42. // TODO: progress bar with character avatars instead of this
  43. /*
  44. await Promise.all(this.teams.map(({ party, channel }) => {
  45. return channel.send(party.members.map(char => {
  46. return `**${char.getName(this.game.guild)}**: ${char._battle.ticksUntil}s until ${char._battle.next}!`
  47. }).join('\n'))
  48. }))
  49. */
  50. for (const char of _.shuffle(this.everyone)) {
  51. if (char.healthState === 'dead') {
  52. // They're dead, so they cannot act.
  53. } else if (char._battle.ticksUntil > 0) {
  54. // Nothing to do.
  55. char._battle.ticksUntil--
  56. } else if (char._battle.next === 'choice') {
  57. // Decision time!
  58. const { move, targets } = await char._battle.ai.moveChoice(char, this)
  59. this.channelOf(char).send(`**${char.getName(this.game.guild)}** is preparing to use _${move.name}_...`)
  60. Object.assign(char._battle, {
  61. next: 'action',
  62. ticksUntil: move.prepareTicks,
  63. moveChosen: move,
  64. targetsChosen: targets,
  65. })
  66. } else if (char._battle.next === 'action') {
  67. // Perform the move.
  68. const { moveChosen, targetsChosen } = char._battle
  69. await this.sendMessageToAll(`**${char.getName(this.game.guild)}** used _${moveChosen.name}_!`)
  70. for (const target of targetsChosen) {
  71. await moveChosen.performOn(target, char, this)
  72. }
  73. Object.assign(char._battle, {
  74. next: 'choice',
  75. ticksUntil: 5, // TODO use gear weight/speed stat to calculate this
  76. moveChosen: undefined,
  77. targetsChosen: undefined,
  78. })
  79. }
  80. }
  81. await time.sleep(time.SECOND)
  82. this.ticks++
  83. if (await this.isComplete()) {
  84. await this.sendMessageToAll('Battle complete')
  85. await time.sleep(time.SECOND * 10)
  86. await this.cleanUp()
  87. return null
  88. } else {
  89. return this
  90. }
  91. }
  92. // Every character of every team, in a one-dimensional array.
  93. get everyone() {
  94. return _.flatten(this.teams.map(({ party }) => party.members))
  95. }
  96. // Helper function for sending a message to all team channels.
  97. sendMessageToAll(msg) {
  98. return Promise.all(this.teams.map(team =>
  99. team.channel.send(msg)
  100. ))
  101. }
  102. // Returns the battle channel for the passed character's team.
  103. channelOf(char) {
  104. for (const { party, channel } of this.teams) {
  105. if (party.members.find(mem => {
  106. return mem.discordID === char.discordID
  107. })) {
  108. return channel
  109. }
  110. }
  111. return null
  112. }
  113. // Called once the battle is complete.
  114. async cleanUp() {
  115. // Delete battle channels
  116. await Promise.all(this.teams.map(team => team.channel.delete()))
  117. }
  118. // A battle is "complete" if every team but one has 0 HP left.
  119. async isComplete() {
  120. return this.teams.filter(({ party }) => {
  121. for (const char of party.members) {
  122. if (char.health > 0) return true // Alive member
  123. }
  124. return false // Entire team is dead :(
  125. }).length <= 1
  126. }
  127. }
  128. module.exports = Battle