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 };