1894 lines
34 KiB
JavaScript
1894 lines
34 KiB
JavaScript
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:** <t:${event.joinUntil}:R>`,
|
|
`**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:** <t:${event.battleEndsAt}:R>`,
|
|
"",
|
|
"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
|
|
}; |