diff --git a/commands/fk.js b/commands/fk.js new file mode 100644 index 0000000..c76345f --- /dev/null +++ b/commands/fk.js @@ -0,0 +1,217 @@ +import { SlashCommandBuilder } from "discord.js"; +import { ADDUP_ID, PICKING_ID } from "../config.json"; +import fs from "fs"; +import path from "path"; + +const fkPath = path.join(__dirname, "..", "fk.json"); + +const storeFk = async (idArr) => { + // store fk history to avoid repeats + if (!idArr || typeof idArr !== "object") + return console.error("Cannot store fk history"); + + if (!fs.existsSync(fkPath)) { + fs.writeFileSync(fkPath, "[]"); + } + let fkHistory = JSON.parse(fs.readFileSync(fkPath)); + + // add to history + fkHistory.push(idArr); + if (fkHistory.length > 10) fkHistory.shift(); + try { + fs.writeFileSync(fkPath, JSON.stringify(fkHistory)); + } catch (error) { + console.error(error); + } + return; +}; + +const getPastFk = async () => { + // get 3 most recent sets of fks to avoid repeats + if (!fs.existsSync(fkPath)) { + fs.writeFileSync(fkPath, "[]"); + } + let fkHistory = JSON.parse(fs.readFileSync(fkPath)); + let recentFk = fkHistory.slice(-3); + + if (!recentFk || typeof recentFk !== "object") + console.error("Invalid fk history"); + if (recentFk.length === 0) return [[], [], []]; + return recentFk; +}; + +const data = new SlashCommandBuilder() + .setName("fk") + .setDescription( + "Moves added players to picking and randomly sits players out" + ) + .addUserOption((option) => + option + .setName("exception1") + .setDescription("Player to protect from sitting out, intended for medic") + ) + .addUserOption((option) => + option + .setName("exception2") + .setDescription("Player to protect from sitting out, intended for medic") + ); + +const execute = async (interaction) => { + // pull users in addup into picking, then randomly select other users in picking to be sat out until there are 18 in picking + await interaction.reply("Randomly choosing fatkids from picking..."); + + // get voice channels + const addup = interaction.guild.channels.cache.find( + (channel) => channel.name === "add-up" || channel.id === ADDUP_ID + ); + const picking = interaction.guild.channels.cache.find( + (channel) => channel.name === "picking" || channel.id === PICKING_ID + ); + + // get members in voice channel + const addupPlayers = []; + const pickingPlayers = []; + addup.members.forEach((member) => addupPlayers.push(member)); + picking.members.forEach((member) => pickingPlayers.push(member)); + + if (pickingPlayers.length < addupPlayers.length) { + // move all addup into empty picking + addup.members.forEach((member) => { + member.voice.setChannel(picking); + }); + return await interaction.followUp( + "Found too few players in picking to fatkid, moved all addup players to picking instead" + ); + } + + const targetPlayerCount = 18; + if (pickingPlayers.length + addupPlayers.length < targetPlayerCount) { + return await interaction.followUp( + `Can't find enough total players, expected ${targetPlayerCount} (found ${ + pickingPlayers.length + addupPlayers.length + })` + ); + } + + // set up past fk protection + let fatkids = [], + recentFkSets = [], + protectedFk = [], + protectedDict = {}, + fkAttempts = 0; + try { + recentFkSets = await getPastFk(); + protectedFk = recentFkSets.flat(); + } catch (error) { + console.error(error); + await interaction.followUp( + "Failed to get history, fk protection may not be applied" + ); + } + + // get exceptions + let exceptions = []; + for (const option of ["exception1", "exception2"]) { + const exception = interaction.options.getUser(option); + if (exception) exceptions.push(exception); + } + if (addupPlayers.length > targetPlayerCount - exceptions.length) { + exceptions = []; + return await interaction.followUp( + "Could not apply exceptions as too many players in addup were found" + ); + } + + // select excess players to be sat out + while (pickingPlayers.length + addupPlayers.length > targetPlayerCount) { + if (pickingPlayers.length === 0) { + return await interaction.followUp( + "Error: ran out of players in picking to fatkid" + ); + } + // remove earliest fk sets as attempts become unreasonably high + if (fkAttempts === 100 || fkAttempts === 250 || fkAttempts === 500) { + console.log(`Shifting fk history ${fkAttempts}`); + recentFkSets.shift(); + protectedFk = recentFkSets.flat(); + } + fkAttempts++; + const idx = Math.floor(Math.random() * pickingPlayers.length); + let fk = pickingPlayers[idx]; + if (exceptions.includes(fk)) { + continue; + } + if (protectedFk.includes(fk.id)) { + if (!protectedDict[fk.id]) protectedDict[fk.id] = 0; // todo: once this works change it to hold the member object so that we don't have to fetch it later + protectedDict[fk.id]++; + } else { + pickingPlayers.splice(idx, 1); + fatkids.push(fk); + } + } + if (Object.keys(protectedDict).length > 0) { + console.log(`Protected fks: ${JSON.stringify(protectedDict)}`); + } + + let errCount = 0; + + // move players from addup to picking + for (const newPlayer of addupPlayers) { + try { + await newPlayer.voice.setChannel(picking); + } catch (error) { + console.error(error); + errCount++; + } + } + + let fkIds = []; + + // move players from picking to fatkid + for (const fk of fatkids) { + try { + fkIds.push(fk.id); + await fk.voice.setChannel(addup); + } catch (error) { + console.error(error); + errCount++; + } + } + + // store fatkid history + try { + storeFk(fkIds); + } catch (error) { + console.error(error); + } + + // build response + let fkStr = `Sat out ${fatkids.length} players${ + fatkids.length > 0 ? ": " : "" + }`; + for (const fk of fatkids) { + fkStr += `${fk.displayName}, `; + } + fkStr = fkStr.slice(0, -2); + if (Object.keys(protectedDict).length > 0) { + fkStr += "Players that were protected at least once: "; + for (const fkId in protectedDict) { + await interaction.guild.members + .fetch(fkId) + .then((member) => { + if (member) { + fkStr += `${member.displayName}, `; + } + }) + .catch(console.error); + } + fkStr = fkStr.slice(0, -2); + } + if (fkAttempts > 100) + fkStr += `\n(fk protection sets ignored: ${3 - recentFkSets.length}/3)`; + if (errCount > 0) fkStr += `\n(error moving ${errCount} members)`; + + interaction.followUp(fkStr); +}; + +export { data, execute }; diff --git a/index.js b/index.js index 341807b..30f95d7 100644 --- a/index.js +++ b/index.js @@ -269,7 +269,7 @@ client.on("error", (error) => { }); client.on("interactionCreate", async (interaction) => { - const command = interaction.commandName; + let command = interaction.commandName; if (!interaction.isChatInputCommand()) return; if ( @@ -400,7 +400,7 @@ client.on("interactionCreate", async (interaction) => { ); } - if (command === "fk" || command === "fatkid") { + /*if (command === "fk" || command === "fatkid") { // pull users in addup into picking, then randomly select other users in picking to be sat out until there are 18 in picking await interaction.reply("Randomly choosing fatkids from picking..."); @@ -521,7 +521,7 @@ client.on("interactionCreate", async (interaction) => { : "" }` ); - } + }*/ /*if (command === "testfk") { // debug fk @@ -831,6 +831,8 @@ client.on("interactionCreate", async (interaction) => { .catch(console.error); } + if (command === "fatkid") command = "fk"; + const fileCommand = client.fileCommands.get(command); if (!fileCommand) return;