index.js 4.6 KB

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