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
File diff suppressed because it is too large Load Diff
+1013
View File
File diff suppressed because it is too large Load Diff
+777
View File
@@ -0,0 +1,777 @@
// utils/nutHeist.js
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
getNutBalance,
transferNuts,
claimCooldown
} = require("./nutEconomy");
/*
* ============================================================
* CONFIG
* ============================================================
*/
const HEIST_COOLDOWN =
60 * 60;
const HEIST_ACTIONS = {
Sneak: {
label:
"Sneak",
emoji:
"🥷",
successChance:
0.55,
minReward:
10,
maxReward:
100,
minLoss:
5,
maxLoss:
40
},
Hack: {
label:
"Hack",
emoji:
"💻",
successChance:
0.40,
minReward:
50,
maxReward:
250,
minLoss:
20,
maxLoss:
100
},
Smash: {
label:
"Smash",
emoji:
"💥",
successChance:
0.25,
minReward:
150,
maxReward:
500,
minLoss:
50,
maxLoss:
200
}
};
/*
* ============================================================
* HELPERS
* ============================================================
*/
function randomInteger(
min,
max
) {
return Math.floor(
Math.random() *
(
max -
min +
1
)
) + min;
}
function getHeistOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
/*
* Coming from Nut Hub.
*
* nutHub:<userId>
*/
if (
parts[0] ===
"nutHub"
) {
return parts[1];
}
/*
* Coming from Heist.
*
* nutHeist:<userId>
*/
if (
parts[0] ===
"nutHeist"
) {
return parts[1];
}
return null;
}
function buildHeistButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutHeistSneak"
)
.setLabel(
"Sneak"
)
.setEmoji(
"🥷"
)
.setStyle(
ButtonStyle.Success
),
new ButtonBuilder()
.setCustomId(
"nutHeistHack"
)
.setLabel(
"Hack"
)
.setEmoji(
"💻"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutHeistSmash"
)
.setLabel(
"Smash"
)
.setEmoji(
"💥"
)
.setStyle(
ButtonStyle.Danger
)
),
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutHubBack"
)
.setLabel(
"Nut Hub"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
function buildReturnButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutHubHeist"
)
.setLabel(
"Heist Menu"
)
.setEmoji(
"🏦"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutHubBack"
)
.setLabel(
"Nut Hub"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
/*
* ============================================================
* TEMPORARY EPHEMERAL MESSAGE
* ============================================================
*/
async function temporaryEphemeralReply(
interaction,
options
) {
const reply =
await interaction.reply({
...options,
flags:
MessageFlags.Ephemeral
});
/*
* Ephemeral messages cannot truly be given
* a Discord-side expiration time.
*
* We attempt to delete the reply after 5 seconds.
*/
setTimeout(
async () => {
try {
await interaction.deleteReply();
}
catch {
// Ignore expired/deleted interaction.
}
},
5000
);
return reply;
}
/*
* ============================================================
* SHOW HEIST MENU
* ============================================================
*/
async function showNutHeist(
interaction
) {
const ownerId =
getHeistOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Das ist nicht deine Nut-Hub-Session."
}
);
}
await interaction.deferUpdate();
const balance =
await getNutBalance(
interaction.user.id
);
const embed =
new EmbedBuilder()
.setTitle(
"🏦 Nut Vault Heist"
)
.setDescription(
[
"Arthonor bewacht seinen Vorrat an Nüssen.",
"",
"Wähle deinen Ansatz.",
"",
"### 🥷 Sneak",
"**55%** Erfolgschance",
"Kleine Beute, geringes Risiko.",
"",
"### 💻 Hack",
"**40%** Erfolgschance",
"Mittlere Beute, mittleres Risiko.",
"",
"### 💥 Smash",
"**25%** Erfolgschance",
"Große Beute, hohes Risiko.",
"",
`Ein Heist hat **${Math.floor(HEIST_COOLDOWN / 60)} Minuten Cooldown**.`
].join("\n")
)
.addFields({
name:
"Deine Nüsse",
value:
`**${balance.toLocaleString()} 🥜**`
})
.setFooter({
text:
`nutHeist:${interaction.user.id}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildHeistButtons()
});
}
/*
* ============================================================
* PERFORM HEIST
* ============================================================
*/
async function performNutHeist(
interaction,
actionName
) {
const ownerId =
getHeistOwner(
interaction
);
/*
* Prevent another player from using
* somebody else's Heist menu.
*/
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Das ist nicht dein Heist."
}
);
}
const action =
HEIST_ACTIONS[
actionName
];
if (
!action
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Unbekannte Heist-Aktion."
}
);
}
/*
* Acknowledge immediately.
*/
await interaction.deferUpdate();
/*
* ========================================================
* CLAIM SHARED HEIST COOLDOWN
* ========================================================
*
* Sneak / Hack / Smash all use the same field:
*
* items_cooldowns
*
* {
* userId: "...",
* nut: ...,
* heist: ...
* }
*/
const cooldown =
await claimCooldown(
interaction.user.id,
"heist",
HEIST_COOLDOWN
);
if (
!cooldown.claimed
) {
const embed =
new EmbedBuilder()
.setTitle(
"🏦 Vault Locked Down"
)
.setDescription(
[
"Arthonor ist noch auf der Hut.",
"",
`Du kannst <t:${cooldown.next}:R> wieder einen Heist versuchen.`
].join("\n")
)
.setFooter({
text:
`nutHeist:${interaction.user.id}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildReturnButtons()
});
}
/*
* ========================================================
* ROLL RESULT
* ========================================================
*/
const success =
Math.random() <
action.successChance;
const playerId =
interaction.user.id;
/*
* Arthonor himself acts as the vault.
*/
const vaultId =
interaction.client.user.id;
/*
* ========================================================
* SUCCESS
* ========================================================
*/
if (
success
) {
let reward =
randomInteger(
action.minReward,
action.maxReward
);
/*
* Never steal more Nuts than
* Arthonor currently owns.
*/
const vaultBalance =
await getNutBalance(
vaultId
);
reward =
Math.min(
reward,
vaultBalance
);
if (
reward <=
0
) {
const embed =
new EmbedBuilder()
.setTitle(
"🏦 The Vault Is Empty"
)
.setDescription(
[
`${action.emoji} **${action.label}** succeeded...`,
"",
"but Arthonor's Nut stash is empty.",
"",
"You escape with nothing."
].join("\n")
)
.setFooter({
text:
`nutHeist:${playerId}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildReturnButtons()
});
}
await transferNuts(
vaultId,
playerId,
reward
);
const balance =
await getNutBalance(
playerId
);
const embed =
new EmbedBuilder()
.setTitle(
"💰 Heist Successful!"
)
.setDescription(
[
`${action.emoji} **${action.label}** worked!`,
"",
`You stole **${reward.toLocaleString()} 🥜** from Arthonor's stash.`
].join("\n")
)
.addFields({
name:
"Deine Nüsse",
value:
`**${balance.toLocaleString()} 🥜**`
})
.setFooter({
text:
`nutHeist:${playerId}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildReturnButtons()
});
}
/*
* ========================================================
* FAILURE
* ========================================================
*/
let loss =
randomInteger(
action.minLoss,
action.maxLoss
);
/*
* Don't take more than the player owns.
*/
const playerBalance =
await getNutBalance(
playerId
);
loss =
Math.min(
loss,
playerBalance
);
if (
loss >
0
) {
await transferNuts(
playerId,
vaultId,
loss
);
}
const balance =
await getNutBalance(
playerId
);
const embed =
new EmbedBuilder()
.setTitle(
"🚨 Heist Failed!"
)
.setDescription(
loss > 0
? [
`${action.emoji} **${action.label}** went wrong.`,
"",
"Arthonor caught you.",
"",
`You lost **${loss.toLocaleString()} 🥜**.`
].join("\n")
: [
`${action.emoji} **${action.label}** went wrong.`,
"",
"Arthonor caught you...",
"",
"but you didn't have any Nuts for him to take."
].join("\n")
)
.addFields({
name:
"Deine Nüsse",
value:
`**${balance.toLocaleString()} 🥜**`
})
.setFooter({
text:
`nutHeist:${playerId}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildReturnButtons()
});
}
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = {
showNutHeist,
performNutHeist
};
+240
View File
@@ -0,0 +1,240 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
getNutBalance
} = require("./nutEconomy");
/*
* ============================================================
* SHOW HUB
* ============================================================
*/
async function showNutHub(
interaction
) {
const balance =
await getNutBalance(
interaction.user.id
);
const embed =
buildNutHubEmbed(
interaction.user,
balance
);
const components =
buildNutHubButtons();
/*
* Slash command / fresh interaction.
*/
if (
!interaction.replied &&
!interaction.deferred
) {
return interaction.reply({
embeds: [
embed
],
components
});
}
/*
* Existing game/menu message.
*/
return interaction.update({
embeds: [
embed
],
components
});
}
/*
* ============================================================
* EMBED
* ============================================================
*/
function buildNutHubEmbed(
user,
balance
) {
return new EmbedBuilder()
.setTitle(
"🥜 Nut Hub"
)
.setDescription(
[
`Willkommen, ${user}!`,
"",
"Hier werden Nüsse gewonnen, verloren und gelegentlich konfisziert.",
"",
"### 🎰 Nut Slots",
"Dreh die Walzen und jag den Jackpot.",
"",
"### 🃏 Nut Blackjack",
"Schlag den Dealer ohne über 21 zu kommen.",
"",
"### 🎡 Nut Roulette",
"Setz auf Rot, Schwarz, Ungerade, Gerade oder Zero.",
"",
"### 🏦 Nut Heist",
"Versuch dich direkt am Nut Vault des Bots.",
"",
"*Choose wisely. The house has very large nuts.*"
].join("\n")
)
.addFields({
name:
"Deine Nüsse",
value:
`**${balance.toLocaleString()} 🥜**`
})
.setThumbnail(
user.displayAvatarURL({
size:
256
})
)
.setFooter({
text:
`nutHub:${user.id}`
});
}
/*
* ============================================================
* BUTTONS
* ============================================================
*/
function buildNutHubButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutHubSlots"
)
.setLabel(
"Slots"
)
.setEmoji(
"🎰"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutHubBlackjack"
)
.setLabel(
"Blackjack"
)
.setEmoji(
"🃏"
)
.setStyle(
ButtonStyle.Success
),
new ButtonBuilder()
.setCustomId(
"nutHubRoulette"
)
.setLabel(
"Roulette"
)
.setEmoji(
"🎡"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutHubHeist"
)
.setLabel(
"Vault Heist"
)
.setEmoji(
"🏦"
)
.setStyle(
ButtonStyle.Danger
)
)
];
}
/*
* ============================================================
* OWNERSHIP
* ============================================================
*/
function getNutHubOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
if (
parts[0] !==
"nutHub"
) {
return null;
}
return parts[1];
}
module.exports = {
showNutHub,
buildNutHubEmbed,
buildNutHubButtons,
getNutHubOwner
};
+975
View File
@@ -0,0 +1,975 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js");
const {
getNutBalance,
transferNuts,
temporaryEphemeralReply
} = require("./nutEconomy");
const ROULETTE_BETS = [
10,
50,
100,
500,
1000
];
const CHOICES = {
Red: {
name:
"Red",
emoji:
"🔴",
multiplier:
2
},
Black: {
name:
"Black",
emoji:
"⚫",
multiplier:
2
},
Odd: {
name:
"Odd",
emoji:
"1️⃣",
multiplier:
2
},
Even: {
name:
"Even",
emoji:
"2️⃣",
multiplier:
2
},
Zero: {
name:
"Zero",
emoji:
"🟢",
multiplier:
36
}
};
const RED_NUMBERS =
new Set([
1,
3,
5,
7,
9,
12,
14,
16,
18,
19,
21,
23,
25,
27,
30,
32,
34,
36
]);
/*
* ============================================================
* MENU
* ============================================================
*/
async function showNutRoulette(
interaction
) {
const ownerId =
getRouletteOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Das ist nicht deine Nut-Hub-Session."
}
);
}
const balance =
await getNutBalance(
interaction.user.id
);
const embed =
new EmbedBuilder()
.setTitle(
"🎡 Nut Roulette"
)
.setDescription(
[
"European Roulette — **0 bis 36**.",
"",
"🔴 **Red** — x2",
"⚫ **Black** — x2",
"1️⃣ **Odd** — x2",
"2️⃣ **Even** — x2",
"🟢 **Zero** — x36",
"",
"Choose your wager."
].join("\n")
)
.addFields({
name:
"Deine Nüsse",
value:
`${balance.toLocaleString()} 🥜`
})
.setFooter({
text:
`nutRouletteMenu:${interaction.user.id}`
});
return interaction.update({
embeds: [
embed
],
components:
buildBetButtons()
});
}
/*
* ============================================================
* WAGER
* ============================================================
*/
async function chooseNutRouletteBet(
interaction,
bet
) {
const ownerId =
getRouletteOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Das ist nicht dein Roulette-Tisch."
}
);
}
if (
!ROULETTE_BETS.includes(
bet
)
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Dieser Einsatz ist ungültig."
}
);
}
const balance =
await getNutBalance(
interaction.user.id
);
if (
balance <
bet
) {
return temporaryEphemeralReply(
interaction,
{
content:
`Du brauchst **${bet.toLocaleString()} Nüsse**.`
}
);
}
const embed =
new EmbedBuilder()
.setTitle(
"🎡 Nut Roulette"
)
.setDescription(
[
`Einsatz: **${bet.toLocaleString()} 🥜**`,
"",
"Wo sollen deine Nüsse landen?",
"",
"🔴 Red",
"⚫ Black",
"1️⃣ Odd",
"2️⃣ Even",
"🟢 Zero"
].join("\n")
)
.setFooter({
text:
`nutRouletteBet:${interaction.user.id}:${bet}`
});
return interaction.update({
embeds: [
embed
],
components:
buildChoiceButtons()
});
}
/*
* ============================================================
* PLAY
* ============================================================
*/
async function playNutRoulette(
interaction,
choiceKey
) {
const choice =
CHOICES[
choiceKey
];
if (!choice) {
return temporaryEphemeralReply(
interaction,
{
content:
"Diese Roulette-Wette existiert nicht."
}
);
}
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
if (
parts[0] !==
"nutRouletteBet"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Diese Roulette-Wette ist nicht mehr gültig."
}
);
}
const ownerId =
parts[1];
const bet =
Number(
parts[2]
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Das ist nicht dein Roulette-Tisch."
}
);
}
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 hast nicht mehr genug Nüsse."
}
);
}
const maxPayout =
bet *
choice.multiplier;
if (
houseBalance +
bet <
maxPayout
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Der Nut Vault kann diesen Einsatz nicht absichern."
}
);
}
await transferNuts(
playerId,
houseId,
bet
);
/*
* Pseudo animation.
*/
await interaction.update({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"Das Rad beginnt sich zu drehen..."
)
],
components: []
});
await delay(
650
);
await interaction.editReply({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"Round and round..."
)
]
});
await delay(
650
);
await interaction.editReply({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"Die Kugel wird langsamer..."
)
]
});
await delay(
750
);
const result =
randomRouletteNumber();
const won =
didBetWin(
choiceKey,
result
);
const payout =
won
? bet *
choice.multiplier
: 0;
if (
payout >
0
) {
await transferNuts(
houseId,
playerId,
payout
);
}
const balance =
await getNutBalance(
playerId
);
const profit =
payout -
bet;
const embed =
new EmbedBuilder()
.setTitle(
won
? "🎉 Nut Roulette — WIN!"
: "🎡 Nut Roulette"
)
.setDescription(
[
"## Die Kugel landet auf...",
"",
`# ${formatRouletteNumber(result)}`,
"",
`${choice.emoji} Deine Wette: **${choice.name}**`,
"",
won
? `🎉 **${payout.toLocaleString()} Nüsse gewonnen!**`
: "Das Haus gewinnt."
].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:
`${balance.toLocaleString()} 🥜`
}
)
.setFooter({
text:
`nutRouletteResult:${playerId}`
});
return interaction.editReply({
embeds: [
embed
],
components:
buildResultButtons()
});
}
/*
* ============================================================
* RULES
* ============================================================
*/
function didBetWin(
choice,
number
) {
if (
number ===
0
) {
return choice ===
"Zero";
}
switch (choice) {
case "Red":
return RED_NUMBERS.has(
number
);
case "Black":
return !RED_NUMBERS.has(
number
);
case "Odd":
return number %
2 ===
1;
case "Even":
return number %
2 ===
0;
default:
return false;
}
}
function formatRouletteNumber(
number
) {
if (
number ===
0
) {
return "🟢 0";
}
if (
RED_NUMBERS.has(
number
)
) {
return `🔴 ${number}`;
}
return `⚫ ${number}`;
}
/*
* ============================================================
* DISPLAY
* ============================================================
*/
function buildSpinEmbed(
choice,
bet,
number,
text
) {
return new EmbedBuilder()
.setTitle(
"🎡 Nut Roulette"
)
.setDescription(
[
"## 🎡 Spinning...",
"",
`### ${formatRouletteNumber(number)}`,
"",
`**${text}**`
].join("\n")
)
.addFields(
{
name:
"Wette",
value:
`${choice.emoji} ${choice.name}`,
inline:
true
},
{
name:
"Einsatz",
value:
`${bet.toLocaleString()} 🥜`,
inline:
true
}
);
}
/*
* ============================================================
* BUTTONS
* ============================================================
*/
function buildBetButtons() {
return [
new ActionRowBuilder()
.addComponents(
...ROULETTE_BETS.map(
bet =>
new ButtonBuilder()
.setCustomId(
`nutRouletteBet${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 buildChoiceButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutRouletteRed"
)
.setLabel(
"Red"
)
.setEmoji(
"🔴"
)
.setStyle(
ButtonStyle.Danger
),
new ButtonBuilder()
.setCustomId(
"nutRouletteBlack"
)
.setLabel(
"Black"
)
.setEmoji(
"⚫"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"nutRouletteOdd"
)
.setLabel(
"Odd"
)
.setEmoji(
"1️⃣"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutRouletteEven"
)
.setLabel(
"Even"
)
.setEmoji(
"2️⃣"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutRouletteZero"
)
.setLabel(
"Zero"
)
.setEmoji(
"🟢"
)
.setStyle(
ButtonStyle.Success
)
)
];
}
function buildResultButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"nutHubRoulette"
)
.setLabel(
"Play Again"
)
.setEmoji(
"🎡"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"nutHubBack"
)
.setLabel(
"Nut Hub"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
/*
* ============================================================
* OWNERSHIP
* ============================================================
*/
function getRouletteOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
if (
parts[0] ===
"nutHub"
) {
return parts[1];
}
if (
parts[0] ===
"nutRouletteMenu"
||
parts[0] ===
"nutRouletteResult"
) {
return parts[1];
}
if (
parts[0] ===
"nutRouletteBet"
) {
return parts[1];
}
return null;
}
function randomRouletteNumber() {
return Math.floor(
Math.random() *
37
);
}
function delay(
ms
) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
module.exports = {
showNutRoulette,
chooseNutRouletteBet,
playNutRoulette
};
+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
};
+9
View File
@@ -0,0 +1,9 @@
module.exports =
function ping(interface, args, type){
if (type = 'legacy'){
// type = legacy
interface.reply('pang!')
} else {
interface.reply('pong!')
}
}