1903 lines
39 KiB
JavaScript
1903 lines
39 KiB
JavaScript
const {
|
||
SlashCommandBuilder,
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle,
|
||
MessageFlags
|
||
} = require("discord.js");
|
||
|
||
const { randomUUID } = require("node:crypto");
|
||
|
||
const {
|
||
getCollections,
|
||
getCookieBalance,
|
||
changeCookies,
|
||
transferCookies,
|
||
getCooldown,
|
||
setCooldown,
|
||
getUnixTime,
|
||
randomInteger,
|
||
randomFloat,
|
||
randomItem,
|
||
formatPercentage
|
||
} = require("../../utils/cookieEconomy");
|
||
|
||
|
||
/*
|
||
* CONFIG
|
||
*/
|
||
|
||
const COLLECT_COOLDOWN =
|
||
60 * 60; // 1 hour
|
||
|
||
const STEAL_COOLDOWN =
|
||
15 * 60; // 15 minutes
|
||
|
||
const BOSS_SPAWN_COST =
|
||
500;
|
||
|
||
const BOSS_JOIN_DURATION =
|
||
30 * 60; // 30 minutes
|
||
|
||
const BOSS_MINIMUM_PLAYERS =
|
||
3;
|
||
|
||
|
||
/*
|
||
* Legacy Abra bonus
|
||
*/
|
||
|
||
const ABRA_ID =
|
||
"378135175059603457";
|
||
|
||
const ABRA_MULTIPLIER =
|
||
1.5;
|
||
|
||
|
||
/*
|
||
* COMMAND
|
||
*/
|
||
|
||
module.exports = {
|
||
data: new SlashCommandBuilder()
|
||
.setName("cookies")
|
||
.setDescription(
|
||
"The ShaiBot cookie economy."
|
||
)
|
||
|
||
/*
|
||
* /cookies collect
|
||
*/
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("collect")
|
||
.setDescription(
|
||
"Collect a fresh batch of cookies."
|
||
)
|
||
)
|
||
|
||
/*
|
||
* /cookies steal
|
||
*/
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("steal")
|
||
.setDescription(
|
||
"Attempt to steal another user's cookies."
|
||
)
|
||
|
||
.addUserOption(option =>
|
||
option
|
||
.setName("target")
|
||
.setDescription(
|
||
"The unfortunate victim."
|
||
)
|
||
.setRequired(true)
|
||
)
|
||
)
|
||
|
||
/*
|
||
* /cookies boss spawn
|
||
*/
|
||
|
||
.addSubcommandGroup(group =>
|
||
group
|
||
.setName("boss")
|
||
.setDescription(
|
||
"Cookie Boss commands."
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("spawn")
|
||
.setDescription(
|
||
`Spawn a Cookie Boss for ${BOSS_SPAWN_COST} Cookies.`
|
||
)
|
||
)
|
||
)
|
||
|
||
/*
|
||
* /cookies check
|
||
*/
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("check")
|
||
.setDescription(
|
||
"Check a cookie balance."
|
||
)
|
||
|
||
.addUserOption(option =>
|
||
option
|
||
.setName("target")
|
||
.setDescription(
|
||
"The user whose cookies you want to check."
|
||
)
|
||
)
|
||
)
|
||
|
||
/*
|
||
* /cookies give
|
||
*/
|
||
|
||
.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)
|
||
)
|
||
)
|
||
|
||
/*
|
||
* /cookies leaderboard
|
||
*/
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("leaderboard")
|
||
.setDescription(
|
||
"Show the richest cookie collectors."
|
||
)
|
||
),
|
||
|
||
|
||
async execute(interaction) {
|
||
const group =
|
||
interaction.options.getSubcommandGroup(
|
||
false
|
||
);
|
||
|
||
const subcommand =
|
||
interaction.options.getSubcommand();
|
||
|
||
|
||
/*
|
||
* Subcommand groups
|
||
*/
|
||
|
||
if (
|
||
group === "boss" &&
|
||
subcommand === "spawn"
|
||
) {
|
||
return spawnBoss(
|
||
interaction
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Normal subcommands
|
||
*/
|
||
|
||
switch (subcommand) {
|
||
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 cookies command.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* /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> 🍪`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
let amount =
|
||
randomInteger(
|
||
1,
|
||
20
|
||
);
|
||
|
||
|
||
/*
|
||
* Legacy Abra bonus
|
||
*/
|
||
|
||
if (
|
||
userId ===
|
||
ABRA_ID
|
||
) {
|
||
amount =
|
||
Math.ceil(
|
||
amount *
|
||
ABRA_MULTIPLIER
|
||
);
|
||
}
|
||
|
||
|
||
await changeCookies(
|
||
userId,
|
||
amount
|
||
);
|
||
|
||
|
||
const nextCollect =
|
||
await setCooldown(
|
||
userId,
|
||
guildId,
|
||
"collect",
|
||
COLLECT_COOLDOWN
|
||
);
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
userId
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Fresh Batch"
|
||
)
|
||
.setDescription(
|
||
randomCollectResponse(
|
||
amount,
|
||
interaction.user
|
||
)
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Collected",
|
||
|
||
value:
|
||
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Next Batch",
|
||
|
||
value:
|
||
`<t:${nextCollect}:R>`,
|
||
|
||
inline:
|
||
false
|
||
}
|
||
)
|
||
.setThumbnail(
|
||
interaction.user.displayAvatarURL({
|
||
size: 256
|
||
})
|
||
);
|
||
|
||
|
||
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:
|
||
"You can't steal from yourself.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ShaiBot itself is reserved for
|
||
* The Crumb Saucer Vault Heist.
|
||
*/
|
||
|
||
if (
|
||
target.id ===
|
||
interaction.client.user.id
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"ShaiBot's vault requires a proper Vault Heist. Visit `/crumbsaucer`.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
if (target.bot) {
|
||
return interaction.reply({
|
||
content:
|
||
"Other bots are protected by Cookie Law.",
|
||
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:
|
||
`${target} doesn't have any Cookies.`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* Dynamic success chance
|
||
*/
|
||
|
||
const successChance =
|
||
getStealChance(
|
||
yourCookies,
|
||
theirCookies
|
||
);
|
||
|
||
|
||
const success =
|
||
Math.random() <
|
||
successChance;
|
||
|
||
|
||
let embed;
|
||
|
||
|
||
/*
|
||
* ========================================================
|
||
* 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;
|
||
}
|
||
|
||
|
||
const [
|
||
yourBalanceAfter,
|
||
targetBalanceAfter
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
user.id
|
||
),
|
||
|
||
getCookieBalance(
|
||
target.id
|
||
)
|
||
]);
|
||
|
||
|
||
const actualPercentage =
|
||
(
|
||
amount /
|
||
theirCookies
|
||
) *
|
||
100;
|
||
|
||
|
||
embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🥷 Cookie 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.displayName}'s Vault`,
|
||
|
||
value:
|
||
`**${targetBalanceAfter.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
.setThumbnail(
|
||
target.displayAvatarURL({
|
||
size: 256
|
||
})
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ========================================================
|
||
* FAILURE
|
||
* ========================================================
|
||
*/
|
||
|
||
else {
|
||
const penaltyPercentage =
|
||
randomFloat(
|
||
0.01,
|
||
0.08
|
||
);
|
||
|
||
|
||
const penalty =
|
||
Math.max(
|
||
1,
|
||
|
||
Math.min(
|
||
yourCookies,
|
||
|
||
Math.floor(
|
||
yourCookies *
|
||
penaltyPercentage
|
||
)
|
||
)
|
||
);
|
||
|
||
|
||
try {
|
||
/*
|
||
* Normal steal failures reward
|
||
* the victim.
|
||
*/
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
const [
|
||
yourBalanceAfter,
|
||
targetBalanceAfter
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
user.id
|
||
),
|
||
|
||
getCookieBalance(
|
||
target.id
|
||
)
|
||
]);
|
||
|
||
|
||
const actualPenaltyPercentage =
|
||
(
|
||
penalty /
|
||
yourCookies
|
||
) *
|
||
100;
|
||
|
||
|
||
embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🚨 Cookie Robbery Failed"
|
||
)
|
||
.setDescription(
|
||
randomStealFailResponse(
|
||
penalty,
|
||
target
|
||
)
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Penalty",
|
||
|
||
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.displayName}'s Vault`,
|
||
|
||
value:
|
||
`**${targetBalanceAfter.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
.setThumbnail(
|
||
target.displayAvatarURL({
|
||
size: 256
|
||
})
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Apply cooldown
|
||
*/
|
||
|
||
const nextSteal =
|
||
await setCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal",
|
||
STEAL_COOLDOWN
|
||
);
|
||
|
||
|
||
embed.addFields({
|
||
name:
|
||
"Next Robbery",
|
||
|
||
value:
|
||
`<t:${nextSteal}:R>`,
|
||
|
||
inline:
|
||
false
|
||
});
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* /cookies boss spawn
|
||
* ============================================================
|
||
*/
|
||
|
||
async function spawnBoss(interaction) {
|
||
if (!interaction.guild) {
|
||
return interaction.reply({
|
||
content:
|
||
"Cookie Bosses can only be summoned inside a server.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
const events =
|
||
db.collection(
|
||
"items_events"
|
||
);
|
||
|
||
|
||
/*
|
||
* Only one unresolved Cookie Boss per guild.
|
||
*/
|
||
|
||
const existing =
|
||
await events.findOne({
|
||
eventType:
|
||
"CookieBoss",
|
||
|
||
guildId:
|
||
interaction.guildId,
|
||
|
||
state: {
|
||
$in: [
|
||
"Joining",
|
||
"Starting",
|
||
"Active",
|
||
"Resolving"
|
||
]
|
||
}
|
||
});
|
||
|
||
|
||
if (existing) {
|
||
return interaction.reply({
|
||
content:
|
||
"There is already an active Cookie Boss in this server.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (
|
||
balance <
|
||
BOSS_SPAWN_COST
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
`You need **${BOSS_SPAWN_COST.toLocaleString()} Cookies** to summon the boss.`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* Unique boss encounter.
|
||
*/
|
||
|
||
const eventId =
|
||
randomUUID();
|
||
|
||
|
||
/*
|
||
* This virtual account holds all committed
|
||
* Cookie Boss currency until the event resolves.
|
||
*/
|
||
|
||
const escrowId =
|
||
`boss:${eventId}`;
|
||
|
||
|
||
const now =
|
||
getUnixTime();
|
||
|
||
|
||
const joinUntil =
|
||
now +
|
||
BOSS_JOIN_DURATION;
|
||
|
||
|
||
/*
|
||
* Spawner cost seeds the reward pool.
|
||
*/
|
||
|
||
try {
|
||
await transferCookies(
|
||
interaction.user.id,
|
||
escrowId,
|
||
BOSS_SPAWN_COST
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"Your Cookie balance changed before the boss could be summoned.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Initial recruitment embed
|
||
*/
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 The Crumbbound Behemoth Appears!"
|
||
)
|
||
.setDescription(
|
||
[
|
||
"*Drawn by the scent of an irresponsibly large Cookie Vault,*",
|
||
"*the Crumbbound Behemoth has lumbered into Shaiwase.*",
|
||
"",
|
||
`**Recruitment ends:** <t:${joinUntil}:R>`,
|
||
`**Minimum adventurers:** ${BOSS_MINIMUM_PLAYERS}`,
|
||
`**Participants:** 0/${BOSS_MINIMUM_PLAYERS}`,
|
||
`**Reward Pool:** ${BOSS_SPAWN_COST.toLocaleString()} 🍪`,
|
||
"",
|
||
"Choose how many Cookies you want to commit to the fight.",
|
||
"Your tier determines your temporary HP and Power.",
|
||
"",
|
||
"If enough players join, the summoner may start the battle early."
|
||
].join("\n")
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`cookieBoss:${eventId}`
|
||
});
|
||
|
||
|
||
/*
|
||
* Join tier buttons
|
||
*/
|
||
|
||
const joinRow =
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieBossJoinScout"
|
||
)
|
||
.setLabel(
|
||
"Scout — 100"
|
||
)
|
||
.setEmoji(
|
||
"🍪"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieBossJoinFighter"
|
||
)
|
||
.setLabel(
|
||
"Fighter — 500"
|
||
)
|
||
.setEmoji(
|
||
"⚔️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieBossJoinChampion"
|
||
)
|
||
.setLabel(
|
||
"Champion — 1,000"
|
||
)
|
||
.setEmoji(
|
||
"🔥"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
)
|
||
);
|
||
|
||
|
||
/*
|
||
* Summoner early-start button.
|
||
*/
|
||
|
||
const startRow =
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieBossStart"
|
||
)
|
||
.setLabel(
|
||
"Start Battle"
|
||
)
|
||
.setEmoji(
|
||
"⚔️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
)
|
||
);
|
||
|
||
|
||
let message;
|
||
|
||
|
||
try {
|
||
message =
|
||
await interaction.reply({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components: [
|
||
joinRow,
|
||
startRow
|
||
],
|
||
|
||
fetchReply:
|
||
true
|
||
});
|
||
|
||
} catch (error) {
|
||
/*
|
||
* Refund spawn cost if Discord message creation fails.
|
||
*/
|
||
|
||
try {
|
||
await transferCookies(
|
||
escrowId,
|
||
interaction.user.id,
|
||
BOSS_SPAWN_COST
|
||
);
|
||
} catch (refundError) {
|
||
console.error(
|
||
"CRITICAL: Failed to refund boss spawn after message failure:",
|
||
refundError
|
||
);
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Persist encounter.
|
||
*/
|
||
|
||
try {
|
||
await events.insertOne({
|
||
eventType:
|
||
"CookieBoss",
|
||
|
||
eventId,
|
||
|
||
escrowId,
|
||
|
||
guildId:
|
||
interaction.guildId,
|
||
|
||
channelId:
|
||
interaction.channelId,
|
||
|
||
messageId:
|
||
message.id,
|
||
|
||
creatorId:
|
||
interaction.user.id,
|
||
|
||
state:
|
||
"Joining",
|
||
|
||
createdAt:
|
||
now,
|
||
|
||
joinUntil,
|
||
|
||
battleStartedAt:
|
||
null,
|
||
|
||
battleEndsAt:
|
||
null,
|
||
|
||
minimumParticipants:
|
||
BOSS_MINIMUM_PLAYERS,
|
||
|
||
rewardPool:
|
||
BOSS_SPAWN_COST,
|
||
|
||
boss: {
|
||
bossId:
|
||
"crumbbound-behemoth",
|
||
|
||
name:
|
||
"The Crumbbound Behemoth",
|
||
|
||
health:
|
||
15000,
|
||
|
||
maxHealth:
|
||
15000,
|
||
|
||
rage:
|
||
0
|
||
},
|
||
|
||
participants:
|
||
{}
|
||
});
|
||
|
||
} catch (error) {
|
||
/*
|
||
* If Mongo fails after the message was posted,
|
||
* remove the message and refund the creator.
|
||
*/
|
||
|
||
try {
|
||
await message.delete();
|
||
} catch {}
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
escrowId,
|
||
interaction.user.id,
|
||
BOSS_SPAWN_COST
|
||
);
|
||
} catch (refundError) {
|
||
console.error(
|
||
"CRITICAL: Failed to refund boss spawn after database failure:",
|
||
refundError
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* /cookies check
|
||
* ============================================================
|
||
*/
|
||
|
||
async function check(interaction) {
|
||
const target =
|
||
interaction.options.getUser(
|
||
"target"
|
||
)
|
||
??
|
||
interaction.user;
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
target.id
|
||
);
|
||
|
||
|
||
const isHouse =
|
||
target.id ===
|
||
interaction.client.user.id;
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
isHouse
|
||
? "🏦 ShaiBot Cookie Vault"
|
||
: "🍪 Cookie Vault"
|
||
)
|
||
.setDescription(
|
||
isHouse
|
||
? `The house currently holds **${balance.toLocaleString()} Cookies**.`
|
||
: `${target} currently has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**.`
|
||
)
|
||
.setThumbnail(
|
||
target.displayAvatarURL({
|
||
size: 256
|
||
})
|
||
);
|
||
|
||
|
||
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
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* Keep bot deposits tied to the game systems.
|
||
*/
|
||
|
||
if (target.bot) {
|
||
return interaction.reply({
|
||
content:
|
||
"Bots don't accept direct Cookie transfers. Visit `/crumbsaucer` if you'd like to donate to the house involuntarily.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
user.id,
|
||
target.id,
|
||
amount
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"You don't have enough Cookies.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
const [
|
||
yourBalance,
|
||
theirBalance
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
user.id
|
||
),
|
||
|
||
getCookieBalance(
|
||
target.id
|
||
)
|
||
]);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Cookies Shared"
|
||
)
|
||
.setDescription(
|
||
`${user} gave ${target} **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**.`
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`**${yourBalance.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
`${target.displayName}'s Vault`,
|
||
|
||
value:
|
||
`**${theirBalance.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
.setThumbnail(
|
||
target.displayAvatarURL({
|
||
size: 256
|
||
})
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* /cookies leaderboard
|
||
* ============================================================
|
||
*/
|
||
|
||
async function leaderboard(interaction) {
|
||
const {
|
||
cookies
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
/*
|
||
* Restrict to Discord snowflake-style user IDs.
|
||
*
|
||
* This prevents temporary boss escrow accounts
|
||
* such as:
|
||
*
|
||
* boss:<uuid>
|
||
*
|
||
* from appearing on the leaderboard.
|
||
*/
|
||
|
||
const results =
|
||
await cookies
|
||
.find({
|
||
userId: {
|
||
$regex:
|
||
/^\d+$/
|
||
},
|
||
|
||
cookies: {
|
||
$gt:
|
||
0
|
||
}
|
||
})
|
||
.sort({
|
||
cookies:
|
||
-1
|
||
})
|
||
.limit(15)
|
||
.toArray();
|
||
|
||
|
||
/*
|
||
* Exclude ShaiBot itself from the player
|
||
* leaderboard.
|
||
*/
|
||
|
||
const playerResults =
|
||
results
|
||
.filter(
|
||
record =>
|
||
record.userId !==
|
||
interaction.client.user.id
|
||
)
|
||
.slice(
|
||
0,
|
||
10
|
||
);
|
||
|
||
|
||
const description =
|
||
playerResults.length
|
||
? playerResults
|
||
.map(
|
||
(
|
||
record,
|
||
index
|
||
) => {
|
||
return (
|
||
`${getLeaderboardMedal(index)} ` +
|
||
`<@${record.userId}> — ` +
|
||
`**${record.cookies.toLocaleString()}** 🍪`
|
||
);
|
||
}
|
||
)
|
||
.join("\n")
|
||
|
||
: "Nobody has collected any Cookies yet.";
|
||
|
||
|
||
const houseBalance =
|
||
await getCookieBalance(
|
||
interaction.client.user.id
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Cookie Leaderboard"
|
||
)
|
||
.setDescription(
|
||
description
|
||
)
|
||
.addFields({
|
||
name:
|
||
"🏦 ShaiBot House Vault",
|
||
|
||
value:
|
||
`**${houseBalance.toLocaleString()} 🍪**`
|
||
})
|
||
.setTimestamp();
|
||
|
||
|
||
if (
|
||
interaction.guild?.iconURL()
|
||
) {
|
||
embed.setThumbnail(
|
||
interaction.guild.iconURL({
|
||
size: 256
|
||
})
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* STEAL HELPERS
|
||
* ============================================================
|
||
*/
|
||
|
||
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;
|
||
|
||
|
||
/*
|
||
* Compare attacker wealth to target wealth.
|
||
*/
|
||
|
||
const wealthRatio =
|
||
attackerCookies /
|
||
Math.max(
|
||
targetCookies,
|
||
1
|
||
);
|
||
|
||
|
||
/*
|
||
* Poorer attackers receive a modest
|
||
* catch-up bonus.
|
||
*/
|
||
|
||
const povertyBonus =
|
||
wealthRatio <
|
||
1
|
||
? (
|
||
1 -
|
||
wealthRatio
|
||
)
|
||
*
|
||
0.18
|
||
|
||
: 0;
|
||
|
||
|
||
/*
|
||
* Rich attackers targeting poorer users
|
||
* receive an anti-bullying 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
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
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%:
|
||
* 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
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* 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
|
||
* ============================================================
|
||
*/
|
||
|
||
function getLeaderboardMedal(
|
||
index
|
||
) {
|
||
switch (index) {
|
||
case 0:
|
||
return "🥇";
|
||
|
||
case 1:
|
||
return "🥈";
|
||
|
||
case 2:
|
||
return "🥉";
|
||
|
||
default:
|
||
return `**${index + 1}.**`;
|
||
}
|
||
} |