942 lines
20 KiB
JavaScript
942 lines
20 KiB
JavaScript
const {
|
|
EmbedBuilder,
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
getCookieBalance,
|
|
transferCookies,
|
|
claimCooldown,
|
|
temporaryEphemeralReply,
|
|
randomFloat,
|
|
formatPercentage
|
|
} = require("./cookieEconomy");
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* CONFIG
|
|
* ============================================================
|
|
*/
|
|
|
|
const HEIST_COOLDOWN =
|
|
60 * 60; // 1 hour
|
|
|
|
|
|
/*
|
|
* All three approaches use the SAME cooldown:
|
|
*
|
|
* cooldowns_cookies.heist
|
|
*
|
|
* Sneak does NOT have its own cooldown.
|
|
* Hack does NOT have its own cooldown.
|
|
* Smash does NOT have its own cooldown.
|
|
*/
|
|
|
|
const HEIST_METHODS = {
|
|
Sneak: {
|
|
baseChance: 0.65,
|
|
|
|
minStealPercent: 0.01,
|
|
maxStealPercent: 0.03,
|
|
maxStealAmount: 1500,
|
|
|
|
minPenaltyPercent: 0.01,
|
|
maxPenaltyPercent: 0.04
|
|
},
|
|
|
|
Hack: {
|
|
baseChance: 0.45,
|
|
|
|
minStealPercent: 0.03,
|
|
maxStealPercent: 0.06,
|
|
maxStealAmount: 3500,
|
|
|
|
minPenaltyPercent: 0.03,
|
|
maxPenaltyPercent: 0.07
|
|
},
|
|
|
|
Smash: {
|
|
baseChance: 0.25,
|
|
|
|
minStealPercent: 0.06,
|
|
maxStealPercent: 0.12,
|
|
maxStealAmount: 7500,
|
|
|
|
minPenaltyPercent: 0.06,
|
|
maxPenaltyPercent: 0.12
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* RESOLVE HEIST
|
|
* ============================================================
|
|
*/
|
|
|
|
async function resolveCookieHeist(
|
|
interaction,
|
|
methodName
|
|
) {
|
|
/*
|
|
* Validate ownership of this Heist menu.
|
|
*
|
|
* Footer:
|
|
*
|
|
* cookieHeist:<userId>
|
|
*/
|
|
|
|
const footer =
|
|
interaction.message
|
|
?.embeds?.[0]
|
|
?.footer
|
|
?.text;
|
|
|
|
|
|
const [
|
|
type,
|
|
ownerId
|
|
] =
|
|
footer?.split(":")
|
|
??
|
|
[];
|
|
|
|
|
|
if (
|
|
type !==
|
|
"cookieHeist"
|
|
||
|
|
ownerId !==
|
|
interaction.user.id
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"This isn't your heist. Open your own from `/crumbsaucer`."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Validate method.
|
|
*/
|
|
|
|
const method =
|
|
HEIST_METHODS[
|
|
methodName
|
|
];
|
|
|
|
|
|
if (!method) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"That heist approach doesn't exist."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* SHARED HEIST COOLDOWN
|
|
* ========================================================
|
|
*/
|
|
|
|
const cooldown =
|
|
await claimCooldown(
|
|
interaction.user.id,
|
|
interaction.guildId,
|
|
"heist",
|
|
HEIST_COOLDOWN
|
|
);
|
|
|
|
|
|
if (
|
|
!cooldown.claimed
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`🏦 The vault is still on high alert.\nYou can attempt another heist <t:${cooldown.next}:R>.`
|
|
},
|
|
6000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Lock this Heist interaction immediately.
|
|
*/
|
|
|
|
await interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🏦 Vault Heist in Progress..."
|
|
)
|
|
.setDescription(
|
|
getHeistProgressText(
|
|
methodName
|
|
)
|
|
)
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${interaction.user.id}`
|
|
})
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
const playerId =
|
|
interaction.user.id;
|
|
|
|
|
|
const vaultId =
|
|
interaction.client.user.id;
|
|
|
|
|
|
/*
|
|
* Re-fetch balances because they may have
|
|
* changed since the menu was opened.
|
|
*/
|
|
|
|
const [
|
|
playerCookies,
|
|
vaultCookies
|
|
] =
|
|
await Promise.all([
|
|
getCookieBalance(
|
|
playerId
|
|
),
|
|
|
|
getCookieBalance(
|
|
vaultId
|
|
)
|
|
]);
|
|
|
|
|
|
if (
|
|
playerCookies <
|
|
1
|
|
||
|
|
vaultCookies <
|
|
1
|
|
) {
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🏦 Heist Aborted"
|
|
)
|
|
.setDescription(
|
|
"There weren't enough Cookies available to resolve the heist."
|
|
)
|
|
.addFields({
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`
|
|
})
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${playerId}`
|
|
})
|
|
],
|
|
|
|
components:
|
|
buildReturnButton()
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Short theatrical delay.
|
|
*/
|
|
|
|
await sleep(
|
|
1200
|
|
);
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* DYNAMIC VAULT SECURITY
|
|
* ========================================================
|
|
*/
|
|
|
|
const securityPenalty =
|
|
getVaultSecurityPenalty(
|
|
vaultCookies
|
|
);
|
|
|
|
|
|
const successChance =
|
|
Math.max(
|
|
0.10,
|
|
|
|
method.baseChance -
|
|
securityPenalty
|
|
);
|
|
|
|
|
|
const success =
|
|
Math.random() <
|
|
successChance;
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* SUCCESS
|
|
* ========================================================
|
|
*/
|
|
|
|
if (success) {
|
|
const rolledPercent =
|
|
randomFloat(
|
|
method.minStealPercent,
|
|
method.maxStealPercent
|
|
);
|
|
|
|
|
|
const amount =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.min(
|
|
Math.floor(
|
|
vaultCookies *
|
|
rolledPercent
|
|
),
|
|
|
|
method.maxStealAmount,
|
|
|
|
vaultCookies
|
|
)
|
|
);
|
|
|
|
|
|
try {
|
|
await transferCookies(
|
|
vaultId,
|
|
playerId,
|
|
amount
|
|
);
|
|
|
|
} catch (error) {
|
|
if (
|
|
error.message ===
|
|
"INSUFFICIENT_COOKIES"
|
|
) {
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🏦 Heist Interrupted"
|
|
)
|
|
.setDescription(
|
|
"The ShaiBot Vault changed before you could complete the robbery."
|
|
)
|
|
.addFields({
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`
|
|
})
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${playerId}`
|
|
})
|
|
],
|
|
|
|
components:
|
|
buildReturnButton()
|
|
});
|
|
}
|
|
|
|
|
|
throw error;
|
|
}
|
|
|
|
|
|
const [
|
|
playerAfter,
|
|
vaultAfter
|
|
] =
|
|
await Promise.all([
|
|
getCookieBalance(
|
|
playerId
|
|
),
|
|
|
|
getCookieBalance(
|
|
vaultId
|
|
)
|
|
]);
|
|
|
|
|
|
const actualPercentage =
|
|
(
|
|
amount /
|
|
vaultCookies
|
|
)
|
|
*
|
|
100;
|
|
|
|
|
|
const embed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"💥 Vault Breached!"
|
|
)
|
|
|
|
.setDescription(
|
|
getSuccessText(
|
|
methodName,
|
|
interaction.user,
|
|
amount
|
|
)
|
|
)
|
|
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Approach",
|
|
|
|
value:
|
|
methodName,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Success Chance",
|
|
|
|
value:
|
|
`**${formatPercentage(successChance)}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Vault Taken",
|
|
|
|
value:
|
|
`**${actualPercentage.toFixed(2)}%**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Cookies Stolen",
|
|
|
|
value:
|
|
`**${amount.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Your Vault",
|
|
|
|
value:
|
|
`**${playerAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"House Vault",
|
|
|
|
value:
|
|
`**${vaultAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`,
|
|
|
|
inline:
|
|
false
|
|
}
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${playerId}`
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components:
|
|
buildReturnButton()
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* FAILURE
|
|
* ========================================================
|
|
*/
|
|
|
|
const penaltyPercent =
|
|
randomFloat(
|
|
method.minPenaltyPercent,
|
|
method.maxPenaltyPercent
|
|
);
|
|
|
|
|
|
const penalty =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.min(
|
|
Math.floor(
|
|
playerCookies *
|
|
penaltyPercent
|
|
),
|
|
|
|
playerCookies
|
|
)
|
|
);
|
|
|
|
|
|
try {
|
|
/*
|
|
* Heist fines always go into
|
|
* ShaiBot's central vault.
|
|
*/
|
|
|
|
await transferCookies(
|
|
playerId,
|
|
vaultId,
|
|
penalty
|
|
);
|
|
|
|
} catch (error) {
|
|
if (
|
|
error.message ===
|
|
"INSUFFICIENT_COOKIES"
|
|
) {
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🚨 Heist Failed"
|
|
)
|
|
.setDescription(
|
|
"Your Cookie balance changed before the fine could be collected."
|
|
)
|
|
.addFields({
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`
|
|
})
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${playerId}`
|
|
})
|
|
],
|
|
|
|
components:
|
|
buildReturnButton()
|
|
});
|
|
}
|
|
|
|
|
|
throw error;
|
|
}
|
|
|
|
|
|
const [
|
|
playerAfter,
|
|
vaultAfter
|
|
] =
|
|
await Promise.all([
|
|
getCookieBalance(
|
|
playerId
|
|
),
|
|
|
|
getCookieBalance(
|
|
vaultId
|
|
)
|
|
]);
|
|
|
|
|
|
const actualPenaltyPercentage =
|
|
(
|
|
penalty /
|
|
playerCookies
|
|
)
|
|
*
|
|
100;
|
|
|
|
|
|
const embed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🚨 HEIST FAILED"
|
|
)
|
|
|
|
.setDescription(
|
|
getFailureText(
|
|
methodName,
|
|
interaction.user,
|
|
penalty
|
|
)
|
|
)
|
|
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Approach",
|
|
|
|
value:
|
|
methodName,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Success Chance",
|
|
|
|
value:
|
|
`**${formatPercentage(successChance)}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Fine",
|
|
|
|
value:
|
|
`**${penalty.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Vault Lost",
|
|
|
|
value:
|
|
`**${actualPenaltyPercentage.toFixed(1)}%**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Your Vault",
|
|
|
|
value:
|
|
`**${playerAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"House Vault",
|
|
|
|
value:
|
|
`**${vaultAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`,
|
|
|
|
inline:
|
|
false
|
|
}
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`cookieHeist:${playerId}`
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components:
|
|
buildReturnButton()
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* RETURN TO CRUMB SAUCER
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildReturnButton() {
|
|
return [
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerBack"
|
|
)
|
|
.setLabel(
|
|
"Back to Crumb Saucer"
|
|
)
|
|
.setEmoji(
|
|
"◀️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Secondary
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SECURITY
|
|
* ============================================================
|
|
*/
|
|
|
|
function getVaultSecurityPenalty(
|
|
vaultCookies
|
|
) {
|
|
/*
|
|
* Approximately:
|
|
*
|
|
* 100 -> 5%
|
|
* 1,000 -> 7.5%
|
|
* 10,000 -> 10%
|
|
* 100,000 -> 12.5%
|
|
* 1,000,000 -> 15%
|
|
*/
|
|
|
|
return Math.min(
|
|
Math.log10(
|
|
vaultCookies +
|
|
1
|
|
)
|
|
*
|
|
0.025,
|
|
|
|
0.15
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* PROGRESS TEXT
|
|
* ============================================================
|
|
*/
|
|
|
|
function getHeistProgressText(
|
|
methodName
|
|
) {
|
|
switch (
|
|
methodName
|
|
) {
|
|
case "Sneak":
|
|
return [
|
|
"🥷 You slip through the service entrance...",
|
|
"",
|
|
"The Cookie Vault guards haven't noticed you yet."
|
|
].join("\n");
|
|
|
|
|
|
case "Hack":
|
|
return [
|
|
"💻 You connect to the vault terminal...",
|
|
"",
|
|
"Several extremely suspicious progress bars appear."
|
|
].join("\n");
|
|
|
|
|
|
case "Smash":
|
|
return [
|
|
"💥 You charge directly at the vault...",
|
|
"",
|
|
"Subtlety has been abandoned."
|
|
].join("\n");
|
|
|
|
|
|
default:
|
|
return "The heist is underway...";
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SUCCESS TEXT
|
|
* ============================================================
|
|
*/
|
|
|
|
function getSuccessText(
|
|
methodName,
|
|
user,
|
|
amount
|
|
) {
|
|
const cookies =
|
|
`**${amount.toLocaleString()} Cookies**`;
|
|
|
|
|
|
switch (
|
|
methodName
|
|
) {
|
|
case "Sneak":
|
|
return (
|
|
`🥷 ${user} slipped through security and quietly escaped with ${cookies}.`
|
|
);
|
|
|
|
|
|
case "Hack":
|
|
return (
|
|
`💻 ${user} convinced the vault terminal that ${cookies} belonged to them.`
|
|
);
|
|
|
|
|
|
case "Smash":
|
|
return (
|
|
`💥 ${user} smashed through the Cookie Vault and escaped with ${cookies}!`
|
|
);
|
|
|
|
|
|
default:
|
|
return (
|
|
`${user} escaped with ${cookies}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* FAILURE TEXT
|
|
* ============================================================
|
|
*/
|
|
|
|
function getFailureText(
|
|
methodName,
|
|
user,
|
|
penalty
|
|
) {
|
|
const cookies =
|
|
`**${penalty.toLocaleString()} Cookies**`;
|
|
|
|
|
|
switch (
|
|
methodName
|
|
) {
|
|
case "Sneak":
|
|
return [
|
|
`🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`,
|
|
"",
|
|
`The ShaiBot Vault collected a ${cookies} fine.`
|
|
].join("\n");
|
|
|
|
|
|
case "Hack":
|
|
return [
|
|
`🚨 ${user} entered the password incorrectly one too many times.`,
|
|
"",
|
|
`The ShaiBot Vault collected a ${cookies} security fee.`
|
|
].join("\n");
|
|
|
|
|
|
case "Smash":
|
|
return [
|
|
`🚨 ${user} discovered that the vault door is significantly stronger than expected.`,
|
|
"",
|
|
`Repairs cost ${cookies}. Naturally, you're paying.`
|
|
].join("\n");
|
|
|
|
|
|
default:
|
|
return (
|
|
`The heist failed and cost ${user} ${cookies}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* UTIL
|
|
* ============================================================
|
|
*/
|
|
|
|
function sleep(
|
|
ms
|
|
) {
|
|
return new Promise(
|
|
resolve =>
|
|
setTimeout(
|
|
resolve,
|
|
ms
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* EXPORTS
|
|
* ============================================================
|
|
*/
|
|
|
|
module.exports = {
|
|
resolveCookieHeist,
|
|
HEIST_COOLDOWN
|
|
}; |