Files
Arthonor-Neo/commands/slash/applications/nuts.js
T

1514 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {
SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
getCollections,
getNutBalance,
changeNuts,
getCooldown,
claimCooldown,
getUnixTime,
randomInteger
} = require("../../../utils/nutEconomy");
/*
* ============================================================
* CONFIG
* ============================================================
*/
const asset_path =
"./assets/Command_Nuts/";
const NUT_COOLDOWN =
60 * 60;
/*
* ============================================================
* DELAY
* ============================================================
*/
function delay(ms) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
/*
* ============================================================
* GIVE
* ============================================================
*/
async function nutsGive(
interaction
) {
const from =
interaction.user;
const to =
interaction.options.getUser(
"target",
true
);
let amount =
interaction.options.getNumber(
"amount",
true
);
/*
* Prevent self transfer.
*/
if (
from.id ===
to.id
) {
return interaction.reply({
content:
"Du kannst dir nicht selbst Nüsse geben!",
flags:
MessageFlags.Ephemeral
});
}
const [
yourBalance,
theirBalance
] =
await Promise.all([
getNutBalance(
from.id
),
getNutBalance(
to.id
)
]);
/*
* Keep the existing behavior:
*
* amount <= 0 means give everything.
*/
if (
amount <=
0
) {
amount =
yourBalance;
return tradeWindow(
interaction,
to,
amount,
yourBalance,
theirBalance,
"nuts_give_fake",
"abort"
);
}
if (
yourBalance <
amount
) {
return interaction.reply({
content:
"Du kannst nicht mehr geben als du hast!",
flags:
MessageFlags.Ephemeral
});
}
return tradeWindow(
interaction,
to,
amount,
yourBalance,
theirBalance,
"nuts_give_confirm",
"abort"
);
}
/*
* ============================================================
* GIVE CONFIRMATION WINDOW
* ============================================================
*/
async function tradeWindow(
interaction,
to,
amount,
yourBalance,
theirBalance,
confirmButtonId,
abortButtonId
) {
const embed =
new EmbedBuilder()
.setTitle(
"Gib Nuss!"
)
.setDescription(
`Willst du wirklich **${amount}** Nüsse an <@${to.id}> senden?`
)
.addFields(
{
name:
"Your Current Balance",
value:
`${yourBalance} (**-${amount}**)`,
inline:
true
},
{
name:
"Your New Balance",
value:
`${yourBalance - amount}`,
inline:
true
},
{
name:
"\u200B",
value:
"\u200B"
},
{
name:
"Their Current Balance",
value:
`${theirBalance} (**+${amount}**)`,
inline:
true
},
{
name:
"Their New Balance",
value:
`${theirBalance + amount}`,
inline:
true
}
)
.setThumbnail(
"attachment://nuts_main.png"
)
/*
* Keep enough ownership/transfer state in
* the footer for a future button cleanup.
*/
.setFooter({
text:
`nutsGive:${interaction.user.id}:${to.id}:${amount}`
});
const row =
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setLabel(
"✔"
)
.setCustomId(
confirmButtonId
)
.setStyle(
ButtonStyle.Success
),
new ButtonBuilder()
.setLabel(
"✖"
)
.setCustomId(
abortButtonId
)
.setStyle(
ButtonStyle.Danger
)
);
return interaction.reply({
embeds: [
embed
],
components: [
row
],
files: [
`${asset_path}nuts_main.png`
],
flags:
MessageFlags.Ephemeral
});
}
/*
* ============================================================
* CHECK
* ============================================================
*/
async function nutsCheck(
interaction
) {
let target =
interaction.options.getUser(
"target"
);
let addressing;
if (!target) {
target =
interaction.user;
addressing =
"Du hast";
}
else if (
!target.globalName
) {
addressing =
`${target.username} hat`;
}
else {
addressing =
`${target.globalName} hat`;
}
const balance =
await getNutBalance(
target.id
);
const content =
balance <=
0
? `${addressing} noch keine Nüsse gesammelt :(`
: `${addressing} bereits **${balance}** Nüsse gesammelt!`;
/*
* Existing pseudo counting animation.
*/
let title =
"Zähle Nüsse";
const embed =
new EmbedBuilder();
for (
let i =
0;
i <
4;
i++
) {
embed.setTitle(
title
);
await interaction.editReply({
embeds: [
embed
]
});
title +=
" .";
await delay(
500
);
}
embed
.setTitle(
content
)
.setThumbnail(
"attachment://nuts_main.png"
);
return interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}nuts_main.png`
]
});
}
/*
* ============================================================
* LEADERBOARD
* ============================================================
*/
async function nutsLeaderboard(
interaction
) {
const row =
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setLabel(
"◄"
)
.setStyle(
ButtonStyle.Primary
)
.setCustomId(
"nuts_leaderboard_left"
),
new ButtonBuilder()
.setLabel(
"🧑"
)
.setStyle(
ButtonStyle.Primary
)
.setCustomId(
"nuts_leaderboard_self"
),
new ButtonBuilder()
.setLabel(
"►"
)
.setStyle(
ButtonStyle.Primary
)
.setCustomId(
"nuts_leaderboard_right"
)
);
const {
nuts
} =
getCollections();
const skip =
0;
const nutsData =
await nuts
.find({
nuts: {
$gt:
0
}
})
.sort({
nuts:
-1
})
.skip(
skip
)
.limit(
5
)
.toArray();
let fields =
"";
let placement =
skip +
1;
for (
const data
of nutsData
) {
fields +=
`${placement}. <@${data.userId}> : ${data.nuts} Nuts\r\n`;
placement++;
}
const embed =
new EmbedBuilder()
.setTitle(
"Nuts Leaderboard"
)
.setThumbnail(
interaction.guild?.iconURL({
dynamic:
true
})
)
.setDescription(
fields ||
"Noch keine Nüsse :("
)
.setColor(
"#5865F2"
)
.setFooter({
text:
"Use ◄ ► to navigate"
});
return interaction.editReply({
embeds: [
embed
],
components: [
row
]
});
}
/*
* ============================================================
* STATS
* ============================================================
*
* stats_nuts remains the only statistical collection
* currently required.
*/
async function nutsStats(
interaction
) {
const {
db
} =
getCollections();
const nutsStats =
db.collection(
"stats_nuts"
);
const type =
interaction.options.getString(
"type"
)
||
"lookup";
let target =
interaction.options.getUser(
"lookup"
)
||
{};
if (
type ===
"global"
) {
target.id =
"__global";
}
else if (
!target?.id
) {
target =
interaction.user;
}
const statsDoc =
await nutsStats.findOne({
userId:
target.id
});
const stat =
statsDoc?.stat
||
{};
const totalCount =
Object.values(
stat
)
.reduce(
(
a,
b
) =>
a +
b,
0
);
const totalNuts =
Object.entries(
stat
)
.reduce(
(
sum,
[
number,
count
]
) =>
sum +
Number(
number
)
*
count,
0
);
const rolledNumbers =
Object.entries(
stat
)
.filter(
(
[
,
count
]
) =>
count >
0
);
let nutMin = {
number:
0,
count:
0
};
let nutMax = {
number:
0,
count:
0
};
if (
rolledNumbers.length >
0
) {
nutMin = {
number:
Number(
rolledNumbers[0][0]
),
count:
rolledNumbers[0][1]
};
nutMax = {
...nutMin
};
for (
const [
numStr,
count
]
of rolledNumbers
) {
const number =
Number(
numStr
);
if (
count >
nutMax.count
||
(
count ===
nutMax.count
&&
number >
nutMax.number
)
) {
nutMax = {
number,
count
};
}
if (
count <
nutMin.count
||
(
count ===
nutMin.count
&&
number <
nutMin.number
)
) {
nutMin = {
number,
count
};
}
}
}
const nutAvg =
totalCount
? totalNuts /
totalCount
: 0;
const fields =
Array.from(
{
length:
11
},
(
_,
i
) => ({
name:
`[${i}]`,
value:
`x${stat[i] ?? 0}`,
inline:
true
})
);
const embed =
new EmbedBuilder()
.setTitle(
"Nut Statistic"
)
.setDescription(
`Total Nut Actions: **${totalCount}**\r\n` +
`Total Nuts nutted: **${totalNuts}**\r\n` +
`Nut Average: **${nutAvg.toFixed(3)}**\r\n` +
`Most Common Nut: **${nutMax.number} (x${nutMax.count})**\r\n` +
`Least Common Nut: **${nutMin.number} (x${nutMin.count})**`
)
.addFields(
fields
)
.setColor(
0x51267
)
.setTimestamp()
.setThumbnail(
"attachment://nuts_main.png"
);
return interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}nuts_main.png`
]
});
}
/*
* ============================================================
* COOLDOWN
* ============================================================
*/
async function nutsCooldown(
interaction
) {
const cooldown =
await getCooldown(
interaction.user.id,
"nut"
);
const now =
getUnixTime();
let content =
"Du kannst wieder nussen! :)";
let thumbnail =
"attachment://nuts_main.png";
let file =
`${asset_path}nuts_main.png`;
let title =
"Go Nuts!";
if (
cooldown >
now
) {
content =
`<t:${cooldown}:R> kannst du wieder nussen! ;)`;
thumbnail =
"attachment://nuts_onCD.avif";
file =
`${asset_path}nuts_onCD.avif`;
title =
"To Nut or Not to Nut...";
}
const embed =
new EmbedBuilder()
.setThumbnail(
thumbnail
)
.setTitle(
title
)
.setDescription(
content
);
return interaction.editReply({
embeds: [
embed
],
files: [
file
]
});
}
/*
* ============================================================
* NUT
* ============================================================
*/
async function nutsNut(
interaction,
quickMode
) {
const {
db
} =
getCollections();
const nutsStats =
db.collection(
"stats_nuts"
);
const embed =
new EmbedBuilder();
const assets = {
high:
"attachment://nuts_high.jpg",
normal:
"attachment://nuts_normal.jpg",
low:
"attachment://nuts_low.jpg",
none:
"attachment://nuts_none.avif",
onCD:
"attachment://nuts_onCD.avif",
box:
"attachment://nuts_box.png",
explosion:
"attachment://nuts_explosion.gif"
};
const formatNut =
amount =>
`${amount === 1 ? "eine" : amount} ${amount === 1 ? "Nuss" : "Nüsse"}`;
/*
* ========================================================
* ATOMIC COOLDOWN CLAIM
* ========================================================
*
* This is better than:
*
* read cooldown
* set cooldown
*
* because rapid simultaneous requests cannot both
* successfully claim the action.
*/
const cooldown =
await claimCooldown(
interaction.user.id,
"nut",
NUT_COOLDOWN
);
if (
!cooldown.claimed
) {
embed
.setTitle(
"To Nut or Not to Nut..."
)
.setDescription(
`<t:${cooldown.next}:R> kannst du wieder nussen! ;)`
)
.setThumbnail(
assets.onCD
);
return interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}nuts_onCD.avif`
]
});
}
/*
* Preserve the current 0–9 roll behavior.
*/
const amount =
randomInteger(
0,
9
);
/*
* Balance.
*/
await changeNuts(
interaction.user.id,
amount
);
/*
* Individual Nut statistics.
*/
await nutsStats.updateOne(
{
userId:
interaction.user.id
},
{
$inc: {
[`stat.${amount}`]:
1
},
$setOnInsert: {
userId:
interaction.user.id
}
},
{
upsert:
true
}
);
/*
* Global Nut statistics.
*/
await nutsStats.updateOne(
{
userId:
"__global"
},
{
$inc: {
[`stat.${amount}`]:
1
},
$setOnInsert: {
userId:
"__global"
}
},
{
upsert:
true
}
);
let nutText =
`Du hast ${formatNut(amount)} bekommen! :chestnut:`;
if (
amount <=
0
) {
nutText =
"Du hast keine Nuss bekommen! :yoink_help:";
}
/*
* Quick mode preserves the existing plain text response.
*/
if (quickMode) {
return interaction.editReply({
content:
nutText
});
}
/*
* ========================================================
* PSEUDO ANIMATION
* ========================================================
*/
embed.setImage(
assets.box
);
await interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}nuts_box.png`
]
});
await delay(
1337
);
embed.setImage(
assets.explosion
);
await interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}nuts_explosion.gif`
]
});
await delay(
1337
);
/*
* Result asset.
*/
let assetFile;
if (
amount ===
0
) {
assetFile =
assets.none;
}
else if (
amount >
8
) {
assetFile =
assets.high;
}
else if (
amount >
4
) {
assetFile =
assets.normal;
}
else {
assetFile =
assets.low;
}
embed
.setTitle(
nutText
)
.setThumbnail(
assetFile
)
.setImage(
null
);
return interaction.editReply({
embeds: [
embed
],
files: [
`${asset_path}${assetFile.split("://")[1]}`
]
});
}
/*
* ============================================================
* COMMAND
* ============================================================
*/
module.exports = {
data:
new SlashCommandBuilder()
.setName(
"nuts"
)
.setDescription(
"rund um die nuss"
)
/*
* GIVE
*/
.addSubcommand(s =>
s
.setName(
"give"
)
.setDescription(
"gib nuss!!"
)
.addUserOption(o =>
o
.setName(
"target"
)
.setDescription(
"wer bekommt da nuts?"
)
.setRequired(
true
)
)
.addNumberOption(o =>
o
.setName(
"amount"
)
.setDescription(
"wie viele?"
)
.setRequired(
true
)
)
)
/*
* CHECK
*/
.addSubcommand(s =>
s
.setName(
"check"
)
.setDescription(
"check out deez nuts"
)
.addUserOption(o =>
o
.setName(
"target"
)
.setDescription(
"check out those nuts!"
)
)
)
/*
* LEADERBOARD
*/
.addSubcommand(s =>
s
.setName(
"leaderboard"
)
.setDescription(
"wer hat die dicksten Nüsse?"
)
)
/*
* STATS
*/
.addSubcommand(s =>
s
.setName(
"stats"
)
.setDescription(
"wie viele Nüsse wurden genusst, Genosse?"
)
.addStringOption(o =>
o
.setName(
"type"
)
.setDescription(
"set type to global or user"
)
.addChoices(
{
name:
"global",
value:
"global"
},
{
name:
"user",
value:
"user"
}
)
)
.addUserOption(o =>
o
.setName(
"lookup"
)
.setDescription(
"look up specific user nut statistic"
)
)
)
/*
* COOLDOWN
*/
.addSubcommand(s =>
s
.setName(
"cooldown"
)
.setDescription(
"wie lange bis ich nussen kann?"
)
)
/*
* NUT
*/
.addSubcommand(s =>
s
.setName(
"nut"
)
.setDescription(
"willst du nuss?"
)
)
/*
* QUICK
*/
.addSubcommand(s =>
s
.setName(
"quick"
)
.setDescription(
"A quick nut, mlord?"
)
),
async execute(
interaction
) {
/*
* Public Discord.js API.
*
* Avoid interaction.options._subcommand.
*/
const subcommand =
interaction.options.getSubcommand();
switch (
subcommand
) {
case "give":
return nutsGive(
interaction
);
case "check":
await interaction.deferReply();
return nutsCheck(
interaction
);
case "leaderboard":
await interaction.deferReply();
return nutsLeaderboard(
interaction
);
case "stats":
await interaction.deferReply();
return nutsStats(
interaction
);
case "cooldown":
await interaction.deferReply();
return nutsCooldown(
interaction
);
case "nut":
await interaction.deferReply();
return nutsNut(
interaction,
false
);
case "quick":
await interaction.deferReply();
return nutsNut(
interaction,
true
);
}
}
};