Files
ShaiBot/utils/cookieEconomy.js
T
2026-08-18 18:57:24 +02:00

423 lines
7.1 KiB
JavaScript

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
};