diff --git a/.eslintrc b/.eslintrc index 79d618d..915928f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -38,6 +38,7 @@ "multiline-ternary": "off", "new-cap": ["error", {"properties": false}], "newline-per-chained-call": "off", + "no-await-in-loop": "off", "no-confusing-arrow": "off", "no-console": "off", "no-continue": "off", diff --git a/commands.js b/commands.js index a199a4d..75efc82 100644 --- a/commands.js +++ b/commands.js @@ -421,16 +421,15 @@ class Commands { throw new Error("No event currently running."); } - const standings = Event.getStandings(); - let str = "Standings:"; - - standings.forEach((index) => { - const player = standings[index]; - - str += `\n${index + 1}) ${player.name} - ${player.score} (${player.wins}-${player.losses})`; - }); + let standings; + try { + standings = Event.getStandingsText(); + } catch (err) { + await Discord.queue(`Sorry, ${user}, but there was a server error. roncli will be notified about this.`, channel); + throw new Exception("There was an error getting the standings.", err); + } - await Discord.queue(str, user); + await Discord.queue(standings, user); return true; } @@ -632,7 +631,7 @@ class Commands { throw new Error("Match is not yet reported."); } - if (!match.reported.winner === user.id) { + if (match.reported.winner !== user.id) { await Discord.queue(`Sorry, ${user}, but you can't confirm your own reports!`, channel); throw new Error("Player tried to confirm their own report."); } @@ -690,7 +689,7 @@ class Commands { Event.updateResult(match); - await Discord.queue(`${user}, your match comment has been successfully updated.`); + await Discord.queue(`${user}, your match comment has been successfully updated.`, channel); return true; } @@ -940,9 +939,14 @@ class Commands { await Discord.queue(`Round ${Event.round} starts now!`); try { - matches.forEach(async (match) => { + let str = "Matches:"; + + for (const match of matches) { await Event.createMatch(match[0], match[1]); - }); + str += `\n**${Discord.getGuildUser(match[0]).displayName}** vs **${Discord.getGuildUser(match[1]).displayName}**`; + } + + await Discord.queue(str); } catch (err) { await Discord.queue(`Sorry, ${user}, but there was a problem creating matches for the next round.`, channel); throw err; @@ -1060,6 +1064,8 @@ class Commands { throw err; } + await Discord.queue(`Additional match:\n**${player1.displayName}** vs **${player2.displayName}**`); + return true; } @@ -1228,6 +1234,16 @@ class Commands { throw new Error("Event is not currently running."); } + let standings; + try { + standings = Event.getStandingsText(); + } catch (err) { + await Discord.queue(`Sorry, ${user}, but there is was an error ending the event.`, channel); + throw new Exception("There was an error getting the standings.", err); + } + + await Discord.queue(standings, Discord.resultsChannel); + try { await Event.endEvent(); } catch (err) { diff --git a/database.js b/database.js index 4579a99..fef3755 100644 --- a/database.js +++ b/database.js @@ -145,9 +145,9 @@ class Database { * @return {Promise} A promise that resolves when the players' home levels have been locked. */ static async lockHomeLevelsForDiscordIds(discordIds) { - const players = discordIds.map((discordId, index) => ({index: `player${index}`, discordId})); + const players = discordIds.map((discordId, index) => ({index: `player${index}`, atIndex: `@player${index}`, discordId})); - await db.query(`UPDATE tblHome SET Locked = 1 WHERE DiscordID IN (${players.map((p) => p.index).join(", ")})`, players.reduce((accumulator, player) => { + await db.query(`UPDATE tblHome SET Locked = 1 WHERE DiscordID IN (${players.map((p) => p.atIndex).join(", ")})`, players.reduce((accumulator, player) => { accumulator[player.index] = {type: Db.VARCHAR(50), value: player.discordId}; return accumulator; }, {})); diff --git a/discord.js b/discord.js index e3a3e1d..1bcbebb 100644 --- a/discord.js +++ b/discord.js @@ -40,6 +40,8 @@ const DiscordJs = require("discord.js"), let eventRole, generalChannel, obsGuild, + pilotsChatCategory, + pilotsVoiceChatCategory, resultsChannel, seasonRole; @@ -94,7 +96,7 @@ class Discord { // # ## ### ### ### ## ### ## # # # # # # # # ## ### /** * The results channel. - * @returns {Channel} The results channel. + * @returns {TextChannel} The results channel. */ static get resultsChannel() { return resultsChannel; @@ -115,6 +117,50 @@ class Discord { return discord.id; } + // # # ## # ### ## + // # # # # # # # # + // ### ## # ### # # # ### # # ## # ## + // # # # ## ### # # # # # # ### # # # # ## + // # # ## # # ## # # # # # # # # # ## + // ### ## # # # ### ### ## # # ## ### ## + /** + * The default role for the server. + * @returns {Role} The server's default role. + */ + static get defaultRole() { + return obsGuild.defaultRole; + } + + // # ## # ## # # ## # + // # # # # # # # # # + // ### ## # ## ### ### # ### ### ### # ### ### ## ### ## ### # # + // # # # # # # # ## # # # # # # # # # # # ## # # # # # # # # + // # # # # # # # ## # # # # # ## # # # # ## # ## ## # # # # # + // ### ### ### ## ## ### ## # # # # ## ## # # ## ## # ## # # + // # ### # + /** + * Gets the pilots chat category. + * @returns {TextChannel} The pilots chat category. + */ + static get pilotsChatCategory() { + return pilotsChatCategory; + } + + // # ## # # # # ## # # ## # + // # # # # # # # # # # # + // ### ## # ## ### ### # # ## ## ## ## # ### ### ### # ### ### ## ### ## ### # # + // # # # # # # # ## # # # # # # # ## # # # # # # # # # # # ## # # # # # # # # + // # # # # # # # ## ## # # # # ## # # # # # ## # # # # ## # ## ## # # # # # + // ### ### ### ## ## ### ## ## ### ## ## ## # # # # ## ## # # ## ## # ## # # + // # ### # + /** + * Gets the pilots voice chat category. + * @returns {VoiceChannel} The pilots voice chat category. + */ + static get pilotsVoiceChatCategory() { + return pilotsVoiceChatCategory; + } + // # # // # # // ### ### ### ### ### # # ### @@ -141,12 +187,19 @@ class Discord { eventRole = obsGuild.roles.find((r) => r.name === "In Current Event"); seasonRole = obsGuild.roles.find((r) => r.name === "Season 11 Participant"); + + pilotsChatCategory = obsGuild.channels.find((c) => c.name === "Pilots Chat"); + pilotsVoiceChatCategory = obsGuild.channels.find((c) => c.name === "Pilots Voice Chat"); }); discord.on("disconnect", (ev) => { Log.exception("Disconnected from Discord.", ev); }); + discord.on("error", (ev) => { + Log.exception("Unhandled error.", ev); + }); + discord.addListener("message", (message) => { if (message.guild && message.guild.name === "The Observatory" && message.channel.type === "text") { Discord.message(message.author, message.content, message.channel); @@ -238,7 +291,7 @@ class Discord { * @param {Channel} [channel] The channel to send the message to. * @returns {Promise} A promise that resolves when the message is sent. */ - static queue(message, channel) { + static async queue(message, channel) { if (!channel) { channel = generalChannel; } @@ -252,21 +305,35 @@ class Discord { } }; - if (JSON.stringify(msg).length > 1024) { - return channel.send(message); - } - - return channel.send( - "", - { - embed: { - description: message, - timestamp: new Date(), - color: 0x263686, - footer: {icon_url: Discord.icon, text: "DescentBot"} + try { + if (JSON.stringify(msg).length > 1024) { + while (message.length > 0) { + await channel.send(message.substr(0, 2000)); + if (message.length > 2000) { + message = message.substr(2000, message.length - 2000); + } else { + message = ""; + } } + return void 0; } - ); + + return await channel.send( + "", + { + embed: { + description: message, + timestamp: new Date(), + color: 0x263686, + footer: {icon_url: Discord.icon, text: "DescentBot"} + } + } + ); + } catch (err) { + console.log("Could not send queue."); + console.log(message); + return void 0; + } } // # # ## @@ -282,12 +349,18 @@ class Discord { * @param {Channel} [channel] The channel to send the message to. * @returns {Promise} A promise that resolves when the message is sent. */ - static richQueue(message, channel) { + static async richQueue(message, channel) { if (!channel) { channel = generalChannel; } - return channel.send("", message); + try { + return await channel.send("", message); + } catch (err) { + console.log("Could not send rich queue."); + console.log(message); + return void 0; + } } // # ## @@ -535,11 +608,14 @@ class Discord { * Creates a text channel. * @param {string} name The name of the channel to create. * @param {Channel} category The category to assign the channel to. - * @returns {Promise} A promise that resolves when the channel has been created. + * @returns {Promise} A promise that resolves with the text channel created. */ static async createTextChannel(name, category) { const channel = await obsGuild.createChannel(name, "text"); - return channel.edit({parent_id: category && category.id}); + if (category) { + await channel.setParent(category); + } + return channel; } // # # # # ## # ## @@ -552,11 +628,14 @@ class Discord { * Creates a voice channel. * @param {string} name The name of the channel to create. * @param {Channel} category The category to assign the channel to. - * @returns {Promise} A promise that resolves when the channel has been created. + * @returns {Promise} A promise that resolves with the voice channel created. */ static async createVoiceChannel(name, category) { const channel = await obsGuild.createChannel(name, "voice"); - channel.edit({parent_id: category && category.id}); + if (category) { + await channel.setParent(category); + } + return channel; } // ## # ## diff --git a/event.js b/event.js index 752cdea..f72db09 100644 --- a/event.js +++ b/event.js @@ -177,11 +177,11 @@ class Event { await Discord.richQueue({ embed: { - title: `${player1} vs ${player2}`, + title: `${player1.displayName} vs ${player2.displayName}`, description: "Please begin your match!", timestamp: new Date(), color: 0x263686, - footer: {icon_url: Discord.icon}, + footer: {icon_url: Discord.icon, text: "DescentBot"}, fields: [ { name: "Selected Map", @@ -271,27 +271,40 @@ class Event { embed: { timestamp: new Date(), color: 0x263686, - footer: {icon_url: Discord.icon}, - description: match.homeSelected, - fields: [ - { - name: player1.displayName, - value: match.score[0], - inline: true - }, - { - name: player2.displayName, - value: match.score[1], - inline: true - } - ] + footer: {icon_url: Discord.icon, text: "DescentBot"}, + fields: [] } }; + if (match.round) { + embed.embed.fields.push({ + name: "Round", + value: match.round + }); + } + + embed.embed.fields.push({ + name: player1.displayName, + value: match.score[0], + inline: true + }); + + embed.embed.fields.push({ + name: player2.displayName, + value: match.score[1], + inline: true + }); + + embed.embed.fields.push({ + name: "Map", + value: match.homeSelected, + inline: true + }); + if (match.comments) { Object.keys(match.comments).forEach((id) => { embed.embed.fields.push({ - name: Discord.getGuildUser(id).displayName, + name: `Comment from ${Discord.getGuildUser(id).displayName}:`, value: match.comments[id] }); }); @@ -358,7 +371,7 @@ class Event { return; } - match.results.edit("", Event.getResultEmbed(match), Discord.resultsChannel); + match.results.edit("", Event.getResultEmbed(match)); } // # ## # # # @@ -373,38 +386,63 @@ class Event { * @returns {object[]} An array of player standings. */ static getStandings() { - const standings = {}; + const standings = []; players.forEach((player) => { const id = player.id; - standings[id] = { + standings.push({ + id: player.id, name: Discord.getGuildUser(id).displayName, wins: 0, losses: 0, score: 0, defeated: [] - }; + }); }); matches.filter((m) => m.winner).forEach((match) => { match.players.forEach((id) => { + const standing = standings.find((s) => s.id === id); if (match.winner === id) { - standings[id].wins++; + standing.wins++; } else { - standings[id].losses++; - standings[match.winner].defeated.push(id); + const winner = standings.find((s) => s.id === match.winner); + standing.losses++; + winner.defeated.push(id); } }); }); standings.forEach((player) => { - player.score = player.wins * 3 + player.defeated.reduce((accumulator, currentValue) => accumulator + standings[currentValue].wins); + player.score = player.wins * 3 + player.defeated.reduce((accumulator, currentValue) => accumulator + standings.find((s) => s.id === currentValue).wins, 0); }); -console.log(standings); + return standings.sort((a, b) => b.score + b.wins / 100 - b.losses / 10000 - (a.score + a.wins / 100 - a.losses / 10000)); } + // # ## # # # ### # + // # # # # # # # + // ### ## ### # ### ### ### ### ## ### ### ### # ## # # ### + // # # # ## # # # # # # # # # # # # # # ## # # ## ## # + // ## ## # # # # # ## # # # # # # # ## ## # ## ## # + // # ## ## ## ## # # # # ### ### # # # ### # ## # # ## + // ### ### + /** + * Gets the text of the standings. + * @returns {string} The text of the standings. + */ + static getStandingsText() { + const standings = Event.getStandings(); + let str = "Standings:"; + + standings.forEach((player, index) => { + str += `\n${index + 1}) ${player.name} - ${player.score} (${player.wins}-${player.losses})`; + }); + + return str; + } + // #### # // # # // ## ### ## ### ### # # ## ### ### @@ -467,7 +505,7 @@ console.log(standings); // Potential opponents don't include the first player, potential opponents cannot have played against the first player, and potential opponents or the first player need to be able to host. potentialOpponents = remainingPlayers.filter((p) => p.id !== firstPlayer.id && matches.filter((m) => !m.cancelled && m.players.indexOf(p.id) !== -1 && m.players.indexOf(firstPlayer.id) !== -1).length === 0 && (firstPlayer.eventPlayer.canHost || p.eventPlayer.canHost)); - // Attempt to assign a bye if necessary. + // Attempt to assign a bye if necessary. if (remainingPlayers.length === 1) { if (firstPlayer.matches >= round) { // We can assign the bye. We're done, return true. @@ -521,18 +559,18 @@ console.log(standings); const potentialMatches = []; if (!Event.matchPlayers( - players.filter((player) => !player.withdrawn).map((id) => ({ - id, - eventPlayer: players[id], - ratedPlayer: ratedPlayers.find((p) => p.DiscordID === id) || { - Name: Discord.getGuildUser(id) ? Discord.getGuildUser(id).displayName : `<@${id}>`, - DiscordID: id, + players.filter((player) => !player.withdrawn).map((player) => ({ + id: player.id, + eventPlayer: player, + ratedPlayer: ratedPlayers.find((p) => p.DiscordID === player.id) || { + Name: Discord.getGuildUser(player.id) ? Discord.getGuildUser(player.id).displayName : `<@${player.id}>`, + DiscordID: player.id, Rating: 1500, RatingDeviation: 200, Volatility: 0.06 }, - points: matches.filter((m) => !m.cancelled && m.winner === id).length - (matches.filter((m) => !m.cancelled && m.players.indexOf(id) !== -1).length - matches.filter((m) => !m.cancelled && m.winner === id).length), - matches: matches.filter((m) => !m.cancelled && m.players.indexOf(id) !== -1).length + points: matches.filter((m) => !m.cancelled && m.winner === player.id).length - (matches.filter((m) => !m.cancelled && m.players.indexOf(player.id) !== -1).length - matches.filter((m) => !m.cancelled && m.winner === player.id).length), + matches: matches.filter((m) => !m.cancelled && m.players.indexOf(player.id) !== -1).length })).sort((a, b) => b.points - a.points || b.ratedPlayer.Rating - a.ratedPlayer.Rating || b.matches - a.matches || (Math.random() < 0.5 ? 1 : -1)), potentialMatches )) { @@ -571,8 +609,8 @@ console.log(standings); let textChannel, voiceChannel; try { - textChannel = await Discord.createTextChannel(channelName.toLowerCase().replace(/[^\-a-z0-9]/g, ""), Discord.gamesCategory); - voiceChannel = await Discord.createVoiceChannel(channelName, Discord.gamesCategory); + textChannel = await Discord.createTextChannel(channelName.toLowerCase().replace(/[^\-a-z0-9]/g, ""), Discord.pilotsChatCategory); + voiceChannel = await Discord.createVoiceChannel(channelName, Discord.pilotsVoiceChatCategory); } catch (err) { throw new Exception(`There was an error setting up the match between ${player1.displayName} and ${player2.displayName}.`, err); } @@ -588,24 +626,23 @@ console.log(standings); matches.push(match); // Setup channels - Discord.removePermissions(Discord.findRoleById(Discord.guildId), match.channel); + Discord.removePermissions(Discord.defaultRole, match.channel); Discord.addTextPermissions(player1, match.channel); Discord.addTextPermissions(player2, match.channel); - Discord.removePermissions(Discord.findRoleById(Discord.guildId), match.voice); + Discord.removePermissions(Discord.defaultRole, match.voice); Discord.addVoicePermissions(player1, match.voice); Discord.addVoicePermissions(player2, match.voice); match.homes = Event.getPlayer(player1.id).homes; // Announce match - await Discord.queue(`${player1.displayName} vs ${player2.displayName}`); await Discord.richQueue({ embed: { - title: `${player1} vs ${player2}`, - description: `The voice channel **${channelName}** has been setup for you to use for this match!`, + title: `${player1.displayName} vs ${player2.displayName}`, + description: `The voice channel **${voiceChannel}** has been setup for you to use for this match!`, timestamp: new Date(), color: 0x263686, - footer: {icon_url: Discord.icon}, + footer: {icon_url: Discord.icon, text: "DescentBot"}, fields: [ { name: "Map Selection", diff --git a/log.js b/log.js index ceb9755..0362e81 100644 --- a/log.js +++ b/log.js @@ -106,7 +106,7 @@ class Log { const message = { embed: { color: log.type === "log" ? 0x80FF80 : log.type === "warning" ? 0xFFFF00 : log.type === "exception" ? 0xFF0000 : 0x16F6F8, - footer: {"icon_url": Discord.icon}, + footer: {"icon_url": Discord.icon, text: "DescentBot"}, fields: [], timestamp: log.date }