1693 lines
30 KiB
JavaScript
1693 lines
30 KiB
JavaScript
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 <t:${cooldown}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
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:** <t:${event.joinUntil}:R>`,
|
|
`**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:** <t:${event.battleEndsAt}:R>`
|
|
].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
|
|
}; |