1282 lines
35 KiB
JavaScript
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}.**`;
|
|
}
|
|
}
|