777 lines
15 KiB
JavaScript
777 lines
15 KiB
JavaScript
// 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
|
|
}; |