import fs from "fs"; import path, { parse } from "path"; import { Client, Collection, GatewayIntentBits, Partials } from "discord.js"; const { TOKEN, GUILD_ID, RUNNER_ROLE_ID, COMMAND_CHANNEL_ID, PICKING_ID, BLU_ID, RED_ID, FK_ID, CAPTAIN_ID, } = require("./config.json"); let whitelistStr = require("./config.json").RANKING_WHITELIST; const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent, ], partials: [Partials.Channel, Partials.Message], }); client.fileCommands = new Collection(); const commandsPath = path.join(__dirname, "commands"); const commandFiles = fs.readdirSync(commandsPath); for (const file of commandFiles) { const command = require(`./commands/${file}`); if ("data" in command && "execute" in command) { client.fileCommands.set(command.data.name, command); } else { console.error(`${__filename}: Invalid command file: ${file}`); } } const backticks = "```"; const rankingsPath = path.join(__dirname, "rankings.json"); const matchString = (str, search) => { if (!str || !search) return false; return str.toLowerCase().includes(search.toLowerCase()); }; const findPlayer = (guild, searchName) => { // search display name let player = guild.members.cache.find((member) => matchString(member.displayName, searchName) ); if (!player) { // search global name player = guild.members.cache.find((member) => matchString(member.user.globalName, searchName) ); } if (!player) { // search username player = guild.members.cache.find((member) => matchString(member.user.username, searchName) ); } if (!player) { // match id player = guild.members.cache.find((member) => member.id === searchName); } return player; }; const getApplicableName = (player) => { return ( player.displayName || player.user.globalName || player.user.username || player.id ); }; const avgDiff = (arr) => { // average absolute difference between all pairs of rank properties in a given array let sum = 0; for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr.length; j++) { if (isNaN(arr[i].rank) || isNaN(arr[j].rank)) { console.error(`Invalid array passed to avgDiff ${arr}`); return; } sum += Math.abs(arr[i].rank - arr[j].rank); } } return sum / (arr.length * (arr.length - 1)) / 2; }; // values used for balancing, modifiable // FUN value: Fluctuating Unfairness Normalization value // PASSION: Player Ability/Skill Separation Index Offset Number let funInput = 4; // 1-10 let passionInput = 1; // 1-5 // balance attempt counter, not modifiable let attempts = 0; const balanceArrays = (a1, a2) => { // check to make sure arrays are not empty and that rank properties exist if (a1.length === 0 || a2.length === 0) { console.error(`Empty array(s) passed to balanceArrays ${a1} ${a2}`); return [a1, a2]; } if (!a1[0].rank || !a2[0].rank) { console.error(`Rank property missing ${a1} ${a2}`); return [a1, a2]; } let modArr1 = [...a1]; let modArr2 = [...a2]; let sum1 = modArr1.reduce((acc, player) => acc + player.rank, 0); let sum2 = modArr2.reduce((acc, player) => acc + player.rank, 0); let funValue = funInput * 0.01 + 0.025; let passion = passionInput * 0.1; let unequalAllowance = Math.floor((sum1 + sum2) * funValue); // rebalance until teams fall within constraints attempts = 0; while ( Math.abs(avgDiff(modArr1) - avgDiff(modArr2)) > passion || (Math.abs(sum1 - sum2) > unequalAllowance && attempts < 1000) ) { attempts++; if (attempts > 1000) { console.log( `Too many attempts to balance teams ${attempts} ${unequalAllowance} ${passion} ${avgDiff(modArr1)} ${avgDiff(modArr2)}` ); return [modArr1, modArr2]; } else if (attempts > 600) { unequalAllowance = (sum1 + sum2) * funValue + 2; } else if (attempts > 300) { unequalAllowance = (sum1 + sum2) * funValue + 1; } let rerolls = 0; let ran1 = Math.floor(Math.random() * modArr1.length); let ran2 = Math.floor(Math.random() * modArr2.length); // swap randomly if teams seem even if (Math.abs(sum1 - sum2) < unequalAllowance) { while (modArr1[ran1].rank === modArr2[ran2].rank && rerolls < 100) { rerolls++; ran1 = Math.floor(Math.random() * modArr1.length); ran2 = Math.floor(Math.random() * modArr2.length); } } // else swap players to minimize difference else if (sum1 > sum2) { while (modArr1[ran1].rank <= modArr2[ran2].rank && rerolls < 100) { rerolls++; ran1 = Math.floor(Math.random() * modArr1.length); ran2 = Math.floor(Math.random() * modArr2.length); } } else if (sum1 < sum2) { while (modArr1[ran1].rank >= modArr2[ran2].rank && rerolls < 100) { rerolls++; ran1 = Math.floor(Math.random() * modArr1.length); ran2 = Math.floor(Math.random() * modArr2.length); } } // make the swap let temp = modArr1[ran1]; modArr1[ran1] = modArr2[ran2]; modArr2[ran2] = temp; sum1 = modArr1.reduce((acc, player) => acc + player.rank, 0); sum2 = modArr2.reduce((acc, player) => acc + player.rank, 0); // if array lengths are uneven, act as if the shorter array is padded with the average of all values // a length difference of more than 1 between the two arrays should be disallowed elsewhere if (modArr1.length > modArr2.length) { let avg = sum2 / modArr2.length; sum2 += avg; } else if (modArr1.length < modArr2.length) { let avg = sum1 / modArr1.length; sum1 += avg; } } modArr1.sort((a, b) => b.rank - a.rank); modArr2.sort((a, b) => b.rank - a.rank); console.log( `${ attempts > 999 ? "forced" : "successful" } sort after ${attempts} attempts: ${modArr1.map((i) => i.rank)} ${modArr2.map((i) => i.rank)} ${sum1} ${sum2} (${unequalAllowance}) ${avgDiff(modArr1)} ${avgDiff(modArr2)} (${passion})` ); return [modArr1, modArr2]; }; client.on("ready", () => { console.log(`Logged in as ${client.user.tag}!`); console.log( `Deleting old messages from this bot in command channel: ${COMMAND_CHANNEL_ID}...` ); let channel = client.channels.cache.get(COMMAND_CHANNEL_ID); if (!channel) return console.error("(ready) Can't find command channel"); channel.messages .fetch({ limit: 100 }) .then((messages) => { messages.forEach((message) => { if (message.author.id === client.user.id) { message.delete(); } }); }) .catch(console.error); }); // send message on error client.on("error", (error) => { console.error(error); let channel = client.channels.cache.get(COMMAND_CHANNEL_ID); if (!channel) return console.error("Can't find command channel"); client.channels.cache .get(COMMAND_CHANNEL_ID) .send("Internal error") .catch(console.error); }); client.on("interactionCreate", async (interaction) => { const command = interaction.commandName; if (!interaction.isChatInputCommand()) return; const fileCommand = client.fileCommands.get(command); if (fileCommand) { try { if (fileCommand.permissions) { const member = interaction.member; for (const permission of fileCommand.permissions) { if (!member.roles.cache.has(permission)) { await interaction.reply({ content: "You lack the required permissions", ephemeral: true, }); return; } } } await fileCommand.execute(interaction); } catch (error) { console.error(error); await interaction.reply({ content: "Internal error executing command from file", ephemeral: true, }); } } else if (interaction.channelId !== COMMAND_CHANNEL_ID) { //let isRunner = await interaction.member.roles.cache.has(RUNNER_ROLE_ID); //if (!isRunner) { await interaction.reply({ content: "Wrong channel, or you lack the required permissions", ephemeral: true, }); return; } if ( command === "scout" || command === "soldier" || command === "pyro" || command === "demoman" || command === "demo" || command === "heavy" || command === "engineer" || command === "engi" || command === "medic" || command === "sniper" || command === "spy" ) { // get voice channels const picking = interaction.guild.channels.cache.find( (channel) => channel.name === "picking" || channel.id === PICKING_ID ); if (!picking) return console.error("Can't find channel 'picking'!"); const captain = interaction.guild.channels.cache.find( (channel) => channel.name === "captains" || channel.id === CAPTAIN_ID ); if (!captain) return console.error("Can't find channel 'captains'!"); // make sure user is in picking or captain channel if ( !picking.members.has(interaction.user.id) && !captain.members.has(interaction.user.id) ) { await interaction.reply({ content: "Must be in picking or captains channel to use this command", ephemeral: true, }); return; } else { await interaction.reply({ content: "Checking picking channel...", ephemeral: true, }); // set role name let roleName = command; if (command === "demoman") roleName = "demo"; if (command === "engi") roleName = "engineer"; // check each member in picking channel for role let str = `In picking (${roleName}):`; for (const member of picking.members.values()) { if (member.roles.cache.find((role) => role.name === roleName)) { if (str !== `In picking (${roleName}):`) str += ","; str += " " + getApplicableName(member); } } if (str === `In picking (${roleName}):`) str = `None found ¯\\_(ツ)_/¯ (${roleName})`; // respond return await interaction.followUp(str); } } if (command === "hello") { await interaction.reply("world"); } if ( command === "topicking" || command === "end" || command === "resetteams" ) { await interaction.reply("Moving members..."); // get voice channels const blu = interaction.guild.channels.cache.find( (channel) => channel.name === "blu" || channel.id === BLU_ID ); if (!blu) return console.error("Can't find channel 'blu'!"); const red = interaction.guild.channels.cache.find( (channel) => channel.name === "red" || channel.id === RED_ID ); if (!red) return console.error("Can't find channel 'red'!"); const picking = interaction.guild.channels.cache.find( (channel) => channel.name === "picking" || channel.id === PICKING_ID ); if (!picking) return console.error("Can't find channel 'picking'!"); // get members in voice channel let members = blu.members.concat(red.members); if (members.size === 0) { return await interaction.followUp("Found no members in blu or red"); } let eCount = 0; // move members to picking const moveToPicking = async (member) => { try { await member.voice.setChannel(picking); } catch (error) { console.error(error); eCount++; } }; const moveAllToPicking = async () => { return Promise.all( Array.from(members, async ([memberId, member]) => { await moveToPicking(member); }) ); }; moveAllToPicking().then(() => interaction.followUp( `Moved members in blu and red${ eCount > 0 ? ` (error moving ${eCount} members)` : "" }` ) ); } if (command === "fklist" || command === "listfk") { // moves players in picking to fatkid channel, for use in captain pugs await interaction.reply("Moving fatkids..."); // get voice channels const picking = interaction.guild.channels.cache.find( (channel) => channel.name === "picking" || channel.id === PICKING_ID ); if (!picking) return console.error("Can't find channel 'add-up'!"); const fk = interaction.guild.channels.cache.find( (channel) => channel.name === "fatkid" || channel.id === FK_ID ); if (!fk) return console.error("Can't find channel 'fatkid'!"); // get members in voice channel const members = picking.members; if (members.size === 0) { return await interaction.followUp("Found no members in picking"); } let str = "", eCount = 0; const logFk = async (member) => { try { await member.voice.setChannel(fk); if (str.length > 0) str += ", "; str += getApplicableName(member); } catch (error) { console.error(error); eCount++; } }; const logAllFks = async () => { return Promise.all( Array.from(members, async ([memberId, member]) => { await logFk(member); }) ); }; logAllFks().then(() => interaction.followUp( `Fatkids: ${str}${ eCount > 0 ? ` (error moving ${eCount} members)` : "" }` ) ); } if (command === "pick" || command === "autocaptain") { await interaction.reply("Picking teams..."); // get voice channels const picking = interaction.guild.channels.cache.find( (channel) => channel.name === "picking" || channel.id === PICKING_ID ); if (!picking) return console.error("Can't find channel 'picking'!"); const blu = interaction.guild.channels.cache.find( (channel) => channel.name === "blu" || channel.id === BLU_ID ); if (!blu) return console.error("Can't find channel 'blu'!"); const red = interaction.guild.channels.cache.find( (channel) => channel.name === "red" || channel.id === RED_ID ); if (!red) return console.error("Can't find channel 'red'!"); // get players in voice channel const players = picking.members; if (players.size !== 18) { return await interaction.followUp( `Found ${players.size} players in picking, expected 18` ); } // get rankings let rankings = {}; try { console.log(`Getting rankings at ${rankingsPath}...`); if (!fs.existsSync(rankingsPath)) { fs.writeFileSync(rankingsPath, "{}"); } rankings = JSON.parse(fs.readFileSync(rankingsPath)); } catch (error) { console.error(error); return await message.reply("Error sorting teams"); } // distribute unranked players evenly, then randomly assign ranked players let rankedPlayers = [], bluPlayers = [], redPlayers = [], rng = 0; for (const [playerId, player] of players) { if (!rankings[playerId]) { rng = Math.random(); // if teams are even, randomly assign if (bluPlayers.length === redPlayers.length) { if (rng < 0.5) { bluPlayers.push({ player, rank: 0 }); } else { redPlayers.push({ player, rank: 0 }); } } // otherwise, equalize teams else if (bluPlayers.length < redPlayers.length) { bluPlayers.push({ player, rank: 0 }); } else { redPlayers.push({ player, rank: 0 }); } } // prepare ranked players for random assignment else { rankedPlayers.push(player); } } // slots for ranked players are limited by unranked players, who are sorted differently let bluRankedPlayers = [], redRankedPlayers = [], bluRankSlots = 9 - bluPlayers.length, redRankSlots = 9 - redPlayers.length; // create array with players and their ranks for (const player of rankedPlayers) { rng = Math.random(); if (rng < 0.5) { if (bluRankedPlayers.length < bluRankSlots) { bluRankedPlayers.push({ player, rank: rankings[player.id] }); } else { redRankedPlayers.push({ player, rank: rankings[player.id] }); } } else { if (redRankedPlayers.length < redRankSlots) { redRankedPlayers.push({ player, rank: rankings[player.id] }); } else { bluRankedPlayers.push({ player, rank: rankings[player.id] }); } } } if ( bluPlayers.length + bluRankedPlayers.length !== 9 || redPlayers.length + redRankedPlayers.length !== 9 ) { console.error( `Invalid number of players: ${bluPlayers.length} ${bluRankedPlayers.length} ${redPlayers.length} ${redRankedPlayers.length}` ); return await message.reply("Error sorting teams"); } // bring teams to reasonable balance let [bluBalanced, redBalanced] = balanceArrays( bluRankedPlayers, redRankedPlayers ); bluPlayers = bluPlayers.concat(bluBalanced); redPlayers = redPlayers.concat(redBalanced); // sort alphebetically then build string bluPlayers.sort((a, b) => a.player.displayName.localeCompare(b.player.displayName) ); redPlayers.sort((a, b) => a.player.displayName.localeCompare(b.player.displayName) ); try { let teamStr = `${backticks}BLU:`; for (const player of bluPlayers) { teamStr += `\n${player.player.displayName}`; } teamStr += `\n\nRED:`; for (const player of redPlayers) { teamStr += `\n${player.player.displayName}`; } teamStr += backticks; await interaction.followUp(teamStr); } catch (error) { console.error(error); await interaction.followUp("Couldn't print teams before moving players"); } let moveErr = 0; // move to team voice channels while (bluPlayers.length > 0) { const idx = Math.floor(Math.random() * bluPlayers.length); const player = bluPlayers.splice(idx, 1)[0]; try { await player.player.voice.setChannel(blu); } catch (error) { console.error(error); moveErr++; } } while (redPlayers.length > 0) { const idx = Math.floor(Math.random() * redPlayers.length); const player = redPlayers.splice(idx, 1)[0]; try { await player.player.voice.setChannel(red); } catch (error) { console.error(error); moveErr++; } } interaction.followUp( `Players moved into teams${ moveErr > 0 ? ` (error moving ${moveErr} members)` : "" }` ); } if (command === "clear" || command === "bclear") { await interaction.reply("Clearing messages..."); let channel = client.channels.cache.get(COMMAND_CHANNEL_ID); if (!channel) return console.error("Can't find command channel"); channel.messages .fetch({ limit: 100 }) .then((messages) => { messages.forEach((message) => { if (message.author.id === client.user.id) { message.delete(); } }); }) .catch(console.error); } }); /* * DM commands * - setrank: saves a player's rank * - getrank: prints a player's rank * - rankings: prints all players' ranks * - fun: prints or sets the FUN value * - passion: prints or sets the PASSION value * - simulateteams: simulates autocaptain results * - whitelist: adds an admin to the whitelist to use DM commands * - getwhitelist: prints the whitelist * - clearwhitelist: clears the whitelist completely, security measure */ client.on("messageCreate", async (message) => { if (message.author.bot || message.guild) return; // check if user is whitelisted if (!whitelistStr.includes(message.author.id)) { return; } if (message.content.toLowerCase().includes("hello penguin")) { await message.reply(`Hello ${message.author.username}`); return; } const pickupGuild = client.guilds.cache.get(GUILD_ID); if (!pickupGuild) { await message.reply("Could not find guild"); return; } const args = message.content.toLowerCase().split(" "); if (args[0] === "setrank") { if (args.length < 3) { await message.reply( "Invalid number of arguments. usage: `setrank `" ); return; } const player = findPlayer(pickupGuild, args[1]); if (!player) { await message.reply( `Could not find player ${args[1]}. If this issue persists, try copy/pasting the player's Discord handle or user ID` ); return; } const playerId = player.id; const applicableName = getApplicableName(player); const rank = parseInt(args[2]); if (isNaN(rank) || rank < 0 || rank > 5) { await message.reply( `Invalid rank ${args[2]}. Supply an integer between 0 and 5` ); return; } // create rankings.json if it doesn't exist const rankingsPath = path.join(__dirname, "rankings.json"); if (!fs.existsSync(rankingsPath)) { fs.writeFileSync(rankingsPath, "{}"); } // rankings.json is a map of player's id to rank const rankings = JSON.parse(fs.readFileSync(rankingsPath)); rankings[playerId] = rank; console.log( `Setting rank of ${applicableName}, ${playerId} to ${rank} at ${rankingsPath}...` ); new Promise((resolve, reject) => { fs.writeFile(rankingsPath, JSON.stringify(rankings), (err) => { if (err) reject(err); else resolve(); }); }).then((res, err) => { if (err) { console.error(err); message.reply(`Error setting rank: ${err.message}`); } else { message.reply(`Set rank of ${applicableName} to ${rank}`); } }); } if (args[0] === "getrank") { if (args.length < 2) { await message.reply( "Invalid number of arguments. Usage: `getrank `" ); return; } const player = findPlayer(pickupGuild, args[1]); if (!player) { await message.reply( `Could not find player ${args[1]}. If this issue persists, try copy/pasting the player's Discord handle or user ID` ); return; } const playerId = player.id; const applicableName = getApplicableName(player); try { console.log( `Getting rank of ${applicableName}, ${playerId} at ${rankingsPath}...` ); if (!fs.existsSync(rankingsPath)) { fs.writeFileSync(rankingsPath, "{}"); } const rankings = JSON.parse(fs.readFileSync(rankingsPath)); await message.reply( `${backticks}${applicableName} - ${"*".repeat( rankings[playerId] )}${backticks}` ); } catch (error) { console.error(error); await message.reply(`Error getting rank: ${error.message}`); } } if (args[0] === "rankings") { await message.reply("Getting rankings..."); try { console.log(`Getting rankings at ${rankingsPath}...`); if (!fs.existsSync(rankingsPath)) { fs.writeFileSync(rankingsPath, "{}"); } const rankings = JSON.parse(fs.readFileSync(rankingsPath)); let players = []; for (const [playerId, rank] of Object.entries(rankings)) { if (rank > 0) { const player = await pickupGuild.members.fetch(playerId); if (!player) { console.error(`Could not find player ${playerId}`); continue; } const applicableName = getApplicableName(player); players.push({ name: applicableName, rank }); } } // sort by rank, then name players.sort((a, b) => { if (a.rank === b.rank) { return a.name.localeCompare(b.name); } return b.rank - a.rank; }); // build string let str = ""; const maxNameLength = Math.max(...players.map((p) => p.name.length)); for (const { name, rank } of players) { str += `${name.padEnd(maxNameLength, " ")} - ${"*".repeat(rank)}\n`; } if (str === "") str = "No rankings found"; if (str.length > 2000) { let chunks = str.match(/[\s\S]{1,1990}/g); for (let chunk of chunks) { await message.reply(`${backticks}${chunk}${backticks}`); } } else { await message.reply(`${backticks}${str}${backticks}`); } } catch (error) { console.error(error); await message.reply(`Error getting rankings: ${error.message}`); } } if (args[0] === "fun") { if ( args.length < 2 || isNaN(args[1]) || parseInt(args[1]) < 1 || parseInt(args[1]) > 10 ) return await message.reply( `Current FUN value: ${funInput}\nUsage: \`fun <1-10>\`` ); funInput = parseInt(args[1]); return await message.reply(`FUN value set to ${funInput}`); } if (args[0] === "passion") { if ( args.length < 2 || isNaN(args[1]) || parseInt(args[1]) < 1 || parseInt(args[1]) > 5 ) return await message.reply( `Current PASSION: ${passionInput}\nUsage: \`passion <1-5>\`` ); passionInput = parseInt(args[1]); return await message.reply(`PASSION set to ${passionInput}`); } if (args[0] === "simulateteams") { await message.reply("Simulating teams..."); // get players in voice channel // const players = picking.members; // if (players.size !== 18) { // return await message.reply( // `Found ${players.size} players in picking, expected 18` // ); // } // simulate input is 18 player display names separated by commas const playerInputs = args[1].split(","); if (playerInputs.length !== 18) { return await message.reply( `Found ${playerInputs.length} players in input, expected 18 \nUsage: \`simulateteams ,,...,\`` ); } // get players from input let players = new Map(); for (const input of playerInputs) { const player = findPlayer(pickupGuild, input); if (!player) { await message.reply(`Could not find player ${input}`); return; } players.set(player.id, player); } // get rankings let rankings = {}; try { console.log(`Getting rankings at ${rankingsPath}...`); if (!fs.existsSync(rankingsPath)) { fs.writeFileSync(rankingsPath, "{}"); } rankings = JSON.parse(fs.readFileSync(rankingsPath)); } catch (error) { console.error(error); return await message.reply("Error sorting teams"); } // distribute unranked players evenly, then randomly assign ranked players let rankedPlayers = [], bluPlayers = [], redPlayers = [], rng = 0; for (const [playerId, player] of players) { if (!rankings[playerId]) { rng = Math.random(); // if teams are even, randomly assign if (bluPlayers.length === redPlayers.length) { if (rng < 0.5) { bluPlayers.push({ player, rank: 0 }); } else { redPlayers.push({ player, rank: 0 }); } } // otherwise, equalize teams else if (bluPlayers.length < redPlayers.length) { bluPlayers.push({ player, rank: 0 }); } else { redPlayers.push({ player, rank: 0 }); } } // prepare ranked players for random assignment else { rankedPlayers.push(player); } } console.log( `bluUnranked: ${bluPlayers.length} redUnranked: ${redPlayers.length}` ); // slots for ranked players are limited by unranked players, who are sorted differently let bluRankedPlayers = [], redRankedPlayers = [], bluRankSlots = 9 - bluPlayers.length, redRankSlots = 9 - redPlayers.length; console.log(`bluRankSlots: ${bluRankSlots} redRankSlots: ${redRankSlots}`); // create array with players and their ranks for (const player of rankedPlayers) { rng = Math.random(); console.log( `rng: ${rng}, bluRankedPlayers: ${bluRankedPlayers.length}, redRankedPlayers: ${redRankedPlayers.length}` ); if (rng < 0.5) { if (bluRankedPlayers.length < bluRankSlots) { bluRankedPlayers.push({ player, rank: rankings[player.id] }); } else { redRankedPlayers.push({ player, rank: rankings[player.id] }); } } else { if (redRankedPlayers.length < redRankSlots) { redRankedPlayers.push({ player, rank: rankings[player.id] }); } else { bluRankedPlayers.push({ player, rank: rankings[player.id] }); } } } if ( bluPlayers.length + bluRankedPlayers.length !== 9 || redPlayers.length + redRankedPlayers.length !== 9 ) { console.error( `Invalid number of players: ${bluPlayers.length} ${bluRankedPlayers.length} ${redPlayers.length} ${redRankedPlayers.length}` ); return await message.reply("Error sorting teams"); } // bring teams to reasonable balance let [bluBalanced, redBalanced] = balanceArrays( bluRankedPlayers, redRankedPlayers ); bluPlayers = bluPlayers.concat(bluBalanced); redPlayers = redPlayers.concat(redBalanced); let moveErr = 0; // for sim, print teams after sorting by rank then name bluPlayers.sort((a, b) => { if (a.rank === b.rank) { return a.player.displayName.localeCompare(b.player.displayName); } return b.rank - a.rank; }); redPlayers.sort((a, b) => { if (a.rank === b.rank) { return a.player.displayName.localeCompare(b.player.displayName); } return b.rank - a.rank; }); let simString = `${backticks}BLU:`; // move to team voice channels while (bluPlayers.length > 0) { //let idx = Math.floor(Math.random() * bluPlayers.length); //if (idx === bluPlayers.length) idx--; let idx = 0; const player = bluPlayers.splice(idx, 1)[0]; try { //await player.voice.setChannel(blu); simString += `\n${getApplicableName(player.player)} - ${"*".repeat( player.rank )}`; } catch (error) { console.error(error); moveErr++; } } simString += `\n\nRED:`; while (redPlayers.length > 0) { //let idx = Math.floor(Math.random() * redPlayers.length); //if (idx === redPlayers.length) idx--; let idx = 0; const player = redPlayers.splice(idx, 1)[0]; try { //await player.voice.setChannel(red); simString += `\n${getApplicableName(player.player)} - ${"*".repeat( player.rank )}`; } catch (error) { console.error(error); moveErr++; } } message.reply( `Simulated teams:\n${simString}${backticks}${ moveErr > 0 ? ` (error moving ${moveErr} members)` : "" }${attempts > 999 ? "(forced)" : ""}` ); } if (args[0] === "whitelist") { // add user to config.json whitelist if (args.length < 2) { await message.reply( "Invalid number of arguments. Usage: `whitelist `" ); return; } const name = args[1]; const player = findPlayer(pickupGuild, name); if (!player) { await message.reply( `Could not find player ${name}. If this issue persists, try copy/pasting the player's Discord handle or user ID` ); return; } const playerId = player.id; const applicableName = getApplicableName(player); if (whitelistStr.includes(playerId)) { await message.reply( `User ${applicableName} (${playerId}) is already whitelisted` ); return; } const configPath = path.join(__dirname, "config.json"); const config = JSON.parse(fs.readFileSync(configPath)); whitelistStr += `,${playerId}`; config.RANKING_WHITELIST = whitelistStr; new Promise((resolve, reject) => { fs.writeFile(configPath, JSON.stringify(config), (err) => { if (err) reject(err); else resolve(); }); }).then((res, err) => { if (err) { console.error(err); message.reply(`Error whitelisting user: ${err.message}`); } else { message.reply( `Whitelisted user ${applicableName} (${playerId}). If this was done in error, please contact an admin` ); } }); } if (args[0] === "getwhitelist") { let str = `${backticks}${whitelistStr}${backticks}${backticks}`; const whitelistIds = whitelistStr.split(","); for (const id of whitelistIds) { const player = findPlayer(pickupGuild, id); if (player) { str += `\n${getApplicableName(player)}`; } } str += `${backticks}`; await message.reply(str); } if (args[0] === "clearwhitelist") { if (args[1] !== "confirm") { await message.reply( "This command will clear the whitelist and prevent further DM commands. This will require manual intervention to undo. To confirm, use `clearwhitelist confirm`" ); return; } else { const configPath = path.join(__dirname, "config.json"); const config = JSON.parse(fs.readFileSync(configPath)); whitelistStr = ""; config.RANKING_WHITELIST = whitelistStr; new Promise((resolve, reject) => { fs.writeFile(configPath, JSON.stringify(config), (err) => { if (err) reject(err); else resolve(); }); }).then((res, err) => { if (err) { console.error(err); message.reply(`Error clearing whitelist: ${err.message}`); } else { message.reply("Whitelist cleared, please alert an admin"); } }); } } }); client.login(TOKEN);