Files
2026-09-13 21:56:18 +02:00

1282 lines
35 KiB
JavaScript

const {
SlashCommandBuilder,
EmbedBuilder,
MessageFlags
} = require("discord.js");
const {
getCollections,
getCookieBalance,
transferCookies,
claimCooldown,
randomFloat,
randomItem,
formatPercentage
} = require("../../utils/cookieEconomy");
const {
trackProfileEvent,
getProfileCollections,
getDefaultStats
} = require("../../utils/profileSystem");
const {
collectCookies
} = require("../../utils/crumbCollect");
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;
const result = await collectCookies(interaction, userId);
if (!result.collected) {
return interaction.reply({
content: `The batch is still cooking. Come back <t:${result.cooldown.next}:R> 🍪`,
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setTitle("🍪 Fresh Cookies!")
.setDescription(
randomCollectResponse(
result.amount,
interaction.user
)
)
.addFields(
{
name: "Collected",
value: `**+${result.amount.toLocaleString()} 🍪**`,
inline: true
},
{
name: "Your Vault",
value: `**${result.balance.toLocaleString()} 🍪**`,
inline: true
},
{
name: "Next Batch",
value: `<t:${result.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);
/*
* Command-specific balancing.
*
* Base success chance:
* 100x richer attacker -> 30%
* 10x richer attacker -> 42.5%
* equal wealth -> 55%
* 10x poorer attacker -> 67.5%
* 100x poorer attacker -> 80%
*/
const cooldownSeconds = 15 * 60;
const minSuccessChance = 0.30;
const maxSuccessChance = 0.80;
const baseSuccessChance = 0.55;
const wealthChanceScale = 0.125;
const heatWindowMs = 60 * 60 * 1000;
const heatPerAttempt = 0.075;
const hourlyStealablePercentage = 0.15;
const maxStealAmount = 25000;
const baseFailurePenaltyMin = 0.02;
const baseFailurePenaltyMax = 0.10;
const minPenaltyMultiplier = 0.75;
const maxPenaltyMultiplier = 1.50;
const minRewardMultiplier = 0.75;
const maxRewardMultiplier = 1.25;
const stealTiers = [
{ threshold: 0.50, min: 0.01, max: 0.03 },
{ threshold: 0.80, min: 0.03, max: 0.05 },
{ threshold: 0.95, min: 0.05, max: 0.08 },
{ threshold: 1.00, min: 0.08, max: 0.10 }
];
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
});
}
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
});
}
const { cookies } = getCookieCommandCollections();
const { stats: statsProfile } = getProfileCollections();
const now = new Date();
const stealWindowCutoff = new Date(
now.getTime() - heatWindowMs
);
/*
* Check an active exhausted window before consuming the attacker's
* cooldown. Expired windows are allowed to continue and will be reopened
* only after the attacker passes their own cooldown check.
*/
const existingTargetCookieRecord = await cookies.findOne(
{
userId: target.id
},
{
projection: {
stealable: 1
}
}
);
const existingStealable =
existingTargetCookieRecord?.stealable;
const existingWindowStartedAt =
existingStealable?.windowStartedAt
? new Date(
existingStealable.windowStartedAt
)
: null;
const existingWindowActive =
existingWindowStartedAt &&
existingWindowStartedAt > stealWindowCutoff;
if (
existingWindowActive &&
existingStealable.amount <= 0
) {
const availableAt = Math.floor(
(
existingWindowStartedAt.getTime() +
heatWindowMs
) /
1000
);
return interaction.reply({
content: `${target}'s Cookie vault is no longer exposed. Try again <t:${availableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
const cooldown = await claimCooldown(
user.id,
interaction.guildId,
"steal",
cooldownSeconds
);
if (!cooldown.claimed) {
return interaction.reply({
content: `Lay low for a while. You can steal again <t:${cooldown.next}:R>.`,
flags: MessageFlags.Ephemeral
});
}
/*
* Hourly victim-loss budget.
*
* This is a fixed one-hour window. The timestamp does not move when
* Cookies are stolen. Once the window expires, the first valid robbery
* attempt opens a fresh window and exposes 15% of the victim's current
* balance.
*/
const initialStealableAmount = Math.max(
1,
Math.floor(
theirCookies *
hourlyStealablePercentage
)
);
const stealableWindowUpdate = await cookies.updateOne(
{
userId: target.id,
$or: [
{
"stealable.windowStartedAt": {
$exists: false
}
},
{
"stealable.windowStartedAt": {
$lte: stealWindowCutoff
}
}
]
},
{
$set: {
stealable: {
amount: initialStealableAmount,
windowStartedAt: now
}
}
}
);
const openedStealableWindow =
stealableWindowUpdate.modifiedCount === 1;
const targetCookieRecord = await cookies.findOne(
{
userId: target.id
},
{
projection: {
stealable: 1
}
}
);
const stealable = targetCookieRecord?.stealable;
if (!stealable) {
throw new Error(
`Missing stealable state for Cookie user ${target.id}.`
);
}
if (stealable.amount <= 0) {
const availableAt = Math.floor(
(
new Date(
stealable.windowStartedAt
).getTime() +
heatWindowMs
) /
1000
);
return interaction.reply({
content: `${target}'s Cookie vault is no longer exposed. Try again <t:${availableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
const vaultBreachText = openedStealableWindow
? (
`🔓 **Vault Breached!**\n` +
`You've breached the security of ${target}'s Vault and exposed some of their assets. ` +
`These Cookies can now be stolen until the Vault is repaired!`
)
: null;
/*
* Ensure the victim has a complete profile document before heat state is
* written. This avoids creating a sparse stats_profiles document when a
* player is targeted before any other profile event has initialized them.
*/
await statsProfile.updateOne(
{
userId: target.id,
guildId: interaction.guildId
},
{
$setOnInsert: getDefaultStats(
target.id,
interaction.guildId
)
},
{
upsert: true
}
);
/*
* Rolling heat protection.
*
* Every valid attempt against this target contributes 7.5 percentage
* points of heat and fades linearly to zero over one hour. Attempts by
* all users count toward the same target heat.
*/
await statsProfile.updateOne(
{
userId: target.id,
guildId: interaction.guildId
},
{
$pull: {
"stealProtection.attempts": {
timestamp: {
$lte: stealWindowCutoff
}
}
}
}
);
/*
* Append the current attempt atomically and return the profile state from
* immediately before it was appended. This lets simultaneous attackers
* see each other's newly-created heat as MongoDB serializes the updates,
* while the current attempt still never penalizes itself.
*/
const heatUpdateResult = await statsProfile.findOneAndUpdate(
{
userId: target.id,
guildId: interaction.guildId
},
{
$push: {
"stealProtection.attempts": {
userId: user.id,
timestamp: now
}
}
},
{
returnDocument: "before"
}
);
const targetProfile =
heatUpdateResult?.value ??
heatUpdateResult;
const activeAttempts =
targetProfile?.stealProtection?.attempts ?? [];
const uniqueAttackers = new Set([
...activeAttempts.map(
attempt => attempt.userId
),
user.id
]).size;
/*
* Achievement/event tracking for the victim. The current attempt is
* included, while repeated attempts by the same user only count once
* toward the unique-attacker total.
*/
await trackProfileEvent(
interaction,
target.id,
{
type: "steal_targeted",
attackerUserId: user.id,
uniqueAttackers,
attemptsInWindow: activeAttempts.length + 1,
windowSeconds: heatWindowMs / 1000
}
);
const heat = activeAttempts.reduce(
(total, attempt) => {
const timestamp = new Date(
attempt.timestamp
).getTime();
const age = now.getTime() - timestamp;
if (
!Number.isFinite(age) ||
age < 0 ||
age >= heatWindowMs
) {
return total;
}
const remainingWeight =
1 - age / heatWindowMs;
return total +
heatPerAttempt *
remainingWeight;
},
0
);
const wealthDifference = Math.log10(
(theirCookies + 1) /
(yourCookies + 1)
);
const unheatedSuccessChance = Math.max(
minSuccessChance,
Math.min(
maxSuccessChance,
baseSuccessChance +
wealthDifference *
wealthChanceScale
)
);
const successChance = Math.max(
minSuccessChance,
unheatedSuccessChance - heat
);
const success = Math.random() < successChance;
if (success) {
/*
* Reward scales inversely with final success chance:
* 80% chance -> 0.75x loot range
* 55% chance -> 1.00x loot range
* 30% chance -> 1.25x loot range
*/
const chancePosition =
(successChance - minSuccessChance) /
(maxSuccessChance - minSuccessChance);
const rewardMultiplier =
maxRewardMultiplier -
chancePosition *
(maxRewardMultiplier - minRewardMultiplier);
const lootRoll = Math.random();
const lootTier =
stealTiers.find(
tier => lootRoll < tier.threshold
) ?? stealTiers[stealTiers.length - 1];
const stealPercentage = randomFloat(
lootTier.min * rewardMultiplier,
lootTier.max * rewardMultiplier
);
const requestedAmount = Math.max(
1,
Math.min(
Math.floor(
theirCookies *
stealPercentage
),
theirCookies,
maxStealAmount,
stealable.amount
)
);
/*
* Reserve from the hourly stealable budget atomically before the
* transfer. Concurrent robberies cannot reserve more than remains.
*/
const reservation = await cookies.updateOne(
{
userId: target.id,
"stealable.windowStartedAt": new Date(
stealable.windowStartedAt
),
"stealable.amount": {
$gte: requestedAmount
}
},
{
$inc: {
"stealable.amount": -requestedAmount
}
}
);
if (reservation.modifiedCount !== 1) {
return interaction.reply({
content: `${target}'s robbery protection changed before you could finish the heist. No Cookies were taken.`,
flags: MessageFlags.Ephemeral
});
}
try {
await transferCookies(
target.id,
user.id,
requestedAmount
);
} catch (error) {
/*
* Refund the reservation when the balance transfer cannot be
* completed. The fixed window timestamp remains unchanged.
*/
await cookies.updateOne(
{
userId: target.id,
"stealable.windowStartedAt": new Date(
stealable.windowStartedAt
)
},
{
$inc: {
"stealable.amount": requestedAmount
}
}
);
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;
}
const amount = requestedAmount;
await trackProfileEvent(
interaction,
user.id,
{
type: "steal_success",
source: "steal",
amount,
targetUserId: target.id
}
);
await trackProfileEvent(
interaction,
user.id,
{
type: "cookie_gain",
source: "steal",
amount,
targetUserId: target.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "stolen_from_me",
source: "steal",
amount,
thiefUserId: user.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_loss",
source: "steal",
amount,
thiefUserId: user.id
}
);
const [
yourBalanceAfter,
targetBalanceAfter,
updatedTargetCookieRecord
] = await Promise.all([
getCookieBalance(user.id),
getCookieBalance(target.id),
cookies.findOne(
{
userId: target.id
},
{
projection: {
stealable: 1
}
}
)
]);
const actualPercentage =
amount /
theirCookies *
100;
const remainingStealable = Math.max(
0,
updatedTargetCookieRecord?.stealable?.amount ?? 0
);
const embed = new EmbedBuilder()
.setTitle("🥷 Robbery Successful!")
.setDescription(
[
vaultBreachText,
randomStealSuccessResponse(
amount,
target
)
]
.filter(Boolean)
.join("\n\n")
)
.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: "Vault Exposed",
value: `**${remainingStealable.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 scales with final success chance:
* 30% chance -> 1.5% to 7.5%
* 55% chance -> 2.25% to 11.25%
* 80% chance -> 3% to 15%
*/
const chancePosition =
(successChance - minSuccessChance) /
(maxSuccessChance - minSuccessChance);
const penaltyMultiplier =
minPenaltyMultiplier +
chancePosition *
(maxPenaltyMultiplier - minPenaltyMultiplier);
const penaltyPercentage = randomFloat(
baseFailurePenaltyMin * penaltyMultiplier,
baseFailurePenaltyMax * penaltyMultiplier
);
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;
}
await trackProfileEvent(
interaction,
user.id,
{
type: "cookie_loss",
source: "steal_penalty",
amount: penalty,
targetUserId: target.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_gain",
source: "steal_penalty",
amount: penalty,
thiefUserId: user.id
}
);
const [
yourBalanceAfter,
targetBalanceAfter,
updatedTargetCookieRecord
] = await Promise.all([
getCookieBalance(user.id),
getCookieBalance(target.id),
cookies.findOne(
{
userId: target.id
},
{
projection: {
stealable: 1
}
}
)
]);
const actualPenaltyPercentage =
penalty /
yourCookies *
100;
const remainingStealable = Math.max(
0,
updatedTargetCookieRecord?.stealable?.amount ?? 0
);
const embed = new EmbedBuilder()
.setTitle("🚨 Robbery Failed!")
.setDescription(
[
vaultBreachText,
randomStealFailResponse(
penalty,
target
)
]
.filter(Boolean)
.join("\n\n")
)
.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: "Vault Exposed",
value: `**${remainingStealable.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;
}
await trackProfileEvent(
interaction,
user.id,
{
type: "cookies_given",
source: "give",
amount,
targetUserId: target.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_gain",
source: "give",
amount,
senderUserId: user.id
}
);
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
} = getCookieCommandCollections();
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]
});
}
/*
* ============================================================
* COLLECTION ACCESS
* ============================================================
*
* getCollections() already exposes the Cookie collection in the existing
* application. The fallbacks below allow either camelCase or raw Mongo
* collection names for the two collections used by this command.
*/
function getCookieCommandCollections() {
const collections = getCollections();
const database =
collections.db ??
collections.database;
const cookies =
collections.cookies ??
collections.itemsCookies ??
collections.items_cookies ??
database?.collection?.("items_cookies");
if (!cookies) {
throw new Error(
"getCollections() must expose the items_cookies collection as cookies/itemsCookies/items_cookies, or expose a db handle."
);
}
return { cookies };
}
/*
* ============================================================
* 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);
}
/*
* ============================================================
* LEADERBOARD MEDALS
* ============================================================
*/
function getLeaderboardMedal(index) {
switch (index) {
case 0:
return "🥇";
case 1:
return "🥈";
case 2:
return "🥉";
default:
return `**${index + 1}.**`;
}
}