const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { getCollections, getCookieBalance, transferCookies, temporaryEphemeralReply, temporaryEphemeralFollowUp, getUnixTime, randomInteger } = require("./cookieEconomy"); const EVENT_COLLECTION = "items_events"; const BATTLE_DURATION = 60 * 60; /* * Buy-in determines temporary combat stats. */ const ENTRY_TIERS = { Scout: { cost: 100, health: 180, power: 20 }, Fighter: { cost: 500, health: 280, power: 35 }, Champion: { cost: 1000, health: 380, power: 50 } }; const ACTIONS = { Strike: { multiplier: 1.0, energyCost: 0, cooldown: 10 }, Burst: { multiplier: 2.25, energyCost: 40, cooldown: 30 }, Limit: { multiplier: 4.0, energyCost: 100, cooldown: 90 } }; async function joinBoss( interaction, tierName ) { const event = await getBossFromMessage( interaction ); if (!event) { return temporaryEphemeralReply( interaction, { content: "This boss event 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." } ); } if ( event.participants?.[ interaction.user.id ] ) { return temporaryEphemeralReply( interaction, { content: "You're already signed up for this fight." } ); } const tier = ENTRY_TIERS[ tierName ]; if (!tier) return; const balance = await getCookieBalance( interaction.user.id ); if ( balance < tier.cost ) { return temporaryEphemeralReply( interaction, { content: `You need **${tier.cost} Cookies** for the ${tierName} tier.` } ); } /* * Entry fee goes to encounter escrow, * not directly into the house vault. */ await transferCookies( interaction.user.id, event.escrowId, tier.cost ); const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const now = getUnixTime(); await events.updateOne( { eventId: event.eventId, state: "Joining", [`participants.${interaction.user.id}`]: { $exists: false } }, { $set: { [`participants.${interaction.user.id}`]: { tier: tierName, entry: tier.cost, health: tier.health, maxHealth: tier.health, power: tier.power, energy: 100, maxEnergy: 100, lastEnergyUpdate: now, damage: 0, knockedOut: false, cooldowns: { Strike: 0, Burst: 0, Limit: 0 } } }, $inc: { rewardPool: tier.cost } } ); await refreshBossMessage( interaction.client, event.eventId ); return temporaryEphemeralReply( interaction, { content: `You joined as a **${tierName}** for **${tier.cost} Cookies**.` }, 6000 ); } async function startBossBattle( interaction ) { const event = await getBossFromMessage( interaction ); if (!event) { return temporaryEphemeralReply( interaction, { content: "This boss event 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(); await activateBoss( interaction.client, event.eventId ); } async function activateBoss( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOne({ eventId }); if ( !event || event.state !== "Joining" ) { return false; } const participantCount = Object.keys( event.participants ?? {} ).length; if ( participantCount < event.minimumParticipants ) { return false; } const now = getUnixTime(); /* * Scale boss HP by actual party size. */ const maxHealth = 15000 + ( participantCount * 5000 ); const claimed = await events.updateOne( { eventId, state: "Joining" }, { $set: { state: "Active", battleStartedAt: now, battleEndsAt: now + BATTLE_DURATION, "boss.health": maxHealth, "boss.maxHealth": maxHealth, "boss.rage": 0 } } ); if ( claimed.modifiedCount !== 1 ) { return false; } await refreshBossMessage( client, eventId ); return true; } async function performBossAction( interaction, actionName ) { const event = await getBossFromMessage( interaction ); if (!event) { return temporaryEphemeralReply( interaction, { content: "This boss event no longer exists." } ); } if ( event.state !== "Active" ) { return temporaryEphemeralReply( interaction, { content: "The battle isn't active." } ); } const player = event.participants?.[ interaction.user.id ]; if (!player) { return temporaryEphemeralReply( interaction, { content: "You didn't join this battle." } ); } if (player.knockedOut) { return temporaryEphemeralReply( interaction, { content: "You're knocked out and can't attack." } ); } const action = ACTIONS[ actionName ]; if (!action) return; const now = getUnixTime(); /* * Lazy energy regeneration: * +10 Energy per 10 seconds. */ const elapsed = Math.max( 0, now - player.lastEnergyUpdate ); const regenerated = Math.floor( elapsed / 10 ) * 10; const energy = Math.min( player.maxEnergy, player.energy + regenerated ); const cooldown = player.cooldowns?.[ actionName ] ?? 0; if ( cooldown > now ) { return temporaryEphemeralReply( interaction, { content: `**${actionName}** is ready .` } ); } if ( energy < action.energyCost ) { return temporaryEphemeralReply( interaction, { content: `You need **${action.energyCost} Energy** for ${actionName}. You currently have **${energy}**.` } ); } await interaction.deferUpdate(); const variance = randomInteger( 90, 110 ) / 100; const damage = Math.max( 1, Math.floor( player.power * action.multiplier * variance ) ); const nextCooldown = now + action.cooldown; /* * 35% chance of boss retaliation. */ const retaliates = Math.random() < 0.35; const retaliationDamage = retaliates ? randomInteger( 15, 45 ) : 0; const newHealth = Math.max( 0, player.health - retaliationDamage ); const knockedOut = newHealth <= 0; const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); await events.updateOne( { eventId: event.eventId, state: "Active", "boss.health": { $gt: 0 } }, { $inc: { "boss.health": -damage, [`participants.${interaction.user.id}.damage`]: damage }, $set: { [`participants.${interaction.user.id}.energy`]: energy - action.energyCost, [`participants.${interaction.user.id}.lastEnergyUpdate`]: now, [`participants.${interaction.user.id}.health`]: newHealth, [`participants.${interaction.user.id}.knockedOut`]: knockedOut, [`participants.${interaction.user.id}.cooldowns.${actionName}`]: nextCooldown } } ); const updated = await events.findOne({ eventId: event.eventId }); /* * Clamp boss HP if concurrent attacks pushed it below zero. */ if ( updated.boss.health <= 0 ) { await events.updateOne( { eventId: event.eventId }, { $set: { "boss.health": 0 } } ); await resolveBossVictory( interaction.client, event.eventId ); } else { await refreshBossMessage( interaction.client, event.eventId ); } const result = new EmbedBuilder() .setTitle( `⚔️ ${actionName}` ) .addFields( { name: "Damage", value: `**${damage.toLocaleString()}**`, inline: true }, { name: "HP", value: `**${newHealth}/${player.maxHealth}**`, inline: true }, { name: "Energy", value: `**${energy - action.energyCost}/${player.maxEnergy}**`, inline: true } ); if (retaliates) { result.setDescription( `The Crumbbound Behemoth retaliated for **${retaliationDamage} damage**!` ); } if (knockedOut) { result.setDescription( "💀 The Crumbbound Behemoth knocked you out!" ); } return temporaryEphemeralFollowUp( interaction, { embeds: [ result ] }, 9000 ); } async function resolveBossVictory( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOneAndUpdate( { eventId, state: "Active" }, { $set: { state: "Resolving" } }, { returnDocument: "after" } ); if (!event) return; const participants = Object.entries( event.participants ?? {} ); if (!participants.length) return; const rewardPool = event.rewardPool; const totalDamage = participants.reduce( ( sum, [ , player ] ) => sum + player.damage, 0 ); const evenPool = Math.floor( rewardPool * 0.30 ); const damagePool = rewardPool - evenPool; const evenShare = Math.floor( evenPool / participants.length ); let distributed = 0; const rewards = []; for ( const [ userId, player ] of participants ) { const damageShare = totalDamage > 0 ? Math.floor( damagePool * ( player.damage / totalDamage ) ) : 0; const reward = evenShare + damageShare; if (reward > 0) { await transferCookies( event.escrowId, userId, reward ); distributed += reward; } rewards.push({ userId, reward, damage: player.damage }); } /* * Rounding remainder goes to top damage. */ const remainder = rewardPool - distributed; if ( remainder > 0 ) { const top = rewards .slice() .sort( ( a, b ) => b.damage - a.damage )[0]; if (top) { await transferCookies( event.escrowId, top.userId, remainder ); top.reward += remainder; } } await events.updateOne( { eventId }, { $set: { state: "Defeated", defeatedAt: getUnixTime(), rewards } } ); await refreshBossMessage( client, eventId ); } async function failBoss( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOneAndUpdate( { eventId, state: "Active" }, { $set: { state: "Failed", failedAt: getUnixTime() } }, { returnDocument: "after" } ); if (!event) return; const escrowBalance = await getCookieBalance( event.escrowId ); /* * Failure: * all committed Cookies become house Cookies. */ if ( escrowBalance > 0 ) { await transferCookies( event.escrowId, client.user.id, escrowBalance ); } await refreshBossMessage( client, eventId ); } async function cancelBossAndRefund( client, eventId ) { const { db } = getCollections(); const events = db.collection( EVENT_COLLECTION ); const event = await events.findOneAndUpdate( { eventId, state: "Joining" }, { $set: { state: "Cancelled", cancelledAt: getUnixTime() } }, { returnDocument: "after" } ); if (!event) return; /* * Refund all participant entries. */ for ( const [ userId, player ] of Object.entries( event.participants ?? {} ) ) { if ( player.entry > 0 ) { await transferCookies( event.escrowId, userId, player.entry ); } } /* * Refund the original 500 spawn cost. */ const remaining = await getCookieBalance( event.escrowId ); if ( remaining > 0 ) { await transferCookies( event.escrowId, event.creatorId, remaining ); } await refreshBossMessage( client, eventId ); } async function refreshBossMessage( client, eventId ) { const { db } = getCollections(); const event = await db .collection( EVENT_COLLECTION ) .findOne({ 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 } = buildBossMessage( event ); await message.edit({ embeds: [ embed ], components }); } function buildBossMessage( event ) { const participantCount = Object.keys( event.participants ?? {} ).length; if ( event.state === "Joining" ) { const embed = new EmbedBuilder() .setTitle( "🍪 The Crumbbound Behemoth" ) .setDescription( [ "*A creature of hunger, crumbs, and questionable dietary choices.*", "", `**Recruitment ends:** `, `**Participants:** ${participantCount}/${event.minimumParticipants} required`, `**Reward Pool:** ${event.rewardPool.toLocaleString()} 🍪`, "", participantCount >= event.minimumParticipants ? "✅ The summoner may start the battle early." : "Recruit more adventurers before the fight can begin." ].join("\n") ) .setFooter({ text: `cookieBoss:${event.eventId}` }); return { embed, components: buildJoinButtons() }; } if ( event.state === "Active" ) { const hp = Math.max( 0, event.boss.health ); const hpBar = buildHpBar( hp, event.boss.maxHealth ); const embed = new EmbedBuilder() .setTitle( "⚔️ The Crumbbound Behemoth" ) .setDescription( [ hpBar, `**${hp.toLocaleString()} / ${event.boss.maxHealth.toLocaleString()} HP**`, "", `**Participants:** ${participantCount}`, `**Reward Pool:** ${event.rewardPool.toLocaleString()} 🍪`, `**Battle ends:** ` ].join("\n") ) .setFooter({ text: `cookieBoss:${event.eventId}` }); return { embed, components: buildCombatButtons() }; } if ( event.state === "Defeated" ) { const rewards = ( event.rewards ?? [] ) .sort( ( a, b ) => b.damage - a.damage ) .slice( 0, 10 ) .map( ( player, index ) => `**${index + 1}.** <@${player.userId}> — ` + `${player.damage.toLocaleString()} dmg — ` + `**${player.reward.toLocaleString()} 🍪**` ) .join("\n"); return { embed: new EmbedBuilder() .setTitle( "🏆 The Crumbbound Behemoth Has Fallen!" ) .setDescription( rewards || "The beast has been defeated." ) .setFooter({ text: `cookieBoss:${event.eventId}` }), components: [] }; } if ( event.state === "Failed" ) { return { embed: new EmbedBuilder() .setTitle( "💀 The Crumbbound Behemoth Survived" ) .setDescription( [ "The battle timer expired.", "", `**${event.rewardPool.toLocaleString()} Cookies** were lost to the ShaiBot Vault.` ].join("\n") ) .setFooter({ text: `cookieBoss:${event.eventId}` }), components: [] }; } return { embed: new EmbedBuilder() .setTitle( "🍪 Boss Hunt Cancelled" ) .setDescription( "Not enough adventurers joined. All committed Cookies were refunded." ) .setFooter({ text: `cookieBoss:${event.eventId}` }), components: [] }; } function buildJoinButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieBossJoinScout" ) .setLabel( "Scout — 100" ) .setEmoji( "🍪" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "cookieBossJoinFighter" ) .setLabel( "Fighter — 500" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "cookieBossJoinChampion" ) .setLabel( "Champion — 1000" ) .setEmoji( "🔥" ) .setStyle( ButtonStyle.Danger ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieBossStart" ) .setLabel( "Start Battle" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Success ) ) ]; } function buildCombatButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieBossStrike" ) .setLabel( "Strike" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "cookieBossBurst" ) .setLabel( "Burst" ) .setEmoji( "💥" ) .setStyle( ButtonStyle.Danger ), new ButtonBuilder() .setCustomId( "cookieBossLimit" ) .setLabel( "Limit" ) .setEmoji( "✨" ) .setStyle( ButtonStyle.Success ) ) ]; } async function getBossFromMessage( interaction ) { const footer = interaction.message .embeds[0] ?.footer ?.text; const [ type, eventId ] = footer ?.split(":") ?? []; if ( type !== "cookieBoss" || !eventId ) { return null; } const { db } = getCollections(); return db .collection( EVENT_COLLECTION ) .findOne({ eventId }); } function buildHpBar( current, max, size = 20 ) { const ratio = Math.max( 0, Math.min( 1, current / max ) ); const filled = Math.round( ratio * size ); return ( "█".repeat( filled ) + "░".repeat( size - filled ) ); } module.exports = { joinBoss, startBossBattle, activateBoss, performBossAction, resolveBossVictory, failBoss, cancelBossAndRefund, refreshBossMessage };