// 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: */ if ( parts[0] === "nutHub" ) { return parts[1]; } /* * Coming from Heist. * * nutHeist: */ 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 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 };