Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/loot/56-geschenkpapier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions src/commands/devcommands/loot-drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
ChannelType,
type CommandInteraction,
SlashCommandBuilder,
SlashCommandNumberOption,
} from "discord.js";

import type { BotContext } from "#/context.ts";
import type { ApplicationCommand } from "#/commands/command.ts";
import { ensureChatInputCommand } from "#/utils/interactionUtils.ts";

import * as lootDataService from "#/service/lootData.ts";
import * as lootService from "#/service/loot.ts";

import { postLootDrop, type LootClaimCallback } from "#/service/lootDrop.ts";

export default class LootDropCommand implements ApplicationCommand {
name = "loot-drop";
description = "Drops dir 1 Loot";

applicationCommand = new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addNumberOption(
new SlashCommandNumberOption()
.setName("loot-kind-id")
.setDescription("Loot ID die gedroppt werden soll")
.setMinValue(0),
);

async handleInteraction(interaction: CommandInteraction, context: BotContext) {
const command = ensureChatInputCommand(interaction);

if (command.guild === null) {
throw new Error("Interaction not in guild");
}
if (command.channel?.type !== ChannelType.GuildText) {
throw new Error("Interaction not in text channel");
}

const lootKindId = command.options.getNumber("loot-kind-id", false);
if (lootKindId === null) {
await command.reply({
content: "Es muss eine Loot ID angegeben werden.",
ephemeral: true,
});
return;
}
const lootTemplate = lootDataService.resolveLootTemplate(lootKindId);
if (lootTemplate === undefined) {
await command.reply({
content: `Es konnte kein Loot mit der ID ${lootKindId} gefunden werden.`,
ephemeral: true,
});
return;
}
const predefinedLootClaim: LootClaimCallback = async (winner, message) => {
const loot = await lootService.createLoot(
lootTemplate,
winner,
message,
"drop",
null,
null,
);
if (!loot) return undefined;
return { loot, template: lootTemplate, rarity: undefined, messages: [] };
};
await postLootDrop(context, command.channel, command.user, predefinedLootClaim);
await command.reply({
content: `Es wurde ${lootTemplate.id} gedroppt!`,
ephemeral: true,
});
}
}
70 changes: 69 additions & 1 deletion src/service/lootData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as emoteService from "#/service/emote.ts";
import * as bahnCardService from "#/service/bahncard.ts";
import { GuildMember, type Guild } from "discord.js";
import type { Loot, LootAttribute } from "#/storage/db/model.ts";
import { randomEntry } from "./random.ts";
import log from "#log";

const ACHTUNG_NICHT_DROPBAR_WEIGHT_KG = 0;

Expand Down Expand Up @@ -66,6 +68,7 @@ export const LootKind = Object.freeze({
MAGERQUARK: 53,
NAS: 54,
USB_KABEL: 55,
GESCHENKPAPIER: 56,
} as const);
export type LootKindId = (typeof LootKind)[keyof typeof LootKind];

Expand Down Expand Up @@ -228,7 +231,7 @@ export const lootTemplateMap: Record<LootKindId, LootTemplate> = {
context,
interaction.channel,
interaction.user,
loot.id,
lootDropService.randomizedLootClaim(loot.id),
);
return false;
},
Expand Down Expand Up @@ -872,6 +875,71 @@ export const lootTemplateMap: Record<LootKindId, LootTemplate> = {
asset: "assets/loot/55-usb-kabel.png",
wrapable: true,
},
[LootKind.GESCHENKPAPIER]: {
id: LootKind.GESCHENKPAPIER,
weight: 12,
displayName: "Geschenkpapier",
titleText: "Das sieht ja super cringe aus, wer würde sich denn darüber freuen wollen?",
dropDescription:
"Mit diesem Geschenkpapier sollte man Geschenke einpacken können, aber irgendwie sieht es auch nicht so aus, als wäre das jemals jemandem gelungen.",
emote: "🎁",
asset: "assets/loot/56-geschenkpapier.png",
onUse: async (interaction, context, _wrappingPaper) => {
const inventory = await lootService.getInventoryContents(interaction.user);
const wrapables = inventory.filter(
l => resolveLootTemplate(l.lootKindId)?.wrapable ?? false,
);

if (wrapables.length === 0) {
await interaction.reply({
content: "Du hast nichts, was du einpacken könntest! 😢",
});
return false;
Comment thread
twobiers marked this conversation as resolved.
}

const randomItem = randomEntry(wrapables);
const rarity =
randomItem.attributes.find(a => a.attributeClassId === LootAttributeClass.RARITY) ??
undefined;
const rarityAttributeTemplate = rarity
? resolveLootAttributeTemplate(rarity.attributeKindId)
: undefined;
const lootTemplate = resolveLootTemplate(randomItem.lootKindId);
if (!lootTemplate) {
log.error(
"Failed to resolve loot template for wrapped item: " + randomItem.lootKindId,
);
await interaction.reply({
content: "Ein Fehler ist aufgetreten, versuch es später nochmal! 😢",
});
return false;
}

const transferWrappedItemLoot: lootDropService.LootClaimCallback = async winner => {
const claimedLoot = await lootService.transferLootToUser(
randomItem.id,
winner,
true,
);
return {
loot: claimedLoot,
template: lootTemplate,
rarity: rarityAttributeTemplate,
messages: [],
};
};
const claimed = await lootDropService.postLootDrop(
context,
interaction.channel,
interaction.user,
transferWrappedItemLoot,
);
if (!claimed) {
await lootService.deleteLoot(randomItem.id);
}
return false;
},
},
} as const;

