5090 lines
95 KiB
JavaScript
5090 lines
95 KiB
JavaScript
const {
|
|
EmbedBuilder,
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle,
|
|
MessageFlags
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
getCollections,
|
|
getCookieBalance,
|
|
transferCookies,
|
|
temporaryEphemeralReply,
|
|
temporaryEphemeralFollowUp,
|
|
getUnixTime,
|
|
randomInteger
|
|
} = require("../../utils/cookieEconomy");
|
|
|
|
|
|
const EVENT_COLLECTION =
|
|
"items_events";
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* GENERAL CONFIG
|
|
* ============================================================
|
|
*/
|
|
|
|
const BATTLE_DURATION =
|
|
60 * 60;
|
|
|
|
const BOSS_ACTION_THRESHOLD =
|
|
10;
|
|
|
|
const LIMIT_GAUGE_MAX =
|
|
100;
|
|
|
|
const REVIVE_REQUIRED =
|
|
3;
|
|
|
|
|
|
/*
|
|
* HP restored when a player is revived.
|
|
*
|
|
* This is based on the TARGET'S role.
|
|
*/
|
|
const REVIVE_HP = {
|
|
DPS:
|
|
0.65,
|
|
|
|
Tank:
|
|
0.75,
|
|
|
|
Healer:
|
|
0.85
|
|
};
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* ACTION CONFIG
|
|
* ============================================================
|
|
*/
|
|
|
|
const ACTIONS = {
|
|
Strike: {
|
|
cooldown:
|
|
8,
|
|
|
|
energyGain:
|
|
12,
|
|
|
|
limitGain:
|
|
5,
|
|
|
|
powerMultiplier:
|
|
1.0
|
|
},
|
|
|
|
|
|
Defend: {
|
|
cooldown:
|
|
20,
|
|
|
|
energyCost:
|
|
5,
|
|
|
|
limitGain:
|
|
2,
|
|
|
|
reduction:
|
|
0.25,
|
|
|
|
charges:
|
|
2
|
|
},
|
|
|
|
|
|
Heal: {
|
|
cooldown:
|
|
25,
|
|
|
|
energyCost:
|
|
10,
|
|
|
|
limitGain:
|
|
3,
|
|
|
|
powerMultiplier:
|
|
1.0
|
|
},
|
|
|
|
|
|
Revive: {
|
|
cooldown:
|
|
12,
|
|
|
|
/*
|
|
* Revive costs no Energy.
|
|
*
|
|
* You're already giving up a combat action
|
|
* and advancing the Boss Gauge.
|
|
*/
|
|
energyCost:
|
|
0,
|
|
|
|
limitGain:
|
|
2
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* JOBS
|
|
* ============================================================
|
|
*/
|
|
|
|
const JOBS = {
|
|
DPS: {
|
|
BlackMage: {
|
|
name:
|
|
"Black Mage",
|
|
|
|
class:
|
|
"Thaumaturge",
|
|
|
|
emoji:
|
|
"🔥",
|
|
|
|
strikeMultiplier:
|
|
1.40,
|
|
|
|
strikeEnergy:
|
|
10,
|
|
|
|
strikeLimit:
|
|
5
|
|
},
|
|
|
|
|
|
Dragoon: {
|
|
name:
|
|
"Dragoon",
|
|
|
|
class:
|
|
"Lancer",
|
|
|
|
emoji:
|
|
"🐉",
|
|
|
|
strikeMultiplier:
|
|
1.30,
|
|
|
|
strikeEnergy:
|
|
12,
|
|
|
|
strikeLimit:
|
|
7
|
|
},
|
|
|
|
|
|
Bard: {
|
|
name:
|
|
"Bard",
|
|
|
|
class:
|
|
"Archer",
|
|
|
|
emoji:
|
|
"🏹",
|
|
|
|
strikeMultiplier:
|
|
1.15,
|
|
|
|
strikeEnergy:
|
|
14,
|
|
|
|
strikeLimit:
|
|
9
|
|
}
|
|
},
|
|
|
|
|
|
Tank: {
|
|
Paladin: {
|
|
name:
|
|
"Paladin",
|
|
|
|
class:
|
|
"Gladiator",
|
|
|
|
emoji:
|
|
"⚔️",
|
|
|
|
strikeMultiplier:
|
|
0.90,
|
|
|
|
defendCost:
|
|
2,
|
|
|
|
defendReduction:
|
|
0.55,
|
|
|
|
defendCharges:
|
|
3
|
|
},
|
|
|
|
|
|
Warrior: {
|
|
name:
|
|
"Warrior",
|
|
|
|
class:
|
|
"Marauder",
|
|
|
|
emoji:
|
|
"🪓",
|
|
|
|
strikeMultiplier:
|
|
1.05,
|
|
|
|
defendCost:
|
|
3,
|
|
|
|
defendReduction:
|
|
0.45,
|
|
|
|
defendCharges:
|
|
2
|
|
},
|
|
|
|
|
|
DarkKnight: {
|
|
name:
|
|
"Dark Knight",
|
|
|
|
class:
|
|
null,
|
|
|
|
emoji:
|
|
"🌑",
|
|
|
|
strikeMultiplier:
|
|
1.10,
|
|
|
|
defendCost:
|
|
3,
|
|
|
|
defendReduction:
|
|
0.50,
|
|
|
|
defendCharges:
|
|
2
|
|
}
|
|
},
|
|
|
|
|
|
Healer: {
|
|
WhiteMage: {
|
|
name:
|
|
"White Mage",
|
|
|
|
class:
|
|
"Conjurer",
|
|
|
|
emoji:
|
|
"✨",
|
|
|
|
strikeMultiplier:
|
|
0.75,
|
|
|
|
healCost:
|
|
5,
|
|
|
|
healMultiplier:
|
|
1.55,
|
|
|
|
healLimit:
|
|
3
|
|
},
|
|
|
|
|
|
Scholar: {
|
|
name:
|
|
"Scholar",
|
|
|
|
class:
|
|
"Arcanist",
|
|
|
|
emoji:
|
|
"📖",
|
|
|
|
strikeMultiplier:
|
|
0.80,
|
|
|
|
healCost:
|
|
6,
|
|
|
|
healMultiplier:
|
|
1.35,
|
|
|
|
healLimit:
|
|
4
|
|
},
|
|
|
|
|
|
Astrologian: {
|
|
name:
|
|
"Astrologian",
|
|
|
|
class:
|
|
null,
|
|
|
|
emoji:
|
|
"🌟",
|
|
|
|
strikeMultiplier:
|
|
0.80,
|
|
|
|
healCost:
|
|
6,
|
|
|
|
healMultiplier:
|
|
1.25,
|
|
|
|
healLimit:
|
|
6
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 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 (
|
|
![
|
|
"Joining",
|
|
"Active"
|
|
].includes(
|
|
event.state
|
|
)
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"This encounter can no longer be joined."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
if (
|
|
event.state ===
|
|
"Joining"
|
|
&&
|
|
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
|
|
||
|
|
![
|
|
"Joining",
|
|
"Active"
|
|
].includes(
|
|
event.state
|
|
)
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🍪 Entry 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
|
|
||
|
|
![
|
|
"Joining",
|
|
"Active"
|
|
].includes(
|
|
event.state
|
|
)
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🍪 Entry 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,
|
|
|
|
jobKey,
|
|
|
|
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,
|
|
|
|
/*
|
|
* Revive contribution statistics.
|
|
*/
|
|
reviveAssists:
|
|
0,
|
|
|
|
revives:
|
|
0,
|
|
|
|
/*
|
|
* Progress toward this player's own revival.
|
|
*/
|
|
revive: {
|
|
progress:
|
|
0
|
|
},
|
|
|
|
defense: {
|
|
charges:
|
|
0,
|
|
|
|
reduction:
|
|
0
|
|
},
|
|
|
|
knockedOut:
|
|
false,
|
|
|
|
joinedAt:
|
|
now,
|
|
|
|
cooldowns: {
|
|
Strike:
|
|
0,
|
|
|
|
Defend:
|
|
0,
|
|
|
|
Heal:
|
|
0,
|
|
|
|
Revive:
|
|
0
|
|
}
|
|
};
|
|
|
|
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state: {
|
|
$in: [
|
|
"Joining",
|
|
"Active"
|
|
]
|
|
},
|
|
|
|
[`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
|
|
);
|
|
|
|
|
|
const lateJoin =
|
|
event.state ===
|
|
"Active";
|
|
|
|
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
lateJoin
|
|
? "⚔️ Reinforcements Arrive!"
|
|
: "⚔️ 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**`,
|
|
"",
|
|
lateJoin
|
|
? "You've joined the battle already in progress."
|
|
: "You're ready for battle."
|
|
].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 BATTLE
|
|
* ============================================================
|
|
*/
|
|
|
|
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 =
|
|
12000 +
|
|
(
|
|
participantCount *
|
|
4000
|
|
);
|
|
|
|
|
|
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,
|
|
|
|
"boss.turn":
|
|
0
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
client,
|
|
eventId
|
|
);
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* COMBAT ACTION ROUTER
|
|
* ============================================================
|
|
*/
|
|
|
|
async function performCrumbleStrikeAction(
|
|
interaction,
|
|
actionName
|
|
) {
|
|
const event =
|
|
await getCrumbleStrikeFromPublicMessage(
|
|
interaction
|
|
);
|
|
|
|
|
|
if (
|
|
!event ||
|
|
event.state !==
|
|
"Active"
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"There is no active battle here."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const player =
|
|
event.participants?.[
|
|
interaction.user.id
|
|
];
|
|
|
|
|
|
if (!player) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"You haven't joined this Crumble Strike yet."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Revive is special:
|
|
*
|
|
* a living player selects a KO'd teammate.
|
|
*/
|
|
if (
|
|
actionName ===
|
|
"Revive"
|
|
) {
|
|
if (player.knockedOut) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"💀 You can't revive someone while you're knocked out."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
return showReviveTargets(
|
|
interaction,
|
|
event,
|
|
player
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* KO'd players cannot use normal combat actions.
|
|
*/
|
|
if (player.knockedOut) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"💀 You're knocked out and can no longer act."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
switch (actionName) {
|
|
case "Strike":
|
|
return performStrike(
|
|
interaction,
|
|
event,
|
|
player
|
|
);
|
|
|
|
|
|
case "Defend":
|
|
return performDefend(
|
|
interaction,
|
|
event,
|
|
player
|
|
);
|
|
|
|
|
|
case "Heal":
|
|
return performHeal(
|
|
interaction,
|
|
event,
|
|
player
|
|
);
|
|
|
|
|
|
case "LimitBreak":
|
|
return performLimitBreak(
|
|
interaction,
|
|
event,
|
|
player
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* STRIKE
|
|
* ============================================================
|
|
*/
|
|
|
|
async function performStrike(
|
|
interaction,
|
|
event,
|
|
player
|
|
) {
|
|
const now =
|
|
getUnixTime();
|
|
|
|
|
|
const cooldown =
|
|
player.cooldowns?.Strike ??
|
|
0;
|
|
|
|
|
|
if (
|
|
cooldown >
|
|
now
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`⚔️ Strike is ready <t:${cooldown}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const job =
|
|
getParticipantJob(
|
|
player
|
|
);
|
|
|
|
|
|
const multiplier =
|
|
job?.strikeMultiplier ??
|
|
ACTIONS.Strike.powerMultiplier;
|
|
|
|
|
|
const variance =
|
|
randomInteger(
|
|
90,
|
|
110
|
|
) /
|
|
100;
|
|
|
|
|
|
const damage =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.floor(
|
|
player.power *
|
|
multiplier *
|
|
variance
|
|
)
|
|
);
|
|
|
|
|
|
const energyGain =
|
|
job?.strikeEnergy ??
|
|
ACTIONS.Strike.energyGain;
|
|
|
|
|
|
const limitGain =
|
|
job?.strikeLimit ??
|
|
ACTIONS.Strike.limitGain;
|
|
|
|
|
|
const newEnergy =
|
|
Math.min(
|
|
player.maxEnergy,
|
|
player.energy +
|
|
energyGain
|
|
);
|
|
|
|
|
|
await interaction.deferUpdate();
|
|
|
|
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
"boss.health": {
|
|
$gt:
|
|
0
|
|
},
|
|
|
|
[`participants.${interaction.user.id}.knockedOut`]:
|
|
false,
|
|
|
|
[`participants.${interaction.user.id}.cooldowns.Strike`]: {
|
|
$lte:
|
|
now
|
|
}
|
|
},
|
|
{
|
|
$inc: {
|
|
"boss.health":
|
|
-damage,
|
|
|
|
"boss.actionGauge":
|
|
1,
|
|
|
|
"boss.limitGauge":
|
|
limitGain,
|
|
|
|
[`participants.${interaction.user.id}.damage`]:
|
|
damage
|
|
},
|
|
|
|
$set: {
|
|
[`participants.${interaction.user.id}.energy`]:
|
|
newEnergy,
|
|
|
|
[`participants.${interaction.user.id}.cooldowns.Strike`]:
|
|
now +
|
|
ACTIONS.Strike.cooldown
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
content:
|
|
"That action could not be resolved."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
await clampBossGauges(
|
|
event.eventId
|
|
);
|
|
|
|
|
|
const victory =
|
|
await checkForVictory(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
let bossAttack =
|
|
null;
|
|
|
|
|
|
if (!victory) {
|
|
bossAttack =
|
|
await resolveBossTurnIfReady(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
}
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"⚔️ Strike"
|
|
)
|
|
|
|
.setDescription(
|
|
`${getJobEmoji(player)} **${player.job}** attacks the Crumbbound Behemoth!`
|
|
)
|
|
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Damage",
|
|
|
|
value:
|
|
`**${damage.toLocaleString()}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Energy",
|
|
|
|
value:
|
|
`**${newEnergy}/${player.maxEnergy}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Limit Generated",
|
|
|
|
value:
|
|
`**+${limitGain}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
);
|
|
|
|
|
|
if (bossAttack) {
|
|
resultEmbed.addFields({
|
|
name:
|
|
"👹 Boss Turn",
|
|
|
|
value:
|
|
bossAttack.summary
|
|
});
|
|
}
|
|
|
|
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
embeds: [
|
|
resultEmbed
|
|
]
|
|
},
|
|
8000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* DEFEND
|
|
* ============================================================
|
|
*/
|
|
|
|
async function performDefend(
|
|
interaction,
|
|
event,
|
|
player
|
|
) {
|
|
const now =
|
|
getUnixTime();
|
|
|
|
|
|
const cooldown =
|
|
player.cooldowns?.Defend ??
|
|
0;
|
|
|
|
|
|
if (
|
|
cooldown >
|
|
now
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`🛡️ Defend is ready <t:${cooldown}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const job =
|
|
getParticipantJob(
|
|
player
|
|
);
|
|
|
|
|
|
const energyCost =
|
|
job?.defendCost ??
|
|
ACTIONS.Defend.energyCost;
|
|
|
|
|
|
const reduction =
|
|
job?.defendReduction ??
|
|
ACTIONS.Defend.reduction;
|
|
|
|
|
|
const charges =
|
|
job?.defendCharges ??
|
|
ACTIONS.Defend.charges;
|
|
|
|
|
|
if (
|
|
player.energy <
|
|
energyCost
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`🛡️ Defend requires **${energyCost} Energy**. You currently have **${player.energy}**.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
await interaction.deferUpdate();
|
|
|
|
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
[`participants.${interaction.user.id}.knockedOut`]:
|
|
false,
|
|
|
|
[`participants.${interaction.user.id}.energy`]: {
|
|
$gte:
|
|
energyCost
|
|
},
|
|
|
|
[`participants.${interaction.user.id}.cooldowns.Defend`]: {
|
|
$lte:
|
|
now
|
|
}
|
|
},
|
|
{
|
|
$inc: {
|
|
"boss.actionGauge":
|
|
1,
|
|
|
|
"boss.limitGauge":
|
|
ACTIONS.Defend.limitGain,
|
|
|
|
[`participants.${interaction.user.id}.energy`]:
|
|
-energyCost
|
|
},
|
|
|
|
$set: {
|
|
[`participants.${interaction.user.id}.defense`]:
|
|
{
|
|
charges,
|
|
reduction
|
|
},
|
|
|
|
[`participants.${interaction.user.id}.cooldowns.Defend`]:
|
|
now +
|
|
ACTIONS.Defend.cooldown
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
content:
|
|
"That defensive action could not be resolved."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
await clampBossGauges(
|
|
event.eventId
|
|
);
|
|
|
|
|
|
const bossAttack =
|
|
await resolveBossTurnIfReady(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🛡️ Defend"
|
|
)
|
|
|
|
.setDescription(
|
|
`${getJobEmoji(player)} **${player.job}** prepares for the next attack.`
|
|
)
|
|
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Damage Reduction",
|
|
|
|
value:
|
|
`**${Math.round(reduction * 100)}%**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Charges",
|
|
|
|
value:
|
|
`**${charges}** incoming attack${charges === 1 ? "" : "s"}`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Energy",
|
|
|
|
value:
|
|
`**${player.energy - energyCost}/${player.maxEnergy}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
);
|
|
|
|
|
|
if (bossAttack) {
|
|
resultEmbed.addFields({
|
|
name:
|
|
"👹 Boss Turn",
|
|
|
|
value:
|
|
bossAttack.summary
|
|
});
|
|
}
|
|
|
|
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
embeds: [
|
|
resultEmbed
|
|
]
|
|
},
|
|
8000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* PARTY HEAL
|
|
* ============================================================
|
|
*/
|
|
|
|
async function performHeal(
|
|
interaction,
|
|
event,
|
|
player
|
|
) {
|
|
const now =
|
|
getUnixTime();
|
|
|
|
|
|
const cooldown =
|
|
player.cooldowns?.Heal ??
|
|
0;
|
|
|
|
|
|
if (
|
|
cooldown >
|
|
now
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`❤️ Heal is ready <t:${cooldown}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const job =
|
|
getParticipantJob(
|
|
player
|
|
);
|
|
|
|
|
|
const energyCost =
|
|
job?.healCost ??
|
|
ACTIONS.Heal.energyCost;
|
|
|
|
|
|
const multiplier =
|
|
job?.healMultiplier ??
|
|
ACTIONS.Heal.powerMultiplier;
|
|
|
|
|
|
const limitGain =
|
|
job?.healLimit ??
|
|
ACTIONS.Heal.limitGain;
|
|
|
|
|
|
if (
|
|
player.energy <
|
|
energyCost
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`❤️ Heal requires **${energyCost} Energy**. You currently have **${player.energy}**.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const healPerPlayer =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.floor(
|
|
player.power *
|
|
multiplier
|
|
)
|
|
);
|
|
|
|
|
|
await interaction.deferUpdate();
|
|
|
|
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const latest =
|
|
await events.findOne({
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
});
|
|
|
|
|
|
if (!latest) {
|
|
return;
|
|
}
|
|
|
|
|
|
let totalHealing =
|
|
0;
|
|
|
|
let healedPlayers =
|
|
0;
|
|
|
|
|
|
const setOperations = {};
|
|
|
|
|
|
for (
|
|
const [
|
|
userId,
|
|
target
|
|
]
|
|
of Object.entries(
|
|
latest.participants ??
|
|
{}
|
|
)
|
|
) {
|
|
/*
|
|
* Normal Heal never revives.
|
|
*/
|
|
if (
|
|
target.knockedOut ||
|
|
target.health <=
|
|
0 ||
|
|
target.health >=
|
|
target.maxHealth
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
|
|
const newHealth =
|
|
Math.min(
|
|
target.maxHealth,
|
|
target.health +
|
|
healPerPlayer
|
|
);
|
|
|
|
|
|
const actualHealing =
|
|
newHealth -
|
|
target.health;
|
|
|
|
|
|
if (
|
|
actualHealing <=
|
|
0
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
|
|
setOperations[
|
|
`participants.${userId}.health`
|
|
] =
|
|
newHealth;
|
|
|
|
|
|
totalHealing +=
|
|
actualHealing;
|
|
|
|
healedPlayers++;
|
|
}
|
|
|
|
|
|
setOperations[
|
|
`participants.${interaction.user.id}.cooldowns.Heal`
|
|
] =
|
|
now +
|
|
ACTIONS.Heal.cooldown;
|
|
|
|
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
[`participants.${interaction.user.id}.knockedOut`]:
|
|
false,
|
|
|
|
[`participants.${interaction.user.id}.energy`]: {
|
|
$gte:
|
|
energyCost
|
|
}
|
|
},
|
|
{
|
|
$set:
|
|
setOperations,
|
|
|
|
$inc: {
|
|
"boss.actionGauge":
|
|
1,
|
|
|
|
"boss.limitGauge":
|
|
limitGain,
|
|
|
|
[`participants.${interaction.user.id}.energy`]:
|
|
-energyCost,
|
|
|
|
[`participants.${interaction.user.id}.healing`]:
|
|
totalHealing
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
content:
|
|
"That heal could not be resolved."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
await clampBossGauges(
|
|
event.eventId
|
|
);
|
|
|
|
|
|
const bossAttack =
|
|
await resolveBossTurnIfReady(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"❤️ Party Heal"
|
|
)
|
|
|
|
.setDescription(
|
|
`${getJobEmoji(player)} **${player.job}** restores the party.`
|
|
)
|
|
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Healing",
|
|
|
|
value:
|
|
`**${totalHealing.toLocaleString()} HP**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Players Restored",
|
|
|
|
value:
|
|
`**${healedPlayers}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Energy",
|
|
|
|
value:
|
|
`**${player.energy - energyCost}/${player.maxEnergy}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Limit Generated",
|
|
|
|
value:
|
|
`**+${limitGain}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
);
|
|
|
|
|
|
if (bossAttack) {
|
|
resultEmbed.addFields({
|
|
name:
|
|
"👹 Boss Turn",
|
|
|
|
value:
|
|
bossAttack.summary
|
|
});
|
|
}
|
|
|
|
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
embeds: [
|
|
resultEmbed
|
|
]
|
|
},
|
|
8000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SHOW REVIVE TARGETS
|
|
* ============================================================
|
|
*/
|
|
|
|
async function showReviveTargets(
|
|
interaction,
|
|
event,
|
|
player
|
|
) {
|
|
const now =
|
|
getUnixTime();
|
|
|
|
|
|
const cooldown =
|
|
player.cooldowns?.Revive ??
|
|
0;
|
|
|
|
|
|
if (
|
|
cooldown >
|
|
now
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`🌟 Revive is ready <t:${cooldown}:R>.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const knockedOutPlayers =
|
|
Object.entries(
|
|
event.participants ??
|
|
{}
|
|
)
|
|
.filter(
|
|
(
|
|
[
|
|
,
|
|
participant
|
|
]
|
|
) =>
|
|
participant.knockedOut
|
|
);
|
|
|
|
|
|
if (
|
|
!knockedOutPlayers.length
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"🌟 Nobody currently needs reviving."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Discord String Select Menus allow up to 25 options.
|
|
*/
|
|
const options =
|
|
knockedOutPlayers
|
|
.slice(
|
|
0,
|
|
25
|
|
)
|
|
.map(
|
|
(
|
|
[
|
|
userId,
|
|
target
|
|
]
|
|
) => {
|
|
const progress =
|
|
target.revive?.progress ??
|
|
0;
|
|
|
|
|
|
return new StringSelectMenuOptionBuilder()
|
|
.setLabel(
|
|
`${target.job} — ${progress}/${REVIVE_REQUIRED}`
|
|
)
|
|
.setDescription(
|
|
`${target.role} • ${Math.max(0, target.health)}/${target.maxHealth} HP`
|
|
)
|
|
.setValue(
|
|
userId
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
const menu =
|
|
new StringSelectMenuBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeReviveTarget"
|
|
)
|
|
.setPlaceholder(
|
|
"Choose a fallen adventurer..."
|
|
)
|
|
.addOptions(
|
|
options
|
|
);
|
|
|
|
|
|
const row =
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
menu
|
|
);
|
|
|
|
|
|
const assistPower =
|
|
player.role ===
|
|
"Healer"
|
|
? 2
|
|
: 1;
|
|
|
|
|
|
const embed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
[
|
|
"Choose a fallen party member to assist.",
|
|
"",
|
|
`Your role contributes **+${assistPower} Revival Progress**.`,
|
|
"",
|
|
`A player needs **${REVIVE_REQUIRED} total progress** to return to battle.`,
|
|
"",
|
|
"Using Revive counts as one combat action."
|
|
].join("\n")
|
|
)
|
|
.setFooter({
|
|
text:
|
|
`crumbleStrikeRevive:${event.eventId}:${interaction.user.id}`
|
|
});
|
|
|
|
|
|
return interaction.reply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
|
|
components: [
|
|
row
|
|
],
|
|
|
|
flags:
|
|
MessageFlags.Ephemeral
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* RESOLVE REVIVE TARGET
|
|
* ============================================================
|
|
*/
|
|
|
|
async function resolveCrumbleStrikeRevive(
|
|
interaction
|
|
) {
|
|
const footer =
|
|
interaction.message
|
|
.embeds[0]
|
|
?.footer
|
|
?.text;
|
|
|
|
|
|
const parts =
|
|
footer?.split(":")
|
|
??
|
|
[];
|
|
|
|
|
|
if (
|
|
parts[0] !==
|
|
"crumbleStrikeRevive"
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"This revive interaction is no longer valid."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const eventId =
|
|
parts[1];
|
|
|
|
const ownerId =
|
|
parts[2];
|
|
|
|
|
|
if (
|
|
ownerId !==
|
|
interaction.user.id
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"This revive selection belongs to another player."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const targetId =
|
|
interaction.values?.[0];
|
|
|
|
|
|
if (!targetId) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
"No revive target was selected."
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
/*
|
|
* Re-read immediately before resolving.
|
|
*/
|
|
const event =
|
|
await events.findOne({
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
});
|
|
|
|
|
|
if (!event) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
"This battle is no longer active."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
const player =
|
|
event.participants?.[
|
|
interaction.user.id
|
|
];
|
|
|
|
|
|
const target =
|
|
event.participants?.[
|
|
targetId
|
|
];
|
|
|
|
|
|
if (
|
|
!player ||
|
|
player.knockedOut
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
"You are no longer able to perform this action."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
if (
|
|
!target ||
|
|
!target.knockedOut
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
"That player has already been revived."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
const now =
|
|
getUnixTime();
|
|
|
|
|
|
const cooldown =
|
|
player.cooldowns?.Revive ??
|
|
0;
|
|
|
|
|
|
if (
|
|
cooldown >
|
|
now
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
`You can assist another revival <t:${cooldown}:R>.`
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Healers count double.
|
|
*/
|
|
const revivePower =
|
|
player.role ===
|
|
"Healer"
|
|
? 2
|
|
: 1;
|
|
|
|
|
|
const currentProgress =
|
|
target.revive?.progress ??
|
|
0;
|
|
|
|
|
|
const newProgress =
|
|
Math.min(
|
|
REVIVE_REQUIRED,
|
|
currentProgress +
|
|
revivePower
|
|
);
|
|
|
|
|
|
const completed =
|
|
newProgress >=
|
|
REVIVE_REQUIRED;
|
|
|
|
|
|
const setOperations = {
|
|
[`participants.${interaction.user.id}.cooldowns.Revive`]:
|
|
now +
|
|
ACTIONS.Revive.cooldown
|
|
};
|
|
|
|
|
|
const incOperations = {
|
|
"boss.actionGauge":
|
|
1,
|
|
|
|
"boss.limitGauge":
|
|
ACTIONS.Revive.limitGain,
|
|
|
|
[`participants.${interaction.user.id}.reviveAssists`]:
|
|
revivePower
|
|
};
|
|
|
|
|
|
let restoredHealth =
|
|
0;
|
|
|
|
|
|
if (completed) {
|
|
const revivePercent =
|
|
REVIVE_HP[
|
|
target.role
|
|
]
|
|
??
|
|
0.75;
|
|
|
|
|
|
restoredHealth =
|
|
Math.max(
|
|
1,
|
|
|
|
Math.floor(
|
|
target.maxHealth *
|
|
revivePercent
|
|
)
|
|
);
|
|
|
|
|
|
setOperations[
|
|
`participants.${targetId}.health`
|
|
] =
|
|
restoredHealth;
|
|
|
|
|
|
setOperations[
|
|
`participants.${targetId}.knockedOut`
|
|
] =
|
|
false;
|
|
|
|
|
|
setOperations[
|
|
`participants.${targetId}.energy`
|
|
] =
|
|
0;
|
|
|
|
|
|
setOperations[
|
|
`participants.${targetId}.defense`
|
|
] = {
|
|
charges:
|
|
0,
|
|
|
|
reduction:
|
|
0
|
|
};
|
|
|
|
|
|
setOperations[
|
|
`participants.${targetId}.revive.progress`
|
|
] =
|
|
0;
|
|
|
|
|
|
incOperations[
|
|
`participants.${interaction.user.id}.revives`
|
|
] =
|
|
1;
|
|
}
|
|
|
|
|
|
else {
|
|
setOperations[
|
|
`participants.${targetId}.revive.progress`
|
|
] =
|
|
newProgress;
|
|
}
|
|
|
|
|
|
/*
|
|
* Atomic check:
|
|
*
|
|
* target must still be KO'd and reviver must
|
|
* still be alive.
|
|
*/
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
[`participants.${interaction.user.id}.knockedOut`]:
|
|
false,
|
|
|
|
[`participants.${targetId}.knockedOut`]:
|
|
true,
|
|
|
|
[`participants.${interaction.user.id}.cooldowns.Revive`]: {
|
|
$lte:
|
|
now
|
|
}
|
|
},
|
|
{
|
|
$set:
|
|
setOperations,
|
|
|
|
$inc:
|
|
incOperations
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return interaction.update({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🌟 Revive"
|
|
)
|
|
.setDescription(
|
|
"Someone else changed the battle state before your revival could complete."
|
|
)
|
|
],
|
|
|
|
components: []
|
|
});
|
|
}
|
|
|
|
|
|
await clampBossGauges(
|
|
eventId
|
|
);
|
|
|
|
|
|
/*
|
|
* Reviving still advances the boss toward
|
|
* its next attack.
|
|
*
|
|
* If this fills the gauge, the boss can attack
|
|
* immediately after the revival action.
|
|
*/
|
|
const bossAttack =
|
|
await resolveBossTurnIfReady(
|
|
interaction.client,
|
|
eventId
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
interaction.client,
|
|
eventId
|
|
);
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder();
|
|
|
|
|
|
if (completed) {
|
|
resultEmbed
|
|
.setTitle(
|
|
"🌟 REVIVED!"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
`<@${targetId}> has returned to the fight!`,
|
|
"",
|
|
`❤️ HP restored: **${restoredHealth}/${target.maxHealth}**`,
|
|
"⚡ Energy: **0/100**",
|
|
"",
|
|
`${getRoleEmoji(player.role)} ${player.job} contributed **+${revivePower} Revival Progress**.`
|
|
].join("\n")
|
|
);
|
|
}
|
|
|
|
|
|
else {
|
|
resultEmbed
|
|
.setTitle(
|
|
"🌟 Revival Assist"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
`You assist <@${targetId}>.`,
|
|
"",
|
|
`Revival Progress: **${newProgress}/${REVIVE_REQUIRED}**`,
|
|
"",
|
|
`${getRoleEmoji(player.role)} ${player.job} contributed **+${revivePower}**.`
|
|
].join("\n")
|
|
);
|
|
}
|
|
|
|
|
|
if (bossAttack) {
|
|
resultEmbed.addFields({
|
|
name:
|
|
"👹 Boss Turn",
|
|
|
|
value:
|
|
bossAttack.summary
|
|
});
|
|
}
|
|
|
|
|
|
await interaction.update({
|
|
embeds: [
|
|
resultEmbed
|
|
],
|
|
|
|
components: []
|
|
});
|
|
|
|
|
|
/*
|
|
* Remove the ephemeral result after a while.
|
|
*/
|
|
setTimeout(
|
|
async () => {
|
|
try {
|
|
await interaction.deleteReply();
|
|
} catch {}
|
|
},
|
|
9000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* LIMIT BREAK
|
|
* ============================================================
|
|
*/
|
|
|
|
async function performLimitBreak(
|
|
interaction,
|
|
event,
|
|
player
|
|
) {
|
|
if (
|
|
event.boss.limitGauge <
|
|
LIMIT_GAUGE_MAX
|
|
) {
|
|
return temporaryEphemeralReply(
|
|
interaction,
|
|
{
|
|
content:
|
|
`✨ The Limit Gauge is only **${event.boss.limitGauge}/${LIMIT_GAUGE_MAX}**.`
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const livingPlayers =
|
|
Object.values(
|
|
event.participants ??
|
|
{}
|
|
)
|
|
.filter(
|
|
participant =>
|
|
!participant.knockedOut
|
|
);
|
|
|
|
|
|
const totalPartyPower =
|
|
livingPlayers.reduce(
|
|
(
|
|
total,
|
|
participant
|
|
) =>
|
|
total +
|
|
participant.power,
|
|
|
|
0
|
|
);
|
|
|
|
|
|
const damage =
|
|
Math.floor(
|
|
(
|
|
event.boss.maxHealth *
|
|
0.08
|
|
)
|
|
+
|
|
(
|
|
totalPartyPower *
|
|
8
|
|
)
|
|
);
|
|
|
|
|
|
await interaction.deferUpdate();
|
|
|
|
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const result =
|
|
await events.updateOne(
|
|
{
|
|
eventId:
|
|
event.eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
"boss.limitGauge": {
|
|
$gte:
|
|
LIMIT_GAUGE_MAX
|
|
},
|
|
|
|
"boss.health": {
|
|
$gt:
|
|
0
|
|
},
|
|
|
|
[`participants.${interaction.user.id}.knockedOut`]:
|
|
false
|
|
},
|
|
{
|
|
$inc: {
|
|
"boss.health":
|
|
-damage,
|
|
|
|
"boss.actionGauge":
|
|
1,
|
|
|
|
[`participants.${interaction.user.id}.damage`]:
|
|
damage
|
|
},
|
|
|
|
$set: {
|
|
"boss.limitGauge":
|
|
0
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
result.modifiedCount !==
|
|
1
|
|
) {
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
content:
|
|
"Someone else used the Limit Break first!"
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
const victory =
|
|
await checkForVictory(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
let bossAttack =
|
|
null;
|
|
|
|
|
|
if (!victory) {
|
|
bossAttack =
|
|
await resolveBossTurnIfReady(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
interaction.client,
|
|
event.eventId
|
|
);
|
|
}
|
|
|
|
|
|
const resultEmbed =
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"✨ LIMIT BREAK!"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
`${getJobEmoji(player)} **${player.job}** unleashes`,
|
|
"",
|
|
"## ULTIMATE COOKIE CALAMITY",
|
|
"",
|
|
`💥 **${damage.toLocaleString()} DAMAGE!**`
|
|
].join("\n")
|
|
);
|
|
|
|
|
|
if (bossAttack) {
|
|
resultEmbed.addFields({
|
|
name:
|
|
"👹 Boss Turn",
|
|
|
|
value:
|
|
bossAttack.summary
|
|
});
|
|
}
|
|
|
|
|
|
return temporaryEphemeralFollowUp(
|
|
interaction,
|
|
{
|
|
embeds: [
|
|
resultEmbed
|
|
]
|
|
},
|
|
10000
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* BOSS TURN
|
|
* ============================================================
|
|
*/
|
|
|
|
async function resolveBossTurnIfReady(
|
|
client,
|
|
eventId
|
|
) {
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const event =
|
|
await events.findOne({
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
"boss.actionGauge": {
|
|
$gte:
|
|
BOSS_ACTION_THRESHOLD
|
|
}
|
|
});
|
|
|
|
|
|
if (!event) {
|
|
return null;
|
|
}
|
|
|
|
|
|
/*
|
|
* Claim boss turn.
|
|
*
|
|
* Increment persistent boss turn counter too.
|
|
*/
|
|
const claim =
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active",
|
|
|
|
"boss.actionGauge": {
|
|
$gte:
|
|
BOSS_ACTION_THRESHOLD
|
|
}
|
|
},
|
|
{
|
|
$inc: {
|
|
"boss.actionGauge":
|
|
-BOSS_ACTION_THRESHOLD,
|
|
|
|
"boss.turn":
|
|
1
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
claim.modifiedCount !==
|
|
1
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
|
|
const latest =
|
|
await events.findOne({
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike"
|
|
});
|
|
|
|
|
|
const setOperations = {};
|
|
|
|
const incOperations = {};
|
|
|
|
|
|
let totalDamage =
|
|
0;
|
|
|
|
let totalMitigated =
|
|
0;
|
|
|
|
let knockedOut =
|
|
0;
|
|
|
|
|
|
const baseDamage =
|
|
randomInteger(
|
|
45,
|
|
75
|
|
);
|
|
|
|
|
|
for (
|
|
const [
|
|
userId,
|
|
participant
|
|
]
|
|
of Object.entries(
|
|
latest.participants ??
|
|
{}
|
|
)
|
|
) {
|
|
if (
|
|
participant.knockedOut
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
|
|
let damage =
|
|
baseDamage;
|
|
|
|
|
|
const defense =
|
|
participant.defense ??
|
|
{
|
|
charges: 0,
|
|
reduction: 0
|
|
};
|
|
|
|
|
|
if (
|
|
defense.charges >
|
|
0
|
|
) {
|
|
const reduced =
|
|
Math.floor(
|
|
damage *
|
|
defense.reduction
|
|
);
|
|
|
|
|
|
damage -=
|
|
reduced;
|
|
|
|
|
|
totalMitigated +=
|
|
reduced;
|
|
|
|
|
|
/*
|
|
* Multiple updates to the same participant
|
|
* can be accumulated here.
|
|
*/
|
|
incOperations[
|
|
`participants.${userId}.mitigation`
|
|
] =
|
|
(
|
|
incOperations[
|
|
`participants.${userId}.mitigation`
|
|
]
|
|
??
|
|
0
|
|
)
|
|
+
|
|
reduced;
|
|
|
|
|
|
setOperations[
|
|
`participants.${userId}.defense.charges`
|
|
] =
|
|
Math.max(
|
|
0,
|
|
defense.charges -
|
|
1
|
|
);
|
|
}
|
|
|
|
|
|
const health =
|
|
Math.max(
|
|
0,
|
|
participant.health -
|
|
damage
|
|
);
|
|
|
|
|
|
setOperations[
|
|
`participants.${userId}.health`
|
|
] =
|
|
health;
|
|
|
|
|
|
if (
|
|
health <=
|
|
0
|
|
) {
|
|
setOperations[
|
|
`participants.${userId}.knockedOut`
|
|
] =
|
|
true;
|
|
|
|
|
|
/*
|
|
* Reset their current revival progress
|
|
* whenever they are newly knocked out.
|
|
*/
|
|
setOperations[
|
|
`participants.${userId}.revive.progress`
|
|
] =
|
|
0;
|
|
|
|
|
|
knockedOut++;
|
|
}
|
|
|
|
|
|
totalDamage +=
|
|
damage;
|
|
}
|
|
|
|
|
|
const update = {
|
|
$set:
|
|
setOperations
|
|
};
|
|
|
|
|
|
if (
|
|
Object.keys(
|
|
incOperations
|
|
).length
|
|
) {
|
|
update.$inc =
|
|
incOperations;
|
|
}
|
|
|
|
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
},
|
|
update
|
|
);
|
|
|
|
|
|
return {
|
|
damage:
|
|
totalDamage,
|
|
|
|
mitigation:
|
|
totalMitigated,
|
|
|
|
knockedOut,
|
|
|
|
summary:
|
|
[
|
|
"💥 **Crumbquake!**",
|
|
`Party Damage: **${totalDamage.toLocaleString()}**`,
|
|
|
|
totalMitigated >
|
|
0
|
|
? `Mitigated: **${totalMitigated.toLocaleString()}**`
|
|
: null,
|
|
|
|
knockedOut >
|
|
0
|
|
? `💀 Knocked Out: **${knockedOut}**`
|
|
: null
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* LIMIT GAUGE CLAMP
|
|
* ============================================================
|
|
*/
|
|
|
|
async function clampBossGauges(
|
|
eventId
|
|
) {
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
"boss.limitGauge": {
|
|
$gt:
|
|
LIMIT_GAUGE_MAX
|
|
}
|
|
},
|
|
{
|
|
$set: {
|
|
"boss.limitGauge":
|
|
LIMIT_GAUGE_MAX
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* VICTORY CHECK
|
|
* ============================================================
|
|
*/
|
|
|
|
async function checkForVictory(
|
|
client,
|
|
eventId
|
|
) {
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const event =
|
|
await events.findOne({
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
});
|
|
|
|
|
|
if (
|
|
!event ||
|
|
event.boss.health >
|
|
0
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
},
|
|
{
|
|
$set: {
|
|
"boss.health":
|
|
0
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
return resolveCrumbleStrikeVictory(
|
|
client,
|
|
eventId
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* VICTORY + REWARDS
|
|
* ============================================================
|
|
*/
|
|
|
|
async function resolveCrumbleStrikeVictory(
|
|
client,
|
|
eventId
|
|
) {
|
|
const {
|
|
db
|
|
} =
|
|
getCollections();
|
|
|
|
|
|
const events =
|
|
db.collection(
|
|
EVENT_COLLECTION
|
|
);
|
|
|
|
|
|
const event =
|
|
await events.findOneAndUpdate(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Active"
|
|
},
|
|
{
|
|
$set: {
|
|
state:
|
|
"Resolving",
|
|
|
|
"boss.health":
|
|
0
|
|
}
|
|
},
|
|
{
|
|
returnDocument:
|
|
"after"
|
|
}
|
|
);
|
|
|
|
|
|
if (!event) {
|
|
return false;
|
|
}
|
|
|
|
|
|
const participants =
|
|
Object.entries(
|
|
event.participants ??
|
|
{}
|
|
);
|
|
|
|
|
|
const rewardPool =
|
|
await getCookieBalance(
|
|
event.escrowId
|
|
);
|
|
|
|
|
|
const participationPool =
|
|
Math.floor(
|
|
rewardPool *
|
|
0.30
|
|
);
|
|
|
|
|
|
const contributionPool =
|
|
rewardPool -
|
|
participationPool;
|
|
|
|
|
|
const evenShare =
|
|
participants.length
|
|
? Math.floor(
|
|
participationPool /
|
|
participants.length
|
|
)
|
|
: 0;
|
|
|
|
|
|
const scored =
|
|
participants.map(
|
|
(
|
|
[
|
|
userId,
|
|
participant
|
|
]
|
|
) => {
|
|
return {
|
|
userId,
|
|
participant,
|
|
|
|
contribution:
|
|
getContributionScore(
|
|
participant
|
|
)
|
|
};
|
|
}
|
|
);
|
|
|
|
|
|
const totalContribution =
|
|
scored.reduce(
|
|
(
|
|
total,
|
|
player
|
|
) =>
|
|
total +
|
|
player.contribution,
|
|
|
|
0
|
|
);
|
|
|
|
|
|
const rewards = [];
|
|
|
|
|
|
for (
|
|
const player
|
|
of scored
|
|
) {
|
|
const contributionReward =
|
|
totalContribution >
|
|
0
|
|
? Math.floor(
|
|
contributionPool *
|
|
(
|
|
player.contribution /
|
|
totalContribution
|
|
)
|
|
)
|
|
|
|
: 0;
|
|
|
|
|
|
const reward =
|
|
evenShare +
|
|
contributionReward;
|
|
|
|
|
|
if (
|
|
reward >
|
|
0
|
|
) {
|
|
await safeTransfer(
|
|
event.escrowId,
|
|
player.userId,
|
|
reward
|
|
);
|
|
}
|
|
|
|
|
|
rewards.push({
|
|
userId:
|
|
player.userId,
|
|
|
|
job:
|
|
player.participant.job,
|
|
|
|
role:
|
|
player.participant.role,
|
|
|
|
damage:
|
|
player.participant.damage ??
|
|
0,
|
|
|
|
healing:
|
|
player.participant.healing ??
|
|
0,
|
|
|
|
mitigation:
|
|
player.participant.mitigation ??
|
|
0,
|
|
|
|
reviveAssists:
|
|
player.participant.reviveAssists ??
|
|
0,
|
|
|
|
revives:
|
|
player.participant.revives ??
|
|
0,
|
|
|
|
contribution:
|
|
player.contribution,
|
|
|
|
reward
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Give rounding leftovers to highest contributor.
|
|
*/
|
|
const remaining =
|
|
await getCookieBalance(
|
|
event.escrowId
|
|
);
|
|
|
|
|
|
if (
|
|
remaining >
|
|
0 &&
|
|
rewards.length
|
|
) {
|
|
const top =
|
|
rewards
|
|
.slice()
|
|
.sort(
|
|
(
|
|
a,
|
|
b
|
|
) =>
|
|
b.contribution -
|
|
a.contribution
|
|
)[0];
|
|
|
|
|
|
const paid =
|
|
await safeTransfer(
|
|
event.escrowId,
|
|
top.userId,
|
|
remaining
|
|
);
|
|
|
|
|
|
if (paid) {
|
|
top.reward +=
|
|
remaining;
|
|
}
|
|
}
|
|
|
|
|
|
await events.updateOne(
|
|
{
|
|
eventId,
|
|
|
|
eventType:
|
|
"CrumbleStrike",
|
|
|
|
state:
|
|
"Resolving"
|
|
},
|
|
{
|
|
$set: {
|
|
state:
|
|
"Defeated",
|
|
|
|
defeatedAt:
|
|
getUnixTime(),
|
|
|
|
rewards
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
await refreshCrumbleStrikeMessage(
|
|
client,
|
|
eventId
|
|
);
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* CONTRIBUTION
|
|
* ============================================================
|
|
*/
|
|
|
|
function getContributionScore(
|
|
participant
|
|
) {
|
|
return Math.floor(
|
|
(
|
|
participant.damage ??
|
|
0
|
|
)
|
|
+
|
|
(
|
|
(
|
|
participant.healing ??
|
|
0
|
|
)
|
|
*
|
|
0.80
|
|
)
|
|
+
|
|
(
|
|
(
|
|
participant.mitigation ??
|
|
0
|
|
)
|
|
*
|
|
0.80
|
|
)
|
|
+
|
|
(
|
|
(
|
|
participant.reviveAssists ??
|
|
0
|
|
)
|
|
*
|
|
75
|
|
)
|
|
+
|
|
(
|
|
(
|
|
participant.revives ??
|
|
0
|
|
)
|
|
*
|
|
150
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 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;
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* PUBLIC MESSAGE REFRESH
|
|
* ============================================================
|
|
*/
|
|
|
|
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 EMBED
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildCrumbleStrikeMessage(
|
|
event
|
|
) {
|
|
const participants =
|
|
Object.values(
|
|
event.participants ??
|
|
{}
|
|
);
|
|
|
|
|
|
const participantCount =
|
|
participants.length;
|
|
|
|
|
|
const roleCounts =
|
|
getRoleCounts(
|
|
participants
|
|
);
|
|
|
|
|
|
const knockedOutCount =
|
|
participants.filter(
|
|
participant =>
|
|
participant.knockedOut
|
|
).length;
|
|
|
|
|
|
/*
|
|
* RECRUITMENT
|
|
*/
|
|
|
|
if (
|
|
event.state ===
|
|
"Joining"
|
|
) {
|
|
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()
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* ACTIVE
|
|
*/
|
|
|
|
if (
|
|
event.state ===
|
|
"Active"
|
|
) {
|
|
const hp =
|
|
Math.max(
|
|
0,
|
|
event.boss.health
|
|
);
|
|
|
|
|
|
const limit =
|
|
Math.min(
|
|
LIMIT_GAUGE_MAX,
|
|
event.boss.limitGauge ??
|
|
0
|
|
);
|
|
|
|
|
|
const bossGauge =
|
|
event.boss.actionGauge ??
|
|
0;
|
|
|
|
|
|
return {
|
|
embed:
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"⚔️ Crumble Strike"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
"## The Crumbbound Behemoth",
|
|
"",
|
|
buildBar(
|
|
hp,
|
|
event.boss.maxHealth,
|
|
20
|
|
),
|
|
`**${hp.toLocaleString()} / ${event.boss.maxHealth.toLocaleString()} HP**`,
|
|
"",
|
|
"### ✨ Limit Break",
|
|
buildBar(
|
|
limit,
|
|
LIMIT_GAUGE_MAX,
|
|
20
|
|
),
|
|
`**${limit}/${LIMIT_GAUGE_MAX}**`,
|
|
"",
|
|
`👹 **Boss Turn:** ${bossGauge}/${BOSS_ACTION_THRESHOLD}`,
|
|
"",
|
|
`**Participants:** ${participantCount}`,
|
|
`⚔️ ${roleCounts.DPS} DPS • 🛡️ ${roleCounts.Tank} Tanks • ❤️ ${roleCounts.Healer} Healers`,
|
|
knockedOutCount >
|
|
0
|
|
? `💀 **Knocked Out:** ${knockedOutCount}`
|
|
: null,
|
|
`**Reward Pool:** ${event.rewardPool.toLocaleString()} 🍪`,
|
|
`**Battle ends:** <t:${event.battleEndsAt}:R>`,
|
|
"",
|
|
"### ⚔️ Battle Actions",
|
|
"**⚔️ Strike**",
|
|
"Low damage • Generates Energy + Limit Gauge • 8s cooldown",
|
|
"",
|
|
"**🛡️ Defend**",
|
|
"Reduces damage from upcoming boss attacks • Base cost: 5 Energy",
|
|
"",
|
|
"**❤️ Heal**",
|
|
"Restores HP to all living party members • Base cost: 10 Energy",
|
|
"",
|
|
"**🌟 Revive**",
|
|
"Assist a KO'd player • DPS/Tank +1 • Healer +2 • 3 required",
|
|
"",
|
|
"**✨ Limit Break**",
|
|
"Massive shared attack • Requires a full 100-point Limit Gauge",
|
|
"",
|
|
"*Your Job modifies how effective your actions are.*"
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbleStrike:${event.eventId}`
|
|
}),
|
|
|
|
components: [
|
|
...buildCombatButtons(),
|
|
...buildLateJoinButtons()
|
|
]
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* DEFEATED
|
|
*/
|
|
|
|
if (
|
|
event.state ===
|
|
"Defeated"
|
|
) {
|
|
const rewards =
|
|
(
|
|
event.rewards ??
|
|
[]
|
|
)
|
|
.slice()
|
|
.sort(
|
|
(
|
|
a,
|
|
b
|
|
) =>
|
|
b.contribution -
|
|
a.contribution
|
|
);
|
|
|
|
|
|
const leaderboard =
|
|
rewards
|
|
.slice(
|
|
0,
|
|
10
|
|
)
|
|
.map(
|
|
(
|
|
player,
|
|
index
|
|
) =>
|
|
[
|
|
`**${index + 1}.** <@${player.userId}> — ${player.job}`,
|
|
`⚔️ ${player.damage.toLocaleString()} • ❤️ ${player.healing.toLocaleString()} • 🛡️ ${player.mitigation.toLocaleString()} • 🌟 ${player.revives}`,
|
|
`🍪 **${player.reward.toLocaleString()}**`
|
|
].join("\n")
|
|
)
|
|
.join("\n\n");
|
|
|
|
|
|
return {
|
|
embed:
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🏆 Crumble Strike Complete!"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
"## The Crumbbound Behemoth Has Fallen!",
|
|
"",
|
|
leaderboard ||
|
|
"The beast was defeated."
|
|
].join("\n")
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbleStrike:${event.eventId}`
|
|
}),
|
|
|
|
components: []
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* FAILED
|
|
*/
|
|
|
|
if (
|
|
event.state ===
|
|
"Failed"
|
|
) {
|
|
return {
|
|
embed:
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"💀 Crumble Strike Failed"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
"The Crumbbound Behemoth survived.",
|
|
"",
|
|
`The committed **${event.rewardPool.toLocaleString()} Cookies** were claimed by the ShaiBot Vault.`
|
|
].join("\n")
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbleStrike:${event.eventId}`
|
|
}),
|
|
|
|
components: []
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* CANCELLED
|
|
*/
|
|
|
|
if (
|
|
event.state ===
|
|
"Cancelled"
|
|
) {
|
|
return {
|
|
embed:
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🍪 Crumble Strike Cancelled"
|
|
)
|
|
|
|
.setDescription(
|
|
"Not enough adventurers joined during recruitment. All committed Cookies were refunded."
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`crumbleStrike:${event.eventId}`
|
|
}),
|
|
|
|
components: []
|
|
};
|
|
}
|
|
|
|
|
|
return {
|
|
embed:
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🍪 Crumble Strike"
|
|
)
|
|
|
|
.setDescription(
|
|
`Encounter state: **${event.state}**`
|
|
),
|
|
|
|
components: []
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* COMBAT BUTTONS
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildCombatButtons() {
|
|
return [
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeStrike"
|
|
)
|
|
.setLabel(
|
|
"Strike"
|
|
)
|
|
.setEmoji(
|
|
"⚔️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Danger
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeDefend"
|
|
)
|
|
.setLabel(
|
|
"Defend"
|
|
)
|
|
.setEmoji(
|
|
"🛡️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Primary
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeHeal"
|
|
)
|
|
.setLabel(
|
|
"Heal"
|
|
)
|
|
.setEmoji(
|
|
"❤️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Success
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeRevive"
|
|
)
|
|
.setLabel(
|
|
"Revive"
|
|
)
|
|
.setEmoji(
|
|
"🌟"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Secondary
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeLimitBreak"
|
|
)
|
|
.setLabel(
|
|
"Limit Break"
|
|
)
|
|
.setEmoji(
|
|
"✨"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Secondary
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* LATE JOIN BUTTONS
|
|
* ============================================================
|
|
*/
|
|
|
|
function buildLateJoinButtons() {
|
|
return [
|
|
new ActionRowBuilder()
|
|
.addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeRoleDPS"
|
|
)
|
|
.setLabel(
|
|
"Join DPS"
|
|
)
|
|
.setEmoji(
|
|
"⚔️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Danger
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeRoleTank"
|
|
)
|
|
.setLabel(
|
|
"Join Tank"
|
|
)
|
|
.setEmoji(
|
|
"🛡️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Primary
|
|
),
|
|
|
|
new ButtonBuilder()
|
|
.setCustomId(
|
|
"crumbleStrikeRoleHealer"
|
|
)
|
|
.setLabel(
|
|
"Join Healer"
|
|
)
|
|
.setEmoji(
|
|
"❤️"
|
|
)
|
|
.setStyle(
|
|
ButtonStyle.Success
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* 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 SELECTION
|
|
* ============================================================
|
|
*/
|
|
|
|
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 SELECTION
|
|
* ============================================================
|
|
*/
|
|
|
|
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
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* EVENT 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"
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* SELECTION FOOTER
|
|
* ============================================================
|
|
*/
|
|
|
|
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 {};
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* JOB HELPERS
|
|
* ============================================================
|
|
*/
|
|
|
|
function getParticipantJob(
|
|
participant
|
|
) {
|
|
if (
|
|
participant.jobKey &&
|
|
JOBS[
|
|
participant.role
|
|
]?.[
|
|
participant.jobKey
|
|
]
|
|
) {
|
|
return JOBS[
|
|
participant.role
|
|
][
|
|
participant.jobKey
|
|
];
|
|
}
|
|
|
|
|
|
return Object.values(
|
|
JOBS[
|
|
participant.role
|
|
]
|
|
??
|
|
{}
|
|
)
|
|
.find(
|
|
job =>
|
|
job.name ===
|
|
participant.job
|
|
)
|
|
??
|
|
null;
|
|
}
|
|
|
|
|
|
function getJobEmoji(
|
|
participant
|
|
) {
|
|
return (
|
|
getParticipantJob(
|
|
participant
|
|
)?.emoji
|
|
??
|
|
getRoleEmoji(
|
|
participant.role
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
function formatJobProgression(
|
|
job
|
|
) {
|
|
if (
|
|
job.class
|
|
) {
|
|
return (
|
|
`${job.class} → ${job.name}`
|
|
);
|
|
}
|
|
|
|
|
|
return job.name;
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* DISPLAY HELPERS
|
|
* ============================================================
|
|
*/
|
|
|
|
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 buildBar(
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ============================================================
|
|
* EXPORTS
|
|
* ============================================================
|
|
*/
|
|
|
|
module.exports = {
|
|
JOBS,
|
|
TIERS,
|
|
|
|
chooseCrumbleStrikeRole,
|
|
chooseCrumbleStrikeJob,
|
|
confirmCrumbleStrikeJoin,
|
|
|
|
startCrumbleStrikeBattle,
|
|
activateCrumbleStrike,
|
|
|
|
performCrumbleStrikeAction,
|
|
resolveCrumbleStrikeRevive,
|
|
|
|
resolveCrumbleStrikeVictory,
|
|
failCrumbleStrike,
|
|
cancelCrumbleStrikeAndRefund,
|
|
|
|
refreshCrumbleStrikeMessage
|
|
}; |