795 lines
14 KiB
JavaScript
795 lines
14 KiB
JavaScript
const {
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle
|
||
} = require("discord.js");
|
||
|
||
const {
|
||
getNutBalance,
|
||
transferNuts,
|
||
temporaryEphemeralReply,
|
||
randomItem
|
||
} = require("./nutEconomy");
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CONFIG
|
||
* ============================================================
|
||
*/
|
||
|
||
const SLOT_BETS = [
|
||
10,
|
||
50,
|
||
100,
|
||
500,
|
||
1000
|
||
];
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* NUT SYMBOLS
|
||
* ============================================================
|
||
*
|
||
* All symbols are nut-themed.
|
||
*
|
||
* 🌰 Chestnut
|
||
* 🥜 Peanuts
|
||
* 🥥 Coconut
|
||
* 🍩 Doughnut
|
||
*/
|
||
|
||
const SYMBOLS = [
|
||
"🌰",
|
||
"🥜",
|
||
"🥥",
|
||
"🍩"
|
||
];
|
||
|
||
|
||
/*
|
||
* Weighted reel.
|
||
*
|
||
* Chestnuts are the common symbol.
|
||
* Peanuts are less common.
|
||
* Coconuts are rare.
|
||
* Doughnuts are the jackpot symbol.
|
||
*/
|
||
|
||
const REEL = [
|
||
"🌰",
|
||
"🌰",
|
||
"🌰",
|
||
"🌰",
|
||
"🌰",
|
||
|
||
"🥜",
|
||
"🥜",
|
||
"🥜",
|
||
"🥜",
|
||
|
||
"🥥",
|
||
"🥥",
|
||
|
||
"🍩"
|
||
];
|
||
|
||
|
||
/*
|
||
* Triple-symbol payouts.
|
||
*
|
||
* Payout includes the original wager.
|
||
*/
|
||
|
||
const PAYOUTS = {
|
||
"🌰":
|
||
1.5,
|
||
|
||
"🥜":
|
||
2,
|
||
|
||
"🥥":
|
||
4,
|
||
|
||
"🍩":
|
||
10
|
||
};
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SHOW SLOTS
|
||
* ============================================================
|
||
*/
|
||
|
||
async function showNutSlots(
|
||
interaction
|
||
) {
|
||
|
||
const ownerId =
|
||
getNutSlotsOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Nut-Hub-Session."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Acknowledge the button immediately.
|
||
*/
|
||
await interaction.deferUpdate();
|
||
|
||
|
||
const balance =
|
||
await getNutBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🎰 Nut Slots"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"Dreh die Walzen und jag den **Nut Jackpot**.",
|
||
"",
|
||
"### 🥜 Payouts",
|
||
"🌰 🌰 🌰 — **×1.5**",
|
||
"🥜 🥜 🥜 — **×2**",
|
||
"🥥 🥥 🥥 — **×4**",
|
||
"🍩 🍩 🍩 — **×10**",
|
||
"",
|
||
"Choose your wager."
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields({
|
||
name:
|
||
"Deine Nüsse",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} 🥜**`
|
||
})
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutSlots:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.editReply({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildSlotBetButtons()
|
||
});
|
||
}
|
||
|
||
/*
|
||
* ============================================================
|
||
* PLAY
|
||
* ============================================================
|
||
*/
|
||
|
||
async function playNutSlots(
|
||
interaction,
|
||
bet
|
||
) {
|
||
if (
|
||
!SLOT_BETS.includes(
|
||
bet
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Dieser Einsatz ist nicht verfügbar."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
getNutSlotsOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Slots-Session."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerId =
|
||
interaction.user.id;
|
||
|
||
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
playerBalance,
|
||
houseBalance
|
||
] =
|
||
await Promise.all([
|
||
getNutBalance(
|
||
playerId
|
||
),
|
||
|
||
getNutBalance(
|
||
houseId
|
||
)
|
||
]);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
bet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`Du brauchst **${bet.toLocaleString()} Nüsse** für diesen Einsatz.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Maximum possible return = x10.
|
||
*
|
||
* After receiving the wager, the house must
|
||
* still be able to cover the total payout.
|
||
*/
|
||
|
||
const maxPayout =
|
||
bet *
|
||
10;
|
||
|
||
|
||
if (
|
||
houseBalance +
|
||
bet <
|
||
maxPayout
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Der Nut Vault kann diesen Einsatz aktuell nicht absichern."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
try {
|
||
await transferNuts(
|
||
playerId,
|
||
houseId,
|
||
bet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_NUTS"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Dein Nut-Bestand hat sich verändert."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Initial animation state.
|
||
*/
|
||
|
||
await interaction.update({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
bet,
|
||
[
|
||
"❔",
|
||
"❔",
|
||
"❔"
|
||
],
|
||
"Die Walzen drehen..."
|
||
)
|
||
],
|
||
|
||
components: []
|
||
});
|
||
|
||
|
||
await delay(
|
||
500
|
||
);
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
bet,
|
||
[
|
||
randomReelSymbol(),
|
||
"❔",
|
||
"❔"
|
||
],
|
||
"Die erste Walze stoppt..."
|
||
)
|
||
]
|
||
});
|
||
|
||
|
||
await delay(
|
||
500
|
||
);
|
||
|
||
|
||
const first =
|
||
randomReelSymbol();
|
||
|
||
|
||
const second =
|
||
randomReelSymbol();
|
||
|
||
|
||
await interaction.editReply({
|
||
embeds: [
|
||
buildSpinEmbed(
|
||
bet,
|
||
[
|
||
first,
|
||
second,
|
||
"❔"
|
||
],
|
||
"Noch eine..."
|
||
)
|
||
]
|
||
});
|
||
|
||
|
||
await delay(
|
||
600
|
||
);
|
||
|
||
|
||
/*
|
||
* Final actual result.
|
||
*
|
||
* Generate all three here so animation values
|
||
* don't alter game odds.
|
||
*/
|
||
|
||
const result = [
|
||
randomReelSymbol(),
|
||
randomReelSymbol(),
|
||
randomReelSymbol()
|
||
];
|
||
|
||
|
||
const multiplier =
|
||
getSlotMultiplier(
|
||
result
|
||
);
|
||
|
||
|
||
const payout =
|
||
Math.floor(
|
||
bet *
|
||
multiplier
|
||
);
|
||
|
||
|
||
if (
|
||
payout >
|
||
0
|
||
) {
|
||
await transferNuts(
|
||
houseId,
|
||
playerId,
|
||
payout
|
||
);
|
||
}
|
||
|
||
|
||
const balanceAfter =
|
||
await getNutBalance(
|
||
playerId
|
||
);
|
||
|
||
|
||
const profit =
|
||
payout -
|
||
bet;
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
multiplier >
|
||
0
|
||
? "🎰 Nut Slots — WIN!"
|
||
: "🎰 Nut Slots"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"## " +
|
||
result.join(
|
||
" "
|
||
),
|
||
"",
|
||
multiplier >
|
||
0
|
||
? `🎉 **x${multiplier}**`
|
||
: "Die Nüsse bleiben beim Haus."
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Einsatz",
|
||
|
||
value:
|
||
`${bet.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Payout",
|
||
|
||
value:
|
||
`${payout.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Profit",
|
||
|
||
value:
|
||
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Deine Nüsse",
|
||
|
||
value:
|
||
`${balanceAfter.toLocaleString()} 🥜`
|
||
}
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutSlots:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.editReply({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildSlotResultButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* PAYOUT
|
||
* ============================================================
|
||
*/
|
||
|
||
function getSlotMultiplier(
|
||
result
|
||
) {
|
||
const [
|
||
first,
|
||
second,
|
||
third
|
||
] = result;
|
||
|
||
|
||
/*
|
||
* Triple.
|
||
*/
|
||
|
||
if (
|
||
first === second &&
|
||
second === third
|
||
) {
|
||
return PAYOUTS[first] ?? 0;
|
||
}
|
||
|
||
|
||
/*
|
||
* Two matching symbols.
|
||
*
|
||
* Half the wager is returned.
|
||
*/
|
||
|
||
if (
|
||
first === second ||
|
||
first === third ||
|
||
second === third
|
||
) {
|
||
return 0.5;
|
||
}
|
||
|
||
|
||
/*
|
||
* Nut Salad:
|
||
*
|
||
* Chestnut + Peanut + Coconut
|
||
* in any order.
|
||
*
|
||
* Returns the wager.
|
||
*/
|
||
|
||
const nutSalad =
|
||
new Set(
|
||
result
|
||
);
|
||
|
||
|
||
if (
|
||
nutSalad.size === 3 &&
|
||
nutSalad.has("🌰") &&
|
||
nutSalad.has("🥜") &&
|
||
nutSalad.has("🥥")
|
||
) {
|
||
return 1;
|
||
}
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DISPLAY
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildSpinEmbed(
|
||
bet,
|
||
symbols,
|
||
status
|
||
) {
|
||
return new EmbedBuilder()
|
||
.setTitle(
|
||
"🎰 Nut Slots"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"## " +
|
||
symbols.join(
|
||
" "
|
||
),
|
||
"",
|
||
`**${status}**`
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields({
|
||
name:
|
||
"Einsatz",
|
||
|
||
value:
|
||
`${bet.toLocaleString()} 🥜`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildSlotBetButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
...SLOT_BETS.map(
|
||
bet =>
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
`nutSlotsBet${bet}`
|
||
)
|
||
.setLabel(
|
||
bet.toLocaleString()
|
||
)
|
||
.setEmoji(
|
||
"🥜"
|
||
)
|
||
.setStyle(
|
||
bet >=
|
||
500
|
||
? ButtonStyle.Primary
|
||
: ButtonStyle.Secondary
|
||
)
|
||
)
|
||
),
|
||
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubBack"
|
||
)
|
||
.setLabel(
|
||
"Nut Hub"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
function buildSlotResultButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubSlots"
|
||
)
|
||
.setLabel(
|
||
"Play Again"
|
||
)
|
||
.setEmoji(
|
||
"🎰"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubBack"
|
||
)
|
||
.setLabel(
|
||
"Nut Hub"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SESSION OWNER
|
||
* ============================================================
|
||
*/
|
||
|
||
function getNutSlotsOwner(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
if (
|
||
parts[0] ===
|
||
"nutHub"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
if (
|
||
parts[0] ===
|
||
"nutSlots"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
return null;
|
||
}
|
||
|
||
/*
|
||
* ============================================================
|
||
* RANDOM
|
||
* ============================================================
|
||
*/
|
||
|
||
function randomReelSymbol() {
|
||
return randomItem(
|
||
REEL
|
||
);
|
||
}
|
||
|
||
|
||
function delay(
|
||
ms
|
||
) {
|
||
return new Promise(
|
||
resolve =>
|
||
setTimeout(
|
||
resolve,
|
||
ms
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
module.exports = {
|
||
showNutSlots,
|
||
playNutSlots
|
||
}; |