123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- // Note: Battles are *not* database-backed.
- const discord = require('discord.js')
- const _ = require('lodash')
- const time = require('../util/time')
- const Party = require('../character/party')
- class Battle {
- constructor(game) {
- this.game = game
- this.ticks = 0
- this.teams = []
- }
- // Adds a team (Party) to the fight.
- async addTeam(party) {
- const everyoneRole = this.game.guild.id
- const channel = await this.game.guild.createChannel('battle', 'text', [
- // Permissions! Beware; here be demons.
- {id: everyoneRole, deny: 3136, allow: 0}, // -rw -react
- ...party.members.filter(char => char.discordID).map(char => {
- return {id: char.discordID, deny: 0, allow: 3072} // +rw
- })
- ])
- await party.save() // _id is needed later
- await party.assignRoles(this.game.guild) // Just in case this wasn't done earlier
- for (const char of party.members) {
- char._battle = {
- ai: new char.BattleAI(char, this, {channel, party}),
- next: 'choice', // or 'action', 'wait'
- ticksUntil: 0,
- moveChosen: undefined,
- targetsChosen: undefined,
- }
- }
- this.teams.push({channel, party})
- }
- // Advances the battle by one in-game second.
- async tick() {
- if (await this.isComplete()) {
- // ??
- const e = new Error('Battle complete but tick() called')
- e.battle = this
- throw e
- }
- // TODO: progress bar with character avatars instead of this
- /*
- await Promise.all(this.teams.map(({ party, channel }) => {
- return channel.send(party.members.map(char => {
- return `**${char.getName(this.game.guild)}**: ${char._battle.ticksUntil}s until ${char._battle.next}!`
- }).join('\n'))
- }))
- */
- for (const char of _.shuffle(this.everyone)) {
- const characterLabel = await this.getCharacterLabel(char)
- if (char.healthState === 'dead') {
- // They're dead, so they cannot act.
- } else if (char._battle.ticksUntil > 0) {
- // Nothing to do.
- char._battle.ticksUntil--
- // TODO: use speed stat when next === 'choice'
- } else if (char._battle.next === 'choice') {
- // Decision time!
- const { move, targets } = await char._battle.ai.moveChoice(char, this)
- await this.sendMessageToAll(`${characterLabel} is preparing to use _${move.name}_...`)
- Object.assign(char._battle, {
- next: 'action',
- ticksUntil: move.basePrepareTicks, // TODO: use move 'mastery'
- moveChosen: move,
- targetsChosen: targets,
- })
- } else if (char._battle.next === 'action') {
- // Perform the move.
- const { moveChosen, targetsChosen } = char._battle
- await this.sendMessageToAll(`${characterLabel} used _${moveChosen.name}_!`)
- for (const target of targetsChosen) {
- await moveChosen.performOn(target, char, this)
- }
- Object.assign(char._battle, {
- next: 'wait',
- ticksUntil: moveChosen.baseCooldownTicks, // TODO: use move 'mastery'
- moveChosen: undefined,
- targetsChosen: undefined,
- })
- } else if (char._battle.next === 'wait') {
- // Cooldown complete.
- Object.assign(char._battle, {
- next: 'choice',
- ticksUntil: 5, // TODO use gear weight/speed stat to calculate this
- })
- }
- }
- await time.sleep(time.SECOND)
- this.ticks++
- if (await this.isComplete()) {
- await this.sendMessageToAll('Battle complete')
- await time.sleep(time.SECOND * 10)
- await this.cleanUp()
- return null
- } else {
- return this
- }
- }
- // "Label" of a character, to be displayed in messages.
- async getCharacterLabel(character) {
- const name = character.getName(this.game.guild)
- const party = await character.getParty()
- const partyRole = await party.getRole(this.game.guild)
- return `**${name}** (from ${partyRole})`
- }
- // Every character of every team, in a one-dimensional array.
- get everyone() {
- return _.flatten(this.teams.map(({ party }) => party.members))
- }
- // Helper function for sending a message to all team channels.
- sendMessageToAll(msg) {
- return Promise.all(this.teams.map(team =>
- team.channel.send(msg)
- ))
- }
- // Returns the battle channel for the passed character's team.
- channelOf(char) {
- for (const { party, channel } of this.teams) {
- if (party.members.find(mem => {
- return mem.discordID === char.discordID
- })) {
- return channel
- }
- }
- return null
- }
- // Called once the battle is complete.
- async cleanUp() {
- // Delete battle channels
- await Promise.all(this.teams.map(team => team.channel.delete()))
- }
- // A battle is "complete" if every team but one has 0 HP left.
- async isComplete() {
- return this.teams.filter(({ party }) => {
- for (const char of party.members) {
- if (char.health > 0) return true // Alive member
- }
- return false // Entire team is dead :(
- }).length <= 1
- }
- }
- module.exports = Battle
|