export const lootTemplates: LootTemplate[] = Object.values(lootTemplateMap);
Expand Down
88 changes: 59 additions & 29 deletions src/service/lootDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ButtonStyle,
ChannelType,
ComponentType,
type Message,
type TextChannel,
type User,
type Interaction,
Expand All @@ -21,7 +22,7 @@ import * as sentry from "@sentry/node";

import type { BotContext } from "#/context.ts";
import type { Loot, LootId } from "#/storage/db/model.ts";
import type { LootTemplate, TimeBasedWeightConfig } from "#/storage/loot.ts";
import type { LootAttributeTemplate, LootTemplate, TimeBasedWeightConfig } from "#/storage/loot.ts";
import { randomBoolean, randomEntry, randomEntryWeighted } from "#/service/random.ts";
import * as timeUtils from "#/utils/time.ts";
import { zonedNow } from "#/utils/dateUtils.ts";
Expand Down Expand Up @@ -81,14 +82,64 @@ export async function runDropAttempt(context: BotContext) {
log.info(
`Randomization hit threshold (${lootConfig.dropChance}). Dropping loot to ${targetChannel.name}!`,
);
await postLootDrop(context, targetChannel, undefined, undefined);
await postLootDrop(context, targetChannel, undefined, randomizedLootClaim());
}

export type ClaimedLootDrop = {
loot: Loot;
template: LootTemplate;
rarity: LootAttributeTemplate | undefined;
messages: readonly string[];
};

export type LootClaimCallback = (
winner: User,
message: Message<true>,
) => Promise<ClaimedLootDrop | undefined>;

export function randomizedLootClaim(predecessorLootId: LootId | null = null): LootClaimCallback {
return async (winner, message) => {
Comment thread
twobiers marked this conversation as resolved.
const drop = await randomizedLootDrop(winner);
const loot = await lootService.createLoot(
drop.template,
winner,
message,
"drop",
predecessorLootId,
drop.rarity ?? null,
);
if (!loot) return undefined;
return { loot, ...drop };
};
}

export async function randomizedLootDrop(winner: User): Promise<Omit<ClaimedLootDrop, "loot">> {
const timeBasedWeightKey = getCurrentTimeBasedKey();

const defaultWeights = timeBasedWeightKey
? lootTemplates.map(t => t.timeBasedWeight?.[timeBasedWeightKey] ?? t.weight)
: lootTemplates.map(t => t.weight);

const { messages, weights } = await getDropWeightAdjustments(winner, defaultWeights);

const template = randomEntryWeighted(lootTemplates, weights);

const rarities = lootAttributeTemplates.filter(a => a.classId === LootAttributeClass.RARITY);
const rarityWeights = rarities.map(a => a.initialDropWeight ?? 0);

const rarity =
template.id === LootKind.NICHTS
? undefined
: (randomEntryWeighted(rarities, rarityWeights) ?? undefined);

return { template, rarity, messages };
}

export async function postLootDrop(
context: BotContext,
channel: GuildBasedChannel & TextBasedChannel,
donor: User | undefined,
predecessorLootId: LootId | undefined,
onClaim: LootClaimCallback,
): Promise<Loot | undefined> {
const takeLootButton = new ButtonBuilder()
.setCustomId("take-loot")
Expand Down Expand Up @@ -154,34 +205,11 @@ export async function postLootDrop(
return;
}

const timeBasedWeightKey = getCurrentTimeBasedKey();

const defaultWeights = timeBasedWeightKey
? lootTemplates.map(t => t.timeBasedWeight?.[timeBasedWeightKey] ?? t.weight)
: lootTemplates.map(t => t.weight);

const { messages, weights } = await getDropWeightAdjustments(interaction.user, defaultWeights);

const template = randomEntryWeighted(lootTemplates, weights);

const rarities = lootAttributeTemplates.filter(a => a.classId === LootAttributeClass.RARITY);
const rarityWeights = rarities.map(a => a.initialDropWeight ?? 0);

const rarityAttribute =
template.id === LootKind.NICHTS ? null : randomEntryWeighted(rarities, rarityWeights);

const claimedLoot = await lootService.createLoot(
template,
interaction.user,
message,
"drop",
predecessorLootId ?? null,
rarityAttribute,
);
const claimed = await onClaim(interaction.user, message);
Comment thread
twobiers marked this conversation as resolved.

const reply = await interaction.deferReply({ flags: MessageFlags.Ephemeral });

if (!claimedLoot) {
if (!claimed) {
await reply.edit({
content: `Upsi, da ist was schief gelaufi oder jemand anderes war schnelli ${context.emoji.sadHamster}`,
});
Expand All @@ -190,6 +218,8 @@ export async function postLootDrop(

await reply.delete();

const { loot: claimedLoot, template, rarity: rarityAttribute, messages } = claimed;

log.info(
`User ${interaction.user.username} claimed loot ${claimedLoot.id} (template: ${template.id})`,
);
Expand Down Expand Up @@ -299,7 +329,7 @@ export async function postLootDrop(
message,
"double-or-nothing",
claimedLoot.id,
rarityAttribute,
rarityAttribute ?? null,
);
if (!extraLoot) {
await channel.send(`${winner}, ups, da ist was schief gelaufi ${context.emoji.sadHamster}`);
Expand Down
Loading