index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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(`${characterLabel} is preparing to use _${move.name}_...`)
  61. Object.assign(char._battle, {
  62. next: 'action',
  63. ticksUntil: move.basePrepareTicks, // TODO: use move 'mastery'
  64. moveChosen: move,
  65. targetsChosen: targets,
  66. })
  67. } else if (char._battle.next === 'action') {
  68. // Perform the move.
  69. const { moveChosen, targetsChosen } = char._battle
  70. await this.sendMessageToAll(`${characterLabel} used _${moveChosen.name}_!`)
  71. for (const target of targetsChosen) {
  72. await moveChosen.performOn(target, char, this)
  73. }
  74. Object.assign(char._battle, {
  75. next: 'wait',
  76. ticksUntil: moveChosen.baseCooldownTicks, // TODO: use move 'mastery'
  77. moveChosen: undefined,
  78. targetsChosen: undefined,
  79. })
  80. } else if (char._battle.next === 'wait') {
  81. // Cooldown complete.
  82. Object.assign(char._battle, {
  83. next: 'choice',
  84. ticksUntil: 5, // TODO use gear weight/speed stat to calculate this
  85. })
  86. }
  87. }
  88. await time.sleep(time.SECOND)
  89. this.ticks++
  90. if (await this.isComplete()) {
  91. await this.sendMessageToAll('Battle complete')
  92. await time.sleep(time.SECOND * 10)
  93. await this.cleanUp()
  94. return null
  95. } else {
  96. return this
  97. }
  98. }
  99. // Every character of every team, in a one-dimensional array.
  100. get everyone() {
  101. return _.flatten(this.teams.map(({ party }) => party.members))
  102. }
  103. // Helper function for sending a message to all team channels.
  104. sendMessageToAll(msg) {
  105. return Promise.all(this.teams.map(team =>
  106. team.channel.send(msg)
  107. ))
  108. }
  109. // Returns the battle channel for the passed character's team.
  110. channelOf(char) {
  111. for (const { party, channel } of this.teams) {
  112. if (party.members.find(mem => {
  113. return mem.discordID === char.discordID
  114. })) {
  115. return channel
  116. }
  117. }
  118. return null
  119. }
  120. // Called once the battle is complete.
  121. async cleanUp() {
  122. // Delete battle channels
  123. await Promise.all(this.teams.map(team => team.channel.delete()))
  124. }
  125. // A battle is "complete" if every team but one has 0 HP left.
  126. async isComplete() {
  127. return this.teams.filter(({ party }) => {
  128. for (const char of party.members) {
  129. if (char.health > 0) return true // Alive member
  130. }
  131. return false // Entire team is dead :(
  132. }).length <= 1
  133. }
  134. }
  135. module.exports = Battle