feat: Introduce lootdrop functionality, enabling activity-based spawning and interactive claiming, alongside new configuration parameters.

This commit is contained in:
syntaxbullet
2025-12-18 16:09:52 +01:00
parent e8f6a56057
commit 56ad5b49cd
5 changed files with 219 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import { Message, TextChannel, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } from "discord.js";
import { config } from "@/lib/config";
import { economyService } from "./economy.service";
interface Lootdrop {
messageId: string;
channelId: string;
rewardAmount: number;
currency: string;
claimedBy?: string;
createdAt: Date;
}
class LootdropService {
private channelActivity: Map<string, number[]> = new Map();
private channelCooldowns: Map<string, number> = new Map();
private activeLootdrops: Map<string, Lootdrop> = new Map(); // key: messageId
constructor() {
// Cleanup interval for activity tracking
setInterval(() => this.cleanupActivity(), 60000);
}
private cleanupActivity() {
const now = Date.now();
const window = config.lootdrop.activityWindowMs;
for (const [channelId, timestamps] of this.channelActivity.entries()) {
const validTimestamps = timestamps.filter(t => now - t < window);
if (validTimestamps.length === 0) {
this.channelActivity.delete(channelId);
} else {
this.channelActivity.set(channelId, validTimestamps);
}
}
}
public async processMessage(message: Message) {
if (message.author.bot || !message.guild) return;
const channelId = message.channel.id;
const now = Date.now();
// Check cooldown
const cooldown = this.channelCooldowns.get(channelId);
if (cooldown && now < cooldown) return;
// Track activity
const timestamps = this.channelActivity.get(channelId) || [];
timestamps.push(now);
this.channelActivity.set(channelId, timestamps);
// Filter for window
const window = config.lootdrop.activityWindowMs;
const recentActivity = timestamps.filter(t => now - t < window);
if (recentActivity.length >= config.lootdrop.minMessages) {
// Chance to spawn
if (Math.random() < config.lootdrop.spawnChance) {
await this.spawnLootdrop(message.channel as TextChannel);
// Set cooldown
this.channelCooldowns.set(channelId, now + config.lootdrop.cooldownMs);
// Reset activity for this channel to prevent immediate double spawn?
// Maybe not strictly necessary if cooldown handles it, but good practice.
this.channelActivity.set(channelId, []);
}
}
}
private async spawnLootdrop(channel: TextChannel) {
const min = config.lootdrop.reward.min;
const max = config.lootdrop.reward.max;
const reward = Math.floor(Math.random() * (max - min + 1)) + min;
const currency = config.lootdrop.reward.currency;
const embed = new EmbedBuilder()
.setTitle("💰 LOOTDROP!")
.setDescription(`A lootdrop has appeared! Click the button below to claim **${reward} ${currency}**!`)
.setColor("#FFD700")
.setTimestamp();
const claimButton = new ButtonBuilder()
.setCustomId("lootdrop_claim")
.setLabel("CLAIM REWARD")
.setStyle(ButtonStyle.Success)
.setEmoji("💸");
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(claimButton);
try {
const message = await channel.send({ embeds: [embed], components: [row] });
this.activeLootdrops.set(message.id, {
messageId: message.id,
channelId: channel.id,
rewardAmount: reward,
currency: currency,
createdAt: new Date()
});
// Auto-cleanup unclaimable drops after 10 minutes (optional, prevents memory leak)
setTimeout(() => {
const drop = this.activeLootdrops.get(message.id);
if (drop && !drop.claimedBy) {
this.activeLootdrops.delete(message.id);
// Optionally edit message to say expired
}
}, 600000);
} catch (error) {
console.error("Failed to spawn lootdrop:", error);
}
}
public async tryClaim(messageId: string, userId: string, username: string): Promise<{ success: boolean; amount?: number; currency?: string; error?: string }> {
const drop = this.activeLootdrops.get(messageId);
if (!drop) {
return { success: false, error: "This lootdrop has expired or already been fully processed." };
}
if (drop.claimedBy) {
return { success: false, error: "This lootdrop has already been claimed." };
}
// Lock it
drop.claimedBy = userId;
this.activeLootdrops.set(messageId, drop);
try {
await economyService.modifyUserBalance(
userId,
BigInt(drop.rewardAmount),
"LOOTDROP_CLAIM",
`Claimed lootdrop in channel ${drop.channelId}`
);
// Clean up from memory
this.activeLootdrops.delete(messageId);
return { success: true, amount: drop.rewardAmount, currency: drop.currency };
} catch (error) {
console.error("Error claiming lootdrop:", error);
// Unlock if failed?
drop.claimedBy = undefined;
this.activeLootdrops.set(messageId, drop);
return { success: false, error: "An error occurred while processing the reward." };
}
}
}
export const lootdropService = new LootdropService();