diff --git a/src/commands/moderation/giveWin.ts b/src/commands/moderation/giveWin.ts index a2452d57..0fb4e592 100644 --- a/src/commands/moderation/giveWin.ts +++ b/src/commands/moderation/giveWin.ts @@ -81,9 +81,22 @@ export default { const usersIdInMatch = await getUsersInMatch(matchId) const users = await Promise.all( usersIdInMatch.map(async (userId) => { - const user = await interaction.client.users.fetch(userId) + // Try to get member first for displayName, fallback to user + let displayName: string + try { + const member = await interaction.guild?.members.fetch(userId) + displayName = member?.displayName ?? '' + } catch { + try { + const user = await interaction.client.users.fetch(userId) + displayName = user.username + } catch { + displayName = 'name not found' + } + } + return { - name: user.username, + name: displayName, value: userId, } }), diff --git a/src/commands/other/randomStake.ts b/src/commands/other/randomStake.ts index d38e343a..44cd151c 100644 --- a/src/commands/other/randomStake.ts +++ b/src/commands/other/randomStake.ts @@ -3,33 +3,28 @@ import { getRandomStake } from '../../utils/matchHelpers' import { getMatchData, getMatchIdFromChannel, - getStakeList, setPickedMatchStake, } from '../../utils/queryDB' export default { async execute(interaction: ChatInputCommandInteraction) { try { + const customStake = + interaction.options.getString('custom-stake', false) ?? false + const custom = customStake == 'yes' const matchId = await getMatchIdFromChannel(interaction.channelId) + const stakeChoice = await getRandomStake(custom) if (matchId) { // In a match channel - const stakeList = await getStakeList() - const randomStake = - stakeList[Math.floor(Math.random() * stakeList.length)] const matchData = await getMatchData(matchId) if (!matchData.stake_vote_ended) - await setPickedMatchStake(matchId, randomStake.stake_name) - - const stakeStr = `${randomStake.stake_emote} ${randomStake.stake_name}` - await interaction.reply({ content: stakeStr }) - } else { - // Not in a match channel - use normal logic - const stakeChoice = await getRandomStake() - const stakeStr = `${stakeChoice.stake_emote} ${stakeChoice.stake_name}` - await interaction.reply({ content: stakeStr }) + await setPickedMatchStake(matchId, stakeChoice.stake_name) } + + const stakeStr = `${stakeChoice.stake_emote} ${stakeChoice.stake_name}` + await interaction.reply({ content: stakeStr }) } catch (err: any) { console.error(err) const errorMsg = err.detail || err.message || 'Unknown' diff --git a/src/commands/queues/viewStats.ts b/src/commands/queues/statsQueue.ts similarity index 82% rename from src/commands/queues/viewStats.ts rename to src/commands/queues/statsQueue.ts index 6993fa31..9af6d076 100644 --- a/src/commands/queues/viewStats.ts +++ b/src/commands/queues/statsQueue.ts @@ -1,7 +1,10 @@ import { ChatInputCommandInteraction, MessageFlags } from 'discord.js' import { drawPlayerStatsCanvas } from '../../utils/canvasHelpers' import { getQueueIdFromName, getStatsCanvasUserData } from '../../utils/queryDB' -import { setupViewStatsButtons } from '../../utils/queueHelpers' +import { + setupViewStatsButtons, + setUserQueueRole, +} from '../../utils/queueHelpers' export default { async execute(interaction: ChatInputCommandInteraction) { @@ -19,6 +22,10 @@ export default { files: [statFile], components: [viewStatsButtons], }) + + // Update queue role, just to be sure it's correct when they check + // This is a nice bandaid fix till we update leaderboard roles better + await setUserQueueRole(queueId, targetUser.id) } catch (err: any) { console.error(err) const errorMsg = err.detail || err.message || 'Unknown' diff --git a/src/commands/superCommands/random.ts b/src/commands/superCommands/random.ts index f4ba273c..cb901a23 100644 --- a/src/commands/superCommands/random.ts +++ b/src/commands/superCommands/random.ts @@ -11,7 +11,16 @@ export default { sub.setName('deck').setDescription('Roll a random deck'), ) .addSubcommand((sub) => - sub.setName('stake').setDescription('Roll a random stake'), + sub + .setName('stake') + .setDescription('Roll a random stake') + .addStringOption((option) => + option + .setName('custom-stake') + .setDescription('Whether to include custom stakes or not') + .addChoices([{ name: 'yes', value: 'yes' }]) + .setRequired(false), + ), ), async execute(interaction: ChatInputCommandInteraction) { diff --git a/src/commands/superCommands/stats.ts b/src/commands/superCommands/stats.ts index b2be1a36..10ecb5c3 100644 --- a/src/commands/superCommands/stats.ts +++ b/src/commands/superCommands/stats.ts @@ -1,4 +1,4 @@ -import viewStats from '../queues/viewStats' +import viewStats from '../queues/statsQueue' import { AutocompleteInteraction, ChatInputCommandInteraction, diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 69660ade..ff4b9d78 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -877,6 +877,20 @@ export default { console.error('Failed to fetch original message embed:', err) } + // Add delete button for helpers + const deleteButtonRow = + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`delete-contest-${matchId}`) + .setLabel('Delete Contest Channel') + .setStyle(ButtonStyle.Danger), + ) + + await contestChannel.send({ + content: 'Helpers can delete this contest channel when resolved:', + components: [deleteButtonRow], + }) + await interaction.update({ content: `Contest channel created! Please go here to contest this matchup with the staff: ${contestChannel}`, components: [], @@ -888,6 +902,40 @@ export default { await interaction.deleteReply() } + if (interaction.customId.startsWith('delete-contest-')) { + const matchId = parseInt(interaction.customId.split('-')[2]) + const botSettings = await getSettings() + const member = interaction.member as GuildMember + + // Check if user is a helper + if ( + member && + (member.roles.cache.has(botSettings.helper_role_id) || + member.roles.cache.has(botSettings.queue_helper_role_id)) + ) { + // User is a helper, delete the channel + await interaction.reply({ + content: 'Deleting contest channel...', + flags: MessageFlags.Ephemeral, + }) + + try { + await interaction.channel?.delete() + } catch (err) { + console.error( + `Failed to delete contest channel for match ${matchId}:`, + err, + ) + } + } else { + // User is not a helper + await interaction.reply({ + content: 'Only helpers can delete contest channels.', + flags: MessageFlags.Ephemeral, + }) + } + } + if (interaction.customId.startsWith('rematch-')) { const matchId = parseInt(interaction.customId.split('-')[1]) const matchData = await getMatchData(matchId) diff --git a/src/utils/queryDB.ts b/src/utils/queryDB.ts index 931d5ede..206f4edc 100644 --- a/src/utils/queryDB.ts +++ b/src/utils/queryDB.ts @@ -385,20 +385,22 @@ export async function getLeaderboardPosition( queueId: number, userId: string, ): Promise { - const playersRes = await pool.query( + const result = await pool.query( ` - SELECT user_id - FROM queue_users - WHERE queue_id = $1 - ORDER BY elo DESC + SELECT rank + FROM ( + SELECT user_id, ROW_NUMBER() OVER (ORDER BY elo DESC) as rank + FROM queue_users + WHERE queue_id = $1 + ) ranked + WHERE user_id = $2 `, - [queueId], + [queueId, userId], ) - if (playersRes.rowCount === 0) return null + if (result.rowCount === 0) return null - const players: { user_id: string }[] = playersRes.rows - return players.findIndex((p) => p.user_id === userId) + 1 + return result.rows[0].rank } export async function getLeaderboardQueueRole( @@ -419,8 +421,6 @@ export async function getLeaderboardQueueRole( [queueId, rank], ) - console.log(roleRes.rowCount) - if (roleRes.rowCount === 0) return null return roleRes.rows[0] } @@ -1513,11 +1513,27 @@ export async function getStatsCanvasUserData( date: r.date as Date, })) + // Get queue default_elo for initial data point + const queueSettings = await getQueueSettings(queueId) + const defaultElo = queueSettings.default_elo + const totalChange = eloChanges.reduce((sum, r) => sum + r.change, 0) let running = (p.elo ?? 0) - totalChange - const elo_graph_data = eloChanges.map((r) => { + + const elo_graph_data: { date: Date; rating: number }[] = [] + + // Add initial starting point if there are any matches + if (eloChanges.length > 0) { + const firstMatchDate = new Date(eloChanges[0].date) + const startDate = new Date(firstMatchDate.getTime() - 1000) // 1 second before + elo_graph_data.push({ date: startDate, rating: defaultElo }) + } + + // Add all match data points + eloChanges.forEach((r) => { running += r.change - return { date: r.date, rating: running } + const clampedRating = Math.max(0, running) // Makes sure it stays within the range of 0-9999 + elo_graph_data.push({ date: r.date, rating: clampedRating }) }) // Calculate percentiles for each stat using CTEs diff --git a/src/utils/queueHelpers.ts b/src/utils/queueHelpers.ts index 53004822..7e709d48 100644 --- a/src/utils/queueHelpers.ts +++ b/src/utils/queueHelpers.ts @@ -585,53 +585,65 @@ export async function setUserQueueRole( ): Promise { console.log(`setting queue role for user ${userId} in queue ${queueId}`) const currentRole = await getUserQueueRole(queueId, userId) - const leaderboardRole = await getLeaderboardQueueRole(queueId, userId) const allQueueRoles = await getAllQueueRoles(queueId, false) + const leaderboardRole = await getLeaderboardQueueRole(queueId, userId) const guild = await getGuild() const member = await guild.members.fetch(userId) - // Remove all MMR-based roles (where mmr_threshold is not null) + // Get current member roles + const currentRoleIds = new Set(member.roles.cache.keys()) + + // Determine which queue roles should be added/removed const mmrRoles = allQueueRoles.filter((role) => role.mmr_threshold !== null) - for (const role of mmrRoles) { - try { - await member.roles.remove(role.role_id) - } catch (err) { - console.error(`Failed to remove MMR role ${role.role_id}:`, err) + const leaderboardRoles = allQueueRoles.filter( + (role) => role.leaderboard_min !== null, + ) + + const rolesToRemove: string[] = [] + const rolesToAdd: string[] = [] + + // Check leaderboard roles + for (const role of leaderboardRoles) { + const expectedRole = + leaderboardRole && role.role_id === leaderboardRole.role_id + + if (expectedRole && !currentRoleIds.has(role.role_id)) { + rolesToAdd.push(role.role_id) + } else if (!expectedRole && currentRoleIds.has(role.role_id)) { + rolesToRemove.push(role.role_id) } } - // Add the current MMR-based role if one exists - if (currentRole) { - try { - await member.roles.add(currentRole.role_id) - } catch (err) { - console.error(`Failed to add MMR role ${currentRole.role_id}:`, err) + // Check MMR roles + for (const role of mmrRoles) { + const expectedRole = currentRole && role.role_id === currentRole.role_id + + // If the user has the expected role, check if they are within the MMR range + if (expectedRole && !currentRoleIds.has(role.role_id)) { + rolesToAdd.push(role.role_id) + } else if (!expectedRole && currentRoleIds.has(role.role_id)) { + rolesToRemove.push(role.role_id) } } - // Remove all leaderboard roles (where leaderboard_min is not null) - const leaderboardRoles = allQueueRoles.filter( - (role) => role.leaderboard_min !== null, - ) - for (const role of leaderboardRoles) { - try { - await member.roles.remove(role.role_id) - } catch (err) { - console.error(`Failed to remove leaderboard role ${role.role_id}:`, err) - } + // Do nothing if no role changes are needed + if (rolesToRemove.length === 0 && rolesToAdd.length === 0) { + console.log(`No role changes needed for user ${userId}`) + return } - // Add the current leaderboard role if one exists - if (leaderboardRole) { - try { - await member.roles.add(leaderboardRole.role_id) - } catch (err) { - console.error( - `Failed to add leaderboard role ${leaderboardRole.role_id}:`, - err, - ) + try { + if (rolesToRemove.length > 0) { + await member.roles.remove(rolesToRemove) + console.log(`Removed ${rolesToRemove.length} roles from user ${userId}`) + } + if (rolesToAdd.length > 0) { + await member.roles.add(rolesToAdd) + console.log(`Added ${rolesToAdd.length} roles to user ${userId}`) } + } catch (err) { + console.error(`Failed to update roles for user ${userId}:`, err) } }