Files
ShaiBot/commands/applications/cookies.js
T
2026-08-15 08:44:24 +02:00

1189 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {
SlashCommandBuilder,
EmbedBuilder,
MessageFlags
} = require("discord.js");
const { mClient } = require("../..");
const DATABASE = "neo-db";
const COOKIE_COLLECTION = "items_cookies";
const COOLDOWN_COLLECTION = "cooldowns_cookies";
const COLLECT_COOLDOWN = 60 * 60; // 1 hour
const STEAL_COOLDOWN = 15 * 60; // 15 minutes
const STEAL_SUCCESS_CHANCE = 0.50;
const ABRA_ID = "378135175059603457";
const ABRA_MULTIPLIER = 1.5;
module.exports = {
data: new SlashCommandBuilder()
.setName("cookies")
.setDescription("The ShaiBot cookie economy.")
.addSubcommand(subcommand =>
subcommand
.setName("collect")
.setDescription("Collect a fresh batch of cookies.")
)
.addSubcommand(subcommand =>
subcommand
.setName("steal")
.setDescription("Attempt to steal another user's cookies.")
.addUserOption(option =>
option
.setName("target")
.setDescription("The unfortunate victim.")
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("check")
.setDescription("Check a cookie balance.")
.addUserOption(option =>
option
.setName("target")
.setDescription("The user whose cookies you want to check.")
)
)
.addSubcommand(subcommand =>
subcommand
.setName("give")
.setDescription("Give some of your cookies to another user.")
.addUserOption(option =>
option
.setName("target")
.setDescription("The recipient.")
.setRequired(true)
)
.addIntegerOption(option =>
option
.setName("amount")
.setDescription("How many cookies to give.")
.setRequired(true)
.setMinValue(1)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("leaderboard")
.setDescription("Show the richest cookie collectors.")
),
async execute(interaction) {
switch (interaction.options.getSubcommand()) {
case "collect":
return collect(interaction);
case "steal":
return steal(interaction);
case "check":
return check(interaction);
case "give":
return give(interaction);
case "leaderboard":
return leaderboard(interaction);
default:
return interaction.reply({
content: "Unknown subcommand.",
ephemeral: true
});
}
}
};
/*
* DATABASE
*/
function getCollections() {
const db = mClient.db(DATABASE);
return {
cookies: db.collection(COOKIE_COLLECTION),
cooldowns: db.collection(COOLDOWN_COLLECTION)
};
}
/*
* COOKIE HELPERS
*/
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
}
);
}
/*
* COOKIE TRANSFER
*/
async function transferCookies(
fromUserId,
toUserId,
amount
) {
const { cookies } =
getCollections();
/*
* Atomically debit the sender only if
* they still have enough cookies.
*/
const debit =
await cookies.updateOne(
{
userId:
fromUserId,
cookies: {
$gte:
amount
}
},
{
$inc: {
cookies:
-amount
}
}
);
if (
debit.modifiedCount !== 1
) {
throw new Error(
"INSUFFICIENT_COOKIES"
);
}
/*
* Credit the recipient.
*/
try {
await cookies.updateOne(
{
userId:
toUserId
},
{
$inc: {
cookies:
amount
},
$setOnInsert: {
userId:
toUserId
}
},
{
upsert:
true
}
);
} catch (error) {
/*
* Credit failed.
*
* Attempt to restore the sender's
* cookies so the transfer doesn't
* destroy currency.
*/
try {
await cookies.updateOne(
{
userId:
fromUserId
},
{
$inc: {
cookies:
amount
}
}
);
} catch (rollbackError) {
console.error(
"CRITICAL: Cookie transfer rollback failed:",
rollbackError
);
}
throw error;
}
}
/*
* COOLDOWN HELPERS
*
* cooldowns_cookies:
*
* {
* userId: "...",
* guildId: "...",
* collect: 1786741200,
* steal: 1786738500
* }
*/
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;
}
/*
* /cookies collect
*/
async function collect(interaction) {
const userId = interaction.user.id;
const guildId = interaction.guildId;
const now = getUnixTime();
const cooldown = await getCooldown(
userId,
guildId,
"collect"
);
if (cooldown > now) {
return interaction.reply({
content:
`The batch is still cooking. Come back <t:${cooldown}:R> 🍪`,
ephemeral: true
});
}
let amount = randomInteger(1, 20);
/*
* Legacy Abra bonus.
*/
if (userId === ABRA_ID) {
amount = Math.ceil(
amount * ABRA_MULTIPLIER
);
}
await changeCookies(
userId,
amount
);
await setCooldown(
userId,
guildId,
"collect",
COLLECT_COOLDOWN
);
return interaction.reply({
content: randomCollectResponse(
amount,
interaction.user
)
});
}
/*
* /cookies steal
*/
async function steal(interaction) {
const user =
interaction.user;
const target =
interaction.options.getUser(
"target",
true
);
/*
* VALIDATION
*/
if (target.id === user.id) {
return interaction.reply({
content:
"C'mon, don't embarrass yourself. You can't steal from yourself.",
flags:
MessageFlags.Ephemeral
});
}
if (target.bot) {
return interaction.reply({
content:
"Bots have highly sophisticated Cookie security systems.",
flags:
MessageFlags.Ephemeral
});
}
/*
* COOLDOWN
*/
const now =
getUnixTime();
const cooldown =
await getCooldown(
user.id,
interaction.guildId,
"steal"
);
if (cooldown > now) {
return interaction.reply({
content:
`Lay low for a while. You can steal again <t:${cooldown}:R>.`,
flags:
MessageFlags.Ephemeral
});
}
/*
* BALANCES
*/
const [
yourCookies,
theirCookies
] =
await Promise.all([
getCookieBalance(
user.id
),
getCookieBalance(
target.id
)
]);
if (yourCookies < 1) {
return interaction.reply({
content:
"You need at least **1 Cookie** to attempt a robbery.",
flags:
MessageFlags.Ephemeral
});
}
if (theirCookies < 1) {
return interaction.reply({
content:
`Shame on you! ${target} doesn't have any Cookies. D:`,
flags:
MessageFlags.Ephemeral
});
}
/*
* DYNAMIC SUCCESS CHANCE
*/
const successChance =
getStealChance(
yourCookies,
theirCookies
);
const success =
Math.random() <
successChance;
/*
* SUCCESS
*/
if (success) {
/*
* Roll a weighted percentage
* of the target's entire vault.
*/
const {
amount,
percentage
} =
getStealAmount(
theirCookies
);
try {
await transferCookies(
target.id,
user.id,
amount
);
} catch (error) {
/*
* Target may have spent/given/stolen
* cookies between our balance check
* and the actual transfer.
*/
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return interaction.reply({
content:
`${target}'s Cookie balance changed before you could finish the robbery.`,
flags:
MessageFlags.Ephemeral
});
}
throw error;
}
/*
* Apply cooldown after robbery.
*/
const nextSteal =
await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
const actualPercentage =
(
amount /
theirCookies *
100
);
return interaction.reply({
content:
`${randomStealSuccessResponse(amount, target)}\n` +
`🍪 You made off with **${actualPercentage.toFixed(1)}%** of their vault!\n` +
`🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
`⏱️ Next robbery <t:${nextSteal}:R>`
});
}
/*
* FAILURE
*
* Penalty is also percentage based.
*
* Roll between roughly 1–8% of the
* attacker's current vault.
*/
const penaltyPercentage =
randomFloat(
0.01,
0.08
);
const penalty =
Math.max(
1,
Math.min(
yourCookies,
Math.floor(
yourCookies *
penaltyPercentage
)
)
);
try {
await transferCookies(
user.id,
target.id,
penalty
);
} catch (error) {
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return interaction.reply({
content:
"Your Cookie balance changed before the robbery could finish.",
flags:
MessageFlags.Ephemeral
});
}
throw error;
}
/*
* Apply cooldown.
*/
const nextSteal =
await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
const actualPenaltyPercentage =
(
penalty /
yourCookies *
100
);
return interaction.reply({
content:
`${randomStealFailResponse(penalty, target)}\n` +
`💸 You lost **${actualPenaltyPercentage.toFixed(1)}%** of your vault.\n` +
`🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
`⏱️ Next robbery <t:${nextSteal}:R>`
});
}
/*
* /cookies check
*/
async function check(interaction) {
const target =
interaction.options.getUser("target")
?? interaction.user;
const balance =
await getCookieBalance(target.id);
const mood =
balance === 0
? "😔"
: "🍪";
return interaction.reply({
content:
`${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**! ${mood}`
});
}
/*
* /cookies give
*/
async function give(interaction) {
const user = interaction.user;
const target = interaction.options.getUser(
"target",
true
);
const amount = interaction.options.getInteger(
"amount",
true
);
if (target.id === user.id) {
return interaction.reply({
content:
"You cannot give Cookies to yourself.",
ephemeral: true
});
}
if (target.bot) {
return interaction.reply({
content:
"Bots don't need Cookies. Allegedly.",
ephemeral: true
});
}
try {
await transferCookies(
user.id,
target.id,
amount
);
} catch (error) {
if (error.message === "INSUFFICIENT_COOKIES") {
return interaction.reply({
content:
"We don't run a charity here. You don't have enough Cookies!",
ephemeral: true
});
}
throw error;
}
return interaction.reply({
content:
`${user} shared **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}** with ${target} 🍪`
});
}
/*
* /cookies leaderboard
*/
async function leaderboard(interaction) {
const { cookies } = getCollections();
const results = await cookies
.find({
cookies: {
$gt: 0
}
})
.sort({
cookies: -1
})
.limit(10)
.toArray();
const description = results.length
? results
.map((record, index) => {
const medal = getLeaderboardMedal(index);
return (
`${medal} <@${record.userId}> — ` +
`**${record.cookies.toLocaleString()}** 🍪`
);
})
.join("\n")
: "Nobody has collected any Cookies yet.";
const embed = new EmbedBuilder()
.setTitle("🍪 Cookie Leaderboard")
.setDescription(description)
.setTimestamp();
if (interaction.guild?.iconURL()) {
embed.setThumbnail(
interaction.guild.iconURL({
size: 256
})
);
}
return interaction.reply({
embeds: [embed]
});
}
/*
* COLLECT RESPONSES
*/
function randomCollectResponse(amount, user) {
const cookies =
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
const responses = [
`${user}, I made you ${cookies} 🍪`,
`${user}, here's your ${cookies} 🍪 I ate the rest, sorry >.<`,
`Only ${cookies} 🍪 for you today ${user}... Abra stole the rest :c`,
`${user}, would you like a glass of milk with your ${cookies}? 🥛`,
`${cookies} 🍪 Careful, ${user}! They're still warm :3`,
`${cookies} 🍪 Best batch yet!`,
`${user}, these ${cookies} I bequeath unto thine own self upon this day.`,
`${user}, your ${cookies} are fresh out of the oven. Try not to burn your tongue!`,
`${user}, I bribed the Cookie Monster to spare these ${cookies} just for you.`,
`${user}, Santa came early! You got ${cookies}. Milk not included.`,
`${user}, your ${cookies} are artisanal, gluten-free, and slightly cursed.`,
`A wild baker appeared! They hand ${user} ${cookies} 🍪`,
`${user}, Abra said these ${cookies} are "totally not stolen"... 🤔`,
`${user}, here's your ${cookies} — watch out, they may be booby-trapped.`
];
return randomItem(responses);
}
/*
* STEAL SUCCESS RESPONSES
*/
function randomStealSuccessResponse(amount, target) {
const cookies =
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
const responses = [
`You've successfully stolen ${cookies} from ${target}!`,
`Ninja mode activated. You snatched ${cookies} from ${target} without leaving a crumb behind.`,
`Abra taught you well — ${cookies} from ${target} are now yours!`,
`Like a phantom in the night, you took ${cookies} from ${target}.`,
`You just pulled the sweetest heist: ${cookies} from ${target}!`,
`Mission complete. ${target} is ${cookies} poorer.`,
`You distracted ${target} with a dance and swiped ${cookies}.`,
`Clean getaway! ${target} won't notice the missing ${cookies} until it's too late.`,
`The Cookie Vault has been breached. ${cookies} taken from ${target}.`,
`You emerged from the shadows carrying ${cookies} from ${target}.`
];
return randomItem(responses);
}
/*
* STEAL FAILURE RESPONSES
*/
function randomStealFailResponse(amount, target) {
const cookies =
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
const responses = [
`You were caught red-handed by ${target} and lost ${cookies} as a penalty!`,
`Fail! ${target} caught you mid-bite — you dropped ${cookies}.`,
`Not your best moment... You lost ${cookies} to ${target} in the struggle.`,
`Stealth level: 0. You handed ${cookies} to ${target} as an apology.`,
`The Cookie jar was booby-trapped! You gave ${cookies} to ${target} while running away.`,
`Ouch. The guard dog barked, and now ${target} has your ${cookies}.`,
`You tripped over your own shoelace — ${target} picked up ${cookies}.`,
`Caught by the Cookie Police! ${cookies} confiscated and given to ${target}.`,
`Your pockets had holes... ${target} found the ${cookies} you dropped.`
];
return randomItem(responses);
}
/*
* STEAL HELPERS
*/
function getStealChance(
attackerCookies,
targetCookies
) {
const MIN_CHANCE =
0.15;
const MAX_CHANCE =
0.70;
const BASE_CHANCE =
0.50;
/*
* Wealthy targets are harder to rob.
*
* Logarithmic scaling prevents massive
* vaults from becoming completely immune.
*/
const targetPenalty =
Math.log10(
targetCookies + 1
)
*
0.06;
/*
* Compare attacker wealth to victim.
*/
const wealthRatio =
attackerCookies /
Math.max(
targetCookies,
1
);
/*
* Catch-up mechanic.
*
* If the attacker has less than the target,
* they gain up to +18 percentage points.
*/
const povertyBonus =
wealthRatio < 1
? (
1 -
wealthRatio
)
*
0.18
: 0;
/*
* Anti-bullying penalty.
*
* Rich attackers targeting poorer users
* become progressively less successful.
*/
const bullyPenalty =
wealthRatio > 1
? Math.min(
Math.log10(
wealthRatio + 1
)
*
0.12,
0.20
)
: 0;
const chance =
BASE_CHANCE
-
targetPenalty
+
povertyBonus
-
bullyPenalty;
/*
* Always keep robbery possible,
* but never make it nearly guaranteed.
*/
return Math.max(
MIN_CHANCE,
Math.min(
MAX_CHANCE,
chance
)
);
}
function getStealAmount(
targetCookies
) {
const MAX_STEAL_AMOUNT =
5000;
const percentage =
getStealPercentage();
const calculatedAmount =
Math.floor(
targetCookies *
percentage
);
const amount =
Math.max(
1,
Math.min(
calculatedAmount,
targetCookies,
MAX_STEAL_AMOUNT
)
);
return {
amount,
percentage
};
}
function getStealPercentage() {
const roll =
Math.random();
/*
* 50% chance:
* steal 1–3%
*/
if (roll < 0.50) {
return randomFloat(
0.01,
0.03
);
}
/*
* 30% chance:
* steal 3–5%
*/
if (roll < 0.80) {
return randomFloat(
0.03,
0.05
);
}
/*
* 15% chance:
* steal 5–8%
*/
if (roll < 0.95) {
return randomFloat(
0.05,
0.08
);
}
/*
* 5% jackpot:
* steal 8–10%
*/
return randomFloat(
0.08,
0.10
);
}
function randomFloat(
min,
max
) {
return (
Math.random() *
(max - min)
+
min
);
}
/*
* GENERIC HELPERS
*/
function getUnixTime() {
return Math.floor(
Date.now() / 1000
);
}
function randomInteger(min, max) {
return Math.floor(
Math.random() * (max - min + 1)
) + min;
}
function randomItem(items) {
return items[
Math.floor(
Math.random() * items.length
)
];
}
function formatPercentage(value) {
return `${Math.round(value * 100)}%`;
}
function getLeaderboardMedal(index) {
switch (index) {
case 0:
return "🥇";
case 1:
return "🥈";
case 2:
return "🥉";
default:
return `**${index + 1}.**`;
}
}