diff --git a/config/globals.js b/config/globals.js index 55b4ca2..1662672 100644 --- a/config/globals.js +++ b/config/globals.js @@ -102,182 +102,212 @@ module.exports = { { "id": 133, "name": "Athletics", - primaryColor: "#003831", - secondaryColor: "#EFB21E" + "primaryColor": "#003831", + "secondaryColor": "#EFB21E", + "abbreviation": "OAK" }, { "id": 134, "name": "Pirates", - primaryColor: "#FDB827", - secondaryColor: "#27251F" + "primaryColor": "#FDB827", + "secondaryColor": "#27251F", + "abbreviation": "PIT" }, { "id": 135, "name": "Padres", - primaryColor: "#2F241D", - secondaryColor: "#FFC425" + "primaryColor": "#2F241D", + "secondaryColor": "#FFC425", + "abbreviation": "SD" }, { "id": 136, "name": "Mariners", - primaryColor: "#005C5C", - secondaryColor: "#0C2C56" + "primaryColor": "#005C5C", + "secondaryColor": "#0C2C56", + "abbreviation": "SEA" }, { "id": 137, "name": "Giants", - primaryColor: "#FD5A1E", - secondaryColor: "#27251F" + "primaryColor": "#FD5A1E", + "secondaryColor": "#27251F", + "abbreviation": "SF" }, { "id": 138, "name": "Cardinals", - primaryColor: "#C41E3A", - secondaryColor: "#0C2340" + "primaryColor": "#C41E3A", + "secondaryColor": "#0C2340", + "abbreviation": "STL" }, { "id": 139, "name": "Rays", - primaryColor: "#8FBCE6", - secondaryColor: "#092C5C" + "primaryColor": "#8FBCE6", + "secondaryColor": "#092C5C", + "abbreviation": "TB" }, { "id": 140, "name": "Rangers", - primaryColor: "#003278", - secondaryColor: "#C0111F" + "primaryColor": "#003278", + "secondaryColor": "#C0111F", + "abbreviation": "TEX" }, { "id": 141, "name": "Blue Jays", - primaryColor: "#134A8E", - secondaryColor: "#1D2D5C" + "primaryColor": "#134A8E", + "secondaryColor": "#1D2D5C", + "abbreviation": "TOR" }, { "id": 142, "name": "Twins", - primaryColor: "#002B5C", - secondaryColor: "#D31145" + "primaryColor": "#002B5C", + "secondaryColor": "#D31145", + "abbreviation": "MIN" }, { "id": 143, "name": "Phillies", - primaryColor: "#E81828", - secondaryColor: "#002D72" + "primaryColor": "#E81828", + "secondaryColor": "#002D72", + "abbreviation": "PHI" }, { "id": 144, "name": "Braves", "primaryColor": "#CE1141", - secondaryColor: "#13274F" + "secondaryColor": "#13274F", + "abbreviation": "ATL" }, { "id": 145, "name": "White Sox", - primaryColor: "#27251F", - secondaryColor: "#C4CED4" + "primaryColor": "#27251F", + "secondaryColor": "#C4CED4", + "abbreviation": "CWS" }, { "id": 146, "name": "Marlins", - primaryColor: "#00A3E0", - secondaryColor: "#EF3340", + "primaryColor": "#00A3E0", + "secondaryColor": "#EF3340", + "abbreviation": "MIA" }, { "id": 147, "name": "Yankees", - primaryColor: "#C4CED3", - secondaryColor: "#0C2340" + "primaryColor": "#C4CED3", + "secondaryColor": "#0C2340", + "abbreviation": "NYY" }, { "id": 158, "name": "Brewers", - primaryColor: "#FFC52F", - secondaryColor: "#12284B" + "primaryColor": "#FFC52F", + "secondaryColor": "#12284B", + "abbreviation": "MIL" }, { "id": 108, "name": "Angels", - primaryColor: "#BA0021", - secondaryColor: "#003263" + "primaryColor": "#BA0021", + "secondaryColor": "#003263", + "abbreviation": "LAA" }, { "id": 109, "name": "D-backs", "primaryColor": "#A71930", - "secondaryColor": "#E3D4AD" + "secondaryColor": "#E3D4AD", + "abbreviation": "AZ" }, { "id": 110, "name": "Orioles", - primaryColor: "#DF4601", - secondaryColor: "#000000" + "primaryColor": "#DF4601", + "secondaryColor": "#000000", + "abbreviation": "BAL" }, { "id": 111, "name": "Red Sox", - primaryColor: "#BD3039", - secondaryColor: "#0C2340" + "primaryColor": "#BD3039", + "secondaryColor": "#0C2340", + "abbreviation": "BOS" }, { "id": 112, "name": "Cubs", - primaryColor: "#0E3386", - secondaryColor: "#CC3433" + "primaryColor": "#0E3386", + "secondaryColor": "#CC3433", + "abbreviation": "CHC" }, { "id": 113, "name": "Reds", - primaryColor: "#C6011F", - secondaryColor: "#000000" + "primaryColor": "#C6011F", + "secondaryColor": "#000000", + "abbreviation": "CIN" }, { "id": 114, "name": "Guardians", - primaryColor: "#E50022", - secondaryColor: "#00385D" + "primaryColor": "#E50022", + "secondaryColor": "#00385D", + "abbreviation": "CLE" }, { "id": 115, "name": "Rockies", - primaryColor: "#333366", - secondaryColor: "#C4CED4" + "primaryColor": "#333366", + "secondaryColor": "#C4CED4", + "abbreviation": "COL" }, { "id": 116, "name": "Tigers", - primaryColor: "#0C2340", - secondaryColor: "#FA4616" + "primaryColor": "#0C2340", + "secondaryColor": "#FA4616", + "abbreviation": "DET" }, { "id": 117, "name": "Astros", - primaryColor: "#002D62", - secondaryColor: "#EB6E1F" + "primaryColor": "#002D62", + "secondaryColor": "#EB6E1F", + "abbreviation": "HOU" }, { "id": 118, "name": "Royals", - primaryColor: "#004687", - secondaryColor: "#BD9B60" + "primaryColor": "#004687", + "secondaryColor": "#BD9B60", + "abbreviation": "KC" }, { "id": 119, "name": "Dodgers", - primaryColor: "#005A9C", - secondaryColor: "#EF3E42" + "primaryColor": "#005A9C", + "secondaryColor": "#EF3E42", + "abbreviation": "LAD" }, { "id": 120, "name": "Nationals", - primaryColor: "#AB0003", - secondaryColor: "#14225A" + "primaryColor": "#AB0003", + "secondaryColor": "#14225A", + "abbreviation": "WSH" }, { "id": 121, "name": "Mets", - primaryColor: "#002D72", - secondaryColor: "#FF5910" + "primaryColor": "#002D72", + "secondaryColor": "#FF5910", + "abbreviation": "NYM" } ], HELP_MESSAGE: '`/starters` - examine the starting pitching matchup for the upcoming game.\n' diff --git a/modules/command-util.js b/modules/command-util.js index f01b735..ff4262d 100644 --- a/modules/command-util.js +++ b/modules/command-util.js @@ -562,6 +562,29 @@ module.exports = { } }, + resolvePlayerSelection: async (players, interaction) => { + const buttons = players.map(player => + new ButtonBuilder() + .setCustomId(player.id.toString()) + .setLabel(`${player.fullName} (${globals.TEAMS.find(team => team.id === player.currentTeam.id)?.abbreviation})`) + .setStyle(ButtonStyle.Primary) + ); + const response = await interaction.followUp({ + content: 'I found multiple matches. Which one?', + components: [new ActionRowBuilder().addComponents(buttons)] + }); + const collectorFilter = i => i.user.id === interaction.user.id; + try { + LOGGER.trace('awaiting'); + return await response.awaitMessageComponent({ filter: collectorFilter, time: 20_000 }); + } catch (e) { + await interaction.editReply({ + content: 'Player selection not received within 20 seconds - request was canceled.', + components: [] + }); + } + }, + giveFinalCommandResponse: async (toHandle, options) => { await (globalCache.values.game.isDoubleHeader ? toHandle.update(options) @@ -671,28 +694,32 @@ module.exports = { liveFeed.gameData.teams.home.abbreviation + ' ' + homeScore + '**'); }, - getClosestPlayer: async (playerName, type) => { + getClosestPlayers: async (playerName, type) => { const startTime = performance.now(); const allPlayers = await mlbAPIUtil.players(); const removeDiacritics = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); const normalizedPlayerName = removeDiacritics(playerName.toLowerCase()); - let matchingPlayer; + let matchingPlayers = []; let smallestDistance = Infinity; allPlayers.people.forEach(p => { const currentName = removeDiacritics(`${p.fullName}`.toLowerCase()); const distance = levenshtein(currentName, normalizedPlayerName); - if (distance < smallestDistance + if (distance <= smallestDistance && ( (type === 'Pitcher' && p.primaryPosition.name === 'Pitcher') || (type === 'Batter' && p.primaryPosition.name !== 'Pitcher') )) { - matchingPlayer = p; - smallestDistance = distance; + if (distance < smallestDistance) { + matchingPlayers = [p]; + smallestDistance = distance; + } else { + matchingPlayers.push(p); + } } }); const endTime = performance.now(); LOGGER.trace(`Savant command - getting closest player took ${endTime - startTime} milliseconds.`); - return matchingPlayer; + return matchingPlayers; }, getPitcherEmbed: (pitcher, pitcherInfo, isLiveGame, description, savantMode = false) => { @@ -791,32 +818,39 @@ module.exports = { } }, - getBatterFromUserInputOrLiveFeed: async (playerName) => { - let batter, currentLiveFeed; + getPlayerFromUserInputOrLiveFeed: async (playerName, interaction, type) => { + let player, currentLiveFeed, shouldEditReply, pendingChoice; if (playerName) { - batter = await module.exports.getClosestPlayer(playerName, 'Batter'); - } else { - currentLiveFeed = globalCache.values.game.currentLiveFeed; - if (currentLiveFeed && currentLiveFeed.gameData.status.abstractGameState === 'Live') { - batter = currentLiveFeed.liveData.plays.currentPlay.matchup.batter; + const players = await module.exports.getClosestPlayers(playerName, type); + if (players.length > 1) { + pendingChoice = await module.exports.resolvePlayerSelection(players.slice(0, 5), interaction); + const idString = pendingChoice?.customId; + if (idString) { + player = players.find(player => player.id === parseInt(idString)); + await pendingChoice.deferUpdate(); // This function says it takes reply options, but it doesn't work?? So I've resorted to editing on the next line. + await interaction.editReply({ + content: `Getting stats for ${player.fullName} (${globals.TEAMS.find(team => team.id === player.currentTeam.id)?.abbreviation}). Please wait...`, + components: [] + }); + shouldEditReply = true; + } + } else { + player = players[0]; } - } - - return batter; - }, - - getPitcherFromUserInputOrLiveFeed: async (playerName) => { - let batter, currentLiveFeed; - if (playerName) { - batter = await module.exports.getClosestPlayer(playerName, 'Pitcher'); } else { currentLiveFeed = globalCache.values.game.currentLiveFeed; if (currentLiveFeed && currentLiveFeed.gameData.status.abstractGameState === 'Live') { - batter = currentLiveFeed.liveData.plays.currentPlay.matchup.pitcher; + player = type === 'Pitcher' + ? currentLiveFeed.liveData.plays.currentPlay.matchup.pitcher + : currentLiveFeed.liveData.plays.currentPlay.matchup.batter; } } - return batter; + return { + player, + pendingChoice, + shouldEditReply + }; } }; diff --git a/modules/interaction-handlers.js b/modules/interaction-handlers.js index 903c4b3..0a31b4b 100644 --- a/modules/interaction-handlers.js +++ b/modules/interaction-handlers.js @@ -380,14 +380,15 @@ module.exports = { console.info(`PITCHER command invoked by guild: ${interaction.guildId}`); await interaction.deferReply(); const playerName = interaction.options.getString('player')?.trim(); - const pitcher = await commandUtil.getPitcherFromUserInputOrLiveFeed(playerName); - if (!pitcher) { + const playerResult = await commandUtil.getPlayerFromUserInputOrLiveFeed(playerName, interaction, 'Pitcher'); + const pitcher = playerResult.player; + if (!pitcher && !playerName) { await interaction.followUp('No game is live right now!'); return; } const pitcherInfo = await commandUtil.hydrateProbable(pitcher.id); const attachment = new AttachmentBuilder(Buffer.from(pitcherInfo.spot), { name: 'spot.png' }); - await interaction.followUp({ + const replyOptions = { ephemeral: false, files: [attachment], embeds: [commandUtil.getPitcherEmbed( @@ -404,21 +405,23 @@ module.exports = { ))], components: [], content: '' - }); + }; + await (playerResult.shouldEditReply ? interaction.editReply(replyOptions) : interaction.followUp(replyOptions)); }, batterHandler: async (interaction) => { console.info(`BATTER command invoked by guild: ${interaction.guildId}`); await interaction.deferReply(); const playerName = interaction.options.getString('player')?.trim(); - const batter = await commandUtil.getBatterFromUserInputOrLiveFeed(playerName); - if (!batter) { + const playerResult = await commandUtil.getPlayerFromUserInputOrLiveFeed(playerName, interaction, 'Batter'); + const batter = playerResult.player; + if (!batter && !playerName) { await interaction.followUp('No game is live right now!'); return; } const batterInfo = await commandUtil.hydrateHitter(batter.id); const attachment = new AttachmentBuilder(Buffer.from(batterInfo.spot), { name: 'spot.png' }); - await interaction.followUp({ + const replyOptions = { ephemeral: false, files: [attachment], embeds: [commandUtil.getBatterEmbed( @@ -432,14 +435,16 @@ module.exports = { )], components: [], content: '' - }); + }; + await (playerResult.shouldEditReply ? interaction.editReply(replyOptions) : interaction.followUp(replyOptions)); }, batterSavantHandler: async (interaction) => { await interaction.deferReply(); const playerName = interaction.options.getString('player')?.trim(); - const batter = await commandUtil.getBatterFromUserInputOrLiveFeed(playerName); - if (!batter) { + const playerResult = await commandUtil.getPlayerFromUserInputOrLiveFeed(playerName, interaction, 'Batter'); + const batter = playerResult.player; + if (!batter && !playerName) { await interaction.followUp('No game is live right now!'); return; } @@ -451,13 +456,14 @@ module.exports = { statcastData.mostRecentStatcast, statcastData.metricSummaryJSON[statcastData.mostRecentMetricYear.toString()], batterInfo.spot)), { name: 'savant.png' }); - await interaction.followUp({ + const replyOptions = { ephemeral: false, files: [savantAttachment], embeds: [commandUtil.getBatterEmbed(batter, batterInfo, !playerName, null, true)], components: [], content: '' - }); + }; + await (playerResult.shouldEditReply ? interaction.editReply(replyOptions) : interaction.followUp(replyOptions)); } else { await interaction.followUp({ content: 'There was a problem fetching the savant metrics for this player.' @@ -468,8 +474,9 @@ module.exports = { pitcherSavantHandler: async (interaction) => { await interaction.deferReply(); const playerName = interaction.options.getString('player')?.trim(); - const pitcher = await commandUtil.getPitcherFromUserInputOrLiveFeed(playerName); - if (!pitcher) { + const playerResult = await commandUtil.getPlayerFromUserInputOrLiveFeed(playerName, interaction, 'Pitcher'); + const pitcher = playerResult.player; + if (!pitcher && !playerName) { await interaction.followUp('No game is live right now!'); return; } @@ -481,13 +488,14 @@ module.exports = { statcastData.mostRecentStatcast, statcastData.metricSummaryJSON[statcastData.mostRecentMetricYear.toString()], pitcherInfo.spot)), { name: 'savant.png' }); - await interaction.followUp({ + const replyOptions = { ephemeral: false, files: [savantAttachment], embeds: [commandUtil.getPitcherEmbed(pitcher, pitcherInfo, !playerName, null, true)], components: [], content: '' - }); + }; + await (playerResult.shouldEditReply ? interaction.editReply(replyOptions) : interaction.followUp(replyOptions)); } else { await interaction.followUp({ content: 'There was a problem fetching the savant metrics for this player.'