diff --git a/commands/applications/random.js b/commands/applications/random.js new file mode 100644 index 0000000..c5aa47b --- /dev/null +++ b/commands/applications/random.js @@ -0,0 +1,557 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + AttachmentBuilder +} = require("discord.js"); + +const { mClient } = require("../.."); +const path = require("node:path"); +const jobIconDirectory = path.resolve( + __dirname, + "../../assets/jobs" +); +const expansions = [ + { name: "A Realm Reborn", value: 2 }, + { name: "Heavensward", value: 3 }, + { name: "Stormblood", value: 4 }, + { name: "Shadowbringers", value: 5 }, + { name: "Endwalker", value: 6 }, + { name: "Dawntrail", value: 7 } +]; + +const dutyTypes = [ + { name: "Dungeon", value: "Dungeon" }, + { name: "Trial", value: "Trial" }, + { name: "Raid", value: "Raid" }, + { name: "Alliance Raid", value: "AllianceRaid" }, + { name: "Guildhest", value: "Guildhest" }, + { name: "Variant Dungeon", value: "VariantDungeon" }, + { name: "Criterion", value: "Criterion" } +]; + +const dutyDifficulties = [ + { name: "Normal", value: "Normal" }, + { name: "Hard", value: "Hard" }, + { name: "Extreme", value: "Extreme" }, + { name: "Unreal", value: "Unreal" }, + { name: "Savage", value: "Savage" }, + { name: "Ultimate", value: "Ultimate" }, + { name: "Chaotic", value: "Chaotic" } +]; + +const roulettes = [ + { name: "Leveling", value: "Leveling" }, + { name: "High-level Dungeons", value: "High-level Dungeons" }, + { name: "Level Cap Dungeons", value: "Level Cap Dungeons" }, + { name: "Trials", value: "Trials" }, + { name: "Normal Raids", value: "Normal Raids" }, + { name: "Alliance Raids", value: "Alliance Raids" }, + { name: "Main Scenario", value: "Main Scenario" }, + { name: "Guildhests", value: "Guildhests" }, + { name: "Mentor", value: "Mentor" } +]; + +const roles = [ + { name: "Tank", value: "Tank" }, + { name: "Healer", value: "Healer" }, + { name: "DPS", value: "DPS" } +]; + +const roleTypes = [ + { name: "Tank", value: "Tank" }, + { name: "Pure Healer", value: "Pure" }, + { name: "Shield Healer", value: "Shield" }, + { name: "Melee DPS", value: "Melee" }, + { name: "Physical Ranged DPS", value: "PhysicalRanged" }, + { name: "Magical Ranged DPS", value: "MagicalRanged" } +]; + +const damageTypes = [ + { name: "Physical", value: "Physical" }, + { name: "Magical", value: "Magical" } +]; + +function getJobIcon(job) { + const fileName = `${job.Name.replace(/\s+/g, "")}.png`; + + return path.join( + jobIconDirectory, + fileName + ); +} + +const dutyIcons = { + Guildhest: + "https://ffxiv.consolegameswiki.com/mediawiki/images/6/61/Guildhest.png", + + Dungeon: + "https://ffxiv.consolegameswiki.com/mediawiki/images/a/ab/Dungeon.png", + + Trial: + "https://ffxiv.consolegameswiki.com/mediawiki/images/0/0d/Trial.png", + + Raid: + "https://ffxiv.consolegameswiki.com/mediawiki/images/d/d6/Raid.png", + + AllianceRaid: + "https://ffxiv.consolegameswiki.com/mediawiki/images/d/d6/Raid.png", + + VariantDungeon: + "https://ffxiv.consolegameswiki.com/mediawiki/images/5/5e/Variant_and_criterion_dungeons_icon1.png", + + Criterion: + "https://ffxiv.consolegameswiki.com/mediawiki/images/5/5e/Variant_and_criterion_dungeons_icon1.png" +}; + +const dutyDifficultyIcons = { + Ultimate: + "https://ffxiv.consolegameswiki.com/mediawiki/images/8/8a/Ultimate_Raid.png", + + Chaotic: + "https://ffxiv.consolegameswiki.com/mediawiki/images/a/a9/Chaotic_Alliance_Raid.png" +}; + + +module.exports = { + data: new SlashCommandBuilder() + .setName("random") + .setDescription("Randomly select FFXIV content.") + + .addSubcommand(subcommand => + subcommand + .setName("duty") + .setDescription("Select a random duty to complete.") + + .addIntegerOption(option => + option + .setName("level") + .setDescription("Your current level.") + .setRequired(true) + .setMinValue(10) + .setMaxValue(100) + ) + + .addStringOption(option => + option + .setName("type") + .setDescription("Only select a specific duty type.") + .addChoices(...dutyTypes) + ) + + .addStringOption(option => + option + .setName("difficulty") + .setDescription("Only select a specific difficulty.") + .addChoices(...dutyDifficulties) + ) + + .addIntegerOption(option => + option + .setName("expansion") + .setDescription("Only select duties from an expansion.") + .addChoices(...expansions) + ) + + .addStringOption(option => + option + .setName("roulette") + .setDescription("Only select duties belonging to a roulette.") + .addChoices(...roulettes) + ) + + .addIntegerOption(option => + option + .setName("party-size") + .setDescription("Only select duties with this party size.") + .addChoices( + { name: "4 Players", value: 4 }, + { name: "8 Players", value: 8 }, + { name: "24 Players", value: 24 } + ) + ) + + .addBooleanOption(option => + option + .setName("duty-support") + .setDescription("Filter by Duty Support availability.") + ) + + .addBooleanOption(option => + option + .setName("trusts") + .setDescription("Filter by Trust availability.") + ) + ) + + .addSubcommand(subcommand => + subcommand + .setName("class") + .setDescription("Select a random class or job.") + + .addStringOption(option => + option + .setName("role") + .setDescription("Only select a specific role.") + .addChoices(...roles) + ) + + .addStringOption(option => + option + .setName("role-type") + .setDescription("Only select a specific role subtype.") + .addChoices(...roleTypes) + ) + + .addStringOption(option => + option + .setName("damage-type") + .setDescription("Filter by physical or magical damage.") + .addChoices(...damageTypes) + ) + + .addIntegerOption(option => + option + .setName("expansion") + .setDescription("Only select classes/jobs from an expansion.") + .addChoices(...expansions) + ) + + .addBooleanOption(option => + option + .setName("jobstone") + .setDescription("True for jobs, false for base classes.") + ) + + .addBooleanOption(option => + option + .setName("limited") + .setDescription("Filter by limited-job status.") + ) + + .addIntegerOption(option => + option + .setName("max-starting-level") + .setDescription("Maximum starting level of the class/job.") + .setMinValue(1) + .setMaxValue(100) + ) + ), + + async execute(interaction) { + switch (interaction.options.getSubcommand()) { + case "duty": + return randomDuty(interaction); + + case "class": + return randomClass(interaction); + } + } +}; + + +async function randomDuty(interaction) { + const collection = mClient + .db("neo-db") + .collection("items_duties"); + + const level = interaction.options.getInteger("level"); + const type = interaction.options.getString("type"); + const difficulty = interaction.options.getString("difficulty"); + const expansion = interaction.options.getInteger("expansion"); + const roulette = interaction.options.getString("roulette"); + const partySize = interaction.options.getInteger("party-size"); + const dutySupport = interaction.options.getBoolean("duty-support"); + const trusts = interaction.options.getBoolean("trusts"); + + const query = { + MinimumLevel: { + $lte: level + } + }; + + if (type !== null) + query.Type = type; + + if (difficulty !== null) + query.Difficulty = difficulty; + + if (expansion !== null) + query.Expansion = expansion; + + if (roulette !== null) + query.Roulettes = roulette; + + if (partySize !== null) + query["Party.Size"] = partySize; + + if (dutySupport !== null) + query["Party.CanUseDutySupport"] = dutySupport; + + if (trusts !== null) + query["Party.CanUseTrusts"] = trusts; + + + const [duty] = await collection.aggregate([ + { + $match: query + }, + { + $sample: { + size: 1 + } + } + ]).toArray(); + + + if (!duty) { + return interaction.reply({ + content: "No duties matched those filters.", + ephemeral: true + }); + } + + + const embed = new EmbedBuilder() + .setTitle(duty.Name) + .setThumbnail(getDutyIcon(duty)) + .setDescription( + [ + `**${formatDutyType(duty.Type)}**`, + `${duty.Difficulty} Difficulty`, + `${getExpansionName(duty.Expansion)} • Patch ${formatPatch(duty)}` + ].join("\n") + ) + .addFields( + { + name: "Level", + value: `${duty.MinimumLevel}`, + inline: true + }, + { + name: "Level Sync", + value: `${duty.LevelSync}`, + inline: true + }, + { + name: "Minimum Item Level", + value: duty.MinimumItemLevel + ? `${duty.MinimumItemLevel}` + : "None", + inline: true + }, + { + name: "Party", + value: formatParty(duty.Party) + }, + { + name: "Roulettes", + value: duty.Roulettes?.length + ? duty.Roulettes.join(", ") + : "None" + }, + { + name: "NPC Support", + value: [ + `Duty Support: ${duty.Party.CanUseDutySupport ? "Yes" : "No"}`, + `Trusts: ${duty.Party.CanUseTrusts ? "Yes" : "No"}` + ].join("\n") + } + ); + + + return interaction.reply({ + embeds: [embed] + }); +} + + +async function randomClass(interaction) { + const collection = mClient + .db("neo-db") + .collection("items_classes"); + + const role = interaction.options.getString("role"); + const roleType = interaction.options.getString("role-type"); + const damageType = interaction.options.getString("damage-type"); + const expansion = interaction.options.getInteger("expansion"); + const hasJobStone = interaction.options.getBoolean("jobstone"); + const limited = interaction.options.getBoolean("limited"); + const maxStartingLevel = + interaction.options.getInteger("max-starting-level"); + + const query = {}; + + if (role !== null) + query.Role = role; + + if (roleType !== null) + query.RoleType = roleType; + + if (damageType !== null) + query.DamageType = damageType; + + if (expansion !== null) + query.Expansion = expansion; + + if (hasJobStone !== null) + query.HasJobStone = hasJobStone; + + if (limited !== null) + query.Limited = limited; + + if (maxStartingLevel !== null) { + query.StartingLevel = { + $lte: maxStartingLevel + }; + } + + + const [job] = await collection.aggregate([ + { + $match: query + }, + { + $sample: { + size: 1 + } + } + ]).toArray(); + + + if (!job) { + return interaction.reply({ + content: "No classes or jobs matched those filters.", + ephemeral: true + }); + } + + const icon = new AttachmentBuilder( + getJobIcon(job), + { + name: `${job.Abbreviation}.png` + } + ); + + const embed = new EmbedBuilder() + .setTitle(`${job.Name} (${job.Abbreviation})`) + .setThumbnail(`attachment://${job.Abbreviation}.png`) + .setDescription( + [ + `**${job.Role} • ${formatRoleType(job.RoleType)}**`, + `${job.DamageType} Damage`, + `${getExpansionName(job.Expansion)} • Patch ${formatPatch(job)}` + ].join("\n") + ) + .addFields( + { + name: "Starting Level", + value: `${job.StartingLevel}`, + inline: true + }, + { + name: "Job Stone", + value: job.HasJobStone ? "Yes" : "No", + inline: true + }, + { + name: "Limited Job", + value: job.Limited ? "Yes" : "No", + inline: true + }, + { + name: "Base Class", + value: job.Class ?? "None", + inline: true + } + ); + + + return interaction.reply({ + embeds: [embed], + files: [icon] + }); +} + + +function getDutyIcon(duty) { + return dutyDifficultyIcons[duty.Difficulty] + ?? dutyIcons[duty.Type] + ?? null; +} + + +function getExpansionName(expansion) { + return expansions.find(entry => + entry.value === expansion + )?.name ?? `Expansion ${expansion}`; +} + + +function formatPatch(item) { + if (item.MinorPatch) { + return `${item.Expansion}.${item.MajorPatch}${item.MinorPatch}`; + } + + return `${item.Expansion}.${item.MajorPatch}`; +} + + +function formatDutyType(type) { + switch (type) { + case "AllianceRaid": + return "Alliance Raid"; + + case "VariantDungeon": + return "Variant Dungeon"; + + default: + return type; + } +} + + +function formatRoleType(roleType) { + switch (roleType) { + case "PhysicalRanged": + return "Physical Ranged"; + + case "MagicalRanged": + return "Magical Ranged"; + + case "Pure": + return "Pure Healer"; + + case "Shield": + return "Shield Healer"; + + default: + return roleType; + } +} + + +function formatParty(party) { + const roles = []; + + if (party.Tanks > 0) { + roles.push( + `${party.Tanks} Tank${party.Tanks !== 1 ? "s" : ""}` + ); + } + + if (party.Healers > 0) { + roles.push( + `${party.Healers} Healer${party.Healers !== 1 ? "s" : ""}` + ); + } + + if (party.DPS > 0) { + roles.push(`${party.DPS} DPS`); + } + + if (!roles.length) { + return `${party.Size} Player${party.Size !== 1 ? "s" : ""}`; + } + + return `${party.Size} Players\n${roles.join(" • ")}`; +} \ No newline at end of file