const { MessageFlags } = require("discord.js"); const { mClient } = require("../index"); const DATABASE = "neo-db"; const COOKIE_COLLECTION = "items_cookies"; const COOLDOWN_COLLECTION = "cooldowns_cookies"; function getCollections() { const db = mClient.db(DATABASE); return { db, cookies: db.collection(COOKIE_COLLECTION), cooldowns: db.collection(COOLDOWN_COLLECTION) }; } async function getCookieBalance(userId) { const { cookies } = getCollections(); const result = await cookies.findOne( { userId }, { projection: { _id: 0, cookies: 1 } } ); return result?.cookies ?? 0; } async function changeCookies( userId, amount ) { const { cookies } = getCollections(); return cookies.updateOne( { userId }, { $inc: { cookies: amount }, $setOnInsert: { userId } }, { upsert: true } ); } async function transferCookies( fromUserId, toUserId, amount ) { if ( !Number.isInteger(amount) || amount <= 0 ) { throw new Error( "INVALID_COOKIE_AMOUNT" ); } const { cookies } = getCollections(); const debit = await cookies.updateOne( { userId: fromUserId, cookies: { $gte: amount } }, { $inc: { cookies: -amount } } ); if ( debit.modifiedCount !== 1 ) { throw new Error( "INSUFFICIENT_COOKIES" ); } try { await cookies.updateOne( { userId: toUserId }, { $inc: { cookies: amount }, $setOnInsert: { userId: toUserId } }, { upsert: true } ); } catch (error) { try { await cookies.updateOne( { userId: fromUserId }, { $inc: { cookies: amount } } ); } catch (rollbackError) { console.error( "CRITICAL: Cookie rollback failed:", rollbackError ); } throw error; } } async function getCooldown( userId, guildId, type ) { const { cooldowns } = getCollections(); const result = await cooldowns.findOne( { userId, guildId }, { projection: { _id: 0, [type]: 1 } } ); return result?.[type] ?? 0; } async function setCooldown( userId, guildId, type, duration ) { const { cooldowns } = getCollections(); const now = getUnixTime(); const next = now + duration; await cooldowns.updateOne( { userId, guildId }, { $set: { [type]: next }, $setOnInsert: { userId, guildId } }, { upsert: true } ); return next; } /* * Atomically claim a cooldown. * * Useful for button games where a double-click * could otherwise resolve twice. */ async function claimCooldown( userId, guildId, type, duration ) { const { cooldowns } = getCollections(); const now = getUnixTime(); const next = now + duration; try { const result = await cooldowns.updateOne( { userId, guildId, $or: [ { [type]: { $lte: now } }, { [type]: { $exists: false } } ] }, { $set: { [type]: next }, $setOnInsert: { userId, guildId } }, { upsert: true } ); return { claimed: result.modifiedCount === 1 || result.upsertedCount === 1, next }; } catch (error) { /* * A unique {userId, guildId} index may cause * a duplicate-key error if an active cooldown * already exists. */ if (error?.code === 11000) { const active = await getCooldown( userId, guildId, type ); return { claimed: false, next: active }; } throw error; } } async function temporaryEphemeralReply( interaction, options, deleteAfter = 5000 ) { await interaction.reply({ ...options, flags: MessageFlags.Ephemeral }); setTimeout( async () => { try { await interaction.deleteReply(); } catch {} }, deleteAfter ); } async function temporaryEphemeralFollowUp( interaction, options, deleteAfter = 8000 ) { const message = await interaction.followUp({ ...options, flags: MessageFlags.Ephemeral }); setTimeout( async () => { try { await message.delete(); } catch {} }, deleteAfter ); return message; } function getUnixTime() { return Math.floor( Date.now() / 1000 ); } function randomInteger( min, max ) { return Math.floor( Math.random() * (max - min + 1) ) + min; } function randomFloat( min, max ) { return ( Math.random() * (max - min) ) + min; } function randomItem(items) { return items[ Math.floor( Math.random() * items.length ) ]; } function formatPercentage(value) { return `${Math.round(value * 100)}%`; } module.exports = { DATABASE, COOKIE_COLLECTION, COOLDOWN_COLLECTION, getCollections, getCookieBalance, changeCookies, transferCookies, getCooldown, setCooldown, claimCooldown, temporaryEphemeralReply, temporaryEphemeralFollowUp, getUnixTime, randomInteger, randomFloat, randomItem, formatPercentage };