Files
Arthonor-Neo/commands/slash/applications/wge.js
T
2026-07-02 18:37:22 +02:00

576 lines
16 KiB
JavaScript

const { ModalBuilder, UserSelectMenuBuilder, TextInputBuilder, EmbedBuilder, TimestampStyles, time, StringSelectMenuBuilder, messageLink } = require("@discordjs/builders");
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require('discord.js')
const { SlashCommandBuilder, LabelBuilder, TextInputStyle, MessageFlags } = require("discord.js");
const { mClient } = require("../../..");
const db = mClient.db('neo-db')
require('dotenv').config()
function getRemaining(timer) {
if (timer.state !== 'running') {
return timer.timeRemaining;
}
return Math.max(
0,
timer.timeRemaining - (Date.now() - timer.startedAt)
);
}
function ordinal(n) {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
async function wgeStatus(interaction) {
const wgeColl = db.collection('timer_wge')
const historyColl = db.collection('history_wge');
const results = await wgeColl.findOne({
$and: [
{ debug: false },
{ state: 'running' }
]
})
if (!results) {
return interaction.editReply({
content: 'No WGE Session currently running!',
flags: MessageFlags.Ephemeral
})
}
const historyCount = await historyColl.countDocuments();
const iteration = historyCount + 1;
const createdAt = new Date(results.createdAt)
const expiresAt = new Date(createdAt.getTime() + Number(results.timeRemaining))
const embed = new EmbedBuilder()
.setTitle(`✨ [${ordinal(iteration)}] World's Greatest Expert ✨`)
.setDescription(
`<@${results.assignedUser}> is currently the **World's Greatest Expert** in **${results.topic}**.\n\n` +
`They must submit their entry <t:${Math.floor(expiresAt.getTime() / 1000)}:R>.\n\n` +
`Thank you, <@${results.assigningUser}>, for the nomination!`
)
return interaction.editReply({
embeds: [embed]
})
}
async function wgeStart(interaction) {
const defaultUsers = []
defaultUsers.push(process.env.D_ID)
const participantColl = db.collection('items_wge')
const participants = await participantColl.find({
status: true
}).toArray()
const wgeBaseModal = new ModalBuilder()
.setCustomId('wgeBaseModal')
.setTitle("World's Greatest Expert")
const options = [
{
label: 'RANDOM',
value: process.env.D_ID
}
];
const participantOptions = await Promise.all(
participants.map(async (participant) => {
try {
const member = await interaction.guild.members.fetch(participant.userID);
return {
label: `${member.displayName} (${member.user.username})`,
value: participant.userID
};
} catch {
return null;
}
})
);
options.push(
...participantOptions.filter(option => option !== null)
);
// Using StringSelectMenuBuilder because UserSelectMenuBuilder cannot be filtered for now
const wgeUserCustInput = new StringSelectMenuBuilder()
.setCustomId('wgeCustUserInput')
.setMaxValues(1)
.setMinValues(1)
.setRequired(true)
.addOptions(options)
const wgeTopicInput = new TextInputBuilder()
.setCustomId('wgeTopicInput')
.setStyle(TextInputStyle.Short)
.setRequired(true)
const wgeTopicLabel = new LabelBuilder()
.setLabel("And WHAT are they an expert in?")
.setDescription('Tell me!')
.setTextInputComponent(wgeTopicInput)
const wgeCustUserLabel = new LabelBuilder()
.setLabel('WHO is your greatest expert?')
.setDescription('Select them')
.setStringSelectMenuComponent(wgeUserCustInput)
wgeBaseModal
.addLabelComponents(wgeCustUserLabel, wgeTopicLabel)
await interaction.showModal(wgeBaseModal)
}
async function wgeStop(interaction) {
;
const wgeColl = db.collection('timer_wge');
const timer = await wgeColl.findOne({
debug: false
});
if (!timer) {
return interaction.reply({
content: 'Timer not found.',
flags: MessageFlags.Ephemeral
});
}
// freeze remaining time
const remaining = getRemaining(timer);
await wgeColl.updateOne(
{ _id: timer._id },
{
$set: {
timeRemaining: remaining,
startedAt: null,
state: 'stopped'
}
}
);
await interaction.reply({
content: `🛑 Timer stopped for **${timer.topic}**`
});
}
async function wgeResume(interaction) {
;
const wgeColl = db.collection('timer_wge');
const timer = await wgeColl.findOne({
debug: false
});
if (!timer) {
return interaction.reply({
content: 'Timer not found.',
flags: MessageFlags.Ephemeral
});
}
// freeze remaining time
const remaining = getRemaining(timer);
await wgeColl.updateOne(
{ _id: timer._id },
{
$set: {
timeRemaining: remaining,
startedAt: Date.now(),
state: 'running'
}
}
);
await interaction.reply({
content: `🟢 Timer resumed for **${timer.topic}**`
});
}
async function wgeForfeit(interaction) {
;
const wgeColl = db.collection('timer_wge');
const participateColl = db.collection('items_wge');
// Get active WGE
const wge = await wgeColl.findOne({
debug: false,
state: 'running'
});
if (!wge) {
return interaction.reply({
content: 'No active WGE is currently running.',
flags: MessageFlags.Ephemeral
});
}
// Sanity check: only assigned user can forfeit
if (wge.assignedUser !== interaction.user.id) {
return interaction.reply({
content: `Only <@${wge.assignedUser}> can forfeit the current WGE.`,
flags: MessageFlags.Ephemeral
});
}
// Pick a new participant, excluding current assignee
const [randomParticipant] = await participateColl.aggregate([
{
$match: {
status: true,
userID: { $ne: interaction.user.id }
}
},
{ $sample: { size: 1 } }
]).toArray();
if (!randomParticipant) {
return interaction.reply({
content: 'No one else is participating!',
flags: MessageFlags.Ephemeral
});
}
const member = await interaction.guild.members.fetch(
randomParticipant.userID
);
const nextUser = member.user;
const WGE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
await wgeColl.deleteOne({ _id: wge._id });
await wgeColl.insertOne({
assignedUser: nextUser.id,
assigningUser: wge.assigningUser,
startedAt: Date.now(),
timeRemaining: WGE_DURATION,
state: 'running',
notifications: {
half: false,
day24: false,
complete: false
},
topic: wge.topic,
debug: false,
createdAt: new Date()
});
return interaction.reply({
content: `${interaction.user} has forfeited! <@${nextUser.id}> is the next expert! Their topic is **${wge.topic}**`
});
}
async function wgeParticipate(interaction) {
const status = interaction.options.getBoolean('status')
const user = interaction.user
const coll = db.collection('items_wge')
// Choice was false, ergo not participating
await coll.findOneAndUpdate({
userID: user.id
}, {
$set: {
userID: user.id,
status: status
}
}, {
upsert: true
})
interaction.reply({
content: `You are ${status ? '' : 'no longer '}participating!`,
flags: MessageFlags.Ephemeral
})
}
async function listParticipants(interaction) {
;
const participantColl = db.collection('items_wge');
const participants = await participantColl.find({}).toArray();
const participating = participants
.filter(p => p.status)
.map(p => `<@${p.userID}>`);
const notParticipating = participants
.filter(p => !p.status)
.map(p => `<@${p.userID}>`);
const embed = new EmbedBuilder()
.setTitle('WGE Participation')
.setColor(0x5865F2)
.addFields(
{
name: `Participating (${participating.length})`,
value: participating.join('\n') || 'No participants found.',
inline: true
},
{
name: `Not Participating (${notParticipating.length})`,
value: notParticipating.join('\n') || 'No entries found.',
inline: true
}
)
.setFooter({
text: `Total entries: ${participants.length}`
})
await interaction.reply({
embeds: [embed],
flags: MessageFlags.Ephemeral
});
}
function formatDuration(ms) {
if (ms == null) return "unknown"
const totalSeconds = Math.floor(ms / 1000)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days) parts.push(`${days}d`)
if (hours) parts.push(`${hours}h`)
if (minutes) parts.push(`${minutes}m`)
if (seconds && !days) parts.push(`${seconds}s`) // only show seconds for short durations
return parts.length ? parts.join(" ") : "0s"
}
async function listHistory(interaction) {
const historyColl = db.collection('history_wge')
const wgeHistory = await historyColl
.find({ debug: false })
.sort({ createdAt: -1 })
.toArray()
if (!wgeHistory.length) {
return interaction.reply({
content: "No WGE history found.",
flags: MessageFlags.Ephemeral
})
}
const buildEmbed = (entry, index) => {
const createdTs = entry.createdAt
? Math.floor(new Date(entry.createdAt).getTime() / 1000)
: null
return new EmbedBuilder()
.setTitle(`WGE History (${index + 1}/${wgeHistory.length})`)
.setDescription(
`<@${entry.assignedUser}> was the **World's Greatest Expert** of...\n\n` +
`**${entry.topic}**\n\n` +
`Assigned by: <@${entry.assigningUser}>\n` +
(createdTs ? `Created: <t:${createdTs}:R>` : "")
)
.setColor(0x2b2d31)
}
let page = 0
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('prev')
.setLabel('◀')
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId('next')
.setLabel('▶')
.setStyle(ButtonStyle.Secondary)
.setDisabled(wgeHistory.length <= 1)
)
const msg = await interaction.reply({
embeds: [buildEmbed(wgeHistory[page], page)],
components: [row],
fetchReply: true
})
const collector = msg.createMessageComponentCollector({
componentType: ComponentType.Button,
time: 120000
})
collector.on('collect', async i => {
if (i.user.id !== interaction.user.id) {
return i.reply({
content: "You can't control this pagination.",
flags: MessageFlags.Ephemeral
})
}
if (i.customId === 'prev') {
page--
} else if (i.customId === 'next') {
page++
}
page = Math.max(0, Math.min(page, wgeHistory.length - 1))
row.components[0].setDisabled(page === 0)
row.components[1].setDisabled(page === wgeHistory.length - 1)
await i.update({
embeds: [buildEmbed(wgeHistory[page], page)],
components: [row]
})
})
collector.on('end', async () => {
const disabledRow = new ActionRowBuilder().addComponents(
row.components.map(btn => ButtonBuilder.from(btn).setDisabled(true))
)
await msg.edit({ components: [disabledRow] }).catch(() => { })
})
}
async function wgeList(interaction) {
const thing = interaction.options.getString('thing');
switch (thing) {
case 'participants': {
listParticipants(interaction)
break;
}
case 'history':
listHistory(interaction)
break;
default:
break;
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName('wge')
.setDescription('welcome to worlds greatest experts')
// Show Status of Current Running WGE Session
.addSubcommand(s =>
s
.setName('status')
.setDescription('display current wge session')
)
// Assign Someone to be the next Expert
.addSubcommand(s =>
s
.setName('start')
.setDescription('start a new wge session')
)
// Stop the current Session Timer
.addSubcommand(s =>
s
.setName('stop')
.setDescription('stop the current wge session')
)
// Resume the current Session Timer
.addSubcommand(s =>
s
.setName('resume')
.setDescription('resume the current wge session timer')
)
// Forfeit your current expert topic and assign someone else
.addSubcommand(s =>
s
.setName('forfeit')
.setDescription('forfeit your current topic')
)
// List the previous WGE Sessions
.addSubcommand(s =>
s
.setName('history')
.setDescription('display the previous wge sessions')
)
// List the previous WGE Sessions
.addSubcommand(s =>
s
.setName('participate')
.setDescription('do you want to play a game?')
.addBooleanOption(o =>
o.setName('status')
.setDescription('choose')
.setRequired(true)
)
)
// List the previous WGE Sessions
.addSubcommand(s =>
s
.setName('list')
.setDescription('list things')
.addStringOption(o =>
o.setName('thing')
.setDescription('what would you like to be listed')
.addChoices(
{ name: 'participants', value: 'participants' },
{ name: 'history', value: 'history' }
)
)
)
,
async execute(interaction) {
switch (interaction.options._subcommand) {
case 'status':
await interaction.deferReply()
wgeStatus(interaction)
break;
case 'start':
wgeStart(interaction)
break;
case 'stop':
wgeStop(interaction)
break;
case 'pause':
wgePause(interaction)
break;
case 'resume':
wgeResume(interaction)
break;
case 'forfeit':
wgeForfeit(interaction)
break;
case 'history':
wgeHistory(interaction)
break;
case 'participate':
wgeParticipate(interaction)
break;
case 'list':
wgeList(interaction)
default:
break;
}
}
}