const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require("discord.js"); const { getCollections, getCookieBalance, transferCookies, temporaryEphemeralReply, getUnixTime } = require("./cookieEconomy"); const EVENT_COLLECTION = "items_events"; const BATTLE_DURATION = 60 * 60; /* * JOBS */ const JOBS = { DPS: { BlackMage: { name: "Black Mage", class: "Thaumaturge", emoji: "🔥" }, Dragoon: { name: "Dragoon", class: "Lancer", emoji: "🐉" }, Bard: { name: "Bard", class: "Archer", emoji: "🏹" } }, Tank: { Paladin: { name: "Paladin", class: "Gladiator", emoji: "⚔️" }, Warrior: { name: "Warrior", class: "Marauder", emoji: "🪓" }, DarkKnight: { name: "Dark Knight", class: null, emoji: "🌑" } }, Healer: { WhiteMage: { name: "White Mage", class: "Conjurer", emoji: "✨" }, Scholar: { name: "Scholar", class: "Arcanist", emoji: "📖" }, Astrologian: { name: "Astrologian", class: null, emoji: "🌟" } } }; /* * INVESTMENT TIERS */ const TIERS = { Sprout: { name: "Sprout", emoji: "🌱", cost: 100, health: 180, power: 20 }, Adventurer: { name: "Adventurer", emoji: "⚔️", cost: 500, health: 280, power: 35 }, WarriorOfLight: { name: "Warrior of Light", emoji: "✨", cost: 1000, health: 380, power: 50 } }; /* * ROLE SELECTION */ async function chooseCrumbleStrikeRole( interaction, role ) { const event = await getCrumbleStrikeFromPublicMessage( interaction ); if (!event) { return temporaryEphemeralReply( interaction, { content: "This Crumble Strike encounter no longer exists." } ); } if ( event.state !== "Joining" ) { return temporaryEphemeralReply( interaction, { content: "The recruitment phase has already ended." } ); } if ( event.joinUntil <= getUnixTime() ) { return temporaryEphemeralReply( interaction, { content: "Recruitment has already expired." } ); } const participant = event.participants?.[ interaction.user.id ]; if (participant) { return temporaryEphemeralReply( interaction, { content: `You're already participating as **${participant.job}**.` } ); } if (!JOBS[role]) { return temporaryEphemeralReply( interaction, { content: "That role doesn't exist." } ); } return interaction.reply({ embeds: [ buildJobSelectionEmbed( event, interaction.user, role ) ], components: buildJobButtons( role ), flags: MessageFlags.Ephemeral }); } /* * JOB SELECTION */ async function chooseCrumbleStrikeJob( interaction, role, jobKey ) { const metadata = parseSelectionFooter( interaction ); if ( metadata.type !== "crumbleStrikeJob" || metadata.userId !== interaction.user.id || metadata.role !== role ) { return temporaryEphemeralReply( interaction, { content: "This character selection is no longer valid." } ); } const job = JOBS[role]?.[ jobKey ]; if (!job) { return temporaryEphemeralReply( interaction, { content: "That Job isn't available." } ); } const event = await getCrumbleStrikeByEventId( metadata.eventId ); if ( !event || event.state !== "Joining" || event.joinUntil <= getUnixTime() ) { return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🍪 Recruitment Closed" ) .setDescription( "This encounter is no longer accepting adventurers." ) ], components: [] }); } return interaction.update({ embeds: [ buildTierSelectionEmbed( event, interaction.user, role, jobKey, job ) ], components: buildTierButtons() }); } /* * CONFIRM JOIN */ async function confirmCrumbleStrikeJoin( interaction, tierKey ) { const metadata = parseSelectionFooter( interaction ); if ( metadata.type !== "crumbleStrikeTier" || metadata.userId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This character selection is no longer valid." } ); } const role = metadata.role; const jobKey = metadata.job; const job = JOBS[role]?.[ jobKey ]; const tier = TIERS[ tierKey ]; if ( !job || !tier ) { return temporaryEphemeralReply( interaction, { content: "That character setup is invalid." } ); } const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOne({ eventId: metadata.eventId, eventType: "CrumbleStrike" }); if ( !event || event.state !== "Joining" || event.joinUntil <= getUnixTime() ) { return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🍪 Recruitment Closed" ) .setDescription( "The battle can no longer be joined." ) ], components: [] }); } if ( event.participants?.[ interaction.user.id ] ) { return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🍪 Already Joined" ) .setDescription( "You're already participating." ) ], components: [] }); } const balance = await getCookieBalance( interaction.user.id ); if ( balance < tier.cost ) { return temporaryEphemeralReply( interaction, { content: `You need **${tier.cost.toLocaleString()} Cookies** to join as a ${tier.name}.` } ); } try { await transferCookies( interaction.user.id, event.escrowId, tier.cost ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return temporaryEphemeralReply( interaction, { content: "Your Cookie balance changed before you could join." } ); } throw error; } const now = getUnixTime(); const participant = { role, job: job.name, class: job.class, tier: tierKey, investment: tier.cost, health: tier.health, maxHealth: tier.health, power: tier.power, energy: 0, maxEnergy: 100, damage: 0, healing: 0, mitigation: 0, defense: { charges: 0, reduction: 0 }, knockedOut: false, joinedAt: now, cooldowns: { Strike: 0, Defend: 0, Heal: 0 } }; const result = await events.updateOne( { eventId: event.eventId, eventType: "CrumbleStrike", state: "Joining", [`participants.${interaction.user.id}`]: { $exists: false } }, { $set: { [`participants.${interaction.user.id}`]: participant }, $inc: { rewardPool: tier.cost } } ); if ( result.modifiedCount !== 1 ) { await safeTransfer( event.escrowId, interaction.user.id, tier.cost ); return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🍪 Unable to Join" ) .setDescription( "Your entry could not be registered. Your Cookies were refunded." ) ], components: [] }); } await refreshCrumbleStrikeMessage( interaction.client, event.eventId ); return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "⚔️ You Joined Crumble Strike!" ) .setDescription( [ `### ${job.emoji} ${formatJobProgression(job)}`, "", `${getRoleEmoji(role)} **${role}**`, `${tier.emoji} **${tier.name}**`, "", `🍪 Investment: **${tier.cost.toLocaleString()}**`, `❤️ HP: **${tier.health}**`, `💪 Power: **${tier.power}**`, `⚡ Energy: **0/100**` ].join("\n") ) ], components: [] }); } /* * START BATTLE */ async function startCrumbleStrikeBattle( interaction ) { const event = await getCrumbleStrikeFromPublicMessage( interaction ); if (!event) { return temporaryEphemeralReply( interaction, { content: "This encounter no longer exists." } ); } if ( interaction.user.id !== event.creatorId ) { return temporaryEphemeralReply( interaction, { content: "Only the player who summoned the boss can start the battle early." } ); } const participantCount = Object.keys( event.participants ?? {} ).length; if ( participantCount < event.minimumParticipants ) { return temporaryEphemeralReply( interaction, { content: `You need at least **${event.minimumParticipants} players**. Currently **${participantCount}/${event.minimumParticipants}**.` } ); } await interaction.deferUpdate(); return activateCrumbleStrike( interaction.client, event.eventId ); } /* * ACTIVATE */ async function activateCrumbleStrike( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOne({ eventId, eventType: "CrumbleStrike" }); if ( !event || event.state !== "Joining" ) { return false; } const participantCount = Object.keys( event.participants ?? {} ).length; if ( participantCount < event.minimumParticipants ) { return false; } const now = getUnixTime(); const maxHealth = 15000 + ( participantCount * 5000 ); const result = await events.updateOne( { eventId, eventType: "CrumbleStrike", state: "Joining" }, { $set: { state: "Active", battleStartedAt: now, battleEndsAt: now + BATTLE_DURATION, "boss.health": maxHealth, "boss.maxHealth": maxHealth, "boss.limitGauge": 0, "boss.actionGauge": 0 } } ); if ( result.modifiedCount !== 1 ) { return false; } await refreshCrumbleStrikeMessage( client, eventId ); return true; } /* * FAILURE */ async function failCrumbleStrike( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOneAndUpdate( { eventId, eventType: "CrumbleStrike", state: "Active" }, { $set: { state: "Failed", failedAt: getUnixTime() } }, { returnDocument: "after" } ); if (!event) return false; const escrow = await getCookieBalance( event.escrowId ); if ( escrow > 0 ) { await safeTransfer( event.escrowId, client.user.id, escrow ); } await refreshCrumbleStrikeMessage( client, eventId ); return true; } /* * CANCEL + REFUND */ async function cancelCrumbleStrikeAndRefund( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOneAndUpdate( { eventId, eventType: "CrumbleStrike", state: "Joining" }, { $set: { state: "Cancelled", cancelledAt: getUnixTime() } }, { returnDocument: "after" } ); if (!event) return false; for ( const [ userId, participant ] of Object.entries( event.participants ?? {} ) ) { if ( participant.investment > 0 ) { await safeTransfer( event.escrowId, userId, participant.investment ); } } const remaining = await getCookieBalance( event.escrowId ); if ( remaining > 0 ) { await safeTransfer( event.escrowId, event.creatorId, remaining ); } await refreshCrumbleStrikeMessage( client, eventId ); return true; } /* * REFRESH PUBLIC MESSAGE */ async function refreshCrumbleStrikeMessage( client, eventId ) { const event = await getCrumbleStrikeByEventId( eventId ); if (!event) return; let channel; try { channel = await client.channels.fetch( event.channelId ); } catch { return; } if (!channel) return; let message; try { message = await channel.messages.fetch( event.messageId ); } catch { return; } const { embed, components } = buildCrumbleStrikeMessage( event ); await message.edit({ embeds: [ embed ], components }); } /* * PUBLIC MESSAGE */ function buildCrumbleStrikeMessage( event ) { const participants = Object.values( event.participants ?? {} ); const participantCount = participants.length; if ( event.state === "Joining" ) { const roleCounts = getRoleCounts( participants ); return { embed: new EmbedBuilder() .setTitle( "🍪 Crumble Strike" ) .setDescription( [ "## The Crumbbound Behemoth", "", "*A creature of hunger, crumbs, and extremely questionable dietary choices.*", "", `**Recruitment ends:** `, `**Participants:** ${participantCount}/${event.minimumParticipants} required`, `**Reward Pool:** ${event.rewardPool.toLocaleString()} 🍪`, "", `⚔️ DPS: **${roleCounts.DPS}**`, `🛡️ Tanks: **${roleCounts.Tank}**`, `❤️ Healers: **${roleCounts.Healer}**`, "", participantCount >= event.minimumParticipants ? "✅ The summoner may start the battle early." : "Choose your role below to join." ].join("\n") ) .setFooter({ text: `crumbleStrike:${event.eventId}` }), components: buildRecruitmentButtons() }; } if ( event.state === "Active" ) { const hp = Math.max( 0, event.boss.health ); return { embed: new EmbedBuilder() .setTitle( "⚔️ Crumble Strike" ) .setDescription( [ "## The Crumbbound Behemoth", "", buildHpBar( hp, event.boss.maxHealth ), `**${hp.toLocaleString()} / ${event.boss.maxHealth.toLocaleString()} HP**`, "", `✨ Limit Gauge: **${event.boss.limitGauge ?? 0}/100**`, `👹 Boss Gauge: **${event.boss.actionGauge ?? 0}/10**`, "", `**Participants:** ${participantCount}`, `**Reward Pool:** ${event.rewardPool.toLocaleString()} 🍪`, `**Battle ends:** `, "", "Combat actions are being upgraded next." ].join("\n") ) .setFooter({ text: `crumbleStrike:${event.eventId}` }), components: [] }; } if ( event.state === "Failed" ) { return { embed: new EmbedBuilder() .setTitle( "💀 Crumble Strike Failed" ) .setDescription( [ "The Crumbbound Behemoth survived the encounter.", "", `**${event.rewardPool.toLocaleString()} Cookies** were claimed by the ShaiBot Vault.` ].join("\n") ) .setFooter({ text: `crumbleStrike:${event.eventId}` }), components: [] }; } if ( event.state === "Cancelled" ) { return { embed: new EmbedBuilder() .setTitle( "🍪 Crumble Strike Cancelled" ) .setDescription( "Not enough adventurers joined. All committed Cookies were refunded." ) .setFooter({ text: `crumbleStrike:${event.eventId}` }), components: [] }; } return { embed: new EmbedBuilder() .setTitle( "🍪 Crumble Strike" ) .setDescription( `Encounter state: **${event.state}**` ) .setFooter({ text: `crumbleStrike:${event.eventId}` }), components: [] }; } /* * RECRUITMENT BUTTONS */ function buildRecruitmentButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbleStrikeRoleDPS" ) .setLabel( "DPS" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Danger ), new ButtonBuilder() .setCustomId( "crumbleStrikeRoleTank" ) .setLabel( "Tank" ) .setEmoji( "🛡️" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbleStrikeRoleHealer" ) .setLabel( "Healer" ) .setEmoji( "❤️" ) .setStyle( ButtonStyle.Success ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbleStrikeStart" ) .setLabel( "Start Battle" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Secondary ) ) ]; } /* * JOB MENU */ function buildJobSelectionEmbed( event, user, role ) { const jobs = JOBS[ role ]; const description = Object.values( jobs ) .map( job => `${job.emoji} **${formatJobProgression(job)}**` ) .join("\n"); return new EmbedBuilder() .setTitle( `${getRoleEmoji(role)} Choose Your ${role} Job` ) .setDescription( [ description, "", "Choose the Job you want to bring into battle." ].join("\n") ) .setFooter({ text: `crumbleStrikeJob:${event.eventId}:${user.id}:${role}` }); } function buildJobButtons( role ) { return [ new ActionRowBuilder() .addComponents( ...Object.entries( JOBS[ role ] ) .map( ( [ key, job ] ) => new ButtonBuilder() .setCustomId( `crumbleStrikeJob${key}` ) .setLabel( job.name ) .setEmoji( job.emoji ) .setStyle( ButtonStyle.Primary ) ) ) ]; } /* * TIER MENU */ function buildTierSelectionEmbed( event, user, role, jobKey, job ) { return new EmbedBuilder() .setTitle( `${job.emoji} ${formatJobProgression(job)}` ) .setDescription( [ `${getRoleEmoji(role)} **${role}**`, "", "Choose your commitment.", "", "🌱 **Sprout — 100 🍪**", "Entry-level HP and Power.", "", "⚔️ **Adventurer — 500 🍪**", "Improved HP and Power.", "", "✨ **Warrior of Light — 1,000 🍪**", "Highest starting HP and Power." ].join("\n") ) .setFooter({ text: `crumbleStrikeTier:${event.eventId}:${user.id}:${role}:${jobKey}` }); } function buildTierButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbleStrikeTierSprout" ) .setLabel( "Sprout — 100" ) .setEmoji( "🌱" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "crumbleStrikeTierAdventurer" ) .setLabel( "Adventurer — 500" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbleStrikeTierWarriorOfLight" ) .setLabel( "Warrior of Light — 1,000" ) .setEmoji( "✨" ) .setStyle( ButtonStyle.Success ) ) ]; } /* * LOOKUPS */ async function getCrumbleStrikeFromPublicMessage( interaction ) { const footer = interaction.message .embeds[0] ?.footer ?.text; const [ type, eventId ] = footer?.split(":") ?? []; if ( type !== "crumbleStrike" || !eventId ) { return null; } return getCrumbleStrikeByEventId( eventId ); } async function getCrumbleStrikeByEventId( eventId ) { const { db } = getCollections(); return db .collection( EVENT_COLLECTION ) .findOne({ eventId, eventType: "CrumbleStrike" }); } function parseSelectionFooter( interaction ) { const footer = interaction.message .embeds[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] === "crumbleStrikeJob" ) { return { type: parts[0], eventId: parts[1], userId: parts[2], role: parts[3] }; } if ( parts[0] === "crumbleStrikeTier" ) { return { type: parts[0], eventId: parts[1], userId: parts[2], role: parts[3], job: parts[4] }; } return {}; } /* * DISPLAY */ function formatJobProgression( job ) { if ( job.class ) { return ( `${job.class} → ${job.name}` ); } return job.name; } function getRoleEmoji( role ) { switch (role) { case "DPS": return "⚔️"; case "Tank": return "🛡️"; case "Healer": return "❤️"; default: return "🍪"; } } function getRoleCounts( participants ) { const counts = { DPS: 0, Tank: 0, Healer: 0 }; for ( const participant of participants ) { if ( counts[ participant.role ] !== undefined ) { counts[ participant.role ]++; } } return counts; } function buildHpBar( current, max, size = 20 ) { if ( max <= 0 ) { return "░".repeat( size ); } const ratio = Math.max( 0, Math.min( 1, current / max ) ); const filled = Math.round( ratio * size ); return ( "█".repeat( filled ) + "░".repeat( size - filled ) ); } /* * SAFE COOKIE TRANSFER */ async function safeTransfer( from, to, amount ) { try { await transferCookies( from, to, amount ); return true; } catch (error) { console.error( `Crumble Strike Cookie transfer failed (${from} -> ${to}, ${amount}):`, error ); return false; } } module.exports = { JOBS, TIERS, chooseCrumbleStrikeRole, chooseCrumbleStrikeJob, confirmCrumbleStrikeJoin, startCrumbleStrikeBattle, activateCrumbleStrike, failCrumbleStrike, cancelCrumbleStrikeAndRefund, refreshCrumbleStrikeMessage };