converted ShaiBot casino to Nut Economy

This commit is contained in:
2026-08-19 14:10:38 +02:00
parent a126de21d4
commit 1957991182
55 changed files with 8905 additions and 751 deletions
+795
View File
@@ -0,0 +1,795 @@
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
};