1128 lines
22 KiB
JavaScript
1128 lines
22 KiB
JavaScript
const {
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle
|
||
} = require("discord.js");
|
||
|
||
const {
|
||
getCookieBalance,
|
||
transferCookies,
|
||
temporaryEphemeralReply
|
||
} = require("./cookieEconomy");
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CONFIG
|
||
* ============================================================
|
||
*/
|
||
|
||
const ROULETTE_BETS = [
|
||
10,
|
||
50,
|
||
100,
|
||
500,
|
||
1000
|
||
];
|
||
|
||
|
||
const ROULETTE_CHOICES = {
|
||
Red: {
|
||
name:
|
||
"Red",
|
||
|
||
emoji:
|
||
"🔴",
|
||
|
||
multiplier:
|
||
2
|
||
},
|
||
|
||
|
||
Black: {
|
||
name:
|
||
"Black",
|
||
|
||
emoji:
|
||
"⚫",
|
||
|
||
multiplier:
|
||
2
|
||
},
|
||
|
||
|
||
Odd: {
|
||
name:
|
||
"Odd",
|
||
|
||
emoji:
|
||
"🔢",
|
||
|
||
multiplier:
|
||
2
|
||
},
|
||
|
||
|
||
Even: {
|
||
name:
|
||
"Even",
|
||
|
||
emoji:
|
||
"2️⃣",
|
||
|
||
multiplier:
|
||
2
|
||
},
|
||
|
||
|
||
Zero: {
|
||
name:
|
||
"Zero",
|
||
|
||
emoji:
|
||
"🟢",
|
||
|
||
multiplier:
|
||
36
|
||
}
|
||
};
|
||
|
||
|
||
/*
|
||
* European Roulette red numbers.
|
||
*/
|
||
|
||
const RED_NUMBERS =
|
||
new Set([
|
||
1,
|
||
3,
|
||
5,
|
||
7,
|
||
9,
|
||
12,
|
||
14,
|
||
16,
|
||
18,
|
||
19,
|
||
21,
|
||
23,
|
||
25,
|
||
27,
|
||
30,
|
||
32,
|
||
34,
|
||
36
|
||
]);
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SHOW ROULETTE
|
||
* ============================================================
|
||
*/
|
||
|
||
async function showRoulette(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getRouletteSessionOwner(
|
||
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 Roulette"
|
||
)
|
||
.setDescription(
|
||
[
|
||
"European Roulette — **0 to 36**.",
|
||
"",
|
||
"Choose your wager.",
|
||
"",
|
||
"🔴 **Red** — ×2",
|
||
"⚫ **Black** — ×2",
|
||
"🔢 **Odd** — ×2",
|
||
"2️⃣ **Even** — ×2",
|
||
"🟢 **Zero** — ×36"
|
||
].join("\n")
|
||
)
|
||
.addFields({
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} 🍪**`
|
||
})
|
||
.setFooter({
|
||
text:
|
||
`crumbRouletteMenu:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildBetButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CHOOSE WAGER
|
||
* ============================================================
|
||
*/
|
||
|
||
async function chooseRouletteBet(
|
||
interaction,
|
||
bet
|
||
) {
|
||
if (
|
||
!ROULETTE_BETS.includes(
|
||
bet
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That Roulette wager isn't available."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
getRouletteSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Roulette table."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (
|
||
balance <
|
||
bet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`You need **${bet.toLocaleString()} Cookies** for that wager.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🎡 Crumb Saucer Roulette"
|
||
)
|
||
.setDescription(
|
||
[
|
||
`Wager: **${bet.toLocaleString()} 🍪**`,
|
||
"",
|
||
"Where do you want to place your Cookies?",
|
||
"",
|
||
"🔴 **Red** — ×2",
|
||
"⚫ **Black** — ×2",
|
||
"🔢 **Odd** — ×2",
|
||
"2️⃣ **Even** — ×2",
|
||
"🟢 **Zero** — ×36"
|
||
].join("\n")
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`crumbRouletteBet:${interaction.user.id}:${bet}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildChoiceButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* PLAY ROULETTE
|
||
* ============================================================
|
||
*/
|
||
|
||
async function playRoulette(
|
||
interaction,
|
||
choiceKey
|
||
) {
|
||
const choice =
|
||
ROULETTE_CHOICES[
|
||
choiceKey
|
||
];
|
||
|
||
|
||
if (!choice) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That Roulette bet doesn't exist."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const footer =
|
||
interaction.message
|
||
.embeds[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
if (
|
||
parts[0] !==
|
||
"crumbRouletteBet"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Roulette wager is no longer valid."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
parts[1];
|
||
|
||
|
||
const bet =
|
||
Number(
|
||
parts[2]
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Roulette table."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
!ROULETTE_BETS.includes(
|
||
bet
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That Roulette wager is invalid."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerId =
|
||
interaction.user.id;
|
||
|
||
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
playerBalance,
|
||
houseBalance
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
playerId
|
||
),
|
||
|
||
getCookieBalance(
|
||
houseId
|
||
)
|
||
]);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
bet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"You no longer have enough Cookies for that wager."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Worst payout is Zero at x36.
|
||
*
|
||
* Once the player's wager is transferred to the house,
|
||
* the house must still be able to cover the remaining
|
||
* potential payout.
|
||
*/
|
||
|
||
const maximumPayout =
|
||
bet *
|
||
choice.multiplier;
|
||
|
||
|
||
if (
|
||
houseBalance +
|
||
bet <
|
||
maximumPayout
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"The Crumb Saucer vault can't currently cover that Roulette wager."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Take wager.
|
||
*/
|
||
|
||
try {
|
||
await transferCookies(
|
||
playerId,
|
||
houseId,
|
||
bet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Your balance changed before the wheel could spin."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* ========================================================
|
||
* PSEUDO ANIMATION
|
||
* ========================================================
|
||
*/
|
||
|
||
await interaction.update({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
choice,
|
||
bet,
|
||
randomRouletteNumber(),
|
||
"The wheel begins to spin..."
|
||
)
|
||
],
|
||
|
||
components: []
|
||
});
|
||
|
||
|
||
await sleep(
|
||
650
|
||
);
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
choice,
|
||
bet,
|
||
randomRouletteNumber(),
|
||
"Round and round..."
|
||
)
|
||
],
|
||
|
||
components: []
|
||
});
|
||
|
||
|
||
await sleep(
|
||
650
|
||
);
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
choice,
|
||
bet,
|
||
randomRouletteNumber(),
|
||
"The ball is slowing down..."
|
||
)
|
||
],
|
||
|
||
components: []
|
||
});
|
||
|
||
|
||
await sleep(
|
||
750
|
||
);
|
||
|
||
|
||
/*
|
||
* The real result is only generated after
|
||
* the animation completes.
|
||
*/
|
||
|
||
const result =
|
||
randomRouletteNumber();
|
||
|
||
|
||
const won =
|
||
didRouletteBetWin(
|
||
choiceKey,
|
||
result
|
||
);
|
||
|
||
|
||
const payout =
|
||
won
|
||
? bet *
|
||
choice.multiplier
|
||
: 0;
|
||
|
||
|
||
if (
|
||
payout >
|
||
0
|
||
) {
|
||
try {
|
||
await transferCookies(
|
||
houseId,
|
||
playerId,
|
||
payout
|
||
);
|
||
|
||
} catch (error) {
|
||
console.error(
|
||
"[Roulette] Payout failed:",
|
||
error
|
||
);
|
||
|
||
|
||
return interaction.editReply({
|
||
embeds: [
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🎡 Roulette Error"
|
||
)
|
||
.setDescription(
|
||
"The wheel resolved, but the payout failed. Please contact an administrator."
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
const [
|
||
playerAfter,
|
||
houseAfter
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
playerId
|
||
),
|
||
|
||
getCookieBalance(
|
||
houseId
|
||
)
|
||
]);
|
||
|
||
|
||
const profit =
|
||
payout -
|
||
bet;
|
||
|
||
|
||
const resultEmbed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
won
|
||
? "🎉 Roulette Winner!"
|
||
: "🎡 Roulette"
|
||
)
|
||
.setDescription(
|
||
[
|
||
"## The ball lands on...",
|
||
"",
|
||
`# ${formatRouletteNumber(result)}`,
|
||
"",
|
||
`${choice.emoji} Your bet: **${choice.name}**`,
|
||
"",
|
||
won
|
||
? `🎉 **YOU WIN ${payout.toLocaleString()} 🍪!**`
|
||
: "The house takes this one."
|
||
].join("\n")
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Wager",
|
||
|
||
value:
|
||
`${bet.toLocaleString()} 🍪`,
|
||
|
||
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:
|
||
`${houseAfter.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`crumbRouletteResult:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.editReply({
|
||
embeds: [
|
||
resultEmbed
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RESULT LOGIC
|
||
* ============================================================
|
||
*/
|
||
|
||
function didRouletteBetWin(
|
||
choice,
|
||
number
|
||
) {
|
||
switch (choice) {
|
||
case "Red":
|
||
return (
|
||
number !==
|
||
0
|
||
&&
|
||
RED_NUMBERS.has(
|
||
number
|
||
)
|
||
);
|
||
|
||
|
||
case "Black":
|
||
return (
|
||
number !==
|
||
0
|
||
&&
|
||
!RED_NUMBERS.has(
|
||
number
|
||
)
|
||
);
|
||
|
||
|
||
case "Odd":
|
||
return (
|
||
number !==
|
||
0
|
||
&&
|
||
number %
|
||
2 ===
|
||
1
|
||
);
|
||
|
||
|
||
case "Even":
|
||
return (
|
||
number !==
|
||
0
|
||
&&
|
||
number %
|
||
2 ===
|
||
0
|
||
);
|
||
|
||
|
||
case "Zero":
|
||
return (
|
||
number ===
|
||
0
|
||
);
|
||
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DISPLAY
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildSpinEmbed(
|
||
choice,
|
||
bet,
|
||
number,
|
||
status
|
||
) {
|
||
return new EmbedBuilder()
|
||
.setTitle(
|
||
"🎡 Crumb Saucer Roulette"
|
||
)
|
||
.setDescription(
|
||
[
|
||
"## 🎡 Spinning...",
|
||
"",
|
||
`### ${formatRouletteNumber(number)}`,
|
||
"",
|
||
`**${status}**`
|
||
].join("\n")
|
||
)
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Bet",
|
||
|
||
value:
|
||
`${choice.emoji} ${choice.name}`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Wager",
|
||
|
||
value:
|
||
`${bet.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
function formatRouletteNumber(
|
||
number
|
||
) {
|
||
if (
|
||
number ===
|
||
0
|
||
) {
|
||
return "🟢 0";
|
||
}
|
||
|
||
|
||
if (
|
||
RED_NUMBERS.has(
|
||
number
|
||
)
|
||
) {
|
||
return `🔴 ${number}`;
|
||
}
|
||
|
||
|
||
return `⚫ ${number}`;
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* BUTTON BUILDERS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildBetButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
...ROULETTE_BETS.map(
|
||
bet =>
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
`crumbRouletteBet${bet}`
|
||
)
|
||
.setLabel(
|
||
bet.toLocaleString()
|
||
)
|
||
.setEmoji(
|
||
"🍪"
|
||
)
|
||
.setStyle(
|
||
bet >=
|
||
500
|
||
? ButtonStyle.Primary
|
||
: ButtonStyle.Secondary
|
||
)
|
||
)
|
||
),
|
||
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerBack"
|
||
)
|
||
.setLabel(
|
||
"Back to Crumb Saucer"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
function buildChoiceButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbRouletteRed"
|
||
)
|
||
.setLabel(
|
||
"Red"
|
||
)
|
||
.setEmoji(
|
||
"🔴"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbRouletteBlack"
|
||
)
|
||
.setLabel(
|
||
"Black"
|
||
)
|
||
.setEmoji(
|
||
"⚫"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbRouletteOdd"
|
||
)
|
||
.setLabel(
|
||
"Odd"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbRouletteEven"
|
||
)
|
||
.setLabel(
|
||
"Even"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbRouletteZero"
|
||
)
|
||
.setLabel(
|
||
"Zero"
|
||
)
|
||
.setEmoji(
|
||
"🟢"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
)
|
||
),
|
||
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerRoulette"
|
||
)
|
||
.setLabel(
|
||
"Change Wager"
|
||
)
|
||
.setEmoji(
|
||
"↩️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
function buildResultButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerRoulette"
|
||
)
|
||
.setLabel(
|
||
"Play Again"
|
||
)
|
||
.setEmoji(
|
||
"🎡"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerBack"
|
||
)
|
||
.setLabel(
|
||
"Crumb Saucer"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SESSION OWNER
|
||
* ============================================================
|
||
*/
|
||
|
||
function getRouletteSessionOwner(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
.embeds[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
/*
|
||
* Main Saucer
|
||
*/
|
||
if (
|
||
parts[0] ===
|
||
"crumbSaucer"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
/*
|
||
* Roulette wager menu
|
||
*/
|
||
if (
|
||
parts[0] ===
|
||
"crumbRouletteMenu"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
/*
|
||
* Roulette result
|
||
*/
|
||
if (
|
||
parts[0] ===
|
||
"crumbRouletteResult"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
return null;
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RANDOM
|
||
* ============================================================
|
||
*/
|
||
|
||
function randomRouletteNumber() {
|
||
return Math.floor(
|
||
Math.random() *
|
||
37
|
||
);
|
||
}
|
||
|
||
|
||
function sleep(
|
||
ms
|
||
) {
|
||
return new Promise(
|
||
resolve =>
|
||
setTimeout(
|
||
resolve,
|
||
ms
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
module.exports = {
|
||
showRoulette,
|
||
chooseRouletteBet,
|
||
playRoulette
|
||
}; |