1473 lines
28 KiB
JavaScript
1473 lines
28 KiB
JavaScript
const {
|
|
EmbedBuilder,
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
getCookieBalance,
|
|
transferCookies,
|
|
temporaryEphemeralReply,
|
|
randomItem
|
|
} = require("./cookieEconomy");
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLOT CONFIG
|
|
* ============================================================
|
|
*/
|
|
|
|
const SLOT_BETS = [
|
|
10,
|
|
50,
|
|
100,
|
|
500,
|
|
1000
|
|
];
|
|
|
|
|
|
const SLOT_SYMBOLS = [
|
|
"🍒",
|
|
"🍋",
|
|
"🍪",
|
|
"⭐",
|
|
"💎"
|
|
];
|
|
|
|
|
|
/*
|
|
* Weighted outcomes.
|
|
*/
|
|
|
|
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
|
|
}
|
|
];
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* MAIN MENU
|
|
* ============================================================
|
|
*/
|
|
|
|
async function showCrumbSaucer(
|
|
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 =
|
|
await getCookieBalance(
|
|
interaction.user.id
|
|
);
|
|
|
|
|
|
const embed =
|
|
buildMainMenuEmbed(
|
|
interaction.user,
|
|
balance
|
|
);
|
|
|
|
|
|
const components =
|
|
buildMainMenuButtons();
|
|
|
|
|
|
/*
|
|
* Button:
|
|
* update existing menu/game message.
|
|
*/
|
|
|
|
if (
|
|
interaction.isButton()
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Slash command:
|
|
* create a new Crumb Saucer.
|
|
*/
|
|
|
|
return interaction.reply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLOT MENU
|
|
* ============================================================
|
|
*/
|
|
|
|
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()
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* PLAY SLOTS
|
|
* ============================================================
|
|
*/
|
|
|
|
async function playSlots(
|
|
interaction,
|
|
bet
|
|
) {
|
|
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.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Worst-case payout is x10.
|
|
*
|
|
* Once the house receives the player's bet,
|
|
* it needs another 9x available to cover
|
|
* the complete x10 payout.
|
|
*/
|
|
|
|
if (
|
|
vaultBalance <
|
|
bet * 9
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"The Crumb Saucer vault doesn't currently have enough Cookies to cover that wager."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Take wager first.
|
|
*/
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
/*
|
|
* Lock the buttons while spinning.
|
|
*/
|
|
|
|
await interaction.update({
|
|
embeds: [
|
|
buildSpinningEmbed(
|
|
bet,
|
|
generateRandomGrid(),
|
|
"🎰 Spinning..."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
await sleep(
|
|
700
|
|
);
|
|
|
|
|
|
await interaction.editReply({
|
|
embeds: [
|
|
buildSpinningEmbed(
|
|
bet,
|
|
generateRandomGrid(),
|
|
"🎰 The reels are moving..."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
await sleep(
|
|
700
|
|
);
|
|
|
|
|
|
await interaction.editReply({
|
|
embeds: [
|
|
buildSpinningEmbed(
|
|
bet,
|
|
generateRandomGrid(),
|
|
"🎰 Almost..."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
await sleep(
|
|
700
|
|
);
|
|
|
|
|
|
/*
|
|
* Actual outcome is determined only now.
|
|
*/
|
|
|
|
const outcome =
|
|
rollSlotOutcome();
|
|
|
|
|
|
const grid =
|
|
generateOutcomeGrid(
|
|
outcome
|
|
);
|
|
|
|
|
|
const payout =
|
|
Math.floor(
|
|
bet *
|
|
outcome.multiplier
|
|
);
|
|
|
|
|
|
/*
|
|
* House pays winnings.
|
|
*/
|
|
|
|
if (
|
|
payout >
|
|
0
|
|
) {
|
|
try {
|
|
await transferCookies(
|
|
vaultId,
|
|
playerId,
|
|
payout
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error(
|
|
"CRITICAL: Crumb Saucer 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;
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🎰 Crumb Saucer Slots"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
buildSlotDisplay(
|
|
grid
|
|
),
|
|
"",
|
|
`### ${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
|
|
}
|
|
)
|
|
|
|
/*
|
|
* Store owner + previous wager.
|
|
*
|
|
* Run It Back can now replay the exact
|
|
* same wager without opening the bet menu.
|
|
*/
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbSaucerSlotsResult:${playerId}:${bet}`
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
resultEmbed
|
|
],
|
|
|
|
components:
|
|
buildResultButtons()
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 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
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildMainMenuEmbed(
|
|
user,
|
|
balance
|
|
) {
|
|
return new EmbedBuilder()
|
|
.setTitle(
|
|
"🎰 The Crumb Saucer"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
`Welcome, ${user}!`,
|
|
"",
|
|
"Where Cookies are won, lost,",
|
|
"and occasionally confiscated by ShaiBot.",
|
|
"",
|
|
"### 🎰 Slots",
|
|
"Spin the reels and chase the Cookie jackpot.",
|
|
"",
|
|
"### 🏦 Vault Heist",
|
|
"Attempt to breach ShaiBot's Cookie Vault.",
|
|
"",
|
|
"### 🃏 Blackjack",
|
|
"Beat the dealer without going over 21.",
|
|
"",
|
|
"### 🎡 Roulette",
|
|
"Bet on Red, Black, Odd, Even, or Zero.",
|
|
"",
|
|
"*More attractions coming soon... make a lala offering or buy me a white monster*"
|
|
].join("\n")
|
|
)
|
|
|
|
.addFields({
|
|
name:
|
|
"Your Vault",
|
|
|
|
value:
|
|
`**${balance.toLocaleString()} 🍪**`
|
|
})
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbSaucer:${user.id}`
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* MAIN MENU BUTTONS
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildMainMenuButtons() {
|
|
return [
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerSlots"
|
|
)
|
|
.setLabel(
|
|
"Slots"
|
|
)
|
|
.setEmoji(
|
|
"🎰"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Primary
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerBlackjack"
|
|
)
|
|
.setLabel(
|
|
"Blackjack"
|
|
)
|
|
.setEmoji(
|
|
"🃏"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Success
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerRoulette"
|
|
)
|
|
.setLabel(
|
|
"Roulette"
|
|
)
|
|
.setEmoji(
|
|
"🎡"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Primary
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerHeist"
|
|
)
|
|
.setLabel(
|
|
"Vault Heist"
|
|
)
|
|
.setEmoji(
|
|
"🏦"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Danger
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLOT BET BUTTONS
|
|
* ============================================================
|
|
*/
|
|
|
|
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
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLOT RESULT BUTTONS
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildResultButtons() {
|
|
return [
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
/*
|
|
* Exact same wager immediately.
|
|
*/
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerSlotsRunItBack"
|
|
)
|
|
.setLabel(
|
|
"Run It Back"
|
|
)
|
|
.setEmoji(
|
|
"🔁"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Success
|
|
),
|
|
|
|
/*
|
|
* Return to wager selection.
|
|
*/
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerSlots"
|
|
)
|
|
.setLabel(
|
|
"Change Bet"
|
|
)
|
|
.setEmoji(
|
|
"🎰"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Primary
|
|
),
|
|
|
|
/*
|
|
* Main Saucer.
|
|
*/
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbSaucerBack"
|
|
)
|
|
.setLabel(
|
|
"Crumb Saucer"
|
|
)
|
|
.setEmoji(
|
|
"◀️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Secondary
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLOT LOGIC
|
|
* ============================================================
|
|
*/
|
|
|
|
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 generateRandomGrid() {
|
|
return Array.from(
|
|
{
|
|
length: 3
|
|
},
|
|
|
|
() =>
|
|
Array.from(
|
|
{
|
|
length: 3
|
|
},
|
|
|
|
() =>
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
function generateOutcomeGrid(
|
|
outcome
|
|
) {
|
|
const grid =
|
|
generateRandomGrid();
|
|
|
|
|
|
if (
|
|
outcome.type ===
|
|
"triple"
|
|
) {
|
|
grid[1] = [
|
|
outcome.symbol,
|
|
outcome.symbol,
|
|
outcome.symbol
|
|
];
|
|
|
|
|
|
return grid;
|
|
}
|
|
|
|
|
|
if (
|
|
outcome.type ===
|
|
"pair"
|
|
) {
|
|
const symbol =
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
);
|
|
|
|
|
|
let other;
|
|
|
|
|
|
do {
|
|
other =
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
);
|
|
|
|
} while (
|
|
other ===
|
|
symbol
|
|
);
|
|
|
|
|
|
const oddPosition =
|
|
Math.floor(
|
|
Math.random() *
|
|
3
|
|
);
|
|
|
|
|
|
grid[1] = [
|
|
symbol,
|
|
symbol,
|
|
symbol
|
|
];
|
|
|
|
|
|
grid[1][
|
|
oddPosition
|
|
] =
|
|
other;
|
|
|
|
|
|
return grid;
|
|
}
|
|
|
|
|
|
let line;
|
|
|
|
|
|
do {
|
|
line = [
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
),
|
|
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
),
|
|
|
|
randomItem(
|
|
SLOT_SYMBOLS
|
|
)
|
|
];
|
|
|
|
} while (
|
|
new Set(
|
|
line
|
|
).size <
|
|
3
|
|
);
|
|
|
|
|
|
grid[1] =
|
|
line;
|
|
|
|
|
|
return grid;
|
|
}
|
|
|
|
|
|
function buildSlotDisplay(
|
|
grid
|
|
) {
|
|
return [
|
|
"╔═════════╗",
|
|
`║ ${grid[0][0]} │ ${grid[0][1]} │ ${grid[0][2]} ║`,
|
|
`║ ${grid[1][0]} │ ${grid[1][1]} │ ${grid[1][2]} ║`,
|
|
`║ ${grid[2][0]} │ ${grid[2][1]} │ ${grid[2][2]} ║`,
|
|
"╚═════════╝"
|
|
].join("\n");
|
|
}
|
|
|
|
|
|
function buildSpinningEmbed(
|
|
bet,
|
|
grid,
|
|
status
|
|
) {
|
|
return new EmbedBuilder()
|
|
.setTitle(
|
|
"🎰 Crumb Saucer Slots"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
buildSlotDisplay(
|
|
grid
|
|
),
|
|
"",
|
|
`**${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!";
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 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 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 wager menu.
|
|
*/
|
|
|
|
if (
|
|
parts[0] ===
|
|
"crumbSaucerSlots"
|
|
) {
|
|
return parts[1];
|
|
}
|
|
|
|
|
|
/*
|
|
* Completed Slots result.
|
|
*/
|
|
|
|
if (
|
|
parts[0] ===
|
|
"crumbSaucerSlotsResult"
|
|
) {
|
|
return parts[1];
|
|
}
|
|
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SLEEP
|
|
* ============================================================
|
|
*/
|
|
|
|
function sleep(
|
|
ms
|
|
) {
|
|
return new Promise(
|
|
resolve =>
|
|
setTimeout(
|
|
resolve,
|
|
ms
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* EXPORTS
|
|
* ============================================================
|
|
*/
|
|
|
|
module.exports = {
|
|
showCrumbSaucer,
|
|
|
|
showSlots,
|
|
playSlots,
|
|
runSlotsBack
|
|
}; |