Files
ShaiBot/commands/applications/cookies.js
T
2026-08-22 22:10:41 +02:00

1721 lines
34 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 {
getCollections,
getCookieBalance,
changeCookies,
transferCookies,
claimCooldown,
randomInteger,
randomFloat,
randomItem,
formatPercentage
} = require(
"../../utils/cookieEconomy"
);
const {
trackProfileEvent
} = require(
"../../utils/profileSystem"
);
/*
* ============================================================
* CONFIG
* ============================================================
*/
const COLLECT_COOLDOWN =
60 * 60; // 1 hour
const STEAL_COOLDOWN =
15 * 60; // 15 minutes
const ABRA_ID =
"378135175059603457";
const ABRA_MULTIPLIER =
1.5;
/*
* ============================================================
* COMMAND
* ============================================================
*/
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.",
flags:
MessageFlags.Ephemeral
});
}
}
};
/*
* ============================================================
* /cookies collect
* ============================================================
*/
async function collect(
interaction
) {
const userId =
interaction.user.id;
/*
* Shared Mongo-backed cooldown.
*
* This is the same key the Crumb Saucer
* Cookie Bakery should use.
*/
const cooldown =
await claimCooldown(
userId,
interaction.guildId,
"collect",
COLLECT_COOLDOWN
);
if (
!cooldown.claimed
) {
return interaction.reply({
content:
`The batch is still cooking. Come back <t:${cooldown.next}:R> 🍪`,
flags:
MessageFlags.Ephemeral
});
}
/*
* Generate batch.
*/
let amount =
randomInteger(
1,
20
);
/*
* Legacy Abra bonus.
*/
if (
userId ===
ABRA_ID
) {
amount =
Math.ceil(
amount *
ABRA_MULTIPLIER
);
}
const balance =
await changeCookies(
userId,
amount
);
/*
* Lifetime Cookie gain + achievements.
*/
await trackProfileEvent(
interaction,
userId,
{
type:
"cookie_gain",
amount
}
);
const embed =
new EmbedBuilder()
.setTitle(
"🍪 Fresh Cookies!"
)
.setDescription(
randomCollectResponse(
amount,
interaction.user
)
)
.addFields(
{
name:
"Collected",
value:
`**+${amount.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Your Vault",
value:
`**${balance.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Next Batch",
value:
`<t:${cooldown.next}:R>`,
inline:
false
}
)
.setThumbnail(
interaction.user.displayAvatarURL({
size:
256
})
)
.setTimestamp();
return interaction.reply({
embeds: [
embed
]
});
}
/*
* ============================================================
* /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
});
}
/*
* ========================================================
* 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
});
}
/*
* ========================================================
* COOLDOWN
* ========================================================
*
* Claim before resolving the robbery.
*
* This prevents double clicks / concurrent steals.
*/
const cooldown =
await claimCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
if (
!cooldown.claimed
) {
return interaction.reply({
content:
`Lay low for a while. You can steal again <t:${cooldown.next}:R>.`,
flags:
MessageFlags.Ephemeral
});
}
/*
* ========================================================
* DYNAMIC SUCCESS CHANCE
* ========================================================
*/
const successChance =
getStealChance(
yourCookies,
theirCookies
);
const success =
Math.random() <
successChance;
/*
* ========================================================
* SUCCESS
* ========================================================
*/
if (success) {
const {
amount
} =
getStealAmount(
theirCookies
);
try {
await transferCookies(
target.id,
user.id,
amount
);
} catch (error) {
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;
}
/*
* Thief stats.
*/
await trackProfileEvent(
interaction,
user.id,
{
type:
"steal_success",
amount,
targetUserId:
target.id
}
);
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookie_gain",
amount
}
);
/*
* Victim stats.
*/
await trackProfileEvent(
interaction,
target.id,
{
type:
"stolen_from_me",
amount,
thiefUserId:
user.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_loss",
amount
}
);
const [
yourBalanceAfter,
targetBalanceAfter
] =
await Promise.all([
getCookieBalance(
user.id
),
getCookieBalance(
target.id
)
]);
const actualPercentage =
(
amount /
theirCookies
)
*
100;
const embed =
new EmbedBuilder()
.setTitle(
"🥷 Robbery Successful!"
)
.setDescription(
randomStealSuccessResponse(
amount,
target
)
)
.addFields(
{
name:
"Cookies Stolen",
value:
`**${amount.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Vault Taken",
value:
`**${actualPercentage.toFixed(1)}%**`,
inline:
true
},
{
name:
"Success Chance",
value:
`**${formatPercentage(successChance)}**`,
inline:
true
},
{
name:
"Your Vault",
value:
`**${yourBalanceAfter.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
`${target.username}'s Vault`,
value:
`**${targetBalanceAfter.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Next Robbery",
value:
`<t:${cooldown.next}:R>`,
inline:
false
}
)
.setThumbnail(
target.displayAvatarURL({
size:
256
})
)
.setTimestamp();
return interaction.reply({
embeds: [
embed
]
});
}
/*
* ========================================================
* FAILURE
* ========================================================
*
* Penalty remains percentage based.
*/
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;
}
/*
* Profile tracking.
*/
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookie_loss",
amount:
penalty
}
);
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_gain",
amount:
penalty
}
);
const [
yourBalanceAfter,
targetBalanceAfter
] =
await Promise.all([
getCookieBalance(
user.id
),
getCookieBalance(
target.id
)
]);
const actualPenaltyPercentage =
(
penalty /
yourCookies
)
*
100;
const embed =
new EmbedBuilder()
.setTitle(
"🚨 Robbery Failed!"
)
.setDescription(
randomStealFailResponse(
penalty,
target
)
)
.addFields(
{
name:
"Fine",
value:
`**${penalty.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Vault Lost",
value:
`**${actualPenaltyPercentage.toFixed(1)}%**`,
inline:
true
},
{
name:
"Success Chance",
value:
`**${formatPercentage(successChance)}**`,
inline:
true
},
{
name:
"Your Vault",
value:
`**${yourBalanceAfter.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
`${target.username}'s Vault`,
value:
`**${targetBalanceAfter.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Next Robbery",
value:
`<t:${cooldown.next}:R>`,
inline:
false
}
)
.setThumbnail(
target.displayAvatarURL({
size:
256
})
)
.setTimestamp();
return interaction.reply({
embeds: [
embed
]
});
}
/*
* ============================================================
* /cookies check
* ============================================================
*/
async function check(
interaction
) {
const target =
interaction.options.getUser(
"target"
)
??
interaction.user;
const balance =
await getCookieBalance(
target.id
);
const embed =
new EmbedBuilder()
.setTitle(
"🍪 Cookie Vault"
)
.setDescription(
`${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**.`
)
.addFields({
name:
"Vault Balance",
value:
`**${balance.toLocaleString()} 🍪**`
})
.setThumbnail(
target.displayAvatarURL({
size:
256
})
)
.setTimestamp();
return interaction.reply({
embeds: [
embed
]
});
}
/*
* ============================================================
* /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.",
flags:
MessageFlags.Ephemeral
});
}
if (
target.bot
) {
return interaction.reply({
content:
"Bots don't need Cookies. Allegedly.",
flags:
MessageFlags.Ephemeral
});
}
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!",
flags:
MessageFlags.Ephemeral
});
}
throw error;
}
/*
* Sender lifetime giving.
*/
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookies_given",
amount
}
);
/*
* Recipient lifetime gain.
*/
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_gain",
amount
}
);
const [
senderBalance,
recipientBalance
] =
await Promise.all([
getCookieBalance(
user.id
),
getCookieBalance(
target.id
)
]);
const embed =
new EmbedBuilder()
.setTitle(
"🍪 Cookies Shared!"
)
.setDescription(
`${user} shared **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}** with ${target}.`
)
.addFields(
{
name:
"Transferred",
value:
`**${amount.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Your Vault",
value:
`**${senderBalance.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
`${target.username}'s Vault`,
value:
`**${recipientBalance.toLocaleString()} 🍪**`,
inline:
true
}
)
.setThumbnail(
target.displayAvatarURL({
size:
256
})
)
.setTimestamp();
return interaction.reply({
embeds: [
embed
]
});
}
/*
* ============================================================
* /cookies leaderboard
* ============================================================
*/
async function leaderboard(
interaction
) {
const {
cookies
} =
getCollections();
/*
* Keep ShaiBot's central vault off
* the player leaderboard.
*/
const results =
await cookies
.find({
cookies: {
$gt:
0
},
userId: {
$ne:
interaction.client.user.id
}
})
.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 CHANCE
* ============================================================
*/
function getStealChance(
attackerCookies,
targetCookies
) {
const MIN_CHANCE =
0.15;
const MAX_CHANCE =
0.70;
const BASE_CHANCE =
0.50;
/*
* Wealthier targets become harder to rob.
*/
const targetPenalty =
Math.log10(
targetCookies +
1
)
*
0.06;
/*
* Attacker / victim wealth ratio.
*/
const wealthRatio =
attackerCookies /
Math.max(
targetCookies,
1
);
/*
* Catch-up mechanic.
*
* Poorer attackers receive a small bonus.
*/
const povertyBonus =
wealthRatio <
1
? (
1 -
wealthRatio
)
*
0.18
: 0;
/*
* Rich players bullying poorer targets
* receive a success penalty.
*/
const bullyPenalty =
wealthRatio >
1
? Math.min(
Math.log10(
wealthRatio +
1
)
*
0.12,
0.20
)
: 0;
const chance =
BASE_CHANCE
-
targetPenalty
+
povertyBonus
-
bullyPenalty;
return Math.max(
MIN_CHANCE,
Math.min(
MAX_CHANCE,
chance
)
);
}
/*
* ============================================================
* STEAL AMOUNT
* ============================================================
*/
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
};
}
/*
* ============================================================
* STEAL PERCENTAGE
* ============================================================
*/
function getStealPercentage() {
const roll =
Math.random();
/*
* 50%:
* 1–3%
*/
if (
roll <
0.50
) {
return randomFloat(
0.01,
0.03
);
}
/*
* 30%:
* 3–5%
*/
if (
roll <
0.80
) {
return randomFloat(
0.03,
0.05
);
}
/*
* 15%:
* 5–8%
*/
if (
roll <
0.95
) {
return randomFloat(
0.05,
0.08
);
}
/*
* 5% jackpot:
* 8–10%
*/
return randomFloat(
0.08,
0.10
);
}
/*
* ============================================================
* LEADERBOARD MEDALS
* ============================================================
*/
function getLeaderboardMedal(
index
) {
switch (
index
) {
case 0:
return "🥇";
case 1:
return "🥈";
case 2:
return "🥉";
default:
return `**${index + 1}.**`;
}
}
/*
* ============================================================
* EXPORTS FOR CRUMB SAUCER
* ============================================================
*
* This lets the Saucer reuse the same collect implementation
* later if we decide to route its bakery directly through here.
*/
module.exports.collect =
collect;