Compare commits

..

2 Commits

Author SHA1 Message Date
DeSqBlocki 0407f4d7f1 crumleStrike Roles snapshot 2026-08-18 20:31:03 +02:00
DeSqBlocki 0f48f09bd9 refactored and moved 2026-08-18 20:07:41 +02:00
39 changed files with 11766 additions and 3110 deletions
-18
View File
@@ -1,18 +0,0 @@
const {
performBossAction
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossBurst",
async execute(interaction) {
return performBossAction(
interaction,
"Burst"
);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
joinBoss
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossJoinChampion",
async execute(interaction) {
return joinBoss(
interaction,
"Champion"
);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
joinBoss
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossJoinFighter",
async execute(interaction) {
return joinBoss(
interaction,
"Fighter"
);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
joinBoss
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossJoinScout",
async execute(interaction) {
return joinBoss(
interaction,
"Scout"
);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
performBossAction
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossLimit",
async execute(interaction) {
return performBossAction(
interaction,
"Limit"
);
}
};
-17
View File
@@ -1,17 +0,0 @@
const {
startBossBattle
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossStart",
async execute(interaction) {
return startBossBattle(
interaction
);
}
};
+88 -11
View File
@@ -7,11 +7,19 @@ const {
const { const {
getCookieBalance, getCookieBalance,
getCooldown,
getUnixTime,
temporaryEphemeralReply temporaryEphemeralReply
} = require( } = require(
"../../utils/cookieEconomy" "../../utils/cookieEconomy"
); );
const {
HEIST_COOLDOWN
} = require(
"../../utils/cookieHeist"
);
module.exports = { module.exports = {
name: name:
@@ -19,6 +27,10 @@ module.exports = {
async execute(interaction) { async execute(interaction) {
/*
* Verify ownership of the Crumb Saucer menu.
*/
const footer = const footer =
interaction.message interaction.message
.embeds[0] .embeds[0]
@@ -30,17 +42,13 @@ module.exports = {
type, type,
ownerId ownerId
] = ] =
footer footer?.split(":")
?.split(":") ?? [];
??
[];
if ( if (
type !== type !== "crumbSaucer" ||
"crumbSaucer" || ownerId !== interaction.user.id
ownerId !==
interaction.user.id
) { ) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
@@ -52,6 +60,43 @@ module.exports = {
} }
/*
* ====================================================
* SHARED HEIST COOLDOWN PRE-CHECK
* ====================================================
*/
const now =
getUnixTime();
const cooldown =
await getCooldown(
interaction.user.id,
interaction.guildId,
"heist"
);
if (
cooldown >
now
) {
return temporaryEphemeralReply(
interaction,
{
content:
`🏦 The ShaiBot Vault is still on alert.\nYou can attempt another heist <t:${cooldown}:R>.`
},
6000
);
}
/*
* Balances
*/
const vaultId = const vaultId =
interaction.client.user.id; interaction.client.user.id;
@@ -72,7 +117,8 @@ module.exports = {
if ( if (
player < 1 player <
1
) { ) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
@@ -85,7 +131,8 @@ module.exports = {
if ( if (
vault < 1 vault <
1
) { ) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
@@ -97,11 +144,16 @@ module.exports = {
} }
/*
* Heist menu
*/
const embed = const embed =
new EmbedBuilder() new EmbedBuilder()
.setTitle( .setTitle(
"🏦 ShaiBot Cookie Vault" "🏦 ShaiBot Cookie Vault"
) )
.setDescription( .setDescription(
[ [
`**Vault Balance:** ${vault.toLocaleString()} 🍪`, `**Vault Balance:** ${vault.toLocaleString()} 🍪`,
@@ -119,6 +171,7 @@ module.exports = {
"Low chance • Large haul • Large fine" "Low chance • Large haul • Large fine"
].join("\n") ].join("\n")
) )
.setFooter({ .setFooter({
text: text:
`cookieHeist:${interaction.user.id}` `cookieHeist:${interaction.user.id}`
@@ -172,19 +225,43 @@ module.exports = {
); );
const backButton =
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Back to Crumb Saucer"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
);
return interaction.update({ return interaction.update({
embeds: [ embeds: [
embed embed
], ],
components: [ components: [
heistButtons heistButtons,
backButton
] ]
}); });
} }
}; };
/*
* SECURITY LABEL
*/
function getSecurityLabel( function getSecurityLabel(
vault vault
) { ) {
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeDefend",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Defend"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeHeal",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Heal"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobAstrologian",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"Astrologian"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobBard",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"Bard"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobBlackMage",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"BlackMage"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobDarkKnight",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"DarkKnight"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobDragoon",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"Dragoon"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobPaladin",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"Paladin"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobScholar",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"Scholar"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobWarrior",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"Warrior"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobWhiteMage",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"WhiteMage"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeLimitBreak",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"LimitBreak"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRevive",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Revive"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleDPS",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"DPS"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleHealer",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"Healer"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleTank",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"Tank"
);
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
startCrumbleStrikeBattle
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeStart",
async execute(interaction) {
return startCrumbleStrikeBattle(
interaction
);
}
};
@@ -1,16 +1,16 @@
const { const {
performBossAction performCrumbleStrikeAction
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbleStrike"
); );
module.exports = { module.exports = {
name: name:
"cookieBossStrike", "crumbleStrikeStrike",
async execute(interaction) { async execute(interaction) {
return performBossAction( return performCrumbleStrikeAction(
interaction, interaction,
"Strike" "Strike"
); );
+18
View File
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierAdventurer",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"Adventurer"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierSprout",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"Sprout"
);
}
};
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierWarriorOfLight",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"WarriorOfLight"
);
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-230
View File
@@ -1,230 +0,0 @@
const {
getCollections,
getUnixTime
} = require("../utils/cookieEconomy");
const {
activateBoss,
failBoss,
cancelBossAndRefund
} = require("../utils/cookieBoss");
/*
* How often we check persistent boss timers.
*
* Discord timestamps handle the visible countdown,
* so checking once per minute is plenty.
*/
const CHECK_INTERVAL = 60 * 1000;
/*
* Your index.js automatically loads every file
* inside /handlers and passes the Discord client:
*
* require(`./handlers/${handler}`)(client)
*/
module.exports = (client) => {
/*
* Run shortly after startup.
*
* This catches boss events whose timers expired
* while ShaiBot was offline/restarting.
*/
setTimeout(() => {
processBossEvents(client);
}, 5000);
/*
* Persistent boss event watcher.
*/
setInterval(() => {
processBossEvents(client);
}, CHECK_INTERVAL);
console.log(
"[CookieBoss] Boss event watcher initialized"
);
};
/*
* Check all currently pending Cookie Boss events.
*/
async function processBossEvents(client) {
try {
const {
db
} = getCollections();
const events =
db.collection(
"items_events"
);
const now =
getUnixTime();
/*
* ==========================================
* JOINING PHASE
* ==========================================
*
* Recruitment lasts for 30 minutes.
*
* If 5+ players joined:
* start the battle automatically.
*
* If fewer than 5 joined:
* cancel the boss and refund everyone.
*/
const joiningEvents =
await events
.find({
eventType:
"CookieBoss",
state:
"Joining",
joinUntil: {
$lte: now
}
})
.toArray();
for (
const event
of joiningEvents
) {
const participantCount =
Object.keys(
event.participants ?? {}
).length;
console.log(
`[CookieBoss] Recruitment expired for ${event.eventId} ` +
`(${participantCount}/${event.minimumParticipants} players)`
);
/*
* Enough players joined.
*
* Start the fight automatically.
*/
if (
participantCount >=
event.minimumParticipants
) {
const started =
await activateBoss(
client,
event.eventId
);
if (started) {
console.log(
`[CookieBoss] Battle ${event.eventId} started automatically`
);
}
continue;
}
/*
* Not enough players.
*
* Nobody actually entered combat,
* so refund all committed Cookies.
*/
await cancelBossAndRefund(
client,
event.eventId
);
console.log(
`[CookieBoss] Battle ${event.eventId} cancelled and refunded`
);
}
/*
* ==========================================
* ACTIVE BATTLES
* ==========================================
*
* If the battle timer expires while the
* boss still has HP remaining, the raid fails.
*
* All Cookies held in the encounter escrow
* are transferred to the ShaiBot vault.
*/
const activeEvents =
await events
.find({
eventType:
"CookieBoss",
state:
"Active",
battleEndsAt: {
$lte: now
},
"boss.health": {
$gt: 0
}
})
.toArray();
for (
const event
of activeEvents
) {
console.log(
`[CookieBoss] Battle ${event.eventId} expired ` +
`with ${event.boss.health}/${event.boss.maxHealth} HP remaining`
);
await failBoss(
client,
event.eventId
);
console.log(
`[CookieBoss] Battle ${event.eventId} failed`
);
}
} catch (error) {
/*
* Never let an exception kill the interval.
*
* The next watcher tick will simply try again.
*/
console.error(
"[CookieBoss] Boss watcher error:",
error
);
}
}
+200
View File
@@ -0,0 +1,200 @@
const {
getCollections,
getUnixTime
} = require(
"../utils/cookieEconomy"
);
const {
activateCrumbleStrike,
failCrumbleStrike,
cancelCrumbleStrikeAndRefund
} = require(
"../utils/crumbleStrike"
);
const CHECK_INTERVAL =
60 * 1000;
module.exports = client => {
setTimeout(
() => {
processCrumbleStrikeEvents(
client
);
},
5000
);
setInterval(
() => {
processCrumbleStrikeEvents(
client
);
},
CHECK_INTERVAL
);
console.log(
"[CrumbleStrike] Event watcher initialized"
);
};
async function processCrumbleStrikeEvents(
client
) {
try {
const {
db
} =
getCollections();
const events =
db.collection(
"items_events"
);
const now =
getUnixTime();
/*
* Recruitment expired.
*/
const joiningEvents =
await events
.find({
eventType:
"CrumbleStrike",
state:
"Joining",
joinUntil: {
$lte:
now
}
})
.toArray();
for (
const event
of joiningEvents
) {
const participantCount =
Object.keys(
event.participants ??
{}
).length;
console.log(
`[CrumbleStrike] Recruitment expired for ${event.eventId} ` +
`(${participantCount}/${event.minimumParticipants})`
);
if (
participantCount >=
event.minimumParticipants
) {
const started =
await activateCrumbleStrike(
client,
event.eventId
);
if (started) {
console.log(
`[CrumbleStrike] ${event.eventId} started automatically`
);
}
continue;
}
const cancelled =
await cancelCrumbleStrikeAndRefund(
client,
event.eventId
);
if (cancelled) {
console.log(
`[CrumbleStrike] ${event.eventId} cancelled and refunded`
);
}
}
/*
* Battle expired.
*/
const activeEvents =
await events
.find({
eventType:
"CrumbleStrike",
state:
"Active",
battleEndsAt: {
$lte:
now
},
"boss.health": {
$gt:
0
}
})
.toArray();
for (
const event
of activeEvents
) {
console.log(
`[CrumbleStrike] ${event.eventId} expired with ` +
`${event.boss.health}/${event.boss.maxHealth} HP remaining`
);
const failed =
await failCrumbleStrike(
client,
event.eventId
);
if (failed) {
console.log(
`[CrumbleStrike] ${event.eventId} failed`
);
}
}
} catch (error) {
console.error(
"[CrumbleStrike] Watcher error:",
error
);
}
}
-1693
View File
File diff suppressed because it is too large Load Diff
+958 -172
View File
File diff suppressed because it is too large Load Diff
+255 -180
View File
@@ -1,5 +1,8 @@
const { const {
EmbedBuilder EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js"); } = require("discord.js");
const { const {
@@ -23,86 +26,47 @@ const HEIST_COOLDOWN =
/* /*
* Each approach has a different risk/reward profile. * All three approaches use the SAME cooldown:
* *
* Sneak: * cooldowns_cookies.heist
* High success chance
* Small reward
* Small failure penalty
* *
* Hack: * Sneak does NOT have its own cooldown.
* Medium success chance * Hack does NOT have its own cooldown.
* Medium reward * Smash does NOT have its own cooldown.
* Medium failure penalty
*
* Smash:
* Low success chance
* Large reward
* Large failure penalty
*/ */
const HEIST_METHODS = { const HEIST_METHODS = {
Sneak: { Sneak: {
baseChance: baseChance: 0.65,
0.65,
minStealPercent: minStealPercent: 0.01,
0.01, maxStealPercent: 0.03,
maxStealAmount: 1500,
maxStealPercent: minPenaltyPercent: 0.01,
0.03, maxPenaltyPercent: 0.04
maxStealAmount:
1500,
minPenaltyPercent:
0.01,
maxPenaltyPercent:
0.04
}, },
Hack: { Hack: {
baseChance: baseChance: 0.45,
0.45,
minStealPercent: minStealPercent: 0.03,
0.03, maxStealPercent: 0.06,
maxStealAmount: 3500,
maxStealPercent: minPenaltyPercent: 0.03,
0.06, maxPenaltyPercent: 0.07
maxStealAmount:
3500,
minPenaltyPercent:
0.03,
maxPenaltyPercent:
0.07
}, },
Smash: { Smash: {
baseChance: baseChance: 0.25,
0.25,
minStealPercent: minStealPercent: 0.06,
0.06, maxStealPercent: 0.12,
maxStealAmount: 7500,
maxStealPercent: minPenaltyPercent: 0.06,
0.12, maxPenaltyPercent: 0.12
maxStealAmount:
7500,
minPenaltyPercent:
0.06,
maxPenaltyPercent:
0.12
} }
}; };
@@ -117,15 +81,12 @@ async function resolveCookieHeist(
interaction, interaction,
methodName methodName
) { ) {
/* /*
* The original /cookies heist command stores * Validate ownership of this Heist menu.
* the player ID inside the embed footer:
* *
* cookieHeist:123456789 * Footer:
* *
* This prevents somebody else from pressing * cookieHeist:<userId>
* another player's heist buttons.
*/ */
const footer = const footer =
@@ -139,10 +100,8 @@ async function resolveCookieHeist(
type, type,
ownerId ownerId
] = ] =
footer footer?.split(":")
?.split(":") ?? [];
??
[];
if ( if (
@@ -153,14 +112,14 @@ async function resolveCookieHeist(
interaction, interaction,
{ {
content: content:
"This isn't your heist. Start your own with `/cookies heist`." "This isn't your heist. Open your own from `/crumbsaucer`."
} }
); );
} }
/* /*
* Validate selected method. * Validate method.
*/ */
const method = const method =
@@ -182,11 +141,18 @@ async function resolveCookieHeist(
/* /*
* ======================================================== * ========================================================
* CLAIM COOLDOWN * SHARED HEIST COOLDOWN
* ======================================================== * ========================================================
* *
* This is atomic so rapid double-clicks can't * IMPORTANT:
* resolve the same heist multiple times. *
* This is intentionally always "heist".
*
* It does NOT use:
*
* heistSneak
* heistHack
* heistSmash
*/ */
const cooldown = const cooldown =
@@ -203,18 +169,18 @@ async function resolveCookieHeist(
interaction, interaction,
{ {
content: content:
`You can attempt another vault heist <t:${cooldown.next}:R>.` `🏦 The vault is still on high alert.\nYou can attempt another heist <t:${cooldown.next}:R>.`
} },
6000
); );
} }
/* /*
* ======================================================== * Lock this Heist interaction immediately.
* LOCK THE HEIST MESSAGE
* ========================================================
* *
* Remove all buttons immediately. * Prevents selecting another approach from
* the same message.
*/ */
await interaction.update({ await interaction.update({
@@ -234,12 +200,6 @@ async function resolveCookieHeist(
}); });
/*
* ========================================================
* GET BALANCES
* ========================================================
*/
const playerId = const playerId =
interaction.user.id; interaction.user.id;
@@ -247,12 +207,16 @@ async function resolveCookieHeist(
interaction.client.user.id; interaction.client.user.id;
/*
* Re-fetch balances because they may have
* changed since the menu was opened.
*/
const [ const [
playerCookies, playerCookies,
vaultCookies vaultCookies
] = ] =
await Promise.all([ await Promise.all([
getCookieBalance( getCookieBalance(
playerId playerId
), ),
@@ -260,15 +224,9 @@ async function resolveCookieHeist(
getCookieBalance( getCookieBalance(
vaultId vaultId
) )
]); ]);
/*
* Something may have changed between opening
* the heist menu and pressing the button.
*/
if ( if (
playerCookies < 1 || playerCookies < 1 ||
vaultCookies < 1 vaultCookies < 1
@@ -282,18 +240,23 @@ async function resolveCookieHeist(
.setDescription( .setDescription(
"There weren't enough Cookies available to resolve the heist." "There weren't enough Cookies available to resolve the heist."
) )
.addFields({
name:
"Next Heist",
value:
`<t:${cooldown.next}:R>`
})
], ],
components: [] components:
buildReturnButton()
}); });
} }
/* /*
* Small dramatic pause. * Short theatrical delay.
*
* Only one edit happens after this, so this is
* comfortably within Discord rate limits.
*/ */
await sleep( await sleep(
@@ -303,13 +266,8 @@ async function resolveCookieHeist(
/* /*
* ======================================================== * ========================================================
* VAULT DIFFICULTY * DYNAMIC VAULT SECURITY
* ======================================================== * ========================================================
*
* The richer ShaiBot's vault becomes,
* the harder it is to rob.
*
* Penalty is capped at 15 percentage points.
*/ */
const securityPenalty = const securityPenalty =
@@ -339,38 +297,21 @@ async function resolveCookieHeist(
*/ */
if (success) { if (success) {
const rolledPercent =
/*
* Roll percentage of the current vault.
*/
const percent =
randomFloat( randomFloat(
method.minStealPercent, method.minStealPercent,
method.maxStealPercent method.maxStealPercent
); );
/*
* Apply both:
*
* percentage roll
* AND
* hard maximum
*
* This prevents a massive vault from producing
* absurd payouts.
*/
const amount = const amount =
Math.max( Math.max(
1, 1,
Math.min( Math.min(
Math.floor( Math.floor(
vaultCookies * vaultCookies *
percent rolledPercent
), ),
method.maxStealAmount, method.maxStealAmount,
@@ -380,28 +321,67 @@ async function resolveCookieHeist(
); );
/* try {
* ShaiBot pays the robber.
*/
await transferCookies( await transferCookies(
vaultId, vaultId,
playerId, playerId,
amount 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",
const remaining = value:
await getCookieBalance( `<t:${cooldown.next}:R>`
})
],
components:
buildReturnButton()
});
}
throw error;
}
const [
playerAfter,
vaultAfter
] =
await Promise.all([
getCookieBalance(
playerId
),
getCookieBalance(
vaultId vaultId
); )
]);
const actualPercentage = const actualPercentage =
( (
amount / amount /
vaultCookies vaultCookies
) * )
*
100; 100;
@@ -410,6 +390,7 @@ async function resolveCookieHeist(
.setTitle( .setTitle(
"💥 Vault Breached!" "💥 Vault Breached!"
) )
.setDescription( .setDescription(
getSuccessText( getSuccessText(
methodName, methodName,
@@ -417,8 +398,8 @@ async function resolveCookieHeist(
amount amount
) )
) )
.addFields(
.addFields(
{ {
name: name:
"Approach", "Approach",
@@ -465,10 +446,21 @@ async function resolveCookieHeist(
{ {
name: name:
"Vault Remaining", "Your Vault",
value: value:
`**${remaining.toLocaleString()} 🍪**`, `**${playerAfter.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"House Vault",
value:
`**${vaultAfter.toLocaleString()} 🍪**`,
inline: inline:
true true
@@ -482,9 +474,8 @@ async function resolveCookieHeist(
`<t:${cooldown.next}:R>`, `<t:${cooldown.next}:R>`,
inline: inline:
true false
} }
); );
@@ -493,7 +484,8 @@ async function resolveCookieHeist(
embed embed
], ],
components: [] components:
buildReturnButton()
}); });
} }
@@ -502,11 +494,6 @@ async function resolveCookieHeist(
* ======================================================== * ========================================================
* FAILURE * FAILURE
* ======================================================== * ========================================================
*
* Failure fines the player.
*
* Unlike normal player stealing, the fine goes
* directly into ShaiBot's vault.
*/ */
const penaltyPercent = const penaltyPercent =
@@ -521,7 +508,6 @@ async function resolveCookieHeist(
1, 1,
Math.min( Math.min(
Math.floor( Math.floor(
playerCookies * playerCookies *
penaltyPercent penaltyPercent
@@ -532,19 +518,56 @@ async function resolveCookieHeist(
); );
try {
/*
* Heist fines always go into
* ShaiBot's central vault.
*/
await transferCookies( await transferCookies(
playerId, playerId,
vaultId, vaultId,
penalty 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>`
})
],
components:
buildReturnButton()
});
}
throw error;
}
const [ const [
playerAfter, playerAfter,
vaultAfter vaultAfter
] = ] =
await Promise.all([ await Promise.all([
getCookieBalance( getCookieBalance(
playerId playerId
), ),
@@ -552,15 +575,24 @@ async function resolveCookieHeist(
getCookieBalance( getCookieBalance(
vaultId vaultId
) )
]); ]);
const actualPenaltyPercentage =
(
penalty /
playerCookies
)
*
100;
const embed = const embed =
new EmbedBuilder() new EmbedBuilder()
.setTitle( .setTitle(
"🚨 HEIST FAILED" "🚨 HEIST FAILED"
) )
.setDescription( .setDescription(
getFailureText( getFailureText(
methodName, methodName,
@@ -568,8 +600,8 @@ async function resolveCookieHeist(
penalty penalty
) )
) )
.addFields(
.addFields(
{ {
name: name:
"Approach", "Approach",
@@ -603,6 +635,17 @@ async function resolveCookieHeist(
true true
}, },
{
name:
"Vault Lost",
value:
`**${actualPenaltyPercentage.toFixed(1)}%**`,
inline:
true
},
{ {
name: name:
"Your Vault", "Your Vault",
@@ -633,9 +676,8 @@ async function resolveCookieHeist(
`<t:${cooldown.next}:R>`, `<t:${cooldown.next}:R>`,
inline: inline:
true false
} }
); );
@@ -644,26 +686,51 @@ async function resolveCookieHeist(
embed embed
], ],
components: [] components:
buildReturnButton()
}); });
} }
/* /*
* ============================================================ * ============================================================
* VAULT SECURITY * 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( function getVaultSecurityPenalty(
vaultCookies vaultCookies
) { ) {
/* /*
* Logarithmic scaling means the vault gets harder * Approximately:
* as it becomes richer without becoming impossible.
*
* Examples approximately:
* *
* 100 -> 5% * 100 -> 5%
* 1,000 -> 7.5% * 1,000 -> 7.5%
@@ -673,10 +740,11 @@ function getVaultSecurityPenalty(
*/ */
return Math.min( return Math.min(
Math.log10( Math.log10(
vaultCookies + 1 vaultCookies +
) * 1
)
*
0.025, 0.025,
0.15 0.15
@@ -686,18 +754,16 @@ function getVaultSecurityPenalty(
/* /*
* ============================================================ * ============================================================
* FLAVOR TEXT * PROGRESS TEXT
* ============================================================ * ============================================================
*/ */
function getHeistProgressText( function getHeistProgressText(
methodName methodName
) { ) {
switch ( switch (
methodName methodName
) { ) {
case "Sneak": case "Sneak":
return [ return [
"🥷 You slip through the service entrance...", "🥷 You slip through the service entrance...",
@@ -728,65 +794,73 @@ function getHeistProgressText(
} }
/*
* ============================================================
* SUCCESS TEXT
* ============================================================
*/
function getSuccessText( function getSuccessText(
methodName, methodName,
user, user,
amount amount
) { ) {
const cookies =
const amountText =
`**${amount.toLocaleString()} Cookies**`; `**${amount.toLocaleString()} Cookies**`;
switch ( switch (
methodName methodName
) { ) {
case "Sneak": case "Sneak":
return ( return (
`🥷 ${user} slipped through security and quietly escaped with ${amountText}.` `🥷 ${user} slipped through security and quietly escaped with ${cookies}.`
); );
case "Hack": case "Hack":
return ( return (
`💻 ${user} convinced the vault terminal that ${amountText} belonged to them.` `💻 ${user} convinced the vault terminal that ${cookies} belonged to them.`
); );
case "Smash": case "Smash":
return ( return (
`💥 ${user} smashed through the Cookie Vault and escaped with ${amountText}!` `💥 ${user} smashed through the Cookie Vault and escaped with ${cookies}!`
); );
default: default:
return ( return (
`${user} escaped with ${amountText}.` `${user} escaped with ${cookies}.`
); );
} }
} }
/*
* ============================================================
* FAILURE TEXT
* ============================================================
*/
function getFailureText( function getFailureText(
methodName, methodName,
user, user,
penalty penalty
) { ) {
const cookies =
const penaltyText =
`**${penalty.toLocaleString()} Cookies**`; `**${penalty.toLocaleString()} Cookies**`;
switch ( switch (
methodName methodName
) { ) {
case "Sneak": case "Sneak":
return [ return [
`🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`, `🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`,
"", "",
`The ShaiBot Vault collected a ${penaltyText} fine.` `The ShaiBot Vault collected a ${cookies} fine.`
].join("\n"); ].join("\n");
@@ -794,7 +868,7 @@ function getFailureText(
return [ return [
`🚨 ${user} entered the password incorrectly one too many times.`, `🚨 ${user} entered the password incorrectly one too many times.`,
"", "",
`The ShaiBot Vault collected a ${penaltyText} security fee.` `The ShaiBot Vault collected a ${cookies} security fee.`
].join("\n"); ].join("\n");
@@ -802,13 +876,13 @@ function getFailureText(
return [ return [
`🚨 ${user} discovered that the vault door is significantly stronger than expected.`, `🚨 ${user} discovered that the vault door is significantly stronger than expected.`,
"", "",
`Repairs cost ${penaltyText}. Naturally, you're paying.` `Repairs cost ${cookies}. Naturally, you're paying.`
].join("\n"); ].join("\n");
default: default:
return ( return (
`The heist failed and cost ${user} ${penaltyText}.` `The heist failed and cost ${user} ${cookies}.`
); );
} }
} }
@@ -832,5 +906,6 @@ function sleep(ms) {
module.exports = { module.exports = {
resolveCookieHeist resolveCookieHeist,
HEIST_COOLDOWN
}; };
File diff suppressed because it is too large Load Diff