From d3d511612d9cc029be1a4e0a14abb3925c334a61 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Sat, 21 Feb 2026 01:24:21 +0100 Subject: [PATCH 01/12] Add mutations command, displaying best mutation analysis based on coin per copper ratio --- src/commands/mutations.ts | 242 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 src/commands/mutations.ts diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts new file mode 100644 index 00000000..fb9542e0 --- /dev/null +++ b/src/commands/mutations.ts @@ -0,0 +1,242 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType } from 'discord.js'; +import { FetchProduct, FetchProducts, UserSettings } from '../api/elite.js'; +import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; +import { EliteEmbed } from 'classes/embeds.js'; + +export interface MutationAnalysis { + id: string; + analysisCost: number; + copper: number; +} + +export interface MutationCopperRatio { + id: string; + name: string; + buyCoinPerCopper: number; + buyOrderCoinPerCopper: number; +} + +let mutations: MutationAnalysis[] = [ + { id: "ASHWREATH", analysisCost: 10000, copper: 5 }, + { id: "CHOCONUT", analysisCost: 10000, copper: 5 }, + { id: "DUSTGRAIN", analysisCost: 10000, copper: 5 }, + { id: "GLOOMGOURD", analysisCost: 10000, copper: 5 }, + { id: "LONELILY", analysisCost: 50000, copper: 25 }, + { id: "SCOURROOT", analysisCost: 10000, copper: 5 }, + { id: "SHADEVINE", analysisCost: 10000, copper: 5 }, + { id: "VEILSHROOM", analysisCost: 10000, copper: 5 }, + { id: "WITHERBLOOM", analysisCost: 40000, copper: 20 }, + { id: "CHOCOBERRY", analysisCost: 60000, copper: 30 }, + { id: "CINDERSHADE", analysisCost: 80000, copper: 40 }, + { id: "COALROOT", analysisCost: 80000, copper: 40 }, + { id: "CREAMBLOOM", analysisCost: 60000, copper: 30 }, + { id: "DUSKBLOOM", analysisCost: 80000, copper: 40 }, + { id: "THORNSHADE", analysisCost: 80000, copper: 40 }, + { id: "BLASTBERRY", analysisCost: 240000, copper: 120 }, + { id: "CHEESEBITE", analysisCost: 80000, copper: 80 }, + { id: "CHLORONITE", analysisCost: 40000, copper: 20 }, + { id: "DO_NOT_EAT_SHROOM", analysisCost: 240000, copper: 120 }, + { id: "FLESHTRAP", analysisCost: 360000, copper: 180 }, + { id: "MAGIC_JELLYBEAN", analysisCost: 160000, copper: 80 }, + { id: "NOCTILUME", analysisCost: 300000, copper: 150 }, + { id: "SNOOZLING", analysisCost: 600000, copper: 300 }, + { id: "SOGGYBUD", analysisCost: 60000, copper: 30 }, + { id: "CHORUS_FRUIT", analysisCost: 600000, copper: 300 }, + { id: "PLANTBOY_ADVANCE", analysisCost: 700000, copper: 350 }, + { id: "PUFFERCLOUD", analysisCost: 1000000, copper: 500 }, + { id: "SHELLFRUIT", analysisCost: 500000, copper: 250 }, + { id: "STARTLEVINE", analysisCost: 500000, copper: 250 }, + { id: "STOPLIGHT_PETAL", analysisCost: 4000000, copper: 2000 }, + { id: "THUNDERLING", analysisCost: 800000, copper: 400 }, + { id: "TURTLELLINI", analysisCost: 240000, copper: 120 }, + { id: "ZOMBUD", analysisCost: 1000000, copper: 500 }, + { id: "ALL_IN_ALOE", analysisCost: 4600000, copper: 2300 }, + { id: "DEVOURER", analysisCost: 10000000, copper: 5000 }, + { id: "GLASSCORN", analysisCost: 4000000, copper: 2000 }, + { id: "GODSEED", analysisCost: 1000000, copper: 500 }, + { id: "JERRYFLOWER", analysisCost: 20000, copper: 10 }, + { id: "PHANTOMLEAF", analysisCost: 3000000, copper: 1500 }, + { id: "TIMESTALK", analysisCost: 19000000, copper: 9500 } +]; + +const command = new EliteCommand({ + name: 'mutations', + description: 'Show mutations with the best coin/copper ratios for analysis', + access: CommandAccess.Everywhere, + type: CommandType.Slash, + options: { + synthesis: { + name: 'synthesis', + description: 'Synthesis Chip Bonus (%)', + type: SlashCommandOptionType.Integer, + builder: (b) => b.setMinValue(0).setMaxValue(20), + }, + }, + execute: execute, +}); + +export default command; + +async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { + await interaction.deferReply(); + + const synthesis = interaction.options.getInteger('synthesis', false) ?? 0; + const mutationIds = mutations.map(m => m.id); + const { data: bazaar } = await FetchProducts(mutationIds); + + let mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { + const bazaarItem = bazaar?.items?.[mutation.id]; + const buy = bazaarItem?.bazaar?.buy as number | undefined; + const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; + if (buy === undefined && buyOrder === undefined) { + //if neither buy nor buy order exist, return infinite ratio (Jerryflower) + return { + id: mutation.id, + name: bazaarItem?.name ?? mutation.id, + buyCoinPerCopper: Infinity, + buyOrderCoinPerCopper: Infinity, + }; + } + const copper = mutation.copper * (1 + synthesis / 100); + return { + id: mutation.id, + name: bazaarItem?.name ?? mutation.id, + buyCoinPerCopper: (mutation.analysisCost + (buy ?? 0)) / copper, + buyOrderCoinPerCopper: (mutation.analysisCost + (buyOrder ?? 0)) / copper, + }; + }); + + mutationRatios = mutationRatios.sort((a, b) => a.buyCoinPerCopper - b.buyCoinPerCopper); + + const allItems = Object.values(mutationRatios); + const ITEMS_PER_PAGE = 10; + let page = 0; + const maxPage = Math.max(0, Math.ceil(allItems.length / ITEMS_PER_PAGE) - 1); + + function getPageItems(page: number) { + return allItems.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE); + } + + function getMutationIndex(page: number, index: number) { + return page * ITEMS_PER_PAGE + index + 1; + } + + function buildEmbed(settings: UserSettings | undefined, page: number) { + const pageItems = getPageItems(page); + const names = pageItems.map((item, i) => `**\`#${getMutationIndex(page, i)}\`** ${item.name}`).join('\n'); + const buyRatios = pageItems.map(item => isFinite(item.buyCoinPerCopper) ? + `:coin: \`${item.buyCoinPerCopper.toFixed(2)}\`` : 'N/A').join('\n'); + const sellRatios = pageItems.map(item => isFinite(item.buyOrderCoinPerCopper) ? + `:coin: \`${item.buyOrderCoinPerCopper.toFixed(2)}\`` : 'N/A').join('\n'); + const embed = EliteEmbed(settings) + .setTitle('Mutation Analysis - Coin/Copper Ratios') + .setDescription(`Synthesis Bonus: **${synthesis}%**\nShowing **${page + 1}** - ${maxPage + 1} pages.`) + .addFields([ + { name: 'Mutation', value: names, inline: true }, + { name: 'Cost (Insta Buy)', value: buyRatios, inline: true }, + { name: 'Cost (Buy Order)', value: sellRatios, inline: true }, + ]); + return embed; + } + + function getButtonRow(page: number, maxPage: number) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('first') + .setLabel('First') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId('back') + .setLabel('Back') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId('forward') + .setLabel('Next') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page === maxPage), + new ButtonBuilder() + .setCustomId('last') + .setLabel('Last') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page === maxPage), + ); + } + + const embed = buildEmbed(settings, page); + const reply = await interaction.editReply({ + embeds: [embed], + components: [getButtonRow(page, maxPage)], + }); + + const collector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 60_000, + }); + + collector.on('collect', async (i) => { + if (i.user.id === interaction.user.id) { + collector.resetTimer({ time: 30_000 }); + + if (i.customId === 'first') { + page = 0; + } else if (i.customId === 'back') { + if (page > 0) { + page -= 1; + } + } else if (i.customId === 'forward') { + if (page < maxPage) { + page += 1; + } + } else if (i.customId === 'last') { + if (page !== maxPage) { + page = maxPage; + } + } + + const newEmbed = buildEmbed(settings, page); + await i.update({ + embeds: [newEmbed], + components: [getButtonRow(page, maxPage)], + }).catch(() => { + collector.stop(); + }); + } else { + i.reply({ content: `These buttons aren't for you!`, ephemeral: true }); + } + }); + + collector.on('end', async () => { + const finalEmbed = buildEmbed(settings, page); + await interaction.editReply({ + embeds: [finalEmbed], + components: [], + }); + }); +} + +function getButtonRow(index: number, maxIndex = 1000, leaderboardId?: string) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('first') + .setLabel('First') + .setStyle(ButtonStyle.Secondary) + .setDisabled(index < 12), + new ButtonBuilder() + .setCustomId('back') + .setLabel('Back') + .setStyle(ButtonStyle.Secondary) + .setDisabled(index < 12), + new ButtonBuilder() + .setCustomId('forward') + .setLabel('Next') + .setStyle(ButtonStyle.Secondary) + .setDisabled(index + 12 > maxIndex), + new ButtonBuilder() + .setCustomId('last') + .setLabel('Last') + .setStyle(ButtonStyle.Secondary) + .setDisabled(index + 12 > maxIndex), + ); +} \ No newline at end of file From 7a344e5ce8a61438a7dca3b383481a41de25f5f7 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Sat, 21 Feb 2026 01:47:49 +0100 Subject: [PATCH 02/12] Add rose dragon bonus in calculations --- src/commands/mutations.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index fb9542e0..9fbd81a1 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -68,7 +68,13 @@ const command = new EliteCommand({ synthesis: { name: 'synthesis', description: 'Synthesis Chip Bonus (%)', - type: SlashCommandOptionType.Integer, + type: SlashCommandOptionType.Number, + builder: (b) => b.setMinValue(0).setMaxValue(40), + }, + rose_dragon: { + name: 'rose_dragon', + description: 'Rose Dragon Bonus (%)', + type: SlashCommandOptionType.Number, builder: (b) => b.setMinValue(0).setMaxValue(20), }, }, @@ -80,7 +86,8 @@ export default command; async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { await interaction.deferReply(); - const synthesis = interaction.options.getInteger('synthesis', false) ?? 0; + const synthesis = interaction.options.getNumber('synthesis', false) ?? 0; + const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; const mutationIds = mutations.map(m => m.id); const { data: bazaar } = await FetchProducts(mutationIds); @@ -97,7 +104,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User buyOrderCoinPerCopper: Infinity, }; } - const copper = mutation.copper * (1 + synthesis / 100); + const copper = mutation.copper * (1 + synthesis / 100 + rose_dragon / 100); return { id: mutation.id, name: bazaarItem?.name ?? mutation.id, @@ -130,7 +137,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User `:coin: \`${item.buyOrderCoinPerCopper.toFixed(2)}\`` : 'N/A').join('\n'); const embed = EliteEmbed(settings) .setTitle('Mutation Analysis - Coin/Copper Ratios') - .setDescription(`Synthesis Bonus: **${synthesis}%**\nShowing **${page + 1}** - ${maxPage + 1} pages.`) + .setDescription(`Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**\nShowing **${page + 1}** - ${maxPage + 1} pages.`) .addFields([ { name: 'Mutation', value: names, inline: true }, { name: 'Cost (Insta Buy)', value: buyRatios, inline: true }, From 4074da6f2a9fc52a7d7bbd7ba26367414fe4b942 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Sat, 21 Feb 2026 01:49:09 +0100 Subject: [PATCH 03/12] Now displaying copper gained / total cost --- src/commands/mutations.ts | 219 ++++++++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 79 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 9fbd81a1..b0e87329 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,7 +1,8 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType } from 'discord.js'; -import { FetchProduct, FetchProducts, UserSettings } from '../api/elite.js'; +import { EliteEmbed, NotYoursReply } from 'classes/embeds.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; +import { FetchProducts, UserSettings } from '../api/elite.js'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; -import { EliteEmbed } from 'classes/embeds.js'; + export interface MutationAnalysis { id: string; @@ -12,11 +13,17 @@ export interface MutationAnalysis { export interface MutationCopperRatio { id: string; name: string; + copper: MutationAnalysis['copper']; buyCoinPerCopper: number; + buyCoinTotal: number; buyOrderCoinPerCopper: number; + buyOrderCoinTotal: number; } -let mutations: MutationAnalysis[] = [ +type MutationBuyType = 'instabuy' | 'buyorder'; + +//waiting for farming-weight@14.0 to be usable on the bot repository to moove it +const mutations: MutationAnalysis[] = [ { id: "ASHWREATH", analysisCost: 10000, copper: 5 }, { id: "CHOCONUT", analysisCost: 10000, copper: 5 }, { id: "DUSTGRAIN", analysisCost: 10000, copper: 5 }, @@ -61,7 +68,7 @@ let mutations: MutationAnalysis[] = [ const command = new EliteCommand({ name: 'mutations', - description: 'Show mutations with the best coin/copper ratios for analysis', + description: 'Shows best mutations to analyse for copper', access: CommandAccess.Everywhere, type: CommandType.Slash, options: { @@ -91,7 +98,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const mutationIds = mutations.map(m => m.id); const { data: bazaar } = await FetchProducts(mutationIds); - let mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { + const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { const bazaarItem = bazaar?.items?.[mutation.id]; const buy = bazaarItem?.bazaar?.buy as number | undefined; const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; @@ -100,50 +107,45 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User return { id: mutation.id, name: bazaarItem?.name ?? mutation.id, + copper: mutation.copper, buyCoinPerCopper: Infinity, + buyCoinTotal: Infinity, buyOrderCoinPerCopper: Infinity, + buyOrderCoinTotal: Infinity, }; } const copper = mutation.copper * (1 + synthesis / 100 + rose_dragon / 100); + const buyCoinTotal = mutation.analysisCost + (buy ?? 0); + const buyOrderCoinTotal = mutation.analysisCost + (buyOrder ?? 0); return { id: mutation.id, name: bazaarItem?.name ?? mutation.id, - buyCoinPerCopper: (mutation.analysisCost + (buy ?? 0)) / copper, - buyOrderCoinPerCopper: (mutation.analysisCost + (buyOrder ?? 0)) / copper, + copper: mutation.copper, + buyCoinPerCopper: buyCoinTotal / copper, + buyOrderCoinPerCopper: buyOrderCoinTotal / copper, + buyCoinTotal, + buyOrderCoinTotal, }; }); - mutationRatios = mutationRatios.sort((a, b) => a.buyCoinPerCopper - b.buyCoinPerCopper); - - const allItems = Object.values(mutationRatios); const ITEMS_PER_PAGE = 10; let page = 0; - const maxPage = Math.max(0, Math.ceil(allItems.length / ITEMS_PER_PAGE) - 1); + let selectedType: MutationBuyType = 'instabuy'; // default to instabuy - function getPageItems(page: number) { - return allItems.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE); + function getSortedItems(type: MutationBuyType) { + return mutationRatios.slice().sort((a, b) => { + if (type === 'instabuy') return a.buyCoinPerCopper - b.buyCoinPerCopper; + return a.buyOrderCoinPerCopper - b.buyOrderCoinPerCopper; + }); } - function getMutationIndex(page: number, index: number) { - return page * ITEMS_PER_PAGE + index + 1; + function getPageItems(page: number, type: MutationBuyType) { + const sorted = getSortedItems(type); + return sorted.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE); } - function buildEmbed(settings: UserSettings | undefined, page: number) { - const pageItems = getPageItems(page); - const names = pageItems.map((item, i) => `**\`#${getMutationIndex(page, i)}\`** ${item.name}`).join('\n'); - const buyRatios = pageItems.map(item => isFinite(item.buyCoinPerCopper) ? - `:coin: \`${item.buyCoinPerCopper.toFixed(2)}\`` : 'N/A').join('\n'); - const sellRatios = pageItems.map(item => isFinite(item.buyOrderCoinPerCopper) ? - `:coin: \`${item.buyOrderCoinPerCopper.toFixed(2)}\`` : 'N/A').join('\n'); - const embed = EliteEmbed(settings) - .setTitle('Mutation Analysis - Coin/Copper Ratios') - .setDescription(`Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**\nShowing **${page + 1}** - ${maxPage + 1} pages.`) - .addFields([ - { name: 'Mutation', value: names, inline: true }, - { name: 'Cost (Insta Buy)', value: buyRatios, inline: true }, - { name: 'Cost (Buy Order)', value: sellRatios, inline: true }, - ]); - return embed; + function getMutationIndex(page: number, index: number) { + return page * ITEMS_PER_PAGE + index + 1; } function getButtonRow(page: number, maxPage: number) { @@ -171,79 +173,138 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User ); } - const embed = buildEmbed(settings, page); + function getSelectRow(buyType: MutationBuyType) { + const instaBuyLabel = 'Bazaar Insta Buy'; + const buyOrderLabel = 'Bazaar Buy Order'; + const instaBuyEmoji = '💸'; + const buyOrderEmoji = '📒'; + const selectRow = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('mutation-select') + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(instaBuyLabel) + .setValue('instabuy') + .setEmoji(instaBuyEmoji), + new StringSelectMenuOptionBuilder() + .setLabel(buyOrderLabel) + .setValue('buyorder') + .setEmoji(buyOrderEmoji)) + .setPlaceholder(buyType === 'instabuy' + ? `${instaBuyEmoji} ${instaBuyLabel}` + : `${buyOrderEmoji} ${buyOrderLabel}`) + ); + + return selectRow; + } + + const maxPage = Math.max(0, Math.ceil(mutationRatios.length / ITEMS_PER_PAGE) - 1); + function buildEmbed(page: number, type: MutationBuyType) { + const pageItems = getPageItems(page, type); + const description = `Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**`; + let mutationsField = ''; + pageItems.forEach((item, i) => { + const idx = getMutationIndex(page, i); + let priceText, totalText = ''; + if (type === 'instabuy') { + priceText = isFinite(item.buyCoinPerCopper) ? `\`${item.buyCoinPerCopper.toFixed(2)}\`` : '`N/A`'; + totalText = isFinite(item.buyCoinTotal) ? `${formatNumber(item.buyCoinTotal)}` : '`N/A`'; + } else { + priceText = isFinite(item.buyOrderCoinPerCopper) ? `\`${item.buyOrderCoinPerCopper.toFixed(2)}\`` : '`N/A`'; + totalText = isFinite(item.buyOrderCoinTotal) ? `${formatNumber(item.buyOrderCoinTotal)}` : '`N/A`'; + } + mutationsField += `\`#${idx}\` **${item.name}**: ${priceText} coins/Copper (\`${item.copper.toLocaleString()} Copper\`/\`${totalText}\`)\n`; + }); + return EliteEmbed(settings) + .setTitle('Mutation Analysis - Coin/Copper Ratios') + .setDescription(description) + .addFields({ name: `Mutations - ${type === 'instabuy' ? 'Bazaar Insta Buy' : 'Bazaar Buy Order'}`, value: mutationsField, inline: false }); + } + const reply = await interaction.editReply({ - embeds: [embed], - components: [getButtonRow(page, maxPage)], + embeds: [buildEmbed(page, selectedType)], + components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], }); - const collector = reply.createMessageComponentCollector({ + const buttonCollector = reply.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000, }); - collector.on('collect', async (i) => { - if (i.user.id === interaction.user.id) { - collector.resetTimer({ time: 30_000 }); - - if (i.customId === 'first') { + buttonCollector.on('collect', async (inter) => { + if (inter.user.id !== interaction.user.id) { + return NotYoursReply(inter); + } else { + resetCollectors(); + if (inter.customId === 'first') { page = 0; - } else if (i.customId === 'back') { + } else if (inter.customId === 'back') { if (page > 0) { page -= 1; } - } else if (i.customId === 'forward') { + } else if (inter.customId === 'forward') { if (page < maxPage) { page += 1; } - } else if (i.customId === 'last') { + } else if (inter.customId === 'last') { if (page !== maxPage) { page = maxPage; } } - const newEmbed = buildEmbed(settings, page); - await i.update({ - embeds: [newEmbed], - components: [getButtonRow(page, maxPage)], + await inter.update({ + embeds: [buildEmbed(page, selectedType)], + components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], }).catch(() => { - collector.stop(); + buttonCollector.stop(); }); - } else { - i.reply({ content: `These buttons aren't for you!`, ephemeral: true }); } }); - collector.on('end', async () => { - const finalEmbed = buildEmbed(settings, page); + buttonCollector.on('end', async () => { + clearComponents(); + }); + + const selectCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + time: 60_000, + }); + + selectCollector.on('collect', async (inter) => { + if (inter.user.id !== interaction.user.id) { + return NotYoursReply(inter); + } else { + resetCollectors(); + const value = inter.values[0]; + if (value === 'instabuy' || value === 'buyorder') { + selectedType = value; + page = 0; // reset page + await inter.update({ + embeds: [buildEmbed(page, selectedType)], + components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], + }); + } + } + }); + + selectCollector.on('end', async () => { + clearComponents(); + }); + + function resetCollectors() { + buttonCollector.resetTimer({ time: 30_000 }); + selectCollector.resetTimer({ time: 30_000 }); + } + + async function clearComponents() { await interaction.editReply({ - embeds: [finalEmbed], - components: [], + embeds: [buildEmbed(page, selectedType)], + components: [], //remove buttons & select menu }); - }); + } } -function getButtonRow(index: number, maxIndex = 1000, leaderboardId?: string) { - return new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('first') - .setLabel('First') - .setStyle(ButtonStyle.Secondary) - .setDisabled(index < 12), - new ButtonBuilder() - .setCustomId('back') - .setLabel('Back') - .setStyle(ButtonStyle.Secondary) - .setDisabled(index < 12), - new ButtonBuilder() - .setCustomId('forward') - .setLabel('Next') - .setStyle(ButtonStyle.Secondary) - .setDisabled(index + 12 > maxIndex), - new ButtonBuilder() - .setCustomId('last') - .setLabel('Last') - .setStyle(ButtonStyle.Secondary) - .setDisabled(index + 12 > maxIndex), - ); +function formatNumber(num: number) { + return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(num); } \ No newline at end of file From c9de644772b149fb44084f8e1fd4e56de18394b1 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Fri, 13 Mar 2026 10:37:36 +0100 Subject: [PATCH 04/12] Replaced Mutations interface & hard-set constant buy farming-weigth type --- src/commands/mutations.ts | 113 +++++++++++--------------------------- 1 file changed, 33 insertions(+), 80 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index b0e87329..eb5ed952 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,19 +1,13 @@ import { EliteEmbed, NotYoursReply } from 'classes/embeds.js'; +import { GREENHOUSE_MUTATIONS } from 'farming-weight'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; import { FetchProducts, UserSettings } from '../api/elite.js'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; - -export interface MutationAnalysis { - id: string; - analysisCost: number; - copper: number; -} - export interface MutationCopperRatio { id: string; name: string; - copper: MutationAnalysis['copper']; + copper: number; buyCoinPerCopper: number; buyCoinTotal: number; buyOrderCoinPerCopper: number; @@ -22,49 +16,7 @@ export interface MutationCopperRatio { type MutationBuyType = 'instabuy' | 'buyorder'; -//waiting for farming-weight@14.0 to be usable on the bot repository to moove it -const mutations: MutationAnalysis[] = [ - { id: "ASHWREATH", analysisCost: 10000, copper: 5 }, - { id: "CHOCONUT", analysisCost: 10000, copper: 5 }, - { id: "DUSTGRAIN", analysisCost: 10000, copper: 5 }, - { id: "GLOOMGOURD", analysisCost: 10000, copper: 5 }, - { id: "LONELILY", analysisCost: 50000, copper: 25 }, - { id: "SCOURROOT", analysisCost: 10000, copper: 5 }, - { id: "SHADEVINE", analysisCost: 10000, copper: 5 }, - { id: "VEILSHROOM", analysisCost: 10000, copper: 5 }, - { id: "WITHERBLOOM", analysisCost: 40000, copper: 20 }, - { id: "CHOCOBERRY", analysisCost: 60000, copper: 30 }, - { id: "CINDERSHADE", analysisCost: 80000, copper: 40 }, - { id: "COALROOT", analysisCost: 80000, copper: 40 }, - { id: "CREAMBLOOM", analysisCost: 60000, copper: 30 }, - { id: "DUSKBLOOM", analysisCost: 80000, copper: 40 }, - { id: "THORNSHADE", analysisCost: 80000, copper: 40 }, - { id: "BLASTBERRY", analysisCost: 240000, copper: 120 }, - { id: "CHEESEBITE", analysisCost: 80000, copper: 80 }, - { id: "CHLORONITE", analysisCost: 40000, copper: 20 }, - { id: "DO_NOT_EAT_SHROOM", analysisCost: 240000, copper: 120 }, - { id: "FLESHTRAP", analysisCost: 360000, copper: 180 }, - { id: "MAGIC_JELLYBEAN", analysisCost: 160000, copper: 80 }, - { id: "NOCTILUME", analysisCost: 300000, copper: 150 }, - { id: "SNOOZLING", analysisCost: 600000, copper: 300 }, - { id: "SOGGYBUD", analysisCost: 60000, copper: 30 }, - { id: "CHORUS_FRUIT", analysisCost: 600000, copper: 300 }, - { id: "PLANTBOY_ADVANCE", analysisCost: 700000, copper: 350 }, - { id: "PUFFERCLOUD", analysisCost: 1000000, copper: 500 }, - { id: "SHELLFRUIT", analysisCost: 500000, copper: 250 }, - { id: "STARTLEVINE", analysisCost: 500000, copper: 250 }, - { id: "STOPLIGHT_PETAL", analysisCost: 4000000, copper: 2000 }, - { id: "THUNDERLING", analysisCost: 800000, copper: 400 }, - { id: "TURTLELLINI", analysisCost: 240000, copper: 120 }, - { id: "ZOMBUD", analysisCost: 1000000, copper: 500 }, - { id: "ALL_IN_ALOE", analysisCost: 4600000, copper: 2300 }, - { id: "DEVOURER", analysisCost: 10000000, copper: 5000 }, - { id: "GLASSCORN", analysisCost: 4000000, copper: 2000 }, - { id: "GODSEED", analysisCost: 1000000, copper: 500 }, - { id: "JERRYFLOWER", analysisCost: 20000, copper: 10 }, - { id: "PHANTOMLEAF", analysisCost: 3000000, copper: 1500 }, - { id: "TIMESTALK", analysisCost: 19000000, copper: 9500 } -]; +const mutations = Object.values(GREENHOUSE_MUTATIONS); const command = new EliteCommand({ name: 'mutations', @@ -98,35 +50,36 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const mutationIds = mutations.map(m => m.id); const { data: bazaar } = await FetchProducts(mutationIds); - const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { - const bazaarItem = bazaar?.items?.[mutation.id]; - const buy = bazaarItem?.bazaar?.buy as number | undefined; - const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; - if (buy === undefined && buyOrder === undefined) { - //if neither buy nor buy order exist, return infinite ratio (Jerryflower) - return { - id: mutation.id, - name: bazaarItem?.name ?? mutation.id, - copper: mutation.copper, - buyCoinPerCopper: Infinity, - buyCoinTotal: Infinity, - buyOrderCoinPerCopper: Infinity, - buyOrderCoinTotal: Infinity, - }; - } - const copper = mutation.copper * (1 + synthesis / 100 + rose_dragon / 100); - const buyCoinTotal = mutation.analysisCost + (buy ?? 0); - const buyOrderCoinTotal = mutation.analysisCost + (buyOrder ?? 0); - return { - id: mutation.id, - name: bazaarItem?.name ?? mutation.id, - copper: mutation.copper, - buyCoinPerCopper: buyCoinTotal / copper, - buyOrderCoinPerCopper: buyOrderCoinTotal / copper, - buyCoinTotal, - buyOrderCoinTotal, - }; - }); + const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { + const bazaarItem = bazaar?.items?.[mutation.id]; + const buy = bazaarItem?.bazaar?.buy as number | undefined; + const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; + const analysisCost = mutation.analysis.baseCost; + const copper = mutation.analysis.copper * (1 + synthesis / 100 + rose_dragon / 100); + if (buy === undefined && buyOrder === undefined) { + //if neither buy nor buy order exist, return infinite ratio (Jerryflower) + return { + id: mutation.id, + name: mutation.display.name ?? mutation.id, + copper: mutation.analysis.copper, + buyCoinPerCopper: Infinity, + buyCoinTotal: Infinity, + buyOrderCoinPerCopper: Infinity, + buyOrderCoinTotal: Infinity, + }; + } + const buyCoinTotal = analysisCost + (buy ?? 0); + const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); + return { + id: mutation.id, + name: mutation.display.name ?? mutation.id, + copper: mutation.analysis.copper, + buyCoinPerCopper: buyCoinTotal / copper, + buyOrderCoinPerCopper: buyOrderCoinTotal / copper, + buyCoinTotal, + buyOrderCoinTotal, + }; + }); const ITEMS_PER_PAGE = 10; let page = 0; From 6728f48c7e1d0b14d7ab9d291b20f8840d3dab73 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Mon, 16 Mar 2026 13:55:24 +0100 Subject: [PATCH 05/12] Lint: comply to rules for imports --- src/commands/mutations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index eb5ed952..824b3d73 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,6 +1,6 @@ import { EliteEmbed, NotYoursReply } from 'classes/embeds.js'; -import { GREENHOUSE_MUTATIONS } from 'farming-weight'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; +import { GREENHOUSE_MUTATIONS } from 'farming-weight'; import { FetchProducts, UserSettings } from '../api/elite.js'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; From b15225ac52bfdefa56720674dc90ec3df8cd58f8 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Mon, 16 Mar 2026 17:32:47 +0100 Subject: [PATCH 06/12] Refacto: replace EliteEmbed usages by EliteContainer, to match new components (discord components V2) --- src/commands/mutations.ts | 136 ++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 64 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 824b3d73..aba0df89 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,5 +1,6 @@ -import { EliteEmbed, NotYoursReply } from 'classes/embeds.js'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; +import { NotYoursReply } from 'classes/embeds.js'; +import { EliteContainer } from '../classes/components.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; import { GREENHOUSE_MUTATIONS } from 'farming-weight'; import { FetchProducts, UserSettings } from '../api/elite.js'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; @@ -42,6 +43,7 @@ const command = new EliteCommand({ export default command; + async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { await interaction.deferReply(); @@ -50,36 +52,36 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const mutationIds = mutations.map(m => m.id); const { data: bazaar } = await FetchProducts(mutationIds); - const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { - const bazaarItem = bazaar?.items?.[mutation.id]; - const buy = bazaarItem?.bazaar?.buy as number | undefined; - const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; - const analysisCost = mutation.analysis.baseCost; - const copper = mutation.analysis.copper * (1 + synthesis / 100 + rose_dragon / 100); - if (buy === undefined && buyOrder === undefined) { - //if neither buy nor buy order exist, return infinite ratio (Jerryflower) - return { - id: mutation.id, - name: mutation.display.name ?? mutation.id, - copper: mutation.analysis.copper, - buyCoinPerCopper: Infinity, - buyCoinTotal: Infinity, - buyOrderCoinPerCopper: Infinity, - buyOrderCoinTotal: Infinity, - }; - } - const buyCoinTotal = analysisCost + (buy ?? 0); - const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); - return { - id: mutation.id, - name: mutation.display.name ?? mutation.id, - copper: mutation.analysis.copper, - buyCoinPerCopper: buyCoinTotal / copper, - buyOrderCoinPerCopper: buyOrderCoinTotal / copper, - buyCoinTotal, - buyOrderCoinTotal, - }; - }); + const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { + const bazaarItem = bazaar?.items?.[mutation.id]; + const buy = bazaarItem?.bazaar?.buy as number | undefined; + const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; + const analysisCost = mutation.analysis.baseCost; + const copper = mutation.analysis.copper * (1 + synthesis / 100 + rose_dragon / 100); + if (buy === undefined && buyOrder === undefined) { + //if neither buy nor buy order exist, return infinite ratio (Jerryflower) + return { + id: mutation.id, + name: mutation.display.name ?? mutation.id, + copper: mutation.analysis.copper, + buyCoinPerCopper: Infinity, + buyCoinTotal: Infinity, + buyOrderCoinPerCopper: Infinity, + buyOrderCoinTotal: Infinity, + }; + } + const buyCoinTotal = analysisCost + (buy ?? 0); + const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); + return { + id: mutation.id, + name: mutation.display.name ?? mutation.id, + copper: mutation.analysis.copper, + buyCoinPerCopper: buyCoinTotal / copper, + buyOrderCoinPerCopper: buyOrderCoinTotal / copper, + buyCoinTotal, + buyOrderCoinTotal, + }; + }); const ITEMS_PER_PAGE = 10; let page = 0; @@ -132,30 +134,30 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const instaBuyEmoji = '💸'; const buyOrderEmoji = '📒'; const selectRow = new ActionRowBuilder() - .addComponents( - new StringSelectMenuBuilder() - .setCustomId('mutation-select') - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel(instaBuyLabel) - .setValue('instabuy') - .setEmoji(instaBuyEmoji), - new StringSelectMenuOptionBuilder() - .setLabel(buyOrderLabel) - .setValue('buyorder') - .setEmoji(buyOrderEmoji)) - .setPlaceholder(buyType === 'instabuy' - ? `${instaBuyEmoji} ${instaBuyLabel}` - : `${buyOrderEmoji} ${buyOrderLabel}`) - ); - + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('mutation-select') + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(instaBuyLabel) + .setValue('instabuy') + .setEmoji(instaBuyEmoji), + new StringSelectMenuOptionBuilder() + .setLabel(buyOrderLabel) + .setValue('buyorder') + .setEmoji(buyOrderEmoji)) + .setPlaceholder(buyType === 'instabuy' + ? `${instaBuyEmoji} ${instaBuyLabel}` + : `${buyOrderEmoji} ${buyOrderLabel}`) + ); return selectRow; } const maxPage = Math.max(0, Math.ceil(mutationRatios.length / ITEMS_PER_PAGE) - 1); - function buildEmbed(page: number, type: MutationBuyType) { + + function buildContainer(page: number, type: MutationBuyType) { const pageItems = getPageItems(page, type); - const description = `Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**`; + const boosts = `Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**`; let mutationsField = ''; pageItems.forEach((item, i) => { const idx = getMutationIndex(page, i); @@ -169,15 +171,21 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } mutationsField += `\`#${idx}\` **${item.name}**: ${priceText} coins/Copper (\`${item.copper.toLocaleString()} Copper\`/\`${totalText}\`)\n`; }); - return EliteEmbed(settings) - .setTitle('Mutation Analysis - Coin/Copper Ratios') - .setDescription(description) - .addFields({ name: `Mutations - ${type === 'instabuy' ? 'Bazaar Insta Buy' : 'Bazaar Buy Order'}`, value: mutationsField, inline: false }); + + const container = new EliteContainer(settings) + .addTitle('**Mutation Analysis - Coin/Copper Ratios**') + .addText(boosts) + .addSeparator() + .addDescription(mutationsField) + .addFooter(); + return container; } + let currentContainer = buildContainer(page, selectedType); + const reply = await interaction.editReply({ - embeds: [buildEmbed(page, selectedType)], - components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], + flags: MessageFlags.IsComponentsV2 }); const buttonCollector = reply.createMessageComponentCollector({ @@ -206,9 +214,9 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } } + currentContainer = buildContainer(page, selectedType); await inter.update({ - embeds: [buildEmbed(page, selectedType)], - components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], }).catch(() => { buttonCollector.stop(); }); @@ -233,12 +241,12 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User if (value === 'instabuy' || value === 'buyorder') { selectedType = value; page = 0; // reset page + currentContainer = buildContainer(page, selectedType); await inter.update({ - embeds: [buildEmbed(page, selectedType)], - components: [getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], }); } - } + } }); selectCollector.on('end', async () => { @@ -251,9 +259,9 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } async function clearComponents() { + currentContainer.disableEverything(); await interaction.editReply({ - embeds: [buildEmbed(page, selectedType)], - components: [], //remove buttons & select menu + components: [currentContainer], }); } } From c7e9cfbb62f279009a6bc2051fd499e17b435704 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Mon, 16 Mar 2026 19:56:48 +0100 Subject: [PATCH 07/12] Refacto: multiples optimizations & logic improvements --- src/commands/mutations.ts | 86 ++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index aba0df89..685add87 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,9 +1,9 @@ import { NotYoursReply } from 'classes/embeds.js'; -import { EliteContainer } from '../classes/components.js'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; import { GREENHOUSE_MUTATIONS } from 'farming-weight'; import { FetchProducts, UserSettings } from '../api/elite.js'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; +import { EliteContainer } from '../classes/components.js'; export interface MutationCopperRatio { id: string; @@ -84,14 +84,24 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User }); const ITEMS_PER_PAGE = 10; - let page = 0; - let selectedType: MutationBuyType = 'instabuy'; // default to instabuy + const COLLECTOR_TIMEOUT = 60_000; + const COLLECTOR_RESET = 30_000; + + const state = { + page: 0, + selectedType: 'instabuy' as MutationBuyType + }; // default to instabuy + + const sortedInstabuy = [...mutationRatios].sort( + (a, b) => a.buyCoinPerCopper - b.buyCoinPerCopper + ); + + const sortedBuyOrder = [...mutationRatios].sort( + (a, b) => a.buyOrderCoinPerCopper - b.buyOrderCoinPerCopper + ); function getSortedItems(type: MutationBuyType) { - return mutationRatios.slice().sort((a, b) => { - if (type === 'instabuy') return a.buyCoinPerCopper - b.buyCoinPerCopper; - return a.buyOrderCoinPerCopper - b.buyOrderCoinPerCopper; - }); + return type === "instabuy" ? sortedInstabuy : sortedBuyOrder; } function getPageItems(page: number, type: MutationBuyType) { @@ -99,10 +109,6 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User return sorted.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE); } - function getMutationIndex(page: number, index: number) { - return page * ITEMS_PER_PAGE + index + 1; - } - function getButtonRow(page: number, maxPage: number) { return new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -160,15 +166,19 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const boosts = `Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**`; let mutationsField = ''; pageItems.forEach((item, i) => { - const idx = getMutationIndex(page, i); + const idx = page * ITEMS_PER_PAGE + i + 1; let priceText, totalText = ''; - if (type === 'instabuy') { - priceText = isFinite(item.buyCoinPerCopper) ? `\`${item.buyCoinPerCopper.toFixed(2)}\`` : '`N/A`'; - totalText = isFinite(item.buyCoinTotal) ? `${formatNumber(item.buyCoinTotal)}` : '`N/A`'; - } else { - priceText = isFinite(item.buyOrderCoinPerCopper) ? `\`${item.buyOrderCoinPerCopper.toFixed(2)}\`` : '`N/A`'; - totalText = isFinite(item.buyOrderCoinTotal) ? `${formatNumber(item.buyOrderCoinTotal)}` : '`N/A`'; - } + + const price = type === 'instabuy' + ? item.buyCoinPerCopper + : item.buyOrderCoinPerCopper; + + const total = type === 'instabuy' + ? item.buyCoinTotal + : item.buyOrderCoinTotal; + + priceText = isFinite(price) ? `\`${price.toFixed(2)}\`` : '`N/A`'; + totalText = isFinite(total) ? `${formatNumber(total)}` : '`N/A`'; mutationsField += `\`#${idx}\` **${item.name}**: ${priceText} coins/Copper (\`${item.copper.toLocaleString()} Copper\`/\`${totalText}\`)\n`; }); @@ -181,16 +191,16 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User return container; } - let currentContainer = buildContainer(page, selectedType); + let currentContainer = buildContainer(state.page, state.selectedType); const reply = await interaction.editReply({ - components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], flags: MessageFlags.IsComponentsV2 }); const buttonCollector = reply.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 60_000, + time: COLLECTOR_TIMEOUT, }); buttonCollector.on('collect', async (inter) => { @@ -199,24 +209,24 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } else { resetCollectors(); if (inter.customId === 'first') { - page = 0; + state.page = 0; } else if (inter.customId === 'back') { - if (page > 0) { - page -= 1; + if (state.page > 0) { + state.page -= 1; } } else if (inter.customId === 'forward') { - if (page < maxPage) { - page += 1; + if (state.page < maxPage) { + state.page += 1; } } else if (inter.customId === 'last') { - if (page !== maxPage) { - page = maxPage; + if (state.page !== maxPage) { + state.page = maxPage; } } - currentContainer = buildContainer(page, selectedType); + currentContainer = buildContainer(state.page, state.selectedType); await inter.update({ - components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], }).catch(() => { buttonCollector.stop(); }); @@ -229,7 +239,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const selectCollector = reply.createMessageComponentCollector({ componentType: ComponentType.StringSelect, - time: 60_000, + time: COLLECTOR_TIMEOUT, }); selectCollector.on('collect', async (inter) => { @@ -239,11 +249,11 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User resetCollectors(); const value = inter.values[0]; if (value === 'instabuy' || value === 'buyorder') { - selectedType = value; - page = 0; // reset page - currentContainer = buildContainer(page, selectedType); + state.selectedType = value; + state.page = 0; // reset page + currentContainer = buildContainer(state.page, state.selectedType); await inter.update({ - components: [currentContainer, getSelectRow(selectedType), getButtonRow(page, maxPage)], + components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], }); } } @@ -254,8 +264,8 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User }); function resetCollectors() { - buttonCollector.resetTimer({ time: 30_000 }); - selectCollector.resetTimer({ time: 30_000 }); + buttonCollector.resetTimer({ time: COLLECTOR_RESET }); + selectCollector.resetTimer({ time: COLLECTOR_RESET }); } async function clearComponents() { From c10cecb07b2406c0b5d4b4dadd8ab72f67a755a7 Mon Sep 17 00:00:00 2001 From: ptlthg <24925519+ptlthg@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:20:16 -0400 Subject: [PATCH 08/12] Formatting --- src/commands/mutations.ts | 96 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 685add87..b9a5e923 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -1,9 +1,18 @@ -import { NotYoursReply } from 'classes/embeds.js'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ComponentType, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChatInputCommandInteraction, + ComponentType, + MessageFlags, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} from 'discord.js'; import { GREENHOUSE_MUTATIONS } from 'farming-weight'; -import { FetchProducts, UserSettings } from '../api/elite.js'; -import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index.js'; -import { EliteContainer } from '../classes/components.js'; +import { FetchProducts, UserSettings } from '../api/elite'; +import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index'; +import { EliteContainer } from '../classes/components'; +import { NotYoursReply } from '../classes/embeds'; export interface MutationCopperRatio { id: string; @@ -43,23 +52,23 @@ const command = new EliteCommand({ export default command; - async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { await interaction.deferReply(); const synthesis = interaction.options.getNumber('synthesis', false) ?? 0; const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; - const mutationIds = mutations.map(m => m.id); + const mutationIds = mutations.map((m) => m.id); const { data: bazaar } = await FetchProducts(mutationIds); - const mutationRatios: MutationCopperRatio[] = mutations.map(mutation => { + const mutationRatios: MutationCopperRatio[] = mutations.map((mutation) => { const bazaarItem = bazaar?.items?.[mutation.id]; const buy = bazaarItem?.bazaar?.buy as number | undefined; const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; const analysisCost = mutation.analysis.baseCost; const copper = mutation.analysis.copper * (1 + synthesis / 100 + rose_dragon / 100); + if (buy === undefined && buyOrder === undefined) { - //if neither buy nor buy order exist, return infinite ratio (Jerryflower) + // if neither buy nor buy order exist, return infinite ratio (Jerryflower) return { id: mutation.id, name: mutation.display.name ?? mutation.id, @@ -70,8 +79,10 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User buyOrderCoinTotal: Infinity, }; } + const buyCoinTotal = analysisCost + (buy ?? 0); const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); + return { id: mutation.id, name: mutation.display.name ?? mutation.id, @@ -89,19 +100,15 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const state = { page: 0, - selectedType: 'instabuy' as MutationBuyType + selectedType: 'instabuy' as MutationBuyType, }; // default to instabuy - const sortedInstabuy = [...mutationRatios].sort( - (a, b) => a.buyCoinPerCopper - b.buyCoinPerCopper - ); + const sortedInstabuy = [...mutationRatios].sort((a, b) => a.buyCoinPerCopper - b.buyCoinPerCopper); - const sortedBuyOrder = [...mutationRatios].sort( - (a, b) => a.buyOrderCoinPerCopper - b.buyOrderCoinPerCopper - ); + const sortedBuyOrder = [...mutationRatios].sort((a, b) => a.buyOrderCoinPerCopper - b.buyOrderCoinPerCopper); function getSortedItems(type: MutationBuyType) { - return type === "instabuy" ? sortedInstabuy : sortedBuyOrder; + return type === 'instabuy' ? sortedInstabuy : sortedBuyOrder; } function getPageItems(page: number, type: MutationBuyType) { @@ -139,23 +146,17 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const buyOrderLabel = 'Bazaar Buy Order'; const instaBuyEmoji = '💸'; const buyOrderEmoji = '📒'; - const selectRow = new ActionRowBuilder() - .addComponents( - new StringSelectMenuBuilder() - .setCustomId('mutation-select') - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel(instaBuyLabel) - .setValue('instabuy') - .setEmoji(instaBuyEmoji), - new StringSelectMenuOptionBuilder() - .setLabel(buyOrderLabel) - .setValue('buyorder') - .setEmoji(buyOrderEmoji)) - .setPlaceholder(buyType === 'instabuy' - ? `${instaBuyEmoji} ${instaBuyLabel}` - : `${buyOrderEmoji} ${buyOrderLabel}`) - ); + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('mutation-select') + .addOptions( + new StringSelectMenuOptionBuilder().setLabel(instaBuyLabel).setValue('instabuy').setEmoji(instaBuyEmoji), + new StringSelectMenuOptionBuilder().setLabel(buyOrderLabel).setValue('buyorder').setEmoji(buyOrderEmoji), + ) + .setPlaceholder( + buyType === 'instabuy' ? `${instaBuyEmoji} ${instaBuyLabel}` : `${buyOrderEmoji} ${buyOrderLabel}`, + ), + ); return selectRow; } @@ -167,15 +168,12 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User let mutationsField = ''; pageItems.forEach((item, i) => { const idx = page * ITEMS_PER_PAGE + i + 1; - let priceText, totalText = ''; + let priceText, + totalText = ''; - const price = type === 'instabuy' - ? item.buyCoinPerCopper - : item.buyOrderCoinPerCopper; + const price = type === 'instabuy' ? item.buyCoinPerCopper : item.buyOrderCoinPerCopper; - const total = type === 'instabuy' - ? item.buyCoinTotal - : item.buyOrderCoinTotal; + const total = type === 'instabuy' ? item.buyCoinTotal : item.buyOrderCoinTotal; priceText = isFinite(price) ? `\`${price.toFixed(2)}\`` : '`N/A`'; totalText = isFinite(total) ? `${formatNumber(total)}` : '`N/A`'; @@ -195,7 +193,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const reply = await interaction.editReply({ components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], - flags: MessageFlags.IsComponentsV2 + flags: MessageFlags.IsComponentsV2, }); const buttonCollector = reply.createMessageComponentCollector({ @@ -225,11 +223,13 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } currentContainer = buildContainer(state.page, state.selectedType); - await inter.update({ - components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], - }).catch(() => { - buttonCollector.stop(); - }); + await inter + .update({ + components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], + }) + .catch(() => { + buttonCollector.stop(); + }); } }); @@ -278,4 +278,4 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User function formatNumber(num: number) { return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(num); -} \ No newline at end of file +} From 9e72a0e8604efa13120d39db76de7f43bbfe4351 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Mon, 6 Apr 2026 14:31:53 +0200 Subject: [PATCH 09/12] Change Rose Dragon & Synthesis parameters : use levels instead of bonus amount --- src/commands/mutations.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index b9a5e923..92140638 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -8,7 +8,7 @@ import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } from 'discord.js'; -import { GREENHOUSE_MUTATIONS } from 'farming-weight'; +import { GARDEN_CHIPS, GREENHOUSE_MUTATIONS, Rarity, Stat } from 'farming-weight'; import { FetchProducts, UserSettings } from '../api/elite'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index'; import { EliteContainer } from '../classes/components'; @@ -36,15 +36,15 @@ const command = new EliteCommand({ options: { synthesis: { name: 'synthesis', - description: 'Synthesis Chip Bonus (%)', + description: 'Synthesis Chip Level', type: SlashCommandOptionType.Number, - builder: (b) => b.setMinValue(0).setMaxValue(40), + builder: (b) => b.setMinValue(0).setMaxValue(20), }, rose_dragon: { name: 'rose_dragon', - description: 'Rose Dragon Bonus (%)', + description: 'Rose Dragon Level', type: SlashCommandOptionType.Number, - builder: (b) => b.setMinValue(0).setMaxValue(20), + builder: (b) => b.setMinValue(0).setMaxValue(200), }, }, execute: execute, @@ -56,7 +56,16 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User await interaction.deferReply(); const synthesis = interaction.options.getNumber('synthesis', false) ?? 0; + let synthesisRarity: Rarity = Rarity.Rare; + if (synthesis > 10 && synthesis <= 15) { + synthesisRarity = Rarity.Epic; + } else if (synthesis > 15 && synthesis <= 20) { + synthesisRarity = Rarity.Legendary; + } + const synthesisBonus = (GARDEN_CHIPS?.synthesis?.statsPerRarity?.[synthesisRarity]?.[Stat.BonusCopperPercentage] ?? 0) * synthesis; + console.log(`Synthesis Bonus: ${synthesisBonus}% (Rarity: ${synthesisRarity}, Level: ${synthesis})`); const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; + const roseDragonBonus = rose_dragon > 100 ? rose_dragon * 0.1 : 0; const mutationIds = mutations.map((m) => m.id); const { data: bazaar } = await FetchProducts(mutationIds); @@ -65,14 +74,14 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const buy = bazaarItem?.bazaar?.buy as number | undefined; const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; const analysisCost = mutation.analysis.baseCost; - const copper = mutation.analysis.copper * (1 + synthesis / 100 + rose_dragon / 100); + const copper = mutation.analysis.copper * (1 + synthesisBonus / 100 + roseDragonBonus / 100); if (buy === undefined && buyOrder === undefined) { // if neither buy nor buy order exist, return infinite ratio (Jerryflower) return { id: mutation.id, name: mutation.display.name ?? mutation.id, - copper: mutation.analysis.copper, + copper: copper, buyCoinPerCopper: Infinity, buyCoinTotal: Infinity, buyOrderCoinPerCopper: Infinity, @@ -86,7 +95,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User return { id: mutation.id, name: mutation.display.name ?? mutation.id, - copper: mutation.analysis.copper, + copper: copper, buyCoinPerCopper: buyCoinTotal / copper, buyOrderCoinPerCopper: buyOrderCoinTotal / copper, buyCoinTotal, @@ -164,7 +173,8 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User function buildContainer(page: number, type: MutationBuyType) { const pageItems = getPageItems(page, type); - const boosts = `Synthesis Bonus: **${synthesis}%**\nRose Dragon Bonus: **${rose_dragon}%**`; + // use toFixed(1) to avoid very long decimals due to JS floating point imprecision + const boosts = `Synthesis Chip Rarity (determined by the level): **${synthesisRarity}**\nSynthesis Bonus: **${synthesisBonus.toFixed(1)}%**\nRose Dragon Bonus: **${roseDragonBonus.toFixed(1)}%**`; let mutationsField = ''; pageItems.forEach((item, i) => { const idx = page * ITEMS_PER_PAGE + i + 1; From 42025c5ce4054660814d792307dbf3e65fe9d514 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Mon, 6 Apr 2026 14:34:24 +0200 Subject: [PATCH 10/12] Format mutations.ts --- src/commands/mutations.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 92140638..21b32e76 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -62,7 +62,8 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } else if (synthesis > 15 && synthesis <= 20) { synthesisRarity = Rarity.Legendary; } - const synthesisBonus = (GARDEN_CHIPS?.synthesis?.statsPerRarity?.[synthesisRarity]?.[Stat.BonusCopperPercentage] ?? 0) * synthesis; + const synthesisBonus = + (GARDEN_CHIPS?.synthesis?.statsPerRarity?.[synthesisRarity]?.[Stat.BonusCopperPercentage] ?? 0) * synthesis; console.log(`Synthesis Bonus: ${synthesisBonus}% (Rarity: ${synthesisRarity}, Level: ${synthesis})`); const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; const roseDragonBonus = rose_dragon > 100 ? rose_dragon * 0.1 : 0; @@ -91,7 +92,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const buyCoinTotal = analysisCost + (buy ?? 0); const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); - + return { id: mutation.id, name: mutation.display.name ?? mutation.id, From 24aa0adf8c5e0abad7848f2b288b62d76c70b348 Mon Sep 17 00:00:00 2001 From: KoalaMauve Date: Sat, 11 Apr 2026 13:32:30 +0200 Subject: [PATCH 11/12] Mutations : replaced farmingweight usage to get synthesis bonus value by a factory function, as bonus copper isnt a real stat. --- src/commands/mutations.ts | 50 +++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 21b32e76..511f2448 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -8,7 +8,7 @@ import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } from 'discord.js'; -import { GARDEN_CHIPS, GREENHOUSE_MUTATIONS, Rarity, Stat } from 'farming-weight'; +import { GREENHOUSE_MUTATIONS, Rarity } from 'farming-weight'; import { FetchProducts, UserSettings } from '../api/elite'; import { CommandAccess, CommandType, EliteCommand, SlashCommandOptionType } from '../classes/commands/index'; import { EliteContainer } from '../classes/components'; @@ -24,6 +24,12 @@ export interface MutationCopperRatio { buyOrderCoinTotal: number; } +interface SynthesisChip { + level: number; + rarity: Rarity; + bonus: number; +} + type MutationBuyType = 'instabuy' | 'buyorder'; const mutations = Object.values(GREENHOUSE_MUTATIONS); @@ -55,16 +61,7 @@ export default command; async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { await interaction.deferReply(); - const synthesis = interaction.options.getNumber('synthesis', false) ?? 0; - let synthesisRarity: Rarity = Rarity.Rare; - if (synthesis > 10 && synthesis <= 15) { - synthesisRarity = Rarity.Epic; - } else if (synthesis > 15 && synthesis <= 20) { - synthesisRarity = Rarity.Legendary; - } - const synthesisBonus = - (GARDEN_CHIPS?.synthesis?.statsPerRarity?.[synthesisRarity]?.[Stat.BonusCopperPercentage] ?? 0) * synthesis; - console.log(`Synthesis Bonus: ${synthesisBonus}% (Rarity: ${synthesisRarity}, Level: ${synthesis})`); + const synthesis: SynthesisChip = getSynthesisChipFromLevel(interaction.options.getNumber('synthesis', false) ?? 0); const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; const roseDragonBonus = rose_dragon > 100 ? rose_dragon * 0.1 : 0; const mutationIds = mutations.map((m) => m.id); @@ -75,7 +72,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User const buy = bazaarItem?.bazaar?.buy as number | undefined; const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; const analysisCost = mutation.analysis.baseCost; - const copper = mutation.analysis.copper * (1 + synthesisBonus / 100 + roseDragonBonus / 100); + const copper = mutation.analysis.copper * (1 + synthesis.bonus / 100 + roseDragonBonus / 100); if (buy === undefined && buyOrder === undefined) { // if neither buy nor buy order exist, return infinite ratio (Jerryflower) @@ -174,8 +171,9 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User function buildContainer(page: number, type: MutationBuyType) { const pageItems = getPageItems(page, type); + const rarity = `Synthesis Chip Rarity (determined by the level): **${synthesis.rarity}**\n`; // use toFixed(1) to avoid very long decimals due to JS floating point imprecision - const boosts = `Synthesis Chip Rarity (determined by the level): **${synthesisRarity}**\nSynthesis Bonus: **${synthesisBonus.toFixed(1)}%**\nRose Dragon Bonus: **${roseDragonBonus.toFixed(1)}%**`; + const boosts = `${synthesis.level > 0 ? rarity : ''}Synthesis Bonus: **${synthesis.bonus.toFixed(1)}%**\nRose Dragon Bonus: **${roseDragonBonus.toFixed(1)}%**`; let mutationsField = ''; pageItems.forEach((item, i) => { const idx = page * ITEMS_PER_PAGE + i + 1; @@ -290,3 +288,29 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User function formatNumber(num: number) { return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(num); } + +function getSynthesisChipFromLevel(level: number): SynthesisChip { + let rarity: Rarity; + + if (level > 15 && level <= 20) { + rarity = Rarity.Legendary; + } else if (level > 10) { + rarity = Rarity.Epic; + } else { + rarity = Rarity.Rare; + } + + const synthesisBonusPerRarity: Record = { + [Rarity.Rare]: 1, + [Rarity.Epic]: 1.5, + [Rarity.Legendary]: 2, + }; + + const bonus = synthesisBonusPerRarity[rarity] * level; + + return { + level, + rarity, + bonus, + }; +} From 80d0ac35c8f35b5cd4a3325e6c589fb50d1fe119 Mon Sep 17 00:00:00 2001 From: ptlthg <24925519+ptlthg@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:38:27 -0400 Subject: [PATCH 12/12] Fix option types and tiny changes --- src/commands/mutations.ts | 50 ++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/commands/mutations.ts b/src/commands/mutations.ts index 511f2448..1796e4e1 100644 --- a/src/commands/mutations.ts +++ b/src/commands/mutations.ts @@ -36,20 +36,20 @@ const mutations = Object.values(GREENHOUSE_MUTATIONS); const command = new EliteCommand({ name: 'mutations', - description: 'Shows best mutations to analyse for copper', + description: 'Shows best mutations to analyze for copper', access: CommandAccess.Everywhere, type: CommandType.Slash, options: { synthesis: { name: 'synthesis', description: 'Synthesis Chip Level', - type: SlashCommandOptionType.Number, + type: SlashCommandOptionType.Integer, builder: (b) => b.setMinValue(0).setMaxValue(20), }, rose_dragon: { name: 'rose_dragon', description: 'Rose Dragon Level', - type: SlashCommandOptionType.Number, + type: SlashCommandOptionType.Integer, builder: (b) => b.setMinValue(0).setMaxValue(200), }, }, @@ -61,16 +61,16 @@ export default command; async function execute(interaction: ChatInputCommandInteraction, settings?: UserSettings) { await interaction.deferReply(); - const synthesis: SynthesisChip = getSynthesisChipFromLevel(interaction.options.getNumber('synthesis', false) ?? 0); - const rose_dragon = interaction.options.getNumber('rose_dragon', false) ?? 0; - const roseDragonBonus = rose_dragon > 100 ? rose_dragon * 0.1 : 0; + const synthesis: SynthesisChip = getSynthesisChipFromLevel(interaction.options.getInteger('synthesis', false) ?? 0); + const roseDragonLevel = interaction.options.getInteger('rose_dragon', false) ?? 0; + const roseDragonBonus = roseDragonLevel > 100 ? roseDragonLevel * 0.1 : 0; const mutationIds = mutations.map((m) => m.id); const { data: bazaar } = await FetchProducts(mutationIds); const mutationRatios: MutationCopperRatio[] = mutations.map((mutation) => { const bazaarItem = bazaar?.items?.[mutation.id]; - const buy = bazaarItem?.bazaar?.buy as number | undefined; - const buyOrder = bazaarItem?.bazaar?.buyOrder as number | undefined; + const buy = getPrice(bazaarItem?.bazaar?.averageBuy) ?? getPrice(bazaarItem?.bazaar?.buy); + const buyOrder = getPrice(bazaarItem?.bazaar?.averageBuyOrder) ?? getPrice(bazaarItem?.bazaar?.buyOrder); const analysisCost = mutation.analysis.baseCost; const copper = mutation.analysis.copper * (1 + synthesis.bonus / 100 + roseDragonBonus / 100); @@ -87,8 +87,8 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User }; } - const buyCoinTotal = analysisCost + (buy ?? 0); - const buyOrderCoinTotal = analysisCost + (buyOrder ?? 0); + const buyCoinTotal = getCoinTotal(analysisCost, buy); + const buyOrderCoinTotal = getCoinTotal(analysisCost, buyOrder); return { id: mutation.id, @@ -172,7 +172,6 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User function buildContainer(page: number, type: MutationBuyType) { const pageItems = getPageItems(page, type); const rarity = `Synthesis Chip Rarity (determined by the level): **${synthesis.rarity}**\n`; - // use toFixed(1) to avoid very long decimals due to JS floating point imprecision const boosts = `${synthesis.level > 0 ? rarity : ''}Synthesis Bonus: **${synthesis.bonus.toFixed(1)}%**\nRose Dragon Bonus: **${roseDragonBonus.toFixed(1)}%**`; let mutationsField = ''; pageItems.forEach((item, i) => { @@ -199,6 +198,7 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } let currentContainer = buildContainer(state.page, state.selectedType); + let componentsCleared = false; const reply = await interaction.editReply({ components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], @@ -261,9 +261,13 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User state.selectedType = value; state.page = 0; // reset page currentContainer = buildContainer(state.page, state.selectedType); - await inter.update({ - components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], - }); + await inter + .update({ + components: [currentContainer, getSelectRow(state.selectedType), getButtonRow(state.page, maxPage)], + }) + .catch(() => { + selectCollector.stop(); + }); } } }); @@ -278,10 +282,14 @@ async function execute(interaction: ChatInputCommandInteraction, settings?: User } async function clearComponents() { + if (componentsCleared) return; + componentsCleared = true; currentContainer.disableEverything(); - await interaction.editReply({ - components: [currentContainer], - }); + await interaction + .editReply({ + components: [currentContainer], + }) + .catch(() => undefined); } } @@ -289,6 +297,14 @@ function formatNumber(num: number) { return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(num); } +function getPrice(price: number | null | undefined) { + return price !== undefined && price !== null && price > 0 ? price : undefined; +} + +function getCoinTotal(baseCost: number, bazaarPrice: number | undefined) { + return bazaarPrice === undefined ? Infinity : baseCost + bazaarPrice; +} + function getSynthesisChipFromLevel(level: number): SynthesisChip { let rarity: Rarity;