796 lines
18 KiB
JavaScript
796 lines
18 KiB
JavaScript
const {
|
|
EmbedBuilder,
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
getCookieBalance,
|
|
transferCookies,
|
|
temporaryEphemeralReply,
|
|
randomItem
|
|
} = require("./cookieEconomy");
|
|
|
|
const {
|
|
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
|
|
) {
|
|
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 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 trackProfileEvent(
|
|
interaction,
|
|
playerId,
|
|
{
|
|
type:
|
|
"game_result",
|
|
game:
|
|
"slots",
|
|
result:
|
|
profit > 0
|
|
? "win"
|
|
: profit < 0
|
|
? "loss"
|
|
: "push",
|
|
profit,
|
|
jackpot:
|
|
outcome.type ===
|
|
"triple" &&
|
|
outcome.symbol ===
|
|
"💎"
|
|
}
|
|
);
|
|
|
|
if (profit > 0) {
|
|
await trackProfileEvent(
|
|
interaction,
|
|
playerId,
|
|
{
|
|
type:
|
|
"cookie_gain",
|
|
amount:
|
|
profit
|
|
}
|
|
);
|
|
}
|
|
|
|
else if (profit < 0) {
|
|
await trackProfileEvent(
|
|
interaction,
|
|
playerId,
|
|
{
|
|
type:
|
|
"cookie_loss",
|
|
amount:
|
|
Math.abs(profit)
|
|
}
|
|
);
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
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
|
|
};
|