836 lines
16 KiB
JavaScript
836 lines
16 KiB
JavaScript
const {
|
|
EmbedBuilder
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
getCookieBalance,
|
|
transferCookies,
|
|
claimCooldown,
|
|
temporaryEphemeralReply,
|
|
randomFloat,
|
|
formatPercentage
|
|
} = require("./cookieEconomy");
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* CONFIG
|
|
* ============================================================
|
|
*/
|
|
|
|
const HEIST_COOLDOWN =
|
|
60 * 60; // 1 hour
|
|
|
|
|
|
/*
|
|
* Each approach has a different risk/reward profile.
|
|
*
|
|
* Sneak:
|
|
* High success chance
|
|
* Small reward
|
|
* Small failure penalty
|
|
*
|
|
* Hack:
|
|
* Medium success chance
|
|
* Medium reward
|
|
* Medium failure penalty
|
|
*
|
|
* Smash:
|
|
* Low success chance
|
|
* Large reward
|
|
* Large failure penalty
|
|
*/
|
|
|
|
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
|
|
) {
|
|
|
|
/*
|
|
* The original /cookies heist command stores
|
|
* the player ID inside the embed footer:
|
|
*
|
|
* cookieHeist:123456789
|
|
*
|
|
* This prevents somebody else from pressing
|
|
* another player's heist buttons.
|
|
*/
|
|
|
|
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. Start your own with `/cookies heist`."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Validate selected method.
|
|
*/
|
|
|
|
const method =
|
|
HEIST_METHODS[
|
|
methodName
|
|
];
|
|
|
|
|
|
if (!method) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"That heist approach doesn't exist."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* CLAIM COOLDOWN
|
|
* ========================================================
|
|
*
|
|
* This is atomic so rapid double-clicks can't
|
|
* resolve the same heist multiple times.
|
|
*/
|
|
|
|
const cooldown =
|
|
await claimCooldown(
|
|
interaction.user.id,
|
|
interaction.guildId,
|
|
"heist",
|
|
HEIST_COOLDOWN
|
|
);
|
|
|
|
|
|
if (!cooldown.claimed) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`You can attempt another vault heist <t:${cooldown.next}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* LOCK THE HEIST MESSAGE
|
|
* ========================================================
|
|
*
|
|
* Remove all buttons immediately.
|
|
*/
|
|
|
|
await interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🏦 Vault Heist in Progress..."
|
|
)
|
|
.setDescription(
|
|
getHeistProgressText(
|
|
methodName
|
|
)
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* GET BALANCES
|
|
* ========================================================
|
|
*/
|
|
|
|
const playerId =
|
|
interaction.user.id;
|
|
|
|
const vaultId =
|
|
interaction.client.user.id;
|
|
|
|
|
|
const [
|
|
playerCookies,
|
|
vaultCookies
|
|
] =
|
|
await Promise.all([
|
|
|
|
getCookieBalance(
|
|
playerId
|
|
),
|
|
|
|
getCookieBalance(
|
|
vaultId
|
|
)
|
|
|
|
]);
|
|
|
|
|
|
/*
|
|
* Something may have changed between opening
|
|
* the heist menu and pressing the button.
|
|
*/
|
|
|
|
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."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Small dramatic pause.
|
|
*
|
|
* Only one edit happens after this, so this is
|
|
* comfortably within Discord rate limits.
|
|
*/
|
|
|
|
await sleep(
|
|
1200
|
|
);
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* VAULT DIFFICULTY
|
|
* ========================================================
|
|
*
|
|
* The richer ShaiBot's vault becomes,
|
|
* the harder it is to rob.
|
|
*
|
|
* Penalty is capped at 15 percentage points.
|
|
*/
|
|
|
|
const securityPenalty =
|
|
getVaultSecurityPenalty(
|
|
vaultCookies
|
|
);
|
|
|
|
|
|
const successChance =
|
|
Math.max(
|
|
0.10,
|
|
|
|
method.baseChance -
|
|
securityPenalty
|
|
);
|
|
|
|
|
|
const success =
|
|
Math.random() <
|
|
successChance;
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* SUCCESS
|
|
* ========================================================
|
|
*/
|
|
|
|
if (success) {
|
|
|
|
/*
|
|
* Roll percentage of the current vault.
|
|
*/
|
|
|
|
const percent =
|
|
randomFloat(
|
|
method.minStealPercent,
|
|
method.maxStealPercent
|
|
);
|
|
|
|
|
|
/*
|
|
* Apply both:
|
|
*
|
|
* percentage roll
|
|
* AND
|
|
* hard maximum
|
|
*
|
|
* This prevents a massive vault from producing
|
|
* absurd payouts.
|
|
*/
|
|
|
|
const amount =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.min(
|
|
|
|
Math.floor(
|
|
vaultCookies *
|
|
percent
|
|
),
|
|
|
|
method.maxStealAmount,
|
|
|
|
vaultCookies
|
|
)
|
|
);
|
|
|
|
|
|
/*
|
|
* ShaiBot pays the robber.
|
|
*/
|
|
|
|
await transferCookies(
|
|
vaultId,
|
|
playerId,
|
|
amount
|
|
);
|
|
|
|
|
|
const remaining =
|
|
await 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:
|
|
"Vault Remaining",
|
|
|
|
value:
|
|
`**${remaining.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
|
|
);
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ========================================================
|
|
* FAILURE
|
|
* ========================================================
|
|
*
|
|
* Failure fines the player.
|
|
*
|
|
* Unlike normal player stealing, the fine goes
|
|
* directly into ShaiBot's vault.
|
|
*/
|
|
|
|
const penaltyPercent =
|
|
randomFloat(
|
|
method.minPenaltyPercent,
|
|
method.maxPenaltyPercent
|
|
);
|
|
|
|
|
|
const penalty =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.min(
|
|
|
|
Math.floor(
|
|
playerCookies *
|
|
penaltyPercent
|
|
),
|
|
|
|
playerCookies
|
|
)
|
|
);
|
|
|
|
|
|
await transferCookies(
|
|
playerId,
|
|
vaultId,
|
|
penalty
|
|
);
|
|
|
|
|
|
const [
|
|
playerAfter,
|
|
vaultAfter
|
|
] =
|
|
await Promise.all([
|
|
|
|
getCookieBalance(
|
|
playerId
|
|
),
|
|
|
|
getCookieBalance(
|
|
vaultId
|
|
)
|
|
|
|
]);
|
|
|
|
|
|
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:
|
|
"Your Vault",
|
|
|
|
value:
|
|
`**${playerAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"House Vault",
|
|
|
|
value:
|
|
`**${vaultAfter.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Next Heist",
|
|
|
|
value:
|
|
`<t:${cooldown.next}:R>`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
|
|
);
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* VAULT SECURITY
|
|
* ============================================================
|
|
*/
|
|
|
|
function getVaultSecurityPenalty(
|
|
vaultCookies
|
|
) {
|
|
|
|
/*
|
|
* Logarithmic scaling means the vault gets harder
|
|
* as it becomes richer without becoming impossible.
|
|
*
|
|
* Examples 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
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* FLAVOR 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...";
|
|
}
|
|
}
|
|
|
|
|
|
function getSuccessText(
|
|
methodName,
|
|
user,
|
|
amount
|
|
) {
|
|
|
|
const amountText =
|
|
`**${amount.toLocaleString()} Cookies**`;
|
|
|
|
|
|
switch (
|
|
methodName
|
|
) {
|
|
|
|
case "Sneak":
|
|
return (
|
|
`🥷 ${user} slipped through security and quietly escaped with ${amountText}.`
|
|
);
|
|
|
|
|
|
case "Hack":
|
|
return (
|
|
`💻 ${user} convinced the vault terminal that ${amountText} belonged to them.`
|
|
);
|
|
|
|
|
|
case "Smash":
|
|
return (
|
|
`💥 ${user} smashed through the Cookie Vault and escaped with ${amountText}!`
|
|
);
|
|
|
|
|
|
default:
|
|
return (
|
|
`${user} escaped with ${amountText}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function getFailureText(
|
|
methodName,
|
|
user,
|
|
penalty
|
|
) {
|
|
|
|
const penaltyText =
|
|
`**${penalty.toLocaleString()} Cookies**`;
|
|
|
|
|
|
switch (
|
|
methodName
|
|
) {
|
|
|
|
case "Sneak":
|
|
return [
|
|
`🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`,
|
|
"",
|
|
`The ShaiBot Vault collected a ${penaltyText} fine.`
|
|
].join("\n");
|
|
|
|
|
|
case "Hack":
|
|
return [
|
|
`🚨 ${user} entered the password incorrectly one too many times.`,
|
|
"",
|
|
`The ShaiBot Vault collected a ${penaltyText} security fee.`
|
|
].join("\n");
|
|
|
|
|
|
case "Smash":
|
|
return [
|
|
`🚨 ${user} discovered that the vault door is significantly stronger than expected.`,
|
|
"",
|
|
`Repairs cost ${penaltyText}. Naturally, you're paying.`
|
|
].join("\n");
|
|
|
|
|
|
default:
|
|
return (
|
|
`The heist failed and cost ${user} ${penaltyText}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* UTIL
|
|
* ============================================================
|
|
*/
|
|
|
|
function sleep(ms) {
|
|
return new Promise(
|
|
resolve =>
|
|
setTimeout(
|
|
resolve,
|
|
ms
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
resolveCookieHeist
|
|
}; |