This commit is contained in:
DeSqBlocki
2026-08-22 20:14:09 +02:00
14 changed files with 1463 additions and 444 deletions
+3 -1
View File
@@ -9,7 +9,9 @@ module.exports = {
name: name:
"crumbSaucerBlackjack", "crumbSaucerBlackjack",
async execute(interaction) { async execute(
interaction
) {
return showBlackjack( return showBlackjack(
interaction interaction
); );
+19
View File
@@ -0,0 +1,19 @@
const {
startBlackjackAllIn
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackAllIn",
async execute(
interaction
) {
return startBlackjackAllIn(
interaction
);
}
};
+3 -1
View File
@@ -9,7 +9,9 @@ module.exports = {
name: name:
"crumbBlackjackDouble", "crumbBlackjackDouble",
async execute(interaction) { async execute(
interaction
) {
return doubleBlackjack( return doubleBlackjack(
interaction interaction
); );
+3 -1
View File
@@ -9,7 +9,9 @@ module.exports = {
name: name:
"crumbBlackjackHit", "crumbBlackjackHit",
async execute(interaction) { async execute(
interaction
) {
return hitBlackjack( return hitBlackjack(
interaction interaction
); );
+19
View File
@@ -0,0 +1,19 @@
const {
runBlackjackBack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackRunItBack",
async execute(
interaction
) {
return runBlackjackBack(
interaction
);
}
};
+3 -1
View File
@@ -9,7 +9,9 @@ module.exports = {
name: name:
"crumbBlackjackStand", "crumbBlackjackStand",
async execute(interaction) { async execute(
interaction
) {
return standBlackjack( return standBlackjack(
interaction interaction
); );
+19
View File
@@ -0,0 +1,19 @@
const {
startRouletteAllIn
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteAllIn",
async execute(
interaction
) {
return startRouletteAllIn(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
runRouletteBack
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteRunItBack",
async execute(
interaction
) {
return runRouletteBack(
interaction
);
}
};
+1 -1
View File
@@ -193,7 +193,7 @@ async function collect(interaction) {
let amount = let amount =
randomInteger( randomInteger(
1, 1,
20 200
); );
+54 -23
View File
@@ -91,7 +91,7 @@ async function resolveCookieHeist(
const footer = const footer =
interaction.message interaction.message
.embeds[0] ?.embeds?.[0]
?.footer ?.footer
?.text; ?.text;
@@ -101,12 +101,16 @@ async function resolveCookieHeist(
ownerId ownerId
] = ] =
footer?.split(":") footer?.split(":")
?? []; ??
[];
if ( if (
type !== "cookieHeist" || type !==
ownerId !== interaction.user.id "cookieHeist"
||
ownerId !==
interaction.user.id
) { ) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
@@ -143,16 +147,6 @@ async function resolveCookieHeist(
* ======================================================== * ========================================================
* SHARED HEIST COOLDOWN * SHARED HEIST COOLDOWN
* ======================================================== * ========================================================
*
* IMPORTANT:
*
* This is intentionally always "heist".
*
* It does NOT use:
*
* heistSneak
* heistHack
* heistSmash
*/ */
const cooldown = const cooldown =
@@ -164,7 +158,9 @@ async function resolveCookieHeist(
); );
if (!cooldown.claimed) { if (
!cooldown.claimed
) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
{ {
@@ -178,9 +174,6 @@ async function resolveCookieHeist(
/* /*
* Lock this Heist interaction immediately. * Lock this Heist interaction immediately.
*
* Prevents selecting another approach from
* the same message.
*/ */
await interaction.update({ await interaction.update({
@@ -194,6 +187,10 @@ async function resolveCookieHeist(
methodName methodName
) )
) )
.setFooter({
text:
`cookieHeist:${interaction.user.id}`
})
], ],
components: [] components: []
@@ -203,6 +200,7 @@ async function resolveCookieHeist(
const playerId = const playerId =
interaction.user.id; interaction.user.id;
const vaultId = const vaultId =
interaction.client.user.id; interaction.client.user.id;
@@ -228,8 +226,11 @@ async function resolveCookieHeist(
if ( if (
playerCookies < 1 || playerCookies <
vaultCookies < 1 1
||
vaultCookies <
1
) { ) {
return interaction.editReply({ return interaction.editReply({
embeds: [ embeds: [
@@ -247,6 +248,10 @@ async function resolveCookieHeist(
value: value:
`<t:${cooldown.next}:R>` `<t:${cooldown.next}:R>`
}) })
.setFooter({
text:
`cookieHeist:${playerId}`
})
], ],
components: components:
@@ -349,6 +354,10 @@ async function resolveCookieHeist(
value: value:
`<t:${cooldown.next}:R>` `<t:${cooldown.next}:R>`
}) })
.setFooter({
text:
`cookieHeist:${playerId}`
})
], ],
components: components:
@@ -476,7 +485,12 @@ async function resolveCookieHeist(
inline: inline:
false false
} }
); )
.setFooter({
text:
`cookieHeist:${playerId}`
});
return interaction.editReply({ return interaction.editReply({
@@ -551,6 +565,10 @@ async function resolveCookieHeist(
value: value:
`<t:${cooldown.next}:R>` `<t:${cooldown.next}:R>`
}) })
.setFooter({
text:
`cookieHeist:${playerId}`
})
], ],
components: components:
@@ -678,7 +696,12 @@ async function resolveCookieHeist(
inline: inline:
false false
} }
); )
.setFooter({
text:
`cookieHeist:${playerId}`
});
return interaction.editReply({ return interaction.editReply({
@@ -894,7 +917,9 @@ function getFailureText(
* ============================================================ * ============================================================
*/ */
function sleep(ms) { function sleep(
ms
) {
return new Promise( return new Promise(
resolve => resolve =>
setTimeout( setTimeout(
@@ -905,6 +930,12 @@ function sleep(ms) {
} }
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = { module.exports = {
resolveCookieHeist, resolveCookieHeist,
HEIST_COOLDOWN HEIST_COOLDOWN
+522 -177
View File
File diff suppressed because it is too large Load Diff
+401 -70
View File
@@ -29,62 +29,33 @@ const ROULETTE_BETS = [
const ROULETTE_CHOICES = { const ROULETTE_CHOICES = {
Red: { Red: {
name: name: "Red",
"Red", emoji: "🔴",
multiplier: 2
emoji:
"🔴",
multiplier:
2
}, },
Black: { Black: {
name: name: "Black",
"Black", emoji: "⚫",
multiplier: 2
emoji:
"⚫",
multiplier:
2
}, },
Odd: { Odd: {
name: name: "Odd",
"Odd", emoji: "🔢",
multiplier: 2
emoji:
"🔢",
multiplier:
2
}, },
Even: { Even: {
name: name: "Even",
"Even", emoji: "2️⃣",
multiplier: 2
emoji:
"2️⃣",
multiplier:
2
}, },
Zero: { Zero: {
name: name: "Zero",
"Zero", emoji: "🟢",
multiplier: 36
emoji:
"🟢",
multiplier:
36
} }
}; };
@@ -156,19 +127,21 @@ async function showRoulette(
.setTitle( .setTitle(
"🎡 Crumb Saucer Roulette" "🎡 Crumb Saucer Roulette"
) )
.setDescription( .setDescription(
[ [
"European Roulette — **0 to 36**.", "European Roulette — **0 to 36**.",
"", "",
"Choose your wager.", "Choose your wager.",
"", "",
"🔴 **Red** — ×2", "🔴 **Red** — x2",
"⚫ **Black** — ×2", "⚫ **Black** — x2",
"🔢 **Odd** — ×2", "🔢 **Odd** — x2",
"2️⃣ **Even** — ×2", "2️⃣ **Even** — x2",
"🟢 **Zero** — ×36" "🟢 **Zero** — x36"
].join("\n") ].join("\n")
) )
.addFields({ .addFields({
name: name:
"Your Vault", "Your Vault",
@@ -176,6 +149,7 @@ async function showRoulette(
value: value:
`**${balance.toLocaleString()} 🍪**` `**${balance.toLocaleString()} 🍪**`
}) })
.setFooter({ .setFooter({
text: text:
`crumbRouletteMenu:${interaction.user.id}` `crumbRouletteMenu:${interaction.user.id}`
@@ -195,7 +169,7 @@ async function showRoulette(
/* /*
* ============================================================ * ============================================================
* CHOOSE WAGER * CHOOSE NORMAL WAGER
* ============================================================ * ============================================================
*/ */
@@ -238,6 +212,114 @@ async function chooseRouletteBet(
} }
return showRouletteChoices(
interaction,
bet
);
}
/*
* ============================================================
* ALL IN
* ============================================================
*/
async function startRouletteAllIn(
interaction
) {
const ownerId =
getRouletteSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Roulette table."
}
);
}
/*
* Resolve the balance at click time.
*/
const balance =
await getCookieBalance(
interaction.user.id
);
if (
balance <=
0
) {
return temporaryEphemeralReply(
interaction,
{
content:
"You don't have any Cookies to go All In with."
}
);
}
/*
* All In only selects the wager.
*
* Nothing is transferred until the player
* chooses Red / Black / Odd / Even / Zero.
*/
return showRouletteChoices(
interaction,
balance
);
}
/*
* ============================================================
* SHOW CHOICE MENU
* ============================================================
*
* Shared by:
*
* - normal wager buttons
* - All In
* - Run It Back
*/
async function showRouletteChoices(
interaction,
bet
) {
if (
!Number.isSafeInteger(
bet
)
||
bet <=
0
) {
return temporaryEphemeralReply(
interaction,
{
content:
"That Roulette wager isn't valid."
}
);
}
const balance = const balance =
await getCookieBalance( await getCookieBalance(
interaction.user.id interaction.user.id
@@ -263,19 +345,29 @@ async function chooseRouletteBet(
.setTitle( .setTitle(
"🎡 Crumb Saucer Roulette" "🎡 Crumb Saucer Roulette"
) )
.setDescription( .setDescription(
[ [
`Wager: **${bet.toLocaleString()} 🍪**`, `Wager: **${bet.toLocaleString()} 🍪**`,
"", "",
"Where do you want to place your Cookies?", "Where do you want to place your Cookies?",
"", "",
"🔴 **Red** — ×2", "🔴 **Red** — x2",
"⚫ **Black** — ×2", "⚫ **Black** — x2",
"🔢 **Odd** — ×2", "🔢 **Odd** — x2",
"2️⃣ **Even** — ×2", "2️⃣ **Even** — x2",
"🟢 **Zero** — ×36" "🟢 **Zero** — x36"
].join("\n") ].join("\n")
) )
.addFields({
name:
"Your Vault",
value:
`**${balance.toLocaleString()} 🍪**`
})
.setFooter({ .setFooter({
text: text:
`crumbRouletteBet:${interaction.user.id}:${bet}` `crumbRouletteBet:${interaction.user.id}:${bet}`
@@ -293,6 +385,105 @@ async function chooseRouletteBet(
} }
/*
* ============================================================
* RUN IT BACK
* ============================================================
*
* Remembers ONLY the previous wager.
*
* It does NOT remember Red / Black / Odd / Even / Zero.
*/
async function runRouletteBack(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
/*
* Expected:
*
* crumbRouletteResult:<userId>:<bet>
*/
if (
parts[0] !==
"crumbRouletteResult"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This Roulette result can no longer be replayed."
}
);
}
const ownerId =
parts[1];
const bet =
Number(
parts[2]
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Roulette session."
}
);
}
if (
!Number.isSafeInteger(
bet
)
||
bet <=
0
) {
return temporaryEphemeralReply(
interaction,
{
content:
"The previous wager is no longer valid."
}
);
}
/*
* Same wager, fresh choice.
*/
return showRouletteChoices(
interaction,
bet
);
}
/* /*
* ============================================================ * ============================================================
* PLAY ROULETTE * PLAY ROULETTE
@@ -322,7 +513,7 @@ async function playRoulette(
const footer = const footer =
interaction.message interaction.message
.embeds[0] ?.embeds?.[0]
?.footer ?.footer
?.text; ?.text;
@@ -371,10 +562,22 @@ async function playRoulette(
} }
/*
* IMPORTANT:
*
* Do NOT require ROULETTE_BETS.includes(bet) here.
*
* All In and Run It Back may legitimately use
* amounts outside the predefined wager buttons.
*/
if ( if (
!ROULETTE_BETS.includes( !Number.isSafeInteger(
bet bet
) )
||
bet <=
0
) { ) {
return temporaryEphemeralReply( return temporaryEphemeralReply(
interaction, interaction,
@@ -409,6 +612,11 @@ async function playRoulette(
]); ]);
/*
* Balance may have changed while the player
* was choosing their bet type.
*/
if ( if (
playerBalance < playerBalance <
bet bet
@@ -424,11 +632,17 @@ async function playRoulette(
/* /*
* Worst payout is Zero at x36. * Worst payout depends on the selected wager.
* *
* Once the player's wager is transferred to the house, * Red / Black / Odd / Even:
* the house must still be able to cover the remaining * x2
* potential payout. *
* Zero:
* x36
*
* The player's wager is transferred to the house
* before payout, so houseBalance + bet is available
* after accepting the wager.
*/ */
const maximumPayout = const maximumPayout =
@@ -452,7 +666,9 @@ async function playRoulette(
/* /*
* Take wager. * ========================================================
* TAKE WAGER
* ========================================================
*/ */
try { try {
@@ -545,8 +761,9 @@ async function playRoulette(
/* /*
* The real result is only generated after * ========================================================
* the animation completes. * ACTUAL RESULT
* ========================================================
*/ */
const result = const result =
@@ -567,6 +784,12 @@ async function playRoulette(
: 0; : 0;
/*
* ========================================================
* PAYOUT
* ========================================================
*/
if ( if (
payout > payout >
0 0
@@ -591,9 +814,15 @@ async function playRoulette(
.setTitle( .setTitle(
"🎡 Roulette Error" "🎡 Roulette Error"
) )
.setDescription( .setDescription(
"The wheel resolved, but the payout failed. Please contact an administrator." "The wheel resolved, but the payout failed. Please contact an administrator."
) )
.setFooter({
text:
`crumbRouletteResult:${playerId}:${bet}`
})
], ],
components: components:
@@ -630,6 +859,7 @@ async function playRoulette(
? "🎉 Roulette Winner!" ? "🎉 Roulette Winner!"
: "🎡 Roulette" : "🎡 Roulette"
) )
.setDescription( .setDescription(
[ [
"## The ball lands on...", "## The ball lands on...",
@@ -643,6 +873,7 @@ async function playRoulette(
: "The house takes this one." : "The house takes this one."
].join("\n") ].join("\n")
) )
.addFields( .addFields(
{ {
name: name:
@@ -699,9 +930,17 @@ async function playRoulette(
true true
} }
) )
/*
* Remember only:
*
* owner
* wager
*/
.setFooter({ .setFooter({
text: text:
`crumbRouletteResult:${interaction.user.id}` `crumbRouletteResult:${playerId}:${bet}`
}); });
@@ -728,6 +967,7 @@ function didRouletteBetWin(
) { ) {
switch (choice) { switch (choice) {
case "Red": case "Red":
return ( return (
number !== number !==
0 0
@@ -739,6 +979,7 @@ function didRouletteBetWin(
case "Black": case "Black":
return ( return (
number !== number !==
0 0
@@ -750,6 +991,7 @@ function didRouletteBetWin(
case "Odd": case "Odd":
return ( return (
number !== number !==
0 0
@@ -761,6 +1003,7 @@ function didRouletteBetWin(
case "Even": case "Even":
return ( return (
number !== number !==
0 0
@@ -772,6 +1015,7 @@ function didRouletteBetWin(
case "Zero": case "Zero":
return ( return (
number === number ===
0 0
@@ -779,6 +1023,7 @@ function didRouletteBetWin(
default: default:
return false; return false;
} }
} }
@@ -800,6 +1045,7 @@ function buildSpinEmbed(
.setTitle( .setTitle(
"🎡 Crumb Saucer Roulette" "🎡 Crumb Saucer Roulette"
) )
.setDescription( .setDescription(
[ [
"## 🎡 Spinning...", "## 🎡 Spinning...",
@@ -809,6 +1055,7 @@ function buildSpinEmbed(
`**${status}**` `**${status}**`
].join("\n") ].join("\n")
) )
.addFields( .addFields(
{ {
name: name:
@@ -861,7 +1108,7 @@ function formatRouletteNumber(
/* /*
* ============================================================ * ============================================================
* BUTTON BUILDERS * WAGER BUTTONS
* ============================================================ * ============================================================
*/ */
@@ -875,12 +1122,15 @@ function buildBetButtons() {
.setCustomId( .setCustomId(
`crumbRouletteBet${bet}` `crumbRouletteBet${bet}`
) )
.setLabel( .setLabel(
bet.toLocaleString() bet.toLocaleString()
) )
.setEmoji( .setEmoji(
"🍪" "🍪"
) )
.setStyle( .setStyle(
bet >= bet >=
500 500
@@ -892,6 +1142,20 @@ function buildBetButtons() {
new ActionRowBuilder() new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder()
.setCustomId(
"crumbRouletteAllIn"
)
.setLabel(
"All In"
)
.setEmoji(
"🔥"
)
.setStyle(
ButtonStyle.Danger
),
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId(
"crumbSaucerBack" "crumbSaucerBack"
@@ -910,6 +1174,12 @@ function buildBetButtons() {
} }
/*
* ============================================================
* CHOICE BUTTONS
* ============================================================
*/
function buildChoiceButtons() { function buildChoiceButtons() {
return [ return [
new ActionRowBuilder() new ActionRowBuilder()
@@ -999,16 +1269,46 @@ function buildChoiceButtons() {
} }
/*
* ============================================================
* RESULT BUTTONS
* ============================================================
*/
function buildResultButtons() { function buildResultButtons() {
return [ return [
new ActionRowBuilder() new ActionRowBuilder()
.addComponents( .addComponents(
/*
* Same wager.
*
* Player chooses Red/Black/etc again.
*/
new ButtonBuilder()
.setCustomId(
"crumbRouletteRunItBack"
)
.setLabel(
"Run It Back"
)
.setEmoji(
"🔁"
)
.setStyle(
ButtonStyle.Success
),
/*
* Return to wager selection.
*/
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId(
"crumbSaucerRoulette" "crumbSaucerRoulette"
) )
.setLabel( .setLabel(
"Play Again" "Change Bet"
) )
.setEmoji( .setEmoji(
"🎡" "🎡"
@@ -1017,6 +1317,10 @@ function buildResultButtons() {
ButtonStyle.Primary ButtonStyle.Primary
), ),
/*
* Main Crumb Saucer.
*/
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId(
"crumbSaucerBack" "crumbSaucerBack"
@@ -1046,7 +1350,7 @@ function getRouletteSessionOwner(
) { ) {
const footer = const footer =
interaction.message interaction.message
.embeds[0] ?.embeds?.[0]
?.footer ?.footer
?.text; ?.text;
@@ -1060,6 +1364,7 @@ function getRouletteSessionOwner(
/* /*
* Main Saucer * Main Saucer
*/ */
if ( if (
parts[0] === parts[0] ===
"crumbSaucer" "crumbSaucer"
@@ -1071,6 +1376,7 @@ function getRouletteSessionOwner(
/* /*
* Roulette wager menu * Roulette wager menu
*/ */
if ( if (
parts[0] === parts[0] ===
"crumbRouletteMenu" "crumbRouletteMenu"
@@ -1080,8 +1386,21 @@ function getRouletteSessionOwner(
/* /*
* Roulette result * Roulette choice menu.
*/ */
if (
parts[0] ===
"crumbRouletteBet"
) {
return parts[1];
}
/*
* Roulette result.
*/
if ( if (
parts[0] === parts[0] ===
"crumbRouletteResult" "crumbRouletteResult"
@@ -1121,8 +1440,20 @@ function sleep(
} }
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = { module.exports = {
showRoulette, showRoulette,
chooseRouletteBet, chooseRouletteBet,
playRoulette startRouletteAllIn,
showRouletteChoices,
playRoulette,
runRouletteBack
}; };
+348 -120
View File
@@ -14,7 +14,9 @@ const {
/* /*
* ============================================================
* SLOT CONFIG * SLOT CONFIG
* ============================================================
*/ */
const SLOT_BETS = [ const SLOT_BETS = [
@@ -98,6 +100,40 @@ const SLOT_OUTCOMES = [
async function showCrumbSaucer( async function showCrumbSaucer(
interaction interaction
) { ) {
/*
* If this is being called from a button,
* verify that the player owns the current
* Crumb Saucer / game session.
*
* This prevents another user from pressing
* "Crumb Saucer" / "Back" on somebody else's
* game message and taking over their session.
*/
if (
interaction.isButton()
) {
const ownerId =
getCrumbSaucerSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
}
const balance = const balance =
await getCookieBalance( await getCookieBalance(
interaction.user.id interaction.user.id
@@ -116,11 +152,8 @@ async function showCrumbSaucer(
/* /*
* Slash command:
* reply normally.
*
* Button: * Button:
* update existing menu message. * update existing menu/game message.
*/ */
if ( if (
@@ -136,6 +169,11 @@ async function showCrumbSaucer(
} }
/*
* Slash command:
* create a new Crumb Saucer.
*/
return interaction.reply({ return interaction.reply({
embeds: [ embeds: [
embed embed
@@ -145,51 +183,13 @@ async function showCrumbSaucer(
}); });
} }
function getSlotsSessionOwner(
interaction
) {
const footer =
interaction.message
.embeds[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
/*
* Main Crumb Saucer
*/
if (
parts[0] ===
"crumbSaucer"
) {
return parts[1];
}
/*
* Slots menu/result
*/
if (
parts[0] ===
"crumbSaucerSlots"
) {
return parts[1];
}
return null;
}
/* /*
* ============================================================ * ============================================================
* SLOT MENU * SLOT MENU
* ============================================================ * ============================================================
*/ */
async function showSlots( async function showSlots(
interaction interaction
) { ) {
@@ -224,19 +224,21 @@ async function showSlots(
.setTitle( .setTitle(
"🎰 Crumb Saucer Slots" "🎰 Crumb Saucer Slots"
) )
.setDescription( .setDescription(
[ [
"Welcome to the Cookie reels.", "Welcome to the Cookie reels.",
"", "",
"Choose your wager below.", "Choose your wager below.",
"", "",
"🍋 **Triple Lemon** — ×1.5", "🍋 **Triple Lemon** — x1.5",
"🍒 **Triple Cherry** — ×2", "🍒 **Triple Cherry** — x2",
"🍪 **Triple Cookie** — ×3", "🍪 **Triple Cookie** — x3",
"⭐ **Triple Star** — ×5", "⭐ **Triple Star** — x5",
"💎 **Jackpot** — ×10" "💎 **Jackpot** — x10"
].join("\n") ].join("\n")
) )
.addFields({ .addFields({
name: name:
"Your Vault", "Your Vault",
@@ -244,6 +246,7 @@ async function showSlots(
value: value:
`**${balance.toLocaleString()} 🍪**` `**${balance.toLocaleString()} 🍪**`
}) })
.setFooter({ .setFooter({
text: text:
`crumbSaucerSlots:${interaction.user.id}` `crumbSaucerSlots:${interaction.user.id}`
@@ -260,6 +263,7 @@ async function showSlots(
}); });
} }
/* /*
* ============================================================ * ============================================================
* PLAY SLOTS * PLAY SLOTS
@@ -271,7 +275,7 @@ async function playSlots(
bet bet
) { ) {
const ownerId = const ownerId =
getSlotsOwner( getSlotsSessionOwner(
interaction interaction
); );
@@ -308,6 +312,7 @@ async function playSlots(
const playerId = const playerId =
interaction.user.id; interaction.user.id;
const vaultId = const vaultId =
interaction.client.user.id; interaction.client.user.id;
@@ -388,6 +393,7 @@ async function playSlots(
); );
} }
throw error; throw error;
} }
@@ -409,11 +415,6 @@ async function playSlots(
}); });
/*
* A few slow edits give a slot-machine feel
* without hammering the Discord API.
*/
await sleep( await sleep(
700 700
); );
@@ -478,13 +479,11 @@ async function playSlots(
/* /*
* House pays winnings. * House pays winnings.
*
* The original stake was already transferred
* to the house, so x2 means a total payout of 2x.
*/ */
if ( if (
payout > 0 payout >
0
) { ) {
try { try {
await transferCookies( await transferCookies(
@@ -509,6 +508,10 @@ async function playSlots(
.setDescription( .setDescription(
"The reels stopped, but the payout system had a problem. Please contact an administrator." "The reels stopped, but the payout system had a problem. Please contact an administrator."
) )
.setFooter({
text:
`crumbSaucerSlotsResult:${playerId}:${bet}`
})
], ],
components: components:
@@ -543,6 +546,7 @@ async function playSlots(
.setTitle( .setTitle(
"🎰 Crumb Saucer Slots" "🎰 Crumb Saucer Slots"
) )
.setDescription( .setDescription(
[ [
buildSlotDisplay( buildSlotDisplay(
@@ -552,6 +556,7 @@ async function playSlots(
`### ${getSlotResultName(outcome)}` `### ${getSlotResultName(outcome)}`
].join("\n") ].join("\n")
) )
.addFields( .addFields(
{ {
name: name:
@@ -569,7 +574,7 @@ async function playSlots(
"Multiplier", "Multiplier",
value: value:
`×${outcome.multiplier}`, `x${outcome.multiplier}`,
inline: inline:
true true
@@ -619,9 +624,17 @@ async function playSlots(
true true
} }
) )
/*
* Store owner + previous wager.
*
* Run It Back can now replay the exact
* same wager without opening the bet menu.
*/
.setFooter({ .setFooter({
text: text:
`crumbSaucerSlots:${interaction.user.id}` `crumbSaucerSlotsResult:${playerId}:${bet}`
}); });
@@ -636,6 +649,88 @@ async function playSlots(
} }
/*
* ============================================================
* RUN IT BACK
* ============================================================
*/
async function runSlotsBack(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
if (
parts[0] !==
"crumbSaucerSlotsResult"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This Slots result can no longer be replayed."
}
);
}
const ownerId =
parts[1];
const bet =
Number(
parts[2]
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"These aren't your slots."
}
);
}
if (
!SLOT_BETS.includes(
bet
)
) {
return temporaryEphemeralReply(
interaction,
{
content:
"The previous wager is no longer available."
}
);
}
return playSlots(
interaction,
bet
);
}
/* /*
* ============================================================ * ============================================================
* MAIN MENU EMBED * MAIN MENU EMBED
@@ -650,6 +745,7 @@ function buildMainMenuEmbed(
.setTitle( .setTitle(
"🎰 The Crumb Saucer" "🎰 The Crumb Saucer"
) )
.setDescription( .setDescription(
[ [
`Welcome, ${user}!`, `Welcome, ${user}!`,
@@ -672,6 +768,7 @@ function buildMainMenuEmbed(
"*More attractions coming soon... make a lala offering or buy me a white monster*" "*More attractions coming soon... make a lala offering or buy me a white monster*"
].join("\n") ].join("\n")
) )
.addFields({ .addFields({
name: name:
"Your Vault", "Your Vault",
@@ -679,6 +776,7 @@ function buildMainMenuEmbed(
value: value:
`**${balance.toLocaleString()} 🍪**` `**${balance.toLocaleString()} 🍪**`
}) })
.setFooter({ .setFooter({
text: text:
`crumbSaucer:${user.id}` `crumbSaucer:${user.id}`
@@ -688,7 +786,7 @@ function buildMainMenuEmbed(
/* /*
* ============================================================ * ============================================================
* BUTTONS * MAIN MENU BUTTONS
* ============================================================ * ============================================================
*/ */
@@ -755,6 +853,13 @@ function buildMainMenuButtons() {
]; ];
} }
/*
* ============================================================
* SLOT BET BUTTONS
* ============================================================
*/
function buildSlotBetButtons() { function buildSlotBetButtons() {
return [ return [
new ActionRowBuilder() new ActionRowBuilder()
@@ -850,16 +955,44 @@ function buildSlotBetButtons() {
} }
/*
* ============================================================
* SLOT RESULT BUTTONS
* ============================================================
*/
function buildResultButtons() { function buildResultButtons() {
return [ return [
new ActionRowBuilder() new ActionRowBuilder()
.addComponents( .addComponents(
/*
* Exact same wager immediately.
*/
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlotsRunItBack"
)
.setLabel(
"Run It Back"
)
.setEmoji(
"🔁"
)
.setStyle(
ButtonStyle.Success
),
/*
* Return to wager selection.
*/
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId(
"crumbSaucerSlots" "crumbSaucerSlots"
) )
.setLabel( .setLabel(
"Play Again" "Change Bet"
) )
.setEmoji( .setEmoji(
"🎰" "🎰"
@@ -868,6 +1001,10 @@ function buildResultButtons() {
ButtonStyle.Primary ButtonStyle.Primary
), ),
/*
* Main Saucer.
*/
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId(
"crumbSaucerBack" "crumbSaucerBack"
@@ -920,7 +1057,8 @@ function rollSlotOutcome() {
if ( if (
roll <= 0 roll <=
0
) { ) {
return outcome; return outcome;
} }
@@ -959,10 +1097,6 @@ function generateOutcomeGrid(
generateRandomGrid(); generateRandomGrid();
/*
* Winning middle row.
*/
if ( if (
outcome.type === outcome.type ===
"triple" "triple"
@@ -978,10 +1112,6 @@ function generateOutcomeGrid(
} }
/*
* Pair.
*/
if ( if (
outcome.type === outcome.type ===
"pair" "pair"
@@ -1002,14 +1132,11 @@ function generateOutcomeGrid(
); );
} while ( } while (
other === symbol other ===
symbol
); );
/*
* Randomize the location of the odd symbol.
*/
const oddPosition = const oddPosition =
Math.floor( Math.floor(
Math.random() * Math.random() *
@@ -1034,11 +1161,6 @@ function generateOutcomeGrid(
} }
/*
* Loss:
* ensure all three center symbols differ.
*/
let line; let line;
@@ -1095,6 +1217,7 @@ function buildSpinningEmbed(
.setTitle( .setTitle(
"🎰 Crumb Saucer Slots" "🎰 Crumb Saucer Slots"
) )
.setDescription( .setDescription(
[ [
buildSlotDisplay( buildSlotDisplay(
@@ -1104,6 +1227,7 @@ function buildSpinningEmbed(
`**${status}**` `**${status}**`
].join("\n") ].join("\n")
) )
.addFields({ .addFields({
name: name:
"Bet", "Bet",
@@ -1139,18 +1263,23 @@ function getSlotResultName(
case "🍋": case "🍋":
return "🍋 Triple Lemon!"; return "🍋 Triple Lemon!";
case "🍒": case "🍒":
return "🍒 Triple Cherry!"; return "🍒 Triple Cherry!";
case "🍪": case "🍪":
return "🍪 Triple Cookie!"; return "🍪 Triple Cookie!";
case "⭐": case "⭐":
return "⭐ Triple Star!"; return "⭐ Triple Star!";
case "💎": case "💎":
return "💎 JACKPOT! 💎"; return "💎 JACKPOT! 💎";
default: default:
return "Winner!"; return "Winner!";
} }
@@ -1159,75 +1288,166 @@ function getSlotResultName(
/* /*
* ============================================================ * ============================================================
* OWNER RESOLUTION * SESSION OWNER RESOLUTION
* ============================================================
*
* This intentionally understands every Crumb Saucer
* screen that can navigate back to the main menu.
*
* That prevents another player from pressing the
* Back / Crumb Saucer button on somebody else's
* public interaction.
*/
function getCrumbSaucerSessionOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
switch (
parts[0]
) {
/*
* Main menu
*/
case "crumbSaucer":
/*
* Slots wager menu
*/
case "crumbSaucerSlots":
/*
* Slots completed result
*/
case "crumbSaucerSlotsResult":
/*
* Blackjack wager/result menu
*/
case "crumbSaucerBlackjack":
case "crumbSaucerBlackjackResult":
/*
* Roulette
*/
case "crumbRouletteMenu":
case "crumbRouletteBet":
case "crumbRouletteResult":
/*
* Heist
*/
case "crumbSaucerHeist":
case "cookieHeist":
return parts[1];
/*
* Active Blackjack footer:
*
* crumbBlackjack:<gameId>:<userId>
*/
case "crumbBlackjack":
return parts[2];
default:
return null;
}
}
/*
* ============================================================
* SLOT OWNER RESOLUTION
* ============================================================ * ============================================================
*/ */
function getMenuOwner( function getSlotsSessionOwner(
interaction interaction
) { ) {
const footer = const footer =
interaction.message interaction.message
.embeds[0] ?.embeds?.[0]
?.footer ?.footer
?.text; ?.text;
const [ const parts =
type, footer?.split(":")
userId
] =
footer
?.split(":")
?? ??
[]; [];
/*
* Main Crumb Saucer.
*/
if ( if (
type !== parts[0] ===
"crumbSaucer" "crumbSaucer"
) { ) {
return null; return parts[1];
} }
return userId; /*
} * Slots wager menu.
*/
function getSlotsOwner(
interaction
) {
const footer =
interaction.message
.embeds[0]
?.footer
?.text;
const [
type,
userId
] =
footer
?.split(":")
??
[];
if ( if (
type !== parts[0] ===
"crumbSaucerSlots" "crumbSaucerSlots"
) { ) {
return null; return parts[1];
} }
return userId; /*
* Completed Slots result.
*/
if (
parts[0] ===
"crumbSaucerSlotsResult"
) {
return parts[1];
}
return null;
} }
function sleep(ms) { /*
* ============================================================
* SLEEP
* ============================================================
*/
function sleep(
ms
) {
return new Promise( return new Promise(
resolve => resolve =>
setTimeout( setTimeout(
@@ -1238,8 +1458,16 @@ function sleep(ms) {
} }
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = { module.exports = {
showCrumbSaucer, showCrumbSaucer,
showSlots, showSlots,
playSlots playSlots,
runSlotsBack
}; };