Files
ShaiBot/utils/crumbSlots.js
T
2026-08-27 19:50:12 +02:00

791 lines
17 KiB
JavaScript

const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js");
const {
getCookieBalance,
transferCookies,
temporaryEphemeralReply,
randomItem
} = require("./cookieEconomy");
const {
recordGameOutcome,
trackProfileEvent
} = require("./profileSystem");
const SLOT_BETS = [
10,
50,
100,
500,
1000
];
const SLOT_SYMBOLS = [
"🍒",
"🍋",
"🍪",
"⭐",
"💎"
];
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 }
];
async function showSlots(interaction) {
const ownerId =
getSlotsSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
const balance =
await getCookieBalance(
interaction.user.id
);
const embed =
new EmbedBuilder()
.setTitle(
"🎰 Crumb Saucer Slots"
)
.setDescription(
[
"Welcome to the Cookie reels.",
"",
"Choose your wager below.",
"",
"🍋 **Triple Lemon** — x1.5",
"🍒 **Triple Cherry** — x2",
"🍪 **Triple Cookie** — x3",
"⭐ **Triple Star** — x5",
"💎 **Jackpot** — x10"
].join("\n")
)
.addFields({
name:
"Your Vault",
value:
`**${balance.toLocaleString()} 🍪**`
})
.setFooter({
text:
`crumbSaucerSlots:${interaction.user.id}`
});
return interaction.update({
embeds: [embed],
components:
buildSlotBetButtons()
});
}
async function playSlots(
interaction,
bet,
source = "normal"
) {
const ownerId =
getSlotsSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"These aren't your slots. Open your own Crumb Saucer session."
}
);
}
if (!SLOT_BETS.includes(bet)) {
return temporaryEphemeralReply(
interaction,
{
content:
"That wager isn't available."
}
);
}
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 temporaryEphemeralReply(
interaction,
{
content:
`You need **${bet.toLocaleString()} Cookies** for that wager.`
}
);
}
const maxPayout =
bet * 10;
if (
vaultBalance + bet <
maxPayout
) {
return temporaryEphemeralReply(
interaction,
{
content:
"The Crumb Saucer vault doesn't currently have enough Cookies to cover that wager."
}
);
}
try {
await transferCookies(
playerId,
vaultId,
bet
);
} catch (error) {
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Your Cookie balance changed before the spin could begin."
}
);
}
throw error;
}
await trackProfileEvent(
interaction,
playerId,
{
type: "game_start",
game: "slots",
source,
bet,
wager: bet,
allIn:
bet === playerBalance,
previousAllIn,
previousBet
}
);
await interaction.update({
embeds: [
buildSpinningEmbed(
bet,
["❔", "❔", "❔"],
"The reels are spinning..."
)
],
components: []
});
await sleep(500);
const firstPreview =
randomItem(SLOT_SYMBOLS);
await interaction.editReply({
embeds: [
buildSpinningEmbed(
bet,
[
firstPreview,
"❔",
"❔"
],
"The first reel stops..."
)
],
components: []
});
await sleep(500);
const secondPreview =
randomItem(SLOT_SYMBOLS);
await interaction.editReply({
embeds: [
buildSpinningEmbed(
bet,
[
firstPreview,
secondPreview,
"❔"
],
"One more..."
)
],
components: []
});
await sleep(600);
const outcome =
rollSlotOutcome();
const result =
generateOutcomeLine(
outcome
);
const payout =
Math.floor(
bet *
outcome.multiplier
);
if (payout > 0) {
try {
await transferCookies(
vaultId,
playerId,
payout
);
} catch (error) {
console.error(
"CRITICAL: Crumb Saucer Slots payout failed:",
error
);
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"🎰 Crumb Saucer Error"
)
.setDescription(
"The reels stopped, but the payout system had a problem. Please contact an administrator."
)
.setFooter({
text:
`crumbSaucerSlotsResult:${playerId}:${bet}`
})
],
components:
buildResultButtons()
});
}
}
const [
playerAfter,
vaultAfter
] = await Promise.all([
getCookieBalance(playerId),
getCookieBalance(vaultId)
]);
const profit =
payout - bet;
await recordGameOutcome(
interaction,
playerId,
{
game:
"slots",
result:
profit > 0
? "win"
: profit < 0
? "loss"
: "push",
profit,
triplet:
outcome.type === "triple"
? outcome.symbol
: null,
jackpot:
outcome.type ===
"triple" &&
outcome.symbol ===
"💎"
}
);
const resultEmbed =
new EmbedBuilder()
.setTitle(
"🎰 Crumb Saucer Slots"
)
.setDescription(
[
"## " +
result.join(" "),
"",
`### ${getSlotResultName(outcome)}`
].join("\n")
)
.addFields(
{
name:
"Bet",
value:
`${bet.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Multiplier",
value:
`x${outcome.multiplier}`,
inline:
true
},
{
name:
"Payout",
value:
`${payout.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Profit",
value:
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Your Vault",
value:
`${playerAfter.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"House Vault",
value:
`${vaultAfter.toLocaleString()} 🍪`,
inline:
true
}
)
.setFooter({
text:
`crumbSaucerSlotsResult:${playerId}:${bet}`
});
return interaction.editReply({
embeds: [resultEmbed],
components:
buildResultButtons()
});
}
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,
"run_it_back"
);
}
function buildSlotBetButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots10"
)
.setLabel("10")
.setEmoji("🍪")
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots50"
)
.setLabel("50")
.setEmoji("🍪")
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots100"
)
.setLabel("100")
.setEmoji("🍪")
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots500"
)
.setLabel("500")
.setEmoji("🍪")
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots1000"
)
.setLabel("1,000")
.setEmoji("🍪")
.setStyle(
ButtonStyle.Danger
)
),
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Back to Crumb Saucer"
)
.setEmoji("◀️")
.setStyle(
ButtonStyle.Secondary
)
)
];
}
function buildResultButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlotsRunItBack"
)
.setLabel(
"Run It Back"
)
.setEmoji("🔁")
.setStyle(
ButtonStyle.Success
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerSlots"
)
.setLabel(
"Change Bet"
)
.setEmoji("🎰")
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Crumb Saucer"
)
.setEmoji("◀️")
.setStyle(
ButtonStyle.Secondary
)
)
];
}
function rollSlotOutcome() {
const totalWeight =
SLOT_OUTCOMES.reduce(
(total, outcome) =>
total +
outcome.weight,
0
);
let roll =
Math.random() *
totalWeight;
for (const outcome of SLOT_OUTCOMES) {
roll -= outcome.weight;
if (roll <= 0) {
return outcome;
}
}
return SLOT_OUTCOMES[0];
}
function generateRandomLine() {
return [
randomItem(SLOT_SYMBOLS),
randomItem(SLOT_SYMBOLS),
randomItem(SLOT_SYMBOLS)
];
}
function generateOutcomeLine(
outcome
) {
if (
outcome.type ===
"triple"
) {
return [
outcome.symbol,
outcome.symbol,
outcome.symbol
];
}
if (
outcome.type ===
"pair"
) {
const symbol =
randomItem(SLOT_SYMBOLS);
let other;
do {
other =
randomItem(SLOT_SYMBOLS);
} while (other === symbol);
const result = [
symbol,
symbol,
symbol
];
result[
Math.floor(
Math.random() * 3
)
] = other;
return result;
}
let result;
do {
result =
generateRandomLine();
} while (
new Set(result).size < 3
);
return result;
}
function buildSpinningEmbed(
bet,
symbols,
status
) {
return new EmbedBuilder()
.setTitle(
"🎰 Crumb Saucer Slots"
)
.setDescription(
[
"## " +
symbols.join(" "),
"",
`**${status}**`
].join("\n")
)
.addFields({
name:
"Bet",
value:
`**${bet.toLocaleString()} 🍪**`
});
}
function getSlotResultName(
outcome
) {
if (outcome.type === "loss") {
return "No luck this time.";
}
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 "Winner!";
}
}
function getSlotsSessionOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":") ?? [];
if (
parts[0] ===
"crumbSaucer"
) {
return parts[1];
}
if (
parts[0] ===
"crumbSaucerSlots"
) {
return parts[1];
}
if (
parts[0] ===
"crumbSaucerSlotsResult"
) {
return parts[1];
}
return null;
}
function sleep(ms) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
module.exports = {
showSlots,
playSlots,
runSlotsBack
};