Files
ShaiBot/commands/applications/cookies.js
T

844 lines
18 KiB
JavaScript

const {
SlashCommandBuilder,
EmbedBuilder
} = 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, session = undefined) {
const { cookies } = getCollections();
return cookies.updateOne(
{
userId
},
{
$inc: {
cookies: amount
},
$setOnInsert: {
userId
}
},
{
upsert: true,
session
}
);
}
/*
* COOKIE TRANSFER
*
* This uses a transaction so both balances update together.
*
* MongoDB transactions require Atlas or a replica set.
*/
async function transferCookies(fromUserId, toUserId, amount) {
const { cookies } = getCollections();
const session = mClient.startSession();
try {
await session.withTransaction(async () => {
/*
* Only debit if the sender still has enough cookies.
*
* This protects against two commands being executed
* at almost the same time.
*/
const debit = await cookies.updateOne(
{
userId: fromUserId,
cookies: {
$gte: amount
}
},
{
$inc: {
cookies: -amount
}
},
{
session
}
);
if (debit.modifiedCount !== 1) {
throw new Error("INSUFFICIENT_COOKIES");
}
await cookies.updateOne(
{
userId: toUserId
},
{
$inc: {
cookies: amount
},
$setOnInsert: {
userId: toUserId
}
},
{
upsert: true,
session
}
);
});
} finally {
await session.endSession();
}
}
/*
* 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
);
/*
* Can't rob yourself.
*/
if (target.id === user.id) {
return interaction.reply({
content:
"C'mon, don't embarrass yourself. You can't steal from yourself.",
ephemeral: true
});
}
/*
* Don't let people farm bot accounts.
*/
if (target.bot) {
return interaction.reply({
content:
"Bots have highly sophisticated cookie security systems.",
ephemeral: true
});
}
const now = getUnixTime();
/*
* Check steal cooldown before doing anything expensive.
*/
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>.`,
ephemeral: true
});
}
/*
* Fetch both balances concurrently.
*/
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.",
ephemeral: true
});
}
if (theirCookies < 1) {
return interaction.reply({
content:
`Shame on you! ${target} doesn't have any Cookies. D:`,
ephemeral: true
});
}
/*
* Chaos mode:
* fixed 50% steal chance.
*/
const success =
Math.random() < STEAL_SUCCESS_CHANCE;
/*
* Successful robbery.
*/
if (success) {
const maxSteal = Math.max(
1,
Math.ceil(theirCookies / 4)
);
const amount = Math.min(
theirCookies,
randomInteger(
1,
maxSteal
)
);
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.`,
ephemeral: true
});
}
throw error;
}
const nextSteal = await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
return interaction.reply({
content:
`${randomStealSuccessResponse(amount, target)}\n` +
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` +
`Next robbery <t:${nextSteal}:R>`
});
}
/*
* Failed robbery.
*
* Lose between 1 and roughly 1/6 of your balance.
*/
const maxPenalty = Math.max(
1,
Math.ceil(yourCookies / 6)
);
const penalty = Math.min(
yourCookies,
randomInteger(
1,
maxPenalty
)
);
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.",
ephemeral: true
});
}
throw error;
}
const nextSteal = await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
return interaction.reply({
content:
`${randomStealFailResponse(penalty, target)}\n` +
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` +
`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);
}
/*
* 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}.**`;
}
}