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(" • ")}`; }