diff --git a/src/commands/loot.ts b/src/commands/loot.ts new file mode 100644 index 00000000..dbbec583 --- /dev/null +++ b/src/commands/loot.ts @@ -0,0 +1,74 @@ +import { + type ChatInputCommandInteraction, + type CommandInteraction, + SlashCommandBuilder, + SlashCommandSubcommandBuilder, +} from "discord.js"; + +import type { BotContext } from "@/context.js"; +import type { ApplicationCommand } from "@/commands/command.js"; +import { ensureChatInputCommand } from "@/utils/interactionUtils.js"; + +import * as lootDataService from "@/service/lootData.js"; +import { adjustLootWithWeightEffectsFromUser } from "@/service/lootDrop.js"; + +export default class LootCommand implements ApplicationCommand { + name = "loot"; + description = "Geht's um Loot?"; + + applicationCommand = new SlashCommandBuilder() + .setName(this.name) + .setDescription(this.description) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("wahrscheinlichkeiten") + .setDescription("Zeige die Wahrscheinlichkeiten für Loot Gegenstände an"), + ); + + async handleInteraction(interaction: CommandInteraction, context: BotContext) { + const command = ensureChatInputCommand(interaction); + const subCommand = command.options.getSubcommand(); + switch (subCommand) { + case "wahrscheinlichkeiten": + await this.#showLootProbability(interaction, context); + break; + default: + throw new Error(`Unknown subcommand: "${subCommand}"`); + } + } + + async #showLootProbability(interaction: CommandInteraction, context: BotContext) { + if (!interaction.isChatInputCommand()) { + throw new Error("Interaction is not a chat input command"); + } + if (!interaction.guild || !interaction.channel) { + return; + } + + // TODO: Lowperformer solution. A diagram with graphviz or something would be cooler + const loot = ( + await adjustLootWithWeightEffectsFromUser( + interaction.user, + lootDataService.lootTemplates, + ) + ).loot.filter(l => l.weight > 0); + const totalWeight = loot.reduce((acc, curr) => acc + curr.weight, 0); + const lootWithProbabilitiy = loot + .map(l => ({ + ...l, // Oh no, please optimize for webscale. No need to copy the whole data :cry: + probability: Number(l.weight / totalWeight), + })) + .sort((a, b) => b.probability - a.probability); + + const textRepresentation = lootWithProbabilitiy + .map( + l => + `${l.displayName}: ${l.probability.toLocaleString(undefined, { style: "percent", maximumFractionDigits: 2 })}`, + ) + .join("\n"); + + await interaction.reply( + `Deine persönlichen Loot Wahrscheinlichkeiten:\n\n${textRepresentation}`, + ); + } +} diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index cb4b1a29..9d16c8ba 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs/promises"; import { - ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, @@ -28,6 +27,7 @@ import { } from "@/service/lootData.js"; import log from "@log"; +import type { LootTemplate } from "@/storage/loot.js"; const lootTimeoutMs = 60 * 1000; @@ -152,13 +152,18 @@ export async function postLootDrop( return; } - const defaultWeights = lootTemplates.map(t => t.weight); - const { messages, weights } = await getDropWeightAdjustments(interaction.user, defaultWeights); + const { messages, loot } = await adjustLootWithWeightEffectsFromUser( + interaction.user, + lootTemplates, + ); const rarityWeights = lootAttributeTemplates.map(a => a.initialDropWeight ?? 0); const initialAttribute = randomEntryWeighted(lootAttributeTemplates, rarityWeights); - const template = randomEntryWeighted(lootTemplates, weights); + const template = randomEntryWeighted( + lootTemplates, + loot.map(l => l.weight), + ); const claimedLoot = await lootService.createLoot( template, interaction.user, @@ -228,12 +233,12 @@ export async function postLootDrop( type AdjustmentResult = { messages: string[]; - weights: number[]; + loot: LootTemplate[]; }; -async function getDropWeightAdjustments( +export async function adjustLootWithWeightEffectsFromUser( user: User, - weights: readonly number[], + loot: readonly LootTemplate[], ): Promise { const waste = await lootService.getUserLootCountById(user.id, LootKindId.RADIOACTIVE_WASTE); const messages = []; @@ -254,13 +259,19 @@ async function getDropWeightAdjustments( messages.push("Da du privat versichert bist, hast du die doppelte Chance auf eine AU."); } - const newWeights = [...weights]; - newWeights[LootKindId.NICHTS] = Math.ceil(weights[LootKindId.NICHTS] * wasteFactor) | 0; - newWeights[LootKindId.KRANKSCHREIBUNG] = (weights[LootKindId.KRANKSCHREIBUNG] * pkvFactor) | 0; + const weights = loot.map(t => t.weight); + const newLoot = [...loot]; + const updateEntry = (id: LootKindId, newValue: number) => { + const idx = newLoot.findIndex(l => l.id === id); + console.assert(idx !== -1); + newLoot[idx].weight = newValue; + }; + updateEntry(LootKindId.NICHTS, Math.ceil(weights[LootKindId.NICHTS] * wasteFactor) | 0); + updateEntry(LootKindId.KRANKSCHREIBUNG, (weights[LootKindId.KRANKSCHREIBUNG] * pkvFactor) | 0); return { messages, - weights: newWeights, + loot: newLoot, }; }