1982 lines
37 KiB
JavaScript
1982 lines
37 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;
|
||
|
||
const STEAL_COOLDOWN =
|
||
15 * 60;
|
||
|
||
const SLOT_MIN_BET =
|
||
10;
|
||
|
||
const SLOT_MAX_BET =
|
||
1000;
|
||
|
||
const BOSS_SPAWN_COST =
|
||
500;
|
||
|
||
const BOSS_JOIN_DURATION =
|
||
30 * 60;
|
||
|
||
const BOSS_MINIMUM_PLAYERS =
|
||
5;
|
||
|
||
|
||
const ABRA_ID =
|
||
"378135175059603457";
|
||
|
||
const ABRA_MULTIPLIER =
|
||
1.5;
|
||
|
||
|
||
/*
|
||
* SLOT TABLE
|
||
*
|
||
* RTP ≈ 94%
|
||
*/
|
||
|
||
const SLOT_OUTCOMES = [
|
||
{
|
||
weight: 300,
|
||
type: "loss",
|
||
multiplier: 0
|
||
},
|
||
|
||
{
|
||
weight: 390,
|
||
type: "pair",
|
||
multiplier: 0.5
|
||
},
|
||
|
||
{
|
||
weight: 150,
|
||
type: "triple",
|
||
symbol: "🍋",
|
||
multiplier: 1.5
|
||
},
|
||
|
||
{
|
||
weight: 80,
|
||
type: "triple",
|
||
symbol: "🍒",
|
||
multiplier: 2
|
||
},
|
||
|
||
{
|
||
weight: 45,
|
||
type: "triple",
|
||
symbol: "🍪",
|
||
multiplier: 3
|
||
},
|
||
|
||
{
|
||
weight: 25,
|
||
type: "triple",
|
||
symbol: "⭐",
|
||
multiplier: 5
|
||
},
|
||
|
||
{
|
||
weight: 10,
|
||
type: "triple",
|
||
symbol: "💎",
|
||
multiplier: 10
|
||
}
|
||
];
|
||
|
||
|
||
const SLOT_SYMBOLS = [
|
||
"🍒",
|
||
"🍋",
|
||
"🍪",
|
||
"⭐",
|
||
"💎"
|
||
];
|
||
|
||
|
||
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("heist")
|
||
.setDescription(
|
||
"Attempt to breach the ShaiBot Cookie Vault."
|
||
)
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("slots")
|
||
.setDescription(
|
||
"Play the Shaiwase Cookie Slots."
|
||
)
|
||
|
||
.addIntegerOption(option =>
|
||
option
|
||
.setName("bet")
|
||
.setDescription(
|
||
"How many Cookies to wager."
|
||
)
|
||
.setRequired(true)
|
||
.setMinValue(
|
||
SLOT_MIN_BET
|
||
)
|
||
.setMaxValue(
|
||
SLOT_MAX_BET
|
||
)
|
||
)
|
||
)
|
||
|
||
.addSubcommandGroup(group =>
|
||
group
|
||
.setName("boss")
|
||
.setDescription(
|
||
"Cookie Boss commands."
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("spawn")
|
||
.setDescription(
|
||
`Spawn a Cookie Boss for ${BOSS_SPAWN_COST} Cookies.`
|
||
)
|
||
)
|
||
)
|
||
|
||
.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) {
|
||
const group =
|
||
interaction.options
|
||
.getSubcommandGroup(false);
|
||
|
||
const subcommand =
|
||
interaction.options
|
||
.getSubcommand();
|
||
|
||
|
||
if (
|
||
group === "boss" &&
|
||
subcommand === "spawn"
|
||
) {
|
||
return spawnBoss(
|
||
interaction
|
||
);
|
||
}
|
||
|
||
|
||
switch (subcommand) {
|
||
case "collect":
|
||
return collect(
|
||
interaction
|
||
);
|
||
|
||
case "steal":
|
||
return steal(
|
||
interaction
|
||
);
|
||
|
||
case "heist":
|
||
return heist(
|
||
interaction
|
||
);
|
||
|
||
case "slots":
|
||
return slots(
|
||
interaction
|
||
);
|
||
|
||
case "check":
|
||
return check(
|
||
interaction
|
||
);
|
||
|
||
case "give":
|
||
return give(
|
||
interaction
|
||
);
|
||
|
||
case "leaderboard":
|
||
return leaderboard(
|
||
interaction
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/*
|
||
* COLLECT
|
||
*/
|
||
|
||
async function collect(interaction) {
|
||
const userId =
|
||
interaction.user.id;
|
||
|
||
const now =
|
||
getUnixTime();
|
||
|
||
|
||
const cooldown =
|
||
await getCooldown(
|
||
userId,
|
||
interaction.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
|
||
);
|
||
|
||
|
||
if (
|
||
userId ===
|
||
ABRA_ID
|
||
) {
|
||
amount =
|
||
Math.ceil(
|
||
amount *
|
||
ABRA_MULTIPLIER
|
||
);
|
||
}
|
||
|
||
|
||
await changeCookies(
|
||
userId,
|
||
amount
|
||
);
|
||
|
||
|
||
await setCooldown(
|
||
userId,
|
||
interaction.guildId,
|
||
"collect",
|
||
COLLECT_COOLDOWN
|
||
);
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
userId
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Fresh Batch"
|
||
)
|
||
.setDescription(
|
||
randomCollectResponse(
|
||
amount,
|
||
interaction.user
|
||
)
|
||
)
|
||
.addFields({
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} Cookies**`
|
||
});
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* NORMAL PLAYER STEAL
|
||
*/
|
||
|
||
async function steal(interaction) {
|
||
const user =
|
||
interaction.user;
|
||
|
||
const target =
|
||
interaction.options.getUser(
|
||
"target",
|
||
true
|
||
);
|
||
|
||
|
||
if (
|
||
target.id ===
|
||
user.id
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"You can't steal from yourself.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ShaiBot itself is reserved for /cookies heist.
|
||
*/
|
||
if (
|
||
target.id ===
|
||
interaction.client.user.id
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"The ShaiBot Vault requires a proper `/cookies heist`.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
if (target.bot) {
|
||
return interaction.reply({
|
||
content:
|
||
"Other bots are protected by Cookie Law.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const now =
|
||
getUnixTime();
|
||
|
||
|
||
const cooldown =
|
||
await getCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal"
|
||
);
|
||
|
||
|
||
if (cooldown > now) {
|
||
return interaction.reply({
|
||
content:
|
||
`You can steal again <t:${cooldown}:R>.`,
|
||
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:
|
||
`${target} doesn't have any Cookies.`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const successChance =
|
||
getStealChance(
|
||
yourCookies,
|
||
theirCookies
|
||
);
|
||
|
||
|
||
const success =
|
||
Math.random() <
|
||
successChance;
|
||
|
||
|
||
let embed;
|
||
|
||
|
||
if (success) {
|
||
const {
|
||
amount
|
||
} =
|
||
getStealAmount(
|
||
theirCookies
|
||
);
|
||
|
||
|
||
await transferCookies(
|
||
target.id,
|
||
user.id,
|
||
amount
|
||
);
|
||
|
||
|
||
const actualPercentage =
|
||
(
|
||
amount /
|
||
theirCookies *
|
||
100
|
||
);
|
||
|
||
|
||
embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🥷 Cookie Robbery Successful"
|
||
)
|
||
.setDescription(
|
||
randomStealSuccessResponse(
|
||
amount,
|
||
target
|
||
)
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Vault Taken",
|
||
|
||
value:
|
||
`**${actualPercentage.toFixed(1)}%**`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Success Chance",
|
||
|
||
value:
|
||
`**${formatPercentage(successChance)}**`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
);
|
||
|
||
} else {
|
||
const penaltyPercentage =
|
||
randomFloat(
|
||
0.01,
|
||
0.08
|
||
);
|
||
|
||
|
||
const penalty =
|
||
Math.max(
|
||
1,
|
||
|
||
Math.min(
|
||
yourCookies,
|
||
|
||
Math.floor(
|
||
yourCookies *
|
||
penaltyPercentage
|
||
)
|
||
)
|
||
);
|
||
|
||
|
||
/*
|
||
* Normal steal failure pays the victim.
|
||
*/
|
||
await transferCookies(
|
||
user.id,
|
||
target.id,
|
||
penalty
|
||
);
|
||
|
||
|
||
embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🚨 Cookie Robbery Failed"
|
||
)
|
||
.setDescription(
|
||
randomStealFailResponse(
|
||
penalty,
|
||
target
|
||
)
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Success Chance",
|
||
|
||
value:
|
||
`**${formatPercentage(successChance)}**`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const nextSteal =
|
||
await setCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal",
|
||
STEAL_COOLDOWN
|
||
);
|
||
|
||
|
||
embed.addFields({
|
||
name:
|
||
"Next Robbery",
|
||
|
||
value:
|
||
`<t:${nextSteal}:R>`
|
||
});
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* HEIST ENTRY
|
||
*/
|
||
|
||
async function heist(interaction) {
|
||
const vaultId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
vault,
|
||
player
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
vaultId
|
||
),
|
||
|
||
getCookieBalance(
|
||
interaction.user.id
|
||
)
|
||
]);
|
||
|
||
|
||
if (player < 1) {
|
||
return interaction.reply({
|
||
content:
|
||
"You need Cookies before attempting a vault heist.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
if (vault < 1) {
|
||
return interaction.reply({
|
||
content:
|
||
"The ShaiBot Vault is currently empty.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const security =
|
||
getSecurityLabel(
|
||
vault
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🏦 ShaiBot Cookie Vault"
|
||
)
|
||
.setDescription(
|
||
[
|
||
`**Vault:** ${vault.toLocaleString()} 🍪`,
|
||
`**Security:** ${security}`,
|
||
"",
|
||
"Choose your approach.",
|
||
"",
|
||
"🥷 **Sneak** — safer, smaller haul.",
|
||
"💻 **Hack** — balanced risk and reward.",
|
||
"💥 **Smash** — dangerous, but lucrative."
|
||
].join("\n")
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`cookieHeist:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
const row =
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieHeistSneak"
|
||
)
|
||
.setLabel(
|
||
"Sneak"
|
||
)
|
||
.setEmoji(
|
||
"🥷"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieHeistHack"
|
||
)
|
||
.setLabel(
|
||
"Hack"
|
||
)
|
||
.setEmoji(
|
||
"💻"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieHeistSmash"
|
||
)
|
||
.setLabel(
|
||
"Smash"
|
||
)
|
||
.setEmoji(
|
||
"💥"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
)
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components: [
|
||
row
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* SLOTS
|
||
*/
|
||
|
||
async function slots(interaction) {
|
||
const bet =
|
||
interaction.options.getInteger(
|
||
"bet",
|
||
true
|
||
);
|
||
|
||
|
||
const playerId =
|
||
interaction.user.id;
|
||
|
||
const vaultId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
playerBalance,
|
||
vaultBalance
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
playerId
|
||
),
|
||
|
||
getCookieBalance(
|
||
vaultId
|
||
)
|
||
]);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
bet
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"You don't have enough Cookies for that bet.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* The house needs enough BEFORE the wager
|
||
* to honor a 10x payout after receiving the bet.
|
||
*/
|
||
if (
|
||
vaultBalance <
|
||
bet * 9
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"The ShaiBot Vault doesn't currently have enough liquidity for that wager.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
await transferCookies(
|
||
playerId,
|
||
vaultId,
|
||
bet
|
||
);
|
||
|
||
|
||
const spinning =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🎰 Shaiwase Cookie Slots"
|
||
)
|
||
.setDescription(
|
||
buildSlotDisplay(
|
||
generateRandomGrid()
|
||
)
|
||
)
|
||
.addFields({
|
||
name:
|
||
"Bet",
|
||
|
||
value:
|
||
`**${bet.toLocaleString()} 🍪**`
|
||
});
|
||
|
||
|
||
await interaction.reply({
|
||
embeds: [
|
||
spinning
|
||
]
|
||
});
|
||
|
||
|
||
await sleep(750);
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
EmbedBuilder
|
||
.from(spinning)
|
||
.setDescription(
|
||
buildSlotDisplay(
|
||
generateRandomGrid()
|
||
)
|
||
)
|
||
]
|
||
});
|
||
|
||
|
||
await sleep(750);
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
EmbedBuilder
|
||
.from(spinning)
|
||
.setDescription(
|
||
buildSlotDisplay(
|
||
generateRandomGrid()
|
||
)
|
||
)
|
||
]
|
||
});
|
||
|
||
|
||
await sleep(750);
|
||
|
||
|
||
const outcome =
|
||
rollSlotOutcome();
|
||
|
||
|
||
const grid =
|
||
generateOutcomeGrid(
|
||
outcome
|
||
);
|
||
|
||
|
||
const payout =
|
||
Math.floor(
|
||
bet *
|
||
outcome.multiplier
|
||
);
|
||
|
||
|
||
if (payout > 0) {
|
||
await transferCookies(
|
||
vaultId,
|
||
playerId,
|
||
payout
|
||
);
|
||
}
|
||
|
||
|
||
const [
|
||
finalPlayer,
|
||
finalVault
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
playerId
|
||
),
|
||
|
||
getCookieBalance(
|
||
vaultId
|
||
)
|
||
]);
|
||
|
||
|
||
const profit =
|
||
payout - bet;
|
||
|
||
|
||
const finalEmbed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🎰 Shaiwase Cookie Slots"
|
||
)
|
||
.setDescription(
|
||
buildSlotDisplay(
|
||
grid
|
||
)
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Result",
|
||
|
||
value:
|
||
getSlotResultName(
|
||
outcome
|
||
)
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Bet",
|
||
|
||
value:
|
||
`${bet.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Multiplier",
|
||
|
||
value:
|
||
`×${outcome.multiplier}`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Payout",
|
||
|
||
value:
|
||
`${payout.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Profit",
|
||
|
||
value:
|
||
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`${finalPlayer.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"House Vault",
|
||
|
||
value:
|
||
`${finalVault.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
);
|
||
|
||
|
||
return interaction.editReply({
|
||
embeds: [
|
||
finalEmbed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* BOSS SPAWN
|
||
*/
|
||
|
||
async function spawnBoss(interaction) {
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
const events =
|
||
db.collection(
|
||
"items_events"
|
||
);
|
||
|
||
|
||
/*
|
||
* One unresolved boss per guild.
|
||
*/
|
||
|
||
const existing =
|
||
await events.findOne({
|
||
eventType:
|
||
"CookieBoss",
|
||
|
||
guildId:
|
||
interaction.guildId,
|
||
|
||
state: {
|
||
$in: [
|
||
"Joining",
|
||
"Active",
|
||
"Starting",
|
||
"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} Cookies** to summon the boss.`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
const eventId =
|
||
randomUUID();
|
||
|
||
const escrowId =
|
||
`boss:${eventId}`;
|
||
|
||
const now =
|
||
getUnixTime();
|
||
|
||
const joinUntil =
|
||
now +
|
||
BOSS_JOIN_DURATION;
|
||
|
||
|
||
/*
|
||
* Spawner fee seeds the encounter pool.
|
||
*/
|
||
|
||
await transferCookies(
|
||
interaction.user.id,
|
||
escrowId,
|
||
BOSS_SPAWN_COST
|
||
);
|
||
|
||
|
||
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}`,
|
||
`**Reward Pool:** ${BOSS_SPAWN_COST.toLocaleString()} 🍪`,
|
||
"",
|
||
"Choose how many Cookies you want to commit to the fight.",
|
||
"Your entry tier determines your temporary HP and Power."
|
||
].join("\n")
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`cookieBoss:${eventId}`
|
||
});
|
||
|
||
|
||
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 — 1000"
|
||
)
|
||
.setEmoji(
|
||
"🔥"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
)
|
||
);
|
||
|
||
|
||
const startRow =
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"cookieBossStart"
|
||
)
|
||
.setLabel(
|
||
"Start Battle"
|
||
)
|
||
.setEmoji(
|
||
"⚔️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
)
|
||
);
|
||
|
||
|
||
const message =
|
||
await interaction.reply({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components: [
|
||
joinRow,
|
||
startRow
|
||
],
|
||
|
||
fetchReply:
|
||
true
|
||
});
|
||
|
||
|
||
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:
|
||
{}
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* 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()} Cookies**.`
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* 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:
|
||
"Use the Cookie games if you want to feed the house.",
|
||
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 embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Cookies Shared"
|
||
)
|
||
.setDescription(
|
||
`${user} gave ${target} **${amount.toLocaleString()} Cookies**.`
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [
|
||
embed
|
||
]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* LEADERBOARD
|
||
*/
|
||
|
||
async function leaderboard(interaction) {
|
||
const {
|
||
cookies
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
/*
|
||
* Only Discord snowflake-style account IDs.
|
||
*
|
||
* This prevents boss escrow accounts from appearing.
|
||
*/
|
||
const results =
|
||
await cookies
|
||
.find({
|
||
userId: {
|
||
$regex:
|
||
/^\d+$/
|
||
},
|
||
|
||
cookies: {
|
||
$gt: 0
|
||
}
|
||
})
|
||
.sort({
|
||
cookies: -1
|
||
})
|
||
.limit(10)
|
||
.toArray();
|
||
|
||
|
||
const description =
|
||
results.length
|
||
? results
|
||
.map(
|
||
(
|
||
record,
|
||
index
|
||
) => {
|
||
return (
|
||
`${getLeaderboardMedal(index)} ` +
|
||
`<@${record.userId}> — ` +
|
||
`**${record.cookies.toLocaleString()}** 🍪`
|
||
);
|
||
}
|
||
)
|
||
.join("\n")
|
||
|
||
: "Nobody has collected any Cookies yet.";
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🍪 Cookie Leaderboard"
|
||
)
|
||
.setDescription(
|
||
description
|
||
)
|
||
.setTimestamp();
|
||
|
||
|
||
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;
|
||
|
||
|
||
const targetPenalty =
|
||
Math.log10(
|
||
targetCookies + 1
|
||
) *
|
||
0.06;
|
||
|
||
|
||
const wealthRatio =
|
||
attackerCookies /
|
||
Math.max(
|
||
targetCookies,
|
||
1
|
||
);
|
||
|
||
|
||
const povertyBonus =
|
||
wealthRatio < 1
|
||
? (
|
||
1 -
|
||
wealthRatio
|
||
) *
|
||
0.18
|
||
|
||
: 0;
|
||
|
||
|
||
const bullyPenalty =
|
||
wealthRatio > 1
|
||
? Math.min(
|
||
Math.log10(
|
||
wealthRatio + 1
|
||
) *
|
||
0.12,
|
||
|
||
0.20
|
||
)
|
||
|
||
: 0;
|
||
|
||
|
||
return Math.max(
|
||
MIN_CHANCE,
|
||
|
||
Math.min(
|
||
MAX_CHANCE,
|
||
|
||
BASE_CHANCE
|
||
-
|
||
targetPenalty
|
||
+
|
||
povertyBonus
|
||
-
|
||
bullyPenalty
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
function getStealAmount(
|
||
targetCookies
|
||
) {
|
||
const MAX_STEAL_AMOUNT =
|
||
5000;
|
||
|
||
|
||
const percentage =
|
||
getStealPercentage();
|
||
|
||
|
||
const amount =
|
||
Math.max(
|
||
1,
|
||
|
||
Math.min(
|
||
Math.floor(
|
||
targetCookies *
|
||
percentage
|
||
),
|
||
|
||
targetCookies,
|
||
MAX_STEAL_AMOUNT
|
||
)
|
||
);
|
||
|
||
|
||
return {
|
||
amount,
|
||
percentage
|
||
};
|
||
}
|
||
|
||
|
||
function getStealPercentage() {
|
||
const roll =
|
||
Math.random();
|
||
|
||
|
||
if (roll < 0.50)
|
||
return randomFloat(
|
||
0.01,
|
||
0.03
|
||
);
|
||
|
||
|
||
if (roll < 0.80)
|
||
return randomFloat(
|
||
0.03,
|
||
0.05
|
||
);
|
||
|
||
|
||
if (roll < 0.95)
|
||
return randomFloat(
|
||
0.05,
|
||
0.08
|
||
);
|
||
|
||
|
||
return randomFloat(
|
||
0.08,
|
||
0.10
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* SLOT HELPERS
|
||
*/
|
||
|
||
function rollSlotOutcome() {
|
||
const total =
|
||
SLOT_OUTCOMES.reduce(
|
||
(
|
||
sum,
|
||
outcome
|
||
) =>
|
||
sum +
|
||
outcome.weight,
|
||
|
||
0
|
||
);
|
||
|
||
|
||
let roll =
|
||
Math.random() *
|
||
total;
|
||
|
||
|
||
for (
|
||
const outcome
|
||
of SLOT_OUTCOMES
|
||
) {
|
||
roll -=
|
||
outcome.weight;
|
||
|
||
if (roll <= 0)
|
||
return outcome;
|
||
}
|
||
|
||
|
||
return SLOT_OUTCOMES[0];
|
||
}
|
||
|
||
|
||
function generateRandomGrid() {
|
||
return Array.from(
|
||
{
|
||
length: 3
|
||
},
|
||
|
||
() =>
|
||
Array.from(
|
||
{
|
||
length: 3
|
||
},
|
||
|
||
() =>
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
function generateOutcomeGrid(
|
||
outcome
|
||
) {
|
||
const grid =
|
||
generateRandomGrid();
|
||
|
||
|
||
if (
|
||
outcome.type ===
|
||
"triple"
|
||
) {
|
||
grid[1] = [
|
||
outcome.symbol,
|
||
outcome.symbol,
|
||
outcome.symbol
|
||
];
|
||
|
||
return grid;
|
||
}
|
||
|
||
|
||
if (
|
||
outcome.type ===
|
||
"pair"
|
||
) {
|
||
const symbol =
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
);
|
||
|
||
let other;
|
||
|
||
do {
|
||
other =
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
);
|
||
} while (
|
||
other === symbol
|
||
);
|
||
|
||
|
||
grid[1] = [
|
||
symbol,
|
||
symbol,
|
||
other
|
||
];
|
||
|
||
return grid;
|
||
}
|
||
|
||
|
||
/*
|
||
* Guaranteed non-pair middle line.
|
||
*/
|
||
|
||
let line;
|
||
|
||
do {
|
||
line = [
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
),
|
||
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
),
|
||
|
||
randomItem(
|
||
SLOT_SYMBOLS
|
||
)
|
||
];
|
||
} while (
|
||
new Set(line).size < 3
|
||
);
|
||
|
||
|
||
grid[1] =
|
||
line;
|
||
|
||
|
||
return grid;
|
||
}
|
||
|
||
|
||
function buildSlotDisplay(
|
||
grid
|
||
) {
|
||
return [
|
||
"```",
|
||
`${grid[0][0]} │ ${grid[0][1]} │ ${grid[0][2]}`,
|
||
`${grid[1][0]} │ ${grid[1][1]} │ ${grid[1][2]}`,
|
||
`${grid[2][0]} │ ${grid[2][1]} │ ${grid[2][2]}`,
|
||
"```"
|
||
].join("\n");
|
||
}
|
||
|
||
|
||
function getSlotResultName(
|
||
outcome
|
||
) {
|
||
if (
|
||
outcome.type ===
|
||
"loss"
|
||
) {
|
||
return "No match";
|
||
}
|
||
|
||
|
||
if (
|
||
outcome.type ===
|
||
"pair"
|
||
) {
|
||
return "Pair";
|
||
}
|
||
|
||
|
||
switch (
|
||
outcome.symbol
|
||
) {
|
||
case "🍋":
|
||
return "Triple Lemon";
|
||
|
||
case "🍒":
|
||
return "Triple Cherry";
|
||
|
||
case "🍪":
|
||
return "Triple Cookie";
|
||
|
||
case "⭐":
|
||
return "Triple Star";
|
||
|
||
case "💎":
|
||
return "💎 JACKPOT 💎";
|
||
|
||
default:
|
||
return "Triple";
|
||
}
|
||
}
|
||
|
||
|
||
function getSecurityLabel(
|
||
vault
|
||
) {
|
||
if (vault < 1000)
|
||
return "LOW";
|
||
|
||
if (vault < 10000)
|
||
return "MODERATE";
|
||
|
||
if (vault < 50000)
|
||
return "HIGH";
|
||
|
||
if (vault < 250000)
|
||
return "VERY HIGH";
|
||
|
||
return "MAXIMUM";
|
||
}
|
||
|
||
|
||
/*
|
||
* TEXT
|
||
*/
|
||
|
||
function randomCollectResponse(
|
||
amount,
|
||
user
|
||
) {
|
||
return randomItem([
|
||
`${user}, I made you **${amount} Cookies** 🍪`,
|
||
`${user}, here's **${amount} Cookies**. I ate the rest.`,
|
||
`${user}, your **${amount} Cookies** are fresh out of the oven.`,
|
||
`A wild baker appeared and handed ${user} **${amount} Cookies**.`,
|
||
`${user}, Abra insists these **${amount} Cookies** are totally legitimate.`
|
||
]);
|
||
}
|
||
|
||
|
||
function randomStealSuccessResponse(
|
||
amount,
|
||
target
|
||
) {
|
||
return randomItem([
|
||
`You stole **${amount.toLocaleString()} Cookies** from ${target}!`,
|
||
`Clean getaway. ${target} is **${amount.toLocaleString()} Cookies** poorer.`,
|
||
`The Cookie Vault was breached for **${amount.toLocaleString()} Cookies**.`
|
||
]);
|
||
}
|
||
|
||
|
||
function randomStealFailResponse(
|
||
amount,
|
||
target
|
||
) {
|
||
return randomItem([
|
||
`${target} caught you and took **${amount.toLocaleString()} Cookies**.`,
|
||
`The robbery failed. ${target} gained **${amount.toLocaleString()} Cookies**.`,
|
||
`Cookie Police confiscated **${amount.toLocaleString()} Cookies** and handed them to ${target}.`
|
||
]);
|
||
}
|
||
|
||
|
||
function getLeaderboardMedal(
|
||
index
|
||
) {
|
||
switch (index) {
|
||
case 0:
|
||
return "🥇";
|
||
|
||
case 1:
|
||
return "🥈";
|
||
|
||
case 2:
|
||
return "🥉";
|
||
|
||
default:
|
||
return `**${index + 1}.**`;
|
||
}
|
||
}
|
||
|
||
|
||
function sleep(ms) {
|
||
return new Promise(
|
||
resolve =>
|
||
setTimeout(
|
||
resolve,
|
||
ms
|
||
)
|
||
);
|
||
} |