index.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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. await party.assignRoles(this.game.guild) // Just in case this wasn't done earlier
  24. for (const char of party.members) {
  25. char._battle = {
  26. ai: new char.BattleAI(char, this, {channel, party}),
  27. next: 'choice', // or 'action', 'wait'
  28. ticksUntil: 0,
  29. moveChosen: undefined,
  30. targetsChosen: undefined,
  31. }
  32. }
  33. this.teams.push({channel, party})
  34. }
  35. // Advances the battle by one in-game second.
  36. async tick() {
  37. if (await this.isComplete()) {
  38. // ??
  39. const e = new Error('Battle complete but tick() called')
  40. e.battle = this
  41. throw e
  42. }
  43. // TODO: progress bar with character avatars instead of this
  44. /*
  45. await Promise.all(this.teams.map(({ party, channel }) => {
  46. return channel.send(party.members.map(char => {
  47. return `**${char.getName(this.game.guild)}**: ${char._battle.ticksUntil}s until ${char._battle.next}!`
  48. }).join('\n'))
  49. }))
  50. */
  51. for (const char of _.shuffle(this.everyone)) {
  52. const characterLabel = await this.getCharacterLabel(char)
  53. if (char.healthState === 'dead') {
  54. // They're dead, so they cannot act.
  55. } else if (char._battle.ticksUntil > 0) {
  56. // Nothing to do.
  57. char._battle.ticksUntil--
  58. // TODO: use speed stat when next === 'choice'
  59. } else if (char._battle.next === 'choice') {
  60. // Decision time!
  61. const { move, targets } = await char._battle.ai.moveChoice(char, this)
  62. await this.sendMessageToAll(`${characterLabel} is preparing to use _${move.name}_...`)
  63. Object.assign(char._battle, {
  64. next: 'action',
  65. ticksUntil: move.basePrepareTicks, // TODO: use move 'mastery'
  66. moveChosen: move,
  67. targetsChosen: targets,
  68. })
  69. } else if (char._battle.next === 'action') {
  70. // Perform the move.
  71. const { moveChosen, targetsChosen } = char._battle
  72. await this.sendMessageToAll(`${characterLabel} used _${moveChosen.name}_!`)
  73. for (const target of targetsChosen) {
  74. await moveChosen.performOn(target, char, this)
  75. }
  76. Object.assign(char._battle, {
  77. next: 'wait',
  78. ticksUntil: moveChosen.baseCooldownTicks, // TODO: use move 'mastery'
  79. moveChosen: undefined,
  80. targetsChosen: undefined,
  81. })
  82. } else if (char._battle.next === 'wait') {
  83. // Cooldown complete.
  84. Object.assign(char._battle, {
  85. next: 'choice',
  86. ticksUntil: 5, // TODO use gear weight/speed stat to calculate this
  87. })
  88. }
  89. }
  90. await time.sleep(time.SECOND)
  91. this.ticks++
  92. if (await this.isComplete()) {
  93. await this.sendMessageToAll('Battle complete')
  94. await time.sleep(time.SECOND * 10)
  95. await this.cleanUp()
  96. return null
  97. } else {
  98. return this
  99. }
  100. }
  101. // "Label" of a character, to be displayed in messages.
  102. async getCharacterLabel(character) {
  103. const name = character.getName(this.game.guild)
  104. const party = await character.getParty()
  105. const partyRole = await party.getRole(this.game.guild)
  106. return `**${name}** (from ${partyRole})`
  107. }
  108. // Every character of every team, in a one-dimensional array.
  109. get everyone() {
  110. return _.flatten(this.teams.map(({ party }) => party.members))
  111. }
  112. // Helper function for sending a message to all team channels.
  113. sendMessageToAll(msg) {
  114. return Promise.all(this.teams.map(team =>
  115. team.channel.send(msg)
  116. ))
  117. }
  118. // Returns the battle channel for the passed character's team.
  119. channelOf(char) {
  120. for (const { party, channel } of this.teams) {
  121. if (party.members.find(mem => {
  122. return mem.discordID === char.discordID
  123. })) {
  124. return channel
  125. }
  126. }
  127. return null
  128. }
  129. // Called once the battle is complete.
  130. async cleanUp() {
  131. // Delete battle channels
  132. await Promise.all(this.teams.map(team => team.channel.delete()))
  133. }
  134. // A battle is "complete" if every team but one has 0 HP left.
  135. async isComplete() {
  136. return this.teams.filter(({ party }) => {
  137. for (const char of party.members) {
  138. if (char.health > 0) return true // Alive member
  139. }
  140. return false // Entire team is dead :(
  141. }).length <= 1
  142. }
  143. }
  144. module.exports = Battle