Compare commits
55 commits
main
...
feature/38
Author | SHA1 | Date | |
---|---|---|---|
ff9437ba81 | |||
9a2835a0eb | |||
dd1f259170 | |||
b37c087393 | |||
d874cb7a12 | |||
57c3d603a9 | |||
f8b013a091 | |||
ed52f3e3dc | |||
1cace42983 | |||
5db7cd9f11 | |||
2b96c7c0d3 | |||
3d143e7c73 | |||
d7a5472759 | |||
6c17a67d7a | |||
e67efd4197 | |||
9e963d90cb | |||
27c0e68f7a | |||
4c322f01de | |||
3304779297 | |||
73caf9315d | |||
76af70da06 | |||
8352b377bb | |||
f4c02d3613 | |||
480e496984 | |||
a8a5e39e01 | |||
816e550c84 | |||
cd7e0945a9 | |||
768f64b5ee | |||
1762b525b2 | |||
5ebc5ff27c | |||
2263871b3b | |||
c0e9378813 | |||
52c93c7803 | |||
8683c1e58a | |||
ea0ca17044 | |||
21d11afd31 | |||
a7b03d0355 | |||
761c58fb10 | |||
711d36698b | |||
57e06be9af | |||
d605eb5d59 | |||
66243e6742 | |||
79a4d18df3 | |||
8e4597512f | |||
805dd00357 | |||
5defb682c1 | |||
8bd5f44524 | |||
9302902b17 | |||
480786a1e9 | |||
981cdbfdd7 | |||
7a99d273f6 | |||
ec0292d658 | |||
89c6dc527a | |||
73776408b5 | |||
bf9b748f4f |
55 changed files with 3670 additions and 2019 deletions
|
@ -32,9 +32,8 @@ DB_AUTH_PASS=
|
|||
DB_SYNC=
|
||||
DB_LOGGING=
|
||||
DB_DATA_LOCATION=./.temp/database
|
||||
|
||||
DB_CARD_FILE=:memory:
|
||||
DB_ROOT_HOST=0.0.0.0
|
||||
|
||||
EXPRESS_PORT=3302
|
||||
|
||||
GDRIVESYNC_AUTO=true
|
||||
GDRIVESYNC_AUTO=false
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"globals": {
|
||||
"jest": true,
|
||||
"require": true,
|
||||
"exports": true,
|
||||
"process": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
needs: build
|
||||
runs-on: node
|
||||
steps:
|
||||
- uses: https://github.com/appleboy/ssh-action@v1.0.3
|
||||
- uses: https://github.com/appleboy/ssh-action@v1.1.0
|
||||
env:
|
||||
DB_NAME: ${{ secrets.PROD_DB_NAME }}
|
||||
DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }}
|
||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
needs: build
|
||||
runs-on: node
|
||||
steps:
|
||||
- uses: https://github.com/appleboy/ssh-action@v1.0.3
|
||||
- uses: https://github.com/appleboy/ssh-action@v1.1.0
|
||||
env:
|
||||
DB_NAME: ${{ secrets.STAGE_DB_NAME }}
|
||||
DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }}
|
||||
|
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE `user_effect`;
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE `user_effect` (
|
||||
`Id` varchar(255) NOT NULL,
|
||||
`WhenCreated` datetime NOT NULL,
|
||||
`WhenUpdated` datetime NOT NULL,
|
||||
`Name` varchar(255) NOT NULL,
|
||||
`UserId` varchar(255) NOT NULL,
|
||||
`Unused` int NOT NULL DEFAULT 0,
|
||||
`WhenExpires` datetime NULL,
|
||||
PRIMARY KEY (`Id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
35
docs/google-drive-sync.md
Normal file
35
docs/google-drive-sync.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Google Drive Sync
|
||||
|
||||
The bot relies on an external sync between the local file system and Google
|
||||
Drive in order to get newer cards to the bot. This is done using
|
||||
[Rclone](https://rclone.org/).
|
||||
|
||||
The process for this is done by once the `/gdrivesync` command is executed by
|
||||
an admin user of the bot, which calls the system shell to run rclone to the
|
||||
card folder.
|
||||
|
||||
- The admins who can run the command is specifed in `$BOT_ADMINS`, which are
|
||||
discord user ids separated by commas.
|
||||
- The card folder is located at `$DATA_DIR/cards`.
|
||||
- The source requires rclone's remote to be setup as `card-drop-gdrive`.
|
||||
|
||||
The exact command it runs is: `rclone sync card-drop-gdrive: $DATA_DIR/cards`.
|
||||
|
||||
Once it syncs the database will reread all the cards for updates and then load
|
||||
them into the bot to be given.
|
||||
|
||||
## Safe Mode
|
||||
Safe mode is a function of the bot which disables the `/drop` command function
|
||||
and any other functions which rely on the card metadata. Safe mode is activated
|
||||
upon failure to sync properly. It is disabled once errors are resolved.
|
||||
|
||||
The reason for safe mode is to ensure that the bot stays online for admins to
|
||||
be able to resync the bot in case there's an error without it crashing.
|
||||
|
||||
## Google Drive
|
||||
Please see the Rclone documentation on how to setup a remote using Google
|
||||
Drive. You will need to make an app password for this.
|
||||
|
||||
- scope: `drive.readonly`
|
||||
- root\_folder\_id: The folder id where the cards are located, this can be found
|
||||
by looking at the url when viewing the folder in the browser in google drive.
|
55
eslint.config.mjs
Normal file
55
eslint.config.mjs
Normal file
|
@ -0,0 +1,55 @@
|
|||
import js from "@eslint/js";
|
||||
import ts from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/dist/",
|
||||
"eslint.config.mjs",
|
||||
"jest.config.cjs",
|
||||
"jest.setup.js",
|
||||
"**/.temp/**/*"
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
exports: "writable",
|
||||
module: "writable",
|
||||
require: "writable",
|
||||
process: "writable",
|
||||
console: "writable",
|
||||
jest: "writable",
|
||||
},
|
||||
|
||||
ecmaVersion: 6,
|
||||
sourceType: "script",
|
||||
},
|
||||
|
||||
files: [
|
||||
"./src",
|
||||
"./tests"
|
||||
],
|
||||
|
||||
rules: {
|
||||
camelcase: "error",
|
||||
"brace-style": ["error", "1tbs"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
|
||||
"comma-spacing": ["error", {
|
||||
before: false,
|
||||
after: true,
|
||||
}],
|
||||
|
||||
"comma-style": ["error", "last"],
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"arrow-spacing": "error",
|
||||
"no-var": "error",
|
||||
"prefer-template": "error",
|
||||
"prefer-const": "error",
|
||||
},
|
||||
}
|
||||
];
|
|
@ -1,3 +1,4 @@
|
|||
jest.setTimeout(1 * 1000); // 1 second
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
jest.resetAllMocks();
|
||||
jest.useFakeTimers();
|
35
package.json
35
package.json
|
@ -7,7 +7,7 @@
|
|||
"clean": "rm -rf node_modules/ dist/",
|
||||
"build": "tsc",
|
||||
"start": "node ./dist/bot.js",
|
||||
"test": "echo true",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
|
||||
|
@ -27,37 +27,36 @@
|
|||
"dependencies": {
|
||||
"@discordjs/rest": "^2.0.0",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"canvas": "^2.11.2",
|
||||
"clone-deep": "^4.0.1",
|
||||
"cron": "^3.1.7",
|
||||
"discord.js": "^14.15.3",
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^10.3.10",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-mock-extended": "^3.0.0",
|
||||
"jimp": "^0.22.12",
|
||||
"minimatch": "9.0.5",
|
||||
"jimp": "^1.6.0",
|
||||
"mysql": "^2.18.1",
|
||||
"ts-jest": "^29.0.0",
|
||||
"typeorm": "0.3.20",
|
||||
"winston": "^3.11.0",
|
||||
"winston": "^3.15.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"winston-discord-transport": "^1.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/ws": "^8.17.1"
|
||||
},
|
||||
"resolutions": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"eslint": "^8.56.0",
|
||||
"np": "^9.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"@types/node": "^22.8.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
||||
"@typescript-eslint/parser": "^8.11.0",
|
||||
"eslint": "^9.13.0",
|
||||
"np": "^10.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ const client = new CoreClient([
|
|||
|
||||
Registry.RegisterCommands();
|
||||
Registry.RegisterButtonEvents();
|
||||
Registry.RegisterStringDropdownEvents();
|
||||
|
||||
if (!existsSync(`${process.env.DATA_DIR}/cards`) && process.env.GDRIVESYNC_AUTO && process.env.GDRIVESYNC_AUTO == "true") {
|
||||
console.log("Card directory not found, syncing...");
|
||||
|
|
|
@ -12,6 +12,7 @@ export default class Claim extends ButtonEvent {
|
|||
public override async execute(interaction: ButtonInteraction) {
|
||||
if (!interaction.guild || !interaction.guildId) return;
|
||||
if (!interaction.channel) return;
|
||||
if (!interaction.channel.isSendable()) return;
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
|
@ -58,7 +59,7 @@ export default class Claim extends ButtonEvent {
|
|||
if (!inventory) {
|
||||
inventory = new Inventory(userId, cardNumber, 1);
|
||||
} else {
|
||||
inventory.SetQuantity(inventory.Quantity + 1);
|
||||
inventory.AddQuantity(1);
|
||||
}
|
||||
|
||||
await inventory.Save(Inventory, inventory);
|
||||
|
|
158
src/buttonEvents/Effects.ts
Normal file
158
src/buttonEvents/Effects.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction,ButtonStyle,Embed,EmbedBuilder} from "discord.js";
|
||||
import {ButtonEvent} from "../type/buttonEvent";
|
||||
import EffectHelper from "../helpers/EffectHelper";
|
||||
import { EffectDetails } from "../constants/EffectDetails";
|
||||
import TimeLengthInput from "../helpers/TimeLengthInput";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
|
||||
export default class Effects extends ButtonEvent {
|
||||
public override async execute(interaction: ButtonInteraction) {
|
||||
const action = interaction.customId.split(" ")[1];
|
||||
|
||||
switch (action) {
|
||||
case "list":
|
||||
await this.List(interaction);
|
||||
break;
|
||||
case "use":
|
||||
await this.Use(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async List(interaction: ButtonInteraction) {
|
||||
const pageOption = interaction.customId.split(" ")[2];
|
||||
|
||||
const page = Number(pageOption);
|
||||
|
||||
if (!page) {
|
||||
await interaction.reply("Page option is not a valid number");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
|
||||
|
||||
await interaction.update({
|
||||
embeds: [ result.embed ],
|
||||
components: [ result.row ],
|
||||
});
|
||||
}
|
||||
|
||||
private async Use(interaction: ButtonInteraction) {
|
||||
const subaction = interaction.customId.split(" ")[2];
|
||||
|
||||
switch (subaction) {
|
||||
case "confirm":
|
||||
await this.UseConfirm(interaction);
|
||||
break;
|
||||
case "cancel":
|
||||
await this.UseCancel(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async UseConfirm(interaction: ButtonInteraction) {
|
||||
const id = interaction.customId.split(" ")[3];
|
||||
|
||||
const effectDetail = EffectDetails.get(id);
|
||||
|
||||
if (!effectDetail) {
|
||||
await interaction.reply("Unable to find effect!");
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const whenExpires = new Date(now.getMilliseconds() + effectDetail.duration);
|
||||
|
||||
const result = await EffectHelper.UseEffect(interaction.user.id, id, whenExpires);
|
||||
|
||||
if (result) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Effect Used")
|
||||
.setDescription("You now have an active effect!")
|
||||
.setColor(EmbedColours.Green)
|
||||
.addFields([
|
||||
{
|
||||
name: "Effect",
|
||||
value: effectDetail.friendlyName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Expires",
|
||||
value: `<t:${whenExpires.getMilliseconds()}:f>`,
|
||||
inline: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setLabel("Confirm")
|
||||
.setCustomId(`effects use confirm ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true),
|
||||
new ButtonBuilder()
|
||||
.setLabel("Cancel")
|
||||
.setCustomId(`effects use cancel ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setDisabled(true),
|
||||
]);
|
||||
|
||||
await interaction.update({
|
||||
embeds: [ embed ],
|
||||
components: [ row ],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
|
||||
}
|
||||
|
||||
private async UseCancel(interaction: ButtonInteraction) {
|
||||
const id = interaction.customId.split(" ")[3];
|
||||
|
||||
const effectDetail = EffectDetails.get(id);
|
||||
|
||||
if (!effectDetail) {
|
||||
await interaction.reply("Unable to find effect!");
|
||||
return;
|
||||
}
|
||||
|
||||
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Effect Use Cancelled")
|
||||
.setDescription("The effect from your inventory has not been used")
|
||||
.setColor(EmbedColours.Grey)
|
||||
.addFields([
|
||||
{
|
||||
name: "Effect",
|
||||
value: effectDetail.friendlyName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Expires",
|
||||
value: timeLengthInput.GetLengthShort(),
|
||||
inline: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setLabel("Confirm")
|
||||
.setCustomId(`effects use confirm ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true),
|
||||
new ButtonBuilder()
|
||||
.setLabel("Cancel")
|
||||
.setCustomId(`effects use cancel ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setDisabled(true),
|
||||
]);
|
||||
|
||||
await interaction.update({
|
||||
embeds: [ embed ],
|
||||
components: [ row ],
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ export default class Inventory extends ButtonEvent {
|
|||
const page = interaction.customId.split(" ")[2];
|
||||
|
||||
AppLogger.LogSilly("Button/Inventory", `Parameters: userid=${userid}, page=${page}`);
|
||||
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const member = interaction.guild.members.cache.find(x => x.id == userid) || await interaction.guild.members.fetch(userid);
|
||||
|
@ -34,7 +34,7 @@ export default class Inventory extends ButtonEvent {
|
|||
await interaction.editReply({
|
||||
files: [ embed.image ],
|
||||
embeds: [ embed.embed ],
|
||||
components: [ embed.row ],
|
||||
components: [ embed.row1, embed.row2 ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Button/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);
|
||||
|
|
213
src/buttonEvents/Multidrop.ts
Normal file
213
src/buttonEvents/Multidrop.ts
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { AttachmentBuilder, ButtonInteraction, EmbedBuilder } from "discord.js";
|
||||
import { ButtonEvent } from "../type/buttonEvent";
|
||||
import AppLogger from "../client/appLogger";
|
||||
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import ErrorMessages from "../constants/ErrorMessages";
|
||||
import User from "../database/entities/app/User";
|
||||
import { GetSacrificeAmount } from "../constants/CardRarity";
|
||||
|
||||
export default class Multidrop extends ButtonEvent {
|
||||
public override async execute(interaction: ButtonInteraction) {
|
||||
const action = interaction.customId.split(" ")[1];
|
||||
|
||||
switch (action) {
|
||||
case "keep":
|
||||
await this.Keep(interaction);
|
||||
break;
|
||||
case "sacrifice":
|
||||
await this.Sacrifice(interaction);
|
||||
break;
|
||||
default:
|
||||
await interaction.reply("Invalid action");
|
||||
AppLogger.LogError("Button/Multidrop", `Invalid action, ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async Keep(interaction: ButtonInteraction) {
|
||||
const cardNumber = interaction.customId.split(" ")[2];
|
||||
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
|
||||
const userId = interaction.customId.split(" ")[4];
|
||||
|
||||
if (interaction.user.id != userId) {
|
||||
await interaction.reply("You're not the user this drop was made for!");
|
||||
return;
|
||||
}
|
||||
|
||||
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
|
||||
|
||||
if (!card) {
|
||||
await interaction.reply("Unable to find card.");
|
||||
AppLogger.LogWarn("Button/Multidrop/Keep", `Card not found, ${cardNumber}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardsRemaining < 0) {
|
||||
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await User.FetchOneById(User, interaction.user.id);
|
||||
|
||||
if (!user) {
|
||||
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchUser);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchUser);
|
||||
return;
|
||||
}
|
||||
|
||||
// Claim
|
||||
let inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardNumber);
|
||||
|
||||
if (!inventory) {
|
||||
inventory = new Inventory(interaction.user.id, cardNumber, 1);
|
||||
} else {
|
||||
inventory.AddQuantity(1);
|
||||
}
|
||||
|
||||
await inventory.Save(Inventory, inventory);
|
||||
|
||||
// Pack has ran out
|
||||
if (cardsRemaining == 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription("Your multidrop has ran out! Please buy a new one!")
|
||||
.setColor(EmbedColours.Ok);
|
||||
|
||||
await interaction.update({
|
||||
embeds: [ embed ],
|
||||
attachments: [],
|
||||
components: [],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop next card
|
||||
const randomCard = CardDropHelperMetadata.GetRandomCard();
|
||||
cardsRemaining -= 1;
|
||||
|
||||
if (!randomCard) {
|
||||
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchCard);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchCard);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
try {
|
||||
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
|
||||
const imageFileName = randomCard.card.path.split("/").pop()!;
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
|
||||
const quantityClaimed = inventory ? inventory.Quantity : 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
|
||||
|
||||
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ embed ],
|
||||
files: [ attachment ],
|
||||
components: [ row ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Button/Multidrop/Keep", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
|
||||
|
||||
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
private async Sacrifice(interaction: ButtonInteraction) {
|
||||
const cardNumber = interaction.customId.split(" ")[2];
|
||||
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
|
||||
const userId = interaction.customId.split(" ")[4];
|
||||
|
||||
if (interaction.user.id != userId) {
|
||||
await interaction.reply("You're not the user this drop was made for!");
|
||||
return;
|
||||
}
|
||||
|
||||
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
|
||||
|
||||
if (!card) {
|
||||
await interaction.reply("Unable to find card.");
|
||||
AppLogger.LogWarn("Button/Multidrop/Sacrifice", `Card not found, ${cardNumber}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardsRemaining < 0) {
|
||||
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await User.FetchOneById(User, interaction.user.id);
|
||||
|
||||
if (!user) {
|
||||
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchUser);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchUser);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sacrifice
|
||||
const sacrificeAmount = GetSacrificeAmount(card.card.type);
|
||||
|
||||
user.AddCurrency(sacrificeAmount);
|
||||
|
||||
await user.Save(User, user);
|
||||
|
||||
// Pack has ran out
|
||||
if (cardsRemaining == 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription("Your multidrop has ran out! Please buy a new one!")
|
||||
.setColor(EmbedColours.Ok);
|
||||
|
||||
await interaction.update({
|
||||
embeds: [ embed ],
|
||||
attachments: [],
|
||||
components: [],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop next card
|
||||
const randomCard = CardDropHelperMetadata.GetRandomCard();
|
||||
cardsRemaining -= 1;
|
||||
|
||||
if (!randomCard) {
|
||||
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchCard);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchCard);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
try {
|
||||
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
|
||||
const imageFileName = randomCard.card.path.split("/").pop()!;
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
|
||||
const quantityClaimed = inventory ? inventory.Quantity : 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
|
||||
|
||||
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ embed ],
|
||||
files: [ attachment ],
|
||||
components: [ row ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Button/Multidrop/Sacrifice", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
|
||||
|
||||
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ export default class Sacrifice extends ButtonEvent {
|
|||
private async confirm(interaction: ButtonInteraction) {
|
||||
const userId = interaction.customId.split(" ")[2];
|
||||
const cardNumber = interaction.customId.split(" ")[3];
|
||||
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
|
||||
|
||||
if (userId != interaction.user.id) {
|
||||
await interaction.reply("Only the user who created this sacrifice can confirm it.");
|
||||
|
@ -31,11 +32,16 @@ export default class Sacrifice extends ButtonEvent {
|
|||
|
||||
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
|
||||
|
||||
if (!cardInInventory) {
|
||||
if (!cardInInventory || cardInInventory.Quantity == 0) {
|
||||
await interaction.reply("Unable to find card in inventory.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardInInventory.Quantity < quantity) {
|
||||
await interaction.reply("You can only sacrifice what you own.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
|
||||
|
||||
if (!cardData) {
|
||||
|
@ -50,11 +56,11 @@ export default class Sacrifice extends ButtonEvent {
|
|||
return;
|
||||
}
|
||||
|
||||
cardInInventory.RemoveQuantity(1);
|
||||
cardInInventory.RemoveQuantity(quantity);
|
||||
|
||||
await cardInInventory.Save(Inventory, cardInInventory);
|
||||
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type);
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
|
||||
const cardRarityString = CardRarityToString(cardData.card.type);
|
||||
|
||||
user.AddCurrency(cardValue);
|
||||
|
@ -66,6 +72,7 @@ export default class Sacrifice extends ButtonEvent {
|
|||
`Series: ${cardData.series.name}`,
|
||||
`Rarity: ${cardRarityString}`,
|
||||
`Quantity Owned: ${cardInInventory.Quantity}`,
|
||||
`Quantity To Sacrifice: ${quantity}`,
|
||||
`Sacrifice Amount: ${cardValue}`,
|
||||
];
|
||||
|
||||
|
@ -98,6 +105,7 @@ export default class Sacrifice extends ButtonEvent {
|
|||
private async cancel(interaction: ButtonInteraction) {
|
||||
const userId = interaction.customId.split(" ")[2];
|
||||
const cardNumber = interaction.customId.split(" ")[3];
|
||||
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
|
||||
|
||||
if (userId != interaction.user.id) {
|
||||
await interaction.reply("Only the user who created this sacrifice can cancel it.");
|
||||
|
@ -106,11 +114,16 @@ export default class Sacrifice extends ButtonEvent {
|
|||
|
||||
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
|
||||
|
||||
if (!cardInInventory) {
|
||||
if (!cardInInventory || cardInInventory.Quantity == 0) {
|
||||
await interaction.reply("Unable to find card in inventory.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardInInventory.Quantity < quantity) {
|
||||
await interaction.reply("You can only sacrifice what you own.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
|
||||
|
||||
if (!cardData) {
|
||||
|
@ -118,7 +131,7 @@ export default class Sacrifice extends ButtonEvent {
|
|||
return;
|
||||
}
|
||||
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type);
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
|
||||
const cardRarityString = CardRarityToString(cardData.card.type);
|
||||
|
||||
const description = [
|
||||
|
@ -126,6 +139,7 @@ export default class Sacrifice extends ButtonEvent {
|
|||
`Series: ${cardData.series.name}`,
|
||||
`Rarity: ${cardRarityString}`,
|
||||
`Quantity Owned: ${cardInInventory.Quantity}`,
|
||||
`Quantity To Sacrifice: ${quantity}`,
|
||||
`Sacrifice Amount: ${cardValue}`,
|
||||
];
|
||||
|
||||
|
|
|
@ -28,8 +28,10 @@ export default class Trade extends ButtonEvent {
|
|||
const user2CardNumber = interaction.customId.split(" ")[5];
|
||||
const expiry = interaction.customId.split(" ")[6];
|
||||
const timeoutId = interaction.customId.split(" ")[7];
|
||||
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
|
||||
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
|
||||
|
||||
AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId}`);
|
||||
AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId} user1Quantity=${user1Quantity} user2Quantity=${user2Quantity}`);
|
||||
|
||||
const expiryDate = new Date(expiry);
|
||||
|
||||
|
@ -67,13 +69,13 @@ export default class Trade extends ButtonEvent {
|
|||
return;
|
||||
}
|
||||
|
||||
if (user1UserInventory1.Quantity < 1 || user2UserInventory1.Quantity < 1) {
|
||||
if (user1UserInventory1.Quantity < user1Quantity || user2UserInventory1.Quantity < user2Quantity) {
|
||||
await interaction.reply("One or more of the items you are trying to trade does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
user1UserInventory1.RemoveQuantity(1);
|
||||
user2UserInventory1.RemoveQuantity(1);
|
||||
user1UserInventory1.RemoveQuantity(user1Quantity);
|
||||
user2UserInventory1.RemoveQuantity(user2Quantity);
|
||||
|
||||
await user1UserInventory1.Save(Inventory, user1UserInventory1);
|
||||
await user2UserInventory1.Save(Inventory, user2UserInventory1);
|
||||
|
@ -82,15 +84,15 @@ export default class Trade extends ButtonEvent {
|
|||
let user2UserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(user2UserId, user1CardNumber);
|
||||
|
||||
if (!user1UserInventory2) {
|
||||
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, 1);
|
||||
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, user2Quantity);
|
||||
} else {
|
||||
user1UserInventory2.AddQuantity(1);
|
||||
user1UserInventory2.AddQuantity(user2Quantity);
|
||||
}
|
||||
|
||||
if (!user2UserInventory2) {
|
||||
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, 1);
|
||||
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, user1Quantity);
|
||||
} else {
|
||||
user2UserInventory2.AddQuantity(1);
|
||||
user2UserInventory2.AddQuantity(user1Quantity);
|
||||
}
|
||||
|
||||
await user1UserInventory2.Save(Inventory, user1UserInventory2);
|
||||
|
@ -106,12 +108,12 @@ export default class Trade extends ButtonEvent {
|
|||
.addFields([
|
||||
{
|
||||
name: `${user1User.username} Receives`,
|
||||
value: `${user2Item.id}: ${user2Item.name}`,
|
||||
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${user2User.username} Receives`,
|
||||
value: `${user1Item.id}: ${user1Item.name}`,
|
||||
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
|
@ -144,6 +146,8 @@ export default class Trade extends ButtonEvent {
|
|||
const user2CardNumber = interaction.customId.split(" ")[5];
|
||||
// No need to get expiry date
|
||||
const timeoutId = interaction.customId.split(" ")[7];
|
||||
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
|
||||
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
|
||||
|
||||
AppLogger.LogSilly("Button/Trade/DeclineTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, timeoutId=${timeoutId}`);
|
||||
|
||||
|
@ -178,12 +182,12 @@ export default class Trade extends ButtonEvent {
|
|||
.addFields([
|
||||
{
|
||||
name: `${user1User.username} Receives`,
|
||||
value: `${user2Item.id}: ${user2Item.name}`,
|
||||
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${user2User.username} Receives`,
|
||||
value: `${user1Item.id}: ${user1Item.name}`,
|
||||
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
|
|
25
src/buttonEvents/View.ts
Normal file
25
src/buttonEvents/View.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import {ButtonInteraction} from "discord.js";
|
||||
import {ButtonEvent} from "../type/buttonEvent.js";
|
||||
import CardSearchHelper from "../helpers/CardSearchHelper.js";
|
||||
|
||||
export default class View extends ButtonEvent {
|
||||
public override async execute(interaction: ButtonInteraction) {
|
||||
const page = interaction.customId.split(" ")[1];
|
||||
const results = interaction.customId.split(" ").splice(2);
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const searchResult = await CardSearchHelper.GenerateSearchPageFromQuery(results, interaction.user.id, Number(page));
|
||||
|
||||
if (!searchResult) {
|
||||
await interaction.followUp("No results found");
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ searchResult.embed ],
|
||||
components: [ searchResult.row ],
|
||||
files: [ searchResult.attachment ],
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,11 +17,14 @@ import AppLogger from "./appLogger";
|
|||
import TimerHelper from "../helpers/TimerHelper";
|
||||
import GiveCurrency from "../timers/GiveCurrency";
|
||||
import PurgeClaims from "../timers/PurgeClaims";
|
||||
import StringDropdownEventItem from "../contracts/StringDropdownEventItem";
|
||||
import {StringDropdownEvent} from "../type/stringDropdownEvent";
|
||||
|
||||
export class CoreClient extends Client {
|
||||
private static _commandItems: ICommandItem[];
|
||||
private static _eventExecutors: EventExecutors;
|
||||
private static _buttonEvents: IButtonEventItem[];
|
||||
private static _stringDropdowns: StringDropdownEventItem[];
|
||||
|
||||
private _events: Events;
|
||||
private _util: Util;
|
||||
|
@ -45,6 +48,10 @@ export class CoreClient extends Client {
|
|||
return this._buttonEvents;
|
||||
}
|
||||
|
||||
public static get stringDropdowns(): StringDropdownEventItem[] {
|
||||
return this._stringDropdowns;
|
||||
}
|
||||
|
||||
constructor(intents: number[]) {
|
||||
super({ intents: intents });
|
||||
dotenv.config();
|
||||
|
@ -59,6 +66,7 @@ export class CoreClient extends Client {
|
|||
|
||||
CoreClient._commandItems = [];
|
||||
CoreClient._buttonEvents = [];
|
||||
CoreClient._stringDropdowns = [];
|
||||
|
||||
this._events = new Events();
|
||||
this._util = new Util();
|
||||
|
@ -408,4 +416,19 @@ export class CoreClient extends Client {
|
|||
AppLogger.LogVerbose("Client", `Registered Button Event: ${buttonId}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static RegisterStringDropdownEvent(dropdownId: string, event: StringDropdownEvent, environment: Environment = Environment.All) {
|
||||
const item: StringDropdownEventItem = {
|
||||
DropdownId: dropdownId,
|
||||
Event: event,
|
||||
Environment: environment,
|
||||
};
|
||||
|
||||
if ((environment & CoreClient.Environment) == CoreClient.Environment) {
|
||||
CoreClient._stringDropdowns.push(item);
|
||||
|
||||
AppLogger.LogVerbose("Client", `Registered String Dropdown Event: ${dropdownId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import ChatInputCommand from "./interactionCreate/ChatInputCommand";
|
|||
import Button from "./interactionCreate/Button";
|
||||
import AppLogger from "./appLogger";
|
||||
import NewUserDiscovery from "./interactionCreate/middleware/NewUserDiscovery";
|
||||
import StringDropdown from "./interactionCreate/StringDropdown";
|
||||
|
||||
export class Events {
|
||||
public async onInteractionCreate(interaction: Interaction) {
|
||||
|
@ -19,6 +20,11 @@ export class Events {
|
|||
AppLogger.LogVerbose("Client", `Button: ${interaction.customId}`);
|
||||
Button.onButtonClicked(interaction);
|
||||
}
|
||||
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
AppLogger.LogVerbose("Client", `StringDropdown: ${interaction.customId}`);
|
||||
StringDropdown.onStringDropdownSelected(interaction);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit when bot is logged in and ready to use
|
||||
|
|
29
src/client/interactionCreate/StringDropdown.ts
Normal file
29
src/client/interactionCreate/StringDropdown.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {StringSelectMenuInteraction} from "discord.js";
|
||||
import {CoreClient} from "../client";
|
||||
import AppLogger from "../appLogger";
|
||||
|
||||
export default class StringDropdown {
|
||||
public static async onStringDropdownSelected(interaction: StringSelectMenuInteraction) {
|
||||
if (!interaction.isStringSelectMenu()) return;
|
||||
|
||||
const item = CoreClient.stringDropdowns.find(x => x.DropdownId == interaction.customId.split(" ")[0]);
|
||||
|
||||
if (!item) {
|
||||
AppLogger.LogVerbose("StringDropdown", `Event not found: ${interaction.customId}`);
|
||||
|
||||
await interaction.reply("Event not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AppLogger.LogDebug("StringDropdown", `Executing ${interaction.customId}`);
|
||||
|
||||
item.Event.execute(interaction);
|
||||
} catch (e) {
|
||||
AppLogger.LogError("StringDropdown", `Error occurred while executing event: ${interaction.customId}`);
|
||||
AppLogger.LogError("StringDropdown", e as string);
|
||||
|
||||
await interaction.reply("An error occurred while executing the event");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ import path from "path";
|
|||
import AppLogger from "../client/appLogger";
|
||||
import User from "../database/entities/app/User";
|
||||
import CardConstants from "../constants/CardConstants";
|
||||
import ErrorMessages from "../constants/ErrorMessages";
|
||||
import { DropResult } from "../contracts/SeriesMetadata";
|
||||
import EffectHelper from "../helpers/EffectHelper";
|
||||
|
||||
export default class Drop extends Command {
|
||||
constructor() {
|
||||
|
@ -22,14 +25,13 @@ export default class Drop extends Command {
|
|||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
if (!CoreClient.AllowDrops) {
|
||||
await interaction.reply("Bot is currently syncing, please wait until its done.");
|
||||
await interaction.reply(ErrorMessages.BotSyncing);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Config.GetValue("safemode") == "true") {
|
||||
AppLogger.LogWarn("Commands/Drop", "Safe Mode is active, refusing to send next drop.");
|
||||
|
||||
await interaction.reply("Safe Mode has been activated, please resync to continue.");
|
||||
AppLogger.LogWarn("Commands/Drop", ErrorMessages.SafeMode);
|
||||
await interaction.reply(ErrorMessages.SafeMode);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -43,16 +45,23 @@ export default class Drop extends Command {
|
|||
}
|
||||
|
||||
if (user.Currency < CardConstants.ClaimCost) {
|
||||
await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`);
|
||||
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency));
|
||||
return;
|
||||
}
|
||||
|
||||
const randomCard = CardDropHelperMetadata.GetRandomCard();
|
||||
let randomCard: DropResult | undefined;
|
||||
|
||||
const hasChanceUpEffect = await EffectHelper.HasEffect(interaction.user.id, "unclaimed");
|
||||
|
||||
if (hasChanceUpEffect && Math.random() <= CardConstants.UnusedChanceUpChance) {
|
||||
randomCard = await CardDropHelperMetadata.GetRandomCardUnclaimed(interaction.user.id);
|
||||
} else {
|
||||
randomCard = CardDropHelperMetadata.GetRandomCard();
|
||||
}
|
||||
|
||||
if (!randomCard) {
|
||||
AppLogger.LogWarn("Commands/Drop", "Unable to fetch card, please try again. (randomCard is null)");
|
||||
|
||||
await interaction.reply("Unable to fetch card, please try again.");
|
||||
AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchCard);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
116
src/commands/effects.ts
Normal file
116
src/commands/effects.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder} from "discord.js";
|
||||
import {Command} from "../type/command";
|
||||
import EffectHelper from "../helpers/EffectHelper";
|
||||
import {EffectDetails} from "../constants/EffectDetails";
|
||||
import UserEffect from "../database/entities/app/UserEffect";
|
||||
import TimeLengthInput from "../helpers/TimeLengthInput";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
|
||||
export default class Effects extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.CommandBuilder = new SlashCommandBuilder()
|
||||
.setName("effects")
|
||||
.setDescription("Effects")
|
||||
.addSubcommand(x => x
|
||||
.setName("list")
|
||||
.setDescription("List all effects I have")
|
||||
.addNumberOption(x => x
|
||||
.setName("page")
|
||||
.setDescription("The page number")
|
||||
.setMinValue(1)))
|
||||
.addSubcommand(x => x
|
||||
.setName("use")
|
||||
.setDescription("Use an effect in your inventory")
|
||||
.addStringOption(y => y
|
||||
.setName("id")
|
||||
.setDescription("The effect id to use")
|
||||
.setRequired(true)
|
||||
.setChoices([
|
||||
{ name: "Unclaimed Chance Up", value: "unclaimed" },
|
||||
])));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case "list":
|
||||
await this.List(interaction);
|
||||
break;
|
||||
case "use":
|
||||
await this.Use(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async List(interaction: CommandInteraction) {
|
||||
const pageOption = interaction.options.get("page");
|
||||
|
||||
const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1;
|
||||
|
||||
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [ result.embed ],
|
||||
components: [ result.row ],
|
||||
});
|
||||
}
|
||||
|
||||
private async Use(interaction: CommandInteraction) {
|
||||
const id = interaction.options.get("id", true).value!.toString();
|
||||
|
||||
const effectDetail = EffectDetails.get(id);
|
||||
|
||||
if (!effectDetail) {
|
||||
await interaction.reply("Unable to find effect!");
|
||||
return;
|
||||
}
|
||||
|
||||
const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id);
|
||||
|
||||
if (!canUseEffect) {
|
||||
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
|
||||
return;
|
||||
}
|
||||
|
||||
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Effect Confirmation")
|
||||
.setDescription("Would you like to use this effect?")
|
||||
.setColor(EmbedColours.Ok)
|
||||
.addFields([
|
||||
{
|
||||
name: "Effect",
|
||||
value: effectDetail.friendlyName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Length",
|
||||
value: timeLengthInput.GetLengthShort(),
|
||||
inline: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setLabel("Confirm")
|
||||
.setCustomId(`effects use confirm ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setLabel("Cancel")
|
||||
.setCustomId(`effects use cancel ${effectDetail.id}`)
|
||||
.setStyle(ButtonStyle.Danger),
|
||||
]);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [ embed ],
|
||||
components: [ row ],
|
||||
});
|
||||
}
|
||||
}
|
82
src/commands/id.ts
Normal file
82
src/commands/id.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
|
||||
import { Command } from "../type/command";
|
||||
import { CoreClient } from "../client/client";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
|
||||
import AppLogger from "../client/appLogger";
|
||||
|
||||
export default class Id extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.CommandBuilder = new SlashCommandBuilder()
|
||||
.setName("id")
|
||||
.setDescription("View a specific command by its id")
|
||||
.addStringOption(x =>
|
||||
x
|
||||
.setName("cardnumber")
|
||||
.setDescription("The card number to view")
|
||||
.setRequired(true));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
const cardNumber = interaction.options.get("cardnumber");
|
||||
|
||||
AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
|
||||
|
||||
if (!cardNumber || !cardNumber.value) {
|
||||
await interaction.reply("Card number is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const card = CoreClient.Cards
|
||||
.flatMap(x => x.cards)
|
||||
.find(x => x.id == cardNumber.value);
|
||||
|
||||
if (!card) {
|
||||
await interaction.reply("Card not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const series = CoreClient.Cards
|
||||
.find(x => x.cards.includes(card))!;
|
||||
|
||||
let image: Buffer;
|
||||
const imageFileName = card.path.split("/").pop()!;
|
||||
|
||||
try {
|
||||
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
|
||||
} catch {
|
||||
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
|
||||
|
||||
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
|
||||
const quantityClaimed = inventory ? inventory.Quantity : 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
|
||||
|
||||
try {
|
||||
await interaction.editReply({
|
||||
embeds: [ embed ],
|
||||
files: [ attachment ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`);
|
||||
|
||||
if (e instanceof DiscordAPIError) {
|
||||
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}.`);
|
||||
} else {
|
||||
await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ export default class Inventory extends Command {
|
|||
await interaction.followUp({
|
||||
files: [ embed.image ],
|
||||
embeds: [ embed.embed ],
|
||||
components: [ embed.row ],
|
||||
components: [ embed.row1, embed.row2 ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Commands/Inventory", e as string);
|
||||
|
|
87
src/commands/multidrop.ts
Normal file
87
src/commands/multidrop.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { AttachmentBuilder, CommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
import { Command } from "../type/command";
|
||||
import { CoreClient } from "../client/client";
|
||||
import ErrorMessages from "../constants/ErrorMessages";
|
||||
import Config from "../database/entities/app/Config";
|
||||
import AppLogger from "../client/appLogger";
|
||||
import User from "../database/entities/app/User";
|
||||
import CardConstants from "../constants/CardConstants";
|
||||
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
|
||||
export default class Multidrop extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.CommandBuilder = new SlashCommandBuilder()
|
||||
.setName("multidrop")
|
||||
.setDescription("Drop 11 cards for the price of 10!");
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
if (!CoreClient.AllowDrops) {
|
||||
await interaction.reply(ErrorMessages.BotSyncing);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Config.GetValue("safemode") == "true") {
|
||||
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.SafeMode);
|
||||
await interaction.reply(ErrorMessages.SafeMode);
|
||||
return;
|
||||
}
|
||||
|
||||
let user = await User.FetchOneById(User, interaction.user.id);
|
||||
|
||||
if (!user) {
|
||||
user = new User(interaction.user.id, CardConstants.StartingCurrency);
|
||||
await user.Save(User, user);
|
||||
|
||||
AppLogger.LogInfo("Commands/Multidrop", `New user (${interaction.user.id}) saved to the database`);
|
||||
}
|
||||
|
||||
if (user.Currency < CardConstants.MultidropCost) {
|
||||
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.MultidropCost, user.Currency));
|
||||
return;
|
||||
}
|
||||
|
||||
user.RemoveCurrency(CardConstants.MultidropCost);
|
||||
await user.Save(User, user);
|
||||
|
||||
const randomCard = CardDropHelperMetadata.GetRandomCard();
|
||||
const cardsRemaining = CardConstants.MultidropQuantity - 1;
|
||||
|
||||
if (!randomCard) {
|
||||
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.UnableToFetchCard);
|
||||
await interaction.reply(ErrorMessages.UnableToFetchCard);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path));
|
||||
const imageFileName = randomCard.card.path.split("/").pop()!;
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id);
|
||||
const quantityClaimed = inventory ? inventory.Quantity : 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency);
|
||||
|
||||
const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ embed ],
|
||||
files: [ attachment ],
|
||||
components: [ row ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Commands/Multidrop", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
|
||||
|
||||
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,11 +16,18 @@ export default class Sacrifice extends Command {
|
|||
x
|
||||
.setName("cardnumber")
|
||||
.setDescription("The card to sacrifice from your inventory")
|
||||
.setRequired(true));
|
||||
.setRequired(true))
|
||||
.addNumberOption(x =>
|
||||
x
|
||||
.setName("quantity")
|
||||
.setDescription("The amount to sacrifice (default 1)"));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction<CacheType>): Promise<void> {
|
||||
const cardnumber = interaction.options.get("cardnumber", true);
|
||||
const quantityInput = interaction.options.get("quantity")?.value ?? 1;
|
||||
|
||||
const quantity = Number(quantityInput) || 1;
|
||||
|
||||
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardnumber.value! as string);
|
||||
|
||||
|
@ -29,6 +36,11 @@ export default class Sacrifice extends Command {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cardInInventory.Quantity < quantity) {
|
||||
await interaction.reply(`You can only sacrifice what you own! You have ${cardInInventory.Quantity} of this card`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string);
|
||||
|
||||
if (!cardData) {
|
||||
|
@ -36,7 +48,7 @@ export default class Sacrifice extends Command {
|
|||
return;
|
||||
}
|
||||
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type);
|
||||
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
|
||||
const cardRarityString = CardRarityToString(cardData.card.type);
|
||||
|
||||
const description = [
|
||||
|
@ -44,6 +56,7 @@ export default class Sacrifice extends Command {
|
|||
`Series: ${cardData.series.name}`,
|
||||
`Rarity: ${cardRarityString}`,
|
||||
`Quantity Owned: ${cardInInventory.Quantity}`,
|
||||
`Quantity To Sacrifice: ${quantity}`,
|
||||
`Sacrifice Amount: ${cardValue}`,
|
||||
];
|
||||
|
||||
|
@ -56,11 +69,11 @@ export default class Sacrifice extends Command {
|
|||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!}`)
|
||||
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
|
||||
.setLabel("Confirm")
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!}`)
|
||||
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
|
||||
.setLabel("Cancel")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
]);
|
||||
|
|
|
@ -26,13 +26,26 @@ export default class Trade extends Command {
|
|||
x
|
||||
.setName("receive")
|
||||
.setDescription("Item to receive")
|
||||
.setRequired(true));
|
||||
.setRequired(true))
|
||||
.addNumberOption(x =>
|
||||
x
|
||||
.setName("givequantity")
|
||||
.setDescription("Amount to give"))
|
||||
.addNumberOption(x =>
|
||||
x
|
||||
.setName("receivequantity")
|
||||
.setDescription("Amount to receive"));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
const user = interaction.options.get("user", true).user!;
|
||||
const give = interaction.options.get("give", true);
|
||||
const receive = interaction.options.get("receive", true);
|
||||
const givequantityInput = interaction.options.get("givequantity")?.value ?? 1;
|
||||
const receivequantityInput = interaction.options.get("receivequantity")?.value ?? 1;
|
||||
|
||||
const givequantity = Number(givequantityInput) || 1;
|
||||
const receivequantity = Number(receivequantityInput) || 1;
|
||||
|
||||
AppLogger.LogSilly("Commands/Trade", `Parameters: user=${user.id}, give=${give.value}, receive=${receive.value}`);
|
||||
|
||||
|
@ -44,12 +57,12 @@ export default class Trade extends Command {
|
|||
const user1ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.value!.toString());
|
||||
const user2ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(user.id, receive.value!.toString());
|
||||
|
||||
if (!user1ItemEntity) {
|
||||
if (!user1ItemEntity || user1ItemEntity.Quantity < givequantity) {
|
||||
await interaction.reply("You do not have the item you are trying to trade.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user2ItemEntity) {
|
||||
if (!user2ItemEntity || user2ItemEntity.Quantity < receivequantity) {
|
||||
await interaction.reply("The user you are trying to trade with does not have the item you are trying to trade for.");
|
||||
return;
|
||||
}
|
||||
|
@ -78,12 +91,12 @@ export default class Trade extends Command {
|
|||
.addFields([
|
||||
{
|
||||
name: `${interaction.user.username} Receives`,
|
||||
value: `${user2Item.id}: ${user2Item.name}`,
|
||||
value: `${user2Item.id}: ${user2Item.name} x${receivequantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${user.username} Receives`,
|
||||
value: `${user1Item.id}: ${user1Item.name}`,
|
||||
value: `${user1Item.id}: ${user1Item.name} x${givequantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
|
@ -92,16 +105,16 @@ export default class Trade extends Command {
|
|||
}
|
||||
]);
|
||||
|
||||
const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name), 1000 * 60 * 15); // 15 minutes
|
||||
const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name, givequantity, receivequantity), 1000 * 60 * 15); // 15 minutes
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents([
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
|
||||
.setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`)
|
||||
.setLabel("Accept")
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
|
||||
.setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`)
|
||||
.setLabel("Decline")
|
||||
.setStyle(ButtonStyle.Danger),
|
||||
]);
|
||||
|
@ -109,7 +122,7 @@ export default class Trade extends Command {
|
|||
await interaction.reply({ content: `${user}`, embeds: [ tradeEmbed ], components: [ row ] });
|
||||
}
|
||||
|
||||
private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string) {
|
||||
private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string, user1Quantity: number, user2Quantity: number) {
|
||||
AppLogger.LogSilly("Commands/Trade/AutoDecline", `Auto declining trade between ${user1Username} and ${user2Username}`);
|
||||
|
||||
const tradeEmbed = new EmbedBuilder()
|
||||
|
@ -120,12 +133,12 @@ export default class Trade extends Command {
|
|||
.addFields([
|
||||
{
|
||||
name: `${user1Username} Receives`,
|
||||
value: `${user2CardNumber}: ${user2CardName}`,
|
||||
value: `${user2CardNumber}: ${user2CardName} x${user2Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${user2Username} Receives`,
|
||||
value: `${user1CardNumber}: ${user1CardName}`,
|
||||
value: `${user1CardNumber}: ${user1CardName} x${user1Quantity}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
|
||||
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
import { Command } from "../type/command";
|
||||
import { CoreClient } from "../client/client";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata";
|
||||
import AppLogger from "../client/appLogger";
|
||||
import CardSearchHelper from "../helpers/CardSearchHelper";
|
||||
|
||||
export default class View extends Command {
|
||||
constructor() {
|
||||
|
@ -13,70 +9,32 @@ export default class View extends Command {
|
|||
|
||||
this.CommandBuilder = new SlashCommandBuilder()
|
||||
.setName("view")
|
||||
.setDescription("View a specific command")
|
||||
.setDescription("Search for a card by its name")
|
||||
.addStringOption(x =>
|
||||
x
|
||||
.setName("cardnumber")
|
||||
.setDescription("The card number to view")
|
||||
.setName("name")
|
||||
.setDescription("The card name to search for")
|
||||
.setRequired(true));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction) {
|
||||
const cardNumber = interaction.options.get("cardnumber");
|
||||
const name = interaction.options.get("name", true);
|
||||
|
||||
AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
|
||||
|
||||
if (!cardNumber || !cardNumber.value) {
|
||||
await interaction.reply("Card number is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const card = CoreClient.Cards
|
||||
.flatMap(x => x.cards)
|
||||
.find(x => x.id == cardNumber.value);
|
||||
|
||||
if (!card) {
|
||||
await interaction.reply("Card not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const series = CoreClient.Cards
|
||||
.find(x => x.cards.includes(card))!;
|
||||
|
||||
let image: Buffer;
|
||||
const imageFileName = card.path.split("/").pop()!;
|
||||
|
||||
try {
|
||||
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path));
|
||||
} catch {
|
||||
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
|
||||
|
||||
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
|
||||
return;
|
||||
}
|
||||
AppLogger.LogSilly("Commands/View", `Parameters: name=${name.value}`);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
const searchResult = await CardSearchHelper.GenerateSearchQuery(name.value!.toString(), interaction.user.id, 7);
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id);
|
||||
const quantityClaimed = inventory ? inventory.Quantity : 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName);
|
||||
|
||||
try {
|
||||
await interaction.editReply({
|
||||
embeds: [ embed ],
|
||||
files: [ attachment ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`);
|
||||
|
||||
if (e instanceof DiscordAPIError) {
|
||||
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}.`);
|
||||
} else {
|
||||
await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN.");
|
||||
}
|
||||
if (!searchResult) {
|
||||
await interaction.editReply("No results found");
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ searchResult.embed ],
|
||||
components: [ searchResult.row ],
|
||||
files: [ searchResult.attachment ],
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,4 +3,11 @@ export default class CardConstants {
|
|||
public static readonly TimerGiveAmount = 10;
|
||||
public static readonly DailyCurrency = 100;
|
||||
public static readonly StartingCurrency = 300;
|
||||
|
||||
// Multidrop
|
||||
public static readonly MultidropCost = this.ClaimCost * 10;
|
||||
public static readonly MultidropQuantity = 11;
|
||||
|
||||
// Effects
|
||||
public static readonly UnusedChanceUpChance = 1;
|
||||
}
|
19
src/constants/EffectDetails.ts
Normal file
19
src/constants/EffectDetails.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
class EffectDetail {
|
||||
public readonly id: string;
|
||||
public readonly friendlyName: string;
|
||||
public readonly duration: number;
|
||||
public readonly cost: number;
|
||||
public readonly cooldown: number;
|
||||
|
||||
constructor(id: string, friendlyName: string, duration: number, cost: number, cooldown: number) {
|
||||
this.id = id;
|
||||
this.friendlyName = friendlyName;
|
||||
this.duration = duration;
|
||||
this.cost = cost;
|
||||
this.cooldown = cooldown;
|
||||
}
|
||||
};
|
||||
|
||||
export const EffectDetails = new Map<string, EffectDetail>([
|
||||
[ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ],
|
||||
]);
|
8
src/constants/ErrorMessages.ts
Normal file
8
src/constants/ErrorMessages.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default class ErrorMessages {
|
||||
public static readonly BotSyncing = "Bot is currently syncing, please wait until its done.";
|
||||
public static readonly SafeMode = "Safe Mode has been activated, please resync to continue.";
|
||||
public static readonly UnableToFetchCard = "Unable to fetch card, please try again.";
|
||||
public static readonly UnableToFetchUser = "Unable to fetch user, please try again.";
|
||||
|
||||
public static readonly NotEnoughCurrency = (need: number, have: number) => `Not enough currency! You need ${need} currency, you have ${have}!`;
|
||||
}
|
10
src/contracts/StringDropdownEventItem.ts
Normal file
10
src/contracts/StringDropdownEventItem.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {Environment} from "../constants/Environment";
|
||||
import {StringDropdownEvent} from "../type/stringDropdownEvent";
|
||||
|
||||
interface StringDropdownEventItem {
|
||||
DropdownId: string,
|
||||
Event: StringDropdownEvent,
|
||||
Environment: Environment,
|
||||
}
|
||||
|
||||
export default StringDropdownEventItem;
|
|
@ -29,16 +29,16 @@ export default class Inventory extends AppBaseEntity {
|
|||
this.Quantity = quantity;
|
||||
}
|
||||
|
||||
public AddQuantity(amount: number) {
|
||||
this.Quantity += amount;
|
||||
}
|
||||
|
||||
public RemoveQuantity(amount: number) {
|
||||
if (this.Quantity < amount) return;
|
||||
|
||||
this.Quantity -= amount;
|
||||
}
|
||||
|
||||
public AddQuantity(amount: number) {
|
||||
this.Quantity += amount;
|
||||
}
|
||||
|
||||
public AddClaim(claim: Claim) {
|
||||
this.Claims.push(claim);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class User extends AppBaseEntity {
|
|||
@Column()
|
||||
Currency: number;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
LastUsedDaily?: Date;
|
||||
|
||||
public AddCurrency(amount: number) {
|
||||
|
|
74
src/database/entities/app/UserEffect.ts
Normal file
74
src/database/entities/app/UserEffect.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import {Column, Entity} from "typeorm";
|
||||
import AppBaseEntity from "../../../contracts/AppBaseEntity";
|
||||
import AppDataSource from "../../dataSources/appDataSource";
|
||||
|
||||
@Entity()
|
||||
export default class UserEffect extends AppBaseEntity {
|
||||
constructor(name: string, userId: string, unused: number, WhenExpires?: Date) {
|
||||
super();
|
||||
|
||||
this.Name = name;
|
||||
this.UserId = userId;
|
||||
this.Unused = unused;
|
||||
this.WhenExpires = WhenExpires;
|
||||
}
|
||||
|
||||
@Column()
|
||||
Name: string;
|
||||
|
||||
@Column()
|
||||
UserId: string;
|
||||
|
||||
@Column()
|
||||
Unused: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
WhenExpires?: Date;
|
||||
|
||||
public AddUnused(amount: number) {
|
||||
this.Unused += amount;
|
||||
}
|
||||
|
||||
public UseEffect(whenExpires: Date): boolean {
|
||||
if (this.Unused == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.Unused -= 1;
|
||||
this.WhenExpires = whenExpires;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IsEffectActive(): boolean {
|
||||
const now = new Date();
|
||||
|
||||
if (this.WhenExpires && now < this.WhenExpires) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async FetchOneByUserIdAndName(userId: string, name: string): Promise<UserEffect | null> {
|
||||
const repository = AppDataSource.getRepository(UserEffect);
|
||||
|
||||
const single = await repository.findOne({ where: { UserId: userId, Name: name } });
|
||||
|
||||
return single;
|
||||
}
|
||||
|
||||
public static async FetchAllByUserIdPaginated(userId: string, page: number = 0, itemsPerPage: number = 10): Promise<[UserEffect[], number]> {
|
||||
const repository = AppDataSource.getRepository(UserEffect);
|
||||
|
||||
const query = await repository.createQueryBuilder("effect")
|
||||
.where("effect.UserId = :userId", { userId })
|
||||
.where("effect.Unused > 0")
|
||||
.orderBy("effect.Name", "ASC")
|
||||
.skip(page * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.getManyAndCount();
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import MigrationHelper from "../../../../helpers/MigrationHelper";
|
||||
|
||||
export class CreateUserEffect1729962056556 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
MigrationHelper.Up("1729962056556-createUserEffect", "0.9", [
|
||||
"01-table-userEffect",
|
||||
], queryRunner);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
MigrationHelper.Down("1729962056556-createUserEffect", "0.9", [
|
||||
"01-table-userEffect",
|
||||
], queryRunner);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||
import { CardRarity, CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
|
||||
import { CardRarity, CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity";
|
||||
import CardRarityChances from "../constants/CardRarityChances";
|
||||
import { DropResult } from "../contracts/SeriesMetadata";
|
||||
import { CoreClient } from "../client/client";
|
||||
import AppLogger from "../client/appLogger";
|
||||
import CardConstants from "../constants/CardConstants";
|
||||
import StringTools from "./StringTools";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
|
||||
export default class CardDropHelperMetadata {
|
||||
public static GetRandomCard(): DropResult | undefined {
|
||||
|
@ -58,6 +59,64 @@ export default class CardDropHelperMetadata {
|
|||
};
|
||||
}
|
||||
|
||||
public static async GetRandomCardUnclaimed(userId: string): Promise<DropResult | undefined> {
|
||||
const randomRarity = Math.random() * 100;
|
||||
|
||||
let cardRarity: CardRarity;
|
||||
|
||||
const bronzeChance = CardRarityChances.Bronze;
|
||||
const silverChance = bronzeChance + CardRarityChances.Silver;
|
||||
const goldChance = silverChance + CardRarityChances.Gold;
|
||||
const mangaChance = goldChance + CardRarityChances.Manga;
|
||||
|
||||
if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
|
||||
else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
|
||||
else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
|
||||
else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga;
|
||||
else cardRarity = CardRarity.Legendary;
|
||||
|
||||
const randomCard = await this.GetRandomCardByRarityUnclaimed(cardRarity, userId);
|
||||
|
||||
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardUnclaimed", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`);
|
||||
|
||||
return randomCard;
|
||||
}
|
||||
|
||||
public static async GetRandomCardByRarityUnclaimed(rarity: CardRarity, userId: string): Promise<DropResult | undefined> {
|
||||
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Parameters: rarity=${rarity}, userId=${userId}`);
|
||||
|
||||
const claimedCards = await Inventory.FetchAllByUserId(userId);
|
||||
|
||||
if (!claimedCards) {
|
||||
// They don't have any cards, so safe to get any random card
|
||||
return this.GetRandomCardByRarity(rarity);
|
||||
}
|
||||
|
||||
const allCards = CoreClient.Cards
|
||||
.flatMap(x => x.cards)
|
||||
.filter(x => x.type == rarity)
|
||||
.filter(x => !claimedCards.find(y => y.CardNumber == x.id));
|
||||
|
||||
const randomCardIndex = Math.floor(Math.random() * allCards.length);
|
||||
|
||||
const card = allCards[randomCardIndex];
|
||||
const series = CoreClient.Cards
|
||||
.find(x => x.cards.includes(card));
|
||||
|
||||
if (!series) {
|
||||
AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Random card: ${card.id} ${card.name}`);
|
||||
|
||||
return {
|
||||
series: series,
|
||||
card: card,
|
||||
};
|
||||
}
|
||||
|
||||
public static GetCardByCardNumber(cardNumber: string): DropResult | undefined {
|
||||
AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`);
|
||||
|
||||
|
@ -89,7 +148,7 @@ export default class CardDropHelperMetadata {
|
|||
const hexCode = Number("0x" + drop.card.colour);
|
||||
|
||||
if (hexCode) {
|
||||
colour = hexCode;
|
||||
colour = hexCode;
|
||||
} else {
|
||||
AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
|
||||
}
|
||||
|
@ -149,4 +208,26 @@ export default class CardDropHelperMetadata {
|
|||
.setLabel("Reroll")
|
||||
.setStyle(ButtonStyle.Secondary));
|
||||
}
|
||||
|
||||
public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder {
|
||||
const dropEmbed = this.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency);
|
||||
|
||||
dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} · ${cardsRemaining} Remaining`});
|
||||
|
||||
return dropEmbed;
|
||||
}
|
||||
|
||||
public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder<ButtonBuilder> {
|
||||
return new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`)
|
||||
.setLabel("Keep")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(disabled),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`)
|
||||
.setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} 🪙)`)
|
||||
.setStyle(ButtonStyle.Secondary));
|
||||
}
|
||||
}
|
||||
|
|
117
src/helpers/CardSearchHelper.ts
Normal file
117
src/helpers/CardSearchHelper.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import {ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
||||
import Fuse from "fuse.js";
|
||||
import {CoreClient} from "../client/client.js";
|
||||
import CardDropHelperMetadata from "./CardDropHelperMetadata.js";
|
||||
import Inventory from "../database/entities/app/Inventory.js";
|
||||
import {readFileSync} from "fs";
|
||||
import path from "path";
|
||||
import AppLogger from "../client/appLogger.js";
|
||||
|
||||
interface ReturnedPage {
|
||||
embed: EmbedBuilder,
|
||||
row: ActionRowBuilder<ButtonBuilder>,
|
||||
attachment: AttachmentBuilder,
|
||||
results: string[],
|
||||
}
|
||||
|
||||
export default class CardSearchHelper {
|
||||
public static async GenerateSearchQuery(query: string, userid: string, pages: number): Promise<ReturnedPage | undefined> {
|
||||
AppLogger.LogSilly("CardSearchHelper/GenerateSearchQuery", `Parameters: query=${query}, userid=${userid}, pages=${pages}`);
|
||||
|
||||
const fzf = new Fuse(CoreClient.Cards.flatMap(x => x.cards), { keys: ["name"] });
|
||||
const entries = fzf.search(query)
|
||||
.splice(0, pages);
|
||||
|
||||
const entry = entries[0];
|
||||
const results = entries
|
||||
.flatMap(x => x.item.id);
|
||||
|
||||
if (!entry) {
|
||||
AppLogger.LogVerbose("CardSearchHelper/GenerateSearchQuery", `Unable to find entry: ${query}`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const card = CardDropHelperMetadata.GetCardByCardNumber(entry.item.id);
|
||||
|
||||
if (!card) return undefined;
|
||||
|
||||
let image: Buffer;
|
||||
const imageFileName = card.card.path.split("/").pop()!;
|
||||
|
||||
try {
|
||||
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
|
||||
} catch {
|
||||
AppLogger.LogError("CardSearchHelper/GenerateSearchQuery", `Unable to fetch image for card ${card.card.id}.`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
|
||||
const quantityClaimed = inventory?.Quantity ?? 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`view 0 ${results.join(" ")}`)
|
||||
.setLabel("Previous")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`view 2 ${results.join(" ")}`)
|
||||
.setLabel("Next")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(pages == 1));
|
||||
|
||||
return { embed, row, attachment, results };
|
||||
}
|
||||
|
||||
public static async GenerateSearchPageFromQuery(results: string[], userid: string, page: number): Promise<ReturnedPage | undefined> {
|
||||
const currentPageId = results[page - 1];
|
||||
|
||||
const card = CardDropHelperMetadata.GetCardByCardNumber(currentPageId);
|
||||
|
||||
if (!card) {
|
||||
AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to find card by id: ${currentPageId}.`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let image: Buffer;
|
||||
const imageFileName = card.card.path.split("/").pop()!;
|
||||
|
||||
try {
|
||||
image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path));
|
||||
} catch {
|
||||
AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to fetch image for card ${card.card.id}.`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachment = new AttachmentBuilder(image, { name: imageFileName });
|
||||
|
||||
const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id);
|
||||
const quantityClaimed = inventory?.Quantity ?? 0;
|
||||
|
||||
const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`view ${page - 1} ${results.join(" ")}`)
|
||||
.setLabel("Previous")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(page - 1 == 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`view ${page + 1} ${results.join(" ")}`)
|
||||
.setLabel("Next")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(page == results.length));
|
||||
|
||||
return { embed, row, attachment, results };
|
||||
}
|
||||
}
|
115
src/helpers/EffectHelper.ts
Normal file
115
src/helpers/EffectHelper.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
||||
import UserEffect from "../database/entities/app/UserEffect";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
import {EffectDetails} from "../constants/EffectDetails";
|
||||
|
||||
export default class EffectHelper {
|
||||
public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
|
||||
let effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
|
||||
|
||||
if (!effect) {
|
||||
effect = new UserEffect(name, userId, quantity);
|
||||
} else {
|
||||
effect.AddUnused(quantity);
|
||||
}
|
||||
|
||||
await effect.Save(UserEffect, effect);
|
||||
}
|
||||
|
||||
public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise<boolean> {
|
||||
const canUseEffect = await this.CanUseEffect(userId, name);
|
||||
|
||||
if (!canUseEffect) return false;
|
||||
|
||||
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
|
||||
|
||||
if (!effect) return false;
|
||||
|
||||
effect.UseEffect(whenExpires);
|
||||
|
||||
await effect.Save(UserEffect, effect);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async CanUseEffect(userId: string, name: string): Promise<boolean> {
|
||||
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
|
||||
const now = new Date();
|
||||
|
||||
if (!effect || effect.Unused == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const effectDetail = EffectDetails.get(effect.Id);
|
||||
|
||||
if (!effectDetail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (effect.WhenExpires && now < new Date(effect.WhenExpires.getMilliseconds() + effectDetail.cooldown)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async HasEffect(userId: string, name: string): Promise<boolean> {
|
||||
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
|
||||
const now = new Date();
|
||||
|
||||
if (!effect || !effect.WhenExpires) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now > effect.WhenExpires) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async GenerateEffectEmbed(userId: string, page: number): Promise<{
|
||||
embed: EmbedBuilder,
|
||||
row: ActionRowBuilder<ButtonBuilder>,
|
||||
}> {
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const query = await UserEffect.FetchAllByUserIdPaginated(userId, page - 1, itemsPerPage);
|
||||
|
||||
const effects = query[0];
|
||||
const count = query[1];
|
||||
|
||||
const totalPages = count > 0 ? Math.ceil(count / itemsPerPage) : 1;
|
||||
|
||||
let description = "*none*";
|
||||
|
||||
if (effects.length > 0) {
|
||||
description = effects.map(x => `${x.Name} x${x.Unused}`).join("\n");
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Effects")
|
||||
.setDescription(description)
|
||||
.setColor(EmbedColours.Ok)
|
||||
.setFooter({ text: `Page ${page} of ${totalPages}` });
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`effects list ${page - 1}`)
|
||||
.setLabel("Previous")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(page == 1),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`effects list ${page + 1}`)
|
||||
.setLabel("Next")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(page == totalPages),
|
||||
);
|
||||
|
||||
return {
|
||||
embed,
|
||||
row,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import path from "path";
|
|||
import AppLogger from "../client/appLogger";
|
||||
import {existsSync} from "fs";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
import Jimp from "jimp";
|
||||
import {Jimp} from "jimp";
|
||||
|
||||
interface CardInput {
|
||||
id: string;
|
||||
|
@ -46,7 +46,7 @@ export default class ImageHelper {
|
|||
}
|
||||
}
|
||||
|
||||
const image = await loadImage(await imageData.getBufferAsync("image/png"));
|
||||
const image = await loadImage(await imageData.getBuffer("image/png"));
|
||||
|
||||
const x = i % gridWidth;
|
||||
const y = Math.floor(i / gridWidth);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js";
|
||||
import Inventory from "../database/entities/app/Inventory";
|
||||
import { CoreClient } from "../client/client";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
|
@ -24,7 +24,8 @@ interface InventoryPageCards {
|
|||
|
||||
interface ReturnedInventoryPage {
|
||||
embed: EmbedBuilder,
|
||||
row: ActionRowBuilder<ButtonBuilder>,
|
||||
row1: ActionRowBuilder<ButtonBuilder>,
|
||||
row2: ActionRowBuilder<StringSelectMenuBuilder>,
|
||||
image: AttachmentBuilder,
|
||||
}
|
||||
|
||||
|
@ -99,7 +100,7 @@ export default class InventoryHelper {
|
|||
.setColor(EmbedColours.Ok)
|
||||
.setImage("attachment://page.png");
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
const row1 = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`inventory ${userid} ${page - 1}`)
|
||||
|
@ -112,9 +113,23 @@ export default class InventoryHelper {
|
|||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(page + 1 == pages.length));
|
||||
|
||||
let pageNum = 0;
|
||||
|
||||
const row2 = new ActionRowBuilder<StringSelectMenuBuilder>()
|
||||
.addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId("inventory")
|
||||
.setPlaceholder(`${currentPage.name} (${currentPage.seriesSubpage + 1})`)
|
||||
.addOptions(...pages.map(x =>
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel(`${x.name} (${x.seriesSubpage + 1})`.substring(0, 100))
|
||||
.setDescription(`Page ${pageNum + 1}`)
|
||||
.setDefault(currentPage.id == x.id)
|
||||
.setValue(`${userid} ${pageNum++}`))));
|
||||
|
||||
const buffer = await ImageHelper.GenerateCardImageGrid(currentPage.cards.map(x => ({ id: x.id, path: x.path })));
|
||||
const image = new AttachmentBuilder(buffer, { name: "page.png" });
|
||||
|
||||
return { embed, row, image };
|
||||
return { embed, row1, row2, image };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,4 +118,19 @@ export default class TimeLengthInput {
|
|||
|
||||
return desNumber;
|
||||
}
|
||||
|
||||
public static ConvertFromMilliseconds(ms: number): TimeLengthInput {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
const remainingSeconds = seconds % 60;
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingHours = hours % 24;
|
||||
|
||||
const timeString = `${days}d ${remainingHours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
||||
|
||||
return new TimeLengthInput(timeString);
|
||||
}
|
||||
}
|
|
@ -7,9 +7,12 @@ import AllBalance from "./commands/allbalance";
|
|||
import Balance from "./commands/balance";
|
||||
import Daily from "./commands/daily";
|
||||
import Drop from "./commands/drop";
|
||||
import Effects from "./commands/effects";
|
||||
import Gdrivesync from "./commands/gdrivesync";
|
||||
import Give from "./commands/give";
|
||||
import Id from "./commands/id";
|
||||
import Inventory from "./commands/inventory";
|
||||
import Multidrop from "./commands/multidrop";
|
||||
import Resync from "./commands/resync";
|
||||
import Sacrifice from "./commands/sacrifice";
|
||||
import Series from "./commands/series";
|
||||
|
@ -23,11 +26,17 @@ import Droprarity from "./commands/stage/droprarity";
|
|||
|
||||
// Button Event Imports
|
||||
import Claim from "./buttonEvents/Claim";
|
||||
import EffectsButtonEvent from "./buttonEvents/Effects";
|
||||
import InventoryButtonEvent from "./buttonEvents/Inventory";
|
||||
import MultidropButtonEvent from "./buttonEvents/Multidrop";
|
||||
import Reroll from "./buttonEvents/Reroll";
|
||||
import SacrificeButtonEvent from "./buttonEvents/Sacrifice";
|
||||
import SeriesEvent from "./buttonEvents/Series";
|
||||
import TradeButtonEvent from "./buttonEvents/Trade";
|
||||
import ViewButtonEvent from "./buttonEvents/View";
|
||||
|
||||
// String Dropdown Event Imports
|
||||
import InventoryStringDropdown from "./stringDropdowns/Inventory";
|
||||
|
||||
export default class Registry {
|
||||
public static RegisterCommands() {
|
||||
|
@ -37,9 +46,12 @@ export default class Registry {
|
|||
CoreClient.RegisterCommand("balance", new Balance());
|
||||
CoreClient.RegisterCommand("daily", new Daily());
|
||||
CoreClient.RegisterCommand("drop", new Drop());
|
||||
CoreClient.RegisterCommand("effects", new Effects());
|
||||
CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
|
||||
CoreClient.RegisterCommand("give", new Give());
|
||||
CoreClient.RegisterCommand("id", new Id());
|
||||
CoreClient.RegisterCommand("inventory", new Inventory());
|
||||
CoreClient.RegisterCommand("multidrop", new Multidrop());
|
||||
CoreClient.RegisterCommand("resync", new Resync());
|
||||
CoreClient.RegisterCommand("sacrifice", new Sacrifice());
|
||||
CoreClient.RegisterCommand("series", new Series());
|
||||
|
@ -54,10 +66,17 @@ export default class Registry {
|
|||
|
||||
public static RegisterButtonEvents() {
|
||||
CoreClient.RegisterButtonEvent("claim", new Claim());
|
||||
CoreClient.RegisterButtonEvent("effects", new EffectsButtonEvent());
|
||||
CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
|
||||
CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
|
||||
CoreClient.RegisterButtonEvent("reroll", new Reroll());
|
||||
CoreClient.RegisterButtonEvent("sacrifice", new SacrificeButtonEvent());
|
||||
CoreClient.RegisterButtonEvent("series", new SeriesEvent());
|
||||
CoreClient.RegisterButtonEvent("trade", new TradeButtonEvent());
|
||||
CoreClient.RegisterButtonEvent("view", new ViewButtonEvent());
|
||||
}
|
||||
|
||||
public static RegisterStringDropdownEvents() {
|
||||
CoreClient.RegisterStringDropdownEvent("inventory", new InventoryStringDropdown());
|
||||
}
|
||||
}
|
43
src/stringDropdowns/Inventory.ts
Normal file
43
src/stringDropdowns/Inventory.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {StringSelectMenuInteraction} from "discord.js";
|
||||
import {StringDropdownEvent} from "../type/stringDropdownEvent";
|
||||
import AppLogger from "../client/appLogger";
|
||||
import InventoryHelper from "../helpers/InventoryHelper";
|
||||
|
||||
export default class Inventory extends StringDropdownEvent {
|
||||
public override async execute(interaction: StringSelectMenuInteraction) {
|
||||
if (!interaction.guild) return;
|
||||
|
||||
const userid = interaction.values[0].split(" ")[0];
|
||||
const page = interaction.values[0].split(" ")[1];
|
||||
|
||||
AppLogger.LogDebug("StringDropdown/Inventory", `Parameters: userid=${userid}, page=${page}`);
|
||||
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const member = interaction.guild.members.cache.find(x => x.id == userid) || await interaction.guild.members.fetch(userid);
|
||||
|
||||
if (!member) {
|
||||
await interaction.reply("Unable to find user.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const embed = await InventoryHelper.GenerateInventoryPage(member.user.username, member.user.id, Number(page));
|
||||
|
||||
if (!embed) {
|
||||
await interaction.followUp("No page for user found.");
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
files: [ embed.image ],
|
||||
embeds: [ embed.embed ],
|
||||
components: [ embed.row1, embed.row2 ],
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.LogError("StringDropdown/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);
|
||||
|
||||
await interaction.followUp("An error has occurred running this command.");
|
||||
}
|
||||
}
|
||||
}
|
5
src/type/stringDropdownEvent.ts
Normal file
5
src/type/stringDropdownEvent.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {StringSelectMenuInteraction} from "discord.js";
|
||||
|
||||
export abstract class StringDropdownEvent {
|
||||
abstract execute(interaction: StringSelectMenuInteraction): Promise<void>;
|
||||
}
|
127
tests/buttonEvents/Effects.test.ts
Normal file
127
tests/buttonEvents/Effects.test.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {ButtonInteraction} from "discord.js";
|
||||
import Effects from "../../src/buttonEvents/Effects";
|
||||
import EffectHelper from "../../src/helpers/EffectHelper";
|
||||
|
||||
describe("execute", () => {
|
||||
describe("GIVEN action in custom id is list", () => {
|
||||
const interaction = {
|
||||
customId: "effects list",
|
||||
} as unknown as ButtonInteraction;
|
||||
|
||||
let listSpy: jest.SpyInstance;
|
||||
|
||||
beforeAll(async () => {
|
||||
const effects = new Effects();
|
||||
|
||||
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
|
||||
.mockImplementation();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT list function to be called", () => {
|
||||
expect(listSpy).toHaveBeenCalledTimes(1);
|
||||
expect(listSpy).toHaveBeenCalledWith(interaction);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("List", () => {
|
||||
let interaction: ButtonInteraction;
|
||||
|
||||
const embed = {
|
||||
name: "Embed",
|
||||
};
|
||||
|
||||
const row = {
|
||||
name: "Row",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
interaction = {
|
||||
customId: "effects list",
|
||||
user: {
|
||||
id: "userId",
|
||||
},
|
||||
update: jest.fn(),
|
||||
reply: jest.fn(),
|
||||
} as unknown as ButtonInteraction;
|
||||
});
|
||||
|
||||
describe("GIVEN page is a valid number", () => {
|
||||
beforeEach(async () => {
|
||||
interaction.customId += " 1";
|
||||
|
||||
EffectHelper.GenerateEffectEmbed = jest.fn()
|
||||
.mockResolvedValue({
|
||||
embed,
|
||||
row,
|
||||
});
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT EffectHelper.GenerateEffectEmbed to be called", () => {
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
|
||||
});
|
||||
|
||||
test("EXPECT interaction to be updated", () => {
|
||||
expect(interaction.update).toHaveBeenCalledTimes(1);
|
||||
expect(interaction.update).toHaveBeenCalledWith({
|
||||
embeds: [ embed ],
|
||||
components: [ row ],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN page in custom id is not supplied", () => {
|
||||
beforeEach(async () => {
|
||||
EffectHelper.GenerateEffectEmbed = jest.fn()
|
||||
.mockResolvedValue({
|
||||
embed,
|
||||
row,
|
||||
});
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT interaction to be replied with error", () => {
|
||||
expect(interaction.reply).toHaveBeenCalledTimes(1);
|
||||
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
|
||||
});
|
||||
|
||||
test("EXPECT interaction to not be updated", () => {
|
||||
expect(interaction.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN page in custom id is not a number", () => {
|
||||
beforeEach(async () => {
|
||||
interaction.customId += " test";
|
||||
|
||||
EffectHelper.GenerateEffectEmbed = jest.fn()
|
||||
.mockResolvedValue({
|
||||
embed,
|
||||
row,
|
||||
});
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT interaction to be replied with error", () => {
|
||||
expect(interaction.reply).toHaveBeenCalledTimes(1);
|
||||
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
|
||||
});
|
||||
|
||||
test("EXPECT interaction to not be updated", () => {
|
||||
expect(interaction.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
40
tests/commands/__snapshots__/effects.test.ts.snap
Normal file
40
tests/commands/__snapshots__/effects.test.ts.snap
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`constructor EXPECT CommandBuilder to be defined 1`] = `
|
||||
{
|
||||
"contexts": undefined,
|
||||
"default_member_permissions": undefined,
|
||||
"default_permission": undefined,
|
||||
"description": "Effects",
|
||||
"description_localizations": undefined,
|
||||
"dm_permission": undefined,
|
||||
"integration_types": undefined,
|
||||
"name": "effects",
|
||||
"name_localizations": undefined,
|
||||
"nsfw": undefined,
|
||||
"options": [
|
||||
{
|
||||
"description": "List all effects I have",
|
||||
"description_localizations": undefined,
|
||||
"name": "list",
|
||||
"name_localizations": undefined,
|
||||
"options": [
|
||||
{
|
||||
"autocomplete": undefined,
|
||||
"choices": undefined,
|
||||
"description": "The page number",
|
||||
"description_localizations": undefined,
|
||||
"max_value": undefined,
|
||||
"min_value": 1,
|
||||
"name": "page",
|
||||
"name_localizations": undefined,
|
||||
"required": false,
|
||||
"type": 10,
|
||||
},
|
||||
],
|
||||
"type": 1,
|
||||
},
|
||||
],
|
||||
"type": 1,
|
||||
}
|
||||
`;
|
164
tests/commands/effects.test.ts
Normal file
164
tests/commands/effects.test.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
import {ChatInputCommandInteraction} from "discord.js";
|
||||
import Effects from "../../src/commands/effects";
|
||||
import EffectHelper from "../../src/helpers/EffectHelper";
|
||||
|
||||
describe("constructor", () => {
|
||||
let effects: Effects;
|
||||
|
||||
beforeEach(() => {
|
||||
effects = new Effects();
|
||||
});
|
||||
|
||||
test("EXPECT CommandBuilder to be defined", () => {
|
||||
expect(effects.CommandBuilder).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("execute", () => {
|
||||
describe("GIVEN interaction is not a chat input command", () => {
|
||||
let interaction: ChatInputCommandInteraction;
|
||||
|
||||
let listSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
interaction = {
|
||||
isChatInputCommand: jest.fn().mockReturnValue(false),
|
||||
} as unknown as ChatInputCommandInteraction;
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
|
||||
.mockImplementation();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT isChatInputCommand to have been called", () => {
|
||||
expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("EXPECT nothing to happen", () => {
|
||||
expect(listSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN subcommand is list", () => {
|
||||
let interaction: ChatInputCommandInteraction;
|
||||
|
||||
let listSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
interaction = {
|
||||
isChatInputCommand: jest.fn().mockReturnValue(true),
|
||||
options: {
|
||||
getSubcommand: jest.fn().mockReturnValue("list"),
|
||||
},
|
||||
} as unknown as ChatInputCommandInteraction;
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
|
||||
.mockImplementation();
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT subcommand function to be called", () => {
|
||||
expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("EXPECT list function to be called", () => {
|
||||
expect(listSpy).toHaveBeenCalledTimes(1);
|
||||
expect(listSpy).toHaveBeenCalledWith(interaction);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("List", () => {
|
||||
const effects: Effects = new Effects();
|
||||
let interaction: ChatInputCommandInteraction;
|
||||
|
||||
const embed = {
|
||||
name: "embed",
|
||||
};
|
||||
|
||||
const row = {
|
||||
name: "row",
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
interaction = {
|
||||
isChatInputCommand: jest.fn().mockReturnValue(true),
|
||||
options: {
|
||||
getSubcommand: jest.fn().mockReturnValue("list"),
|
||||
},
|
||||
reply: jest.fn(),
|
||||
user: {
|
||||
id: "userId",
|
||||
},
|
||||
} as unknown as ChatInputCommandInteraction;
|
||||
|
||||
const effects = new Effects();
|
||||
|
||||
EffectHelper.GenerateEffectEmbed = jest.fn().mockReturnValue({
|
||||
embed,
|
||||
row,
|
||||
});
|
||||
|
||||
jest.spyOn(effects as unknown as {"List": () => object}, "List")
|
||||
.mockImplementation();
|
||||
});
|
||||
|
||||
describe("GIVEN page option is supplied", () => {
|
||||
describe("AND page is a valid number", () => {
|
||||
beforeEach(async () => {
|
||||
interaction.options.get = jest.fn().mockReturnValueOnce({
|
||||
value: "2",
|
||||
});
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page", () => {
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 2);
|
||||
});
|
||||
|
||||
test("EXPECT interaction to have been replied", () => {
|
||||
expect(interaction.reply).toHaveBeenCalledTimes(1);
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
embeds: [ embed ],
|
||||
components: [ row ],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AND page is not a valid number", () => {
|
||||
beforeEach(async () => {
|
||||
interaction.options.get = jest.fn().mockReturnValueOnce({
|
||||
value: "test",
|
||||
});
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN page option is not supplied", () => {
|
||||
beforeEach(async () => {
|
||||
interaction.options.get = jest.fn().mockReturnValueOnce(undefined);
|
||||
|
||||
await effects.execute(interaction);
|
||||
});
|
||||
|
||||
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
|
||||
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
|
||||
});
|
||||
});
|
||||
});
|
103
tests/database/entities/app/UserEffect.test.ts
Normal file
103
tests/database/entities/app/UserEffect.test.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import UserEffect from "../../../../src/database/entities/app/UserEffect";
|
||||
|
||||
let userEffect: UserEffect;
|
||||
const now = new Date();
|
||||
|
||||
beforeEach(() => {
|
||||
userEffect = new UserEffect("name", "userId", 1);
|
||||
});
|
||||
|
||||
describe("AddUnused", () => {
|
||||
beforeEach(() => {
|
||||
userEffect.AddUnused(1);
|
||||
});
|
||||
|
||||
test("EXPECT unused to be the amount more", () => {
|
||||
expect(userEffect.Unused).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UseEffect", () => {
|
||||
describe("GIVEN Unused is 0", () => {
|
||||
let result: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
userEffect.Unused = 0;
|
||||
|
||||
result = userEffect.UseEffect(now);
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("EXPECT details not to be changed", () => {
|
||||
expect(userEffect.Unused).toBe(0);
|
||||
expect(userEffect.WhenExpires).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN Unused is greater than 0", () => {
|
||||
let result: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
result = userEffect.UseEffect(now);
|
||||
});
|
||||
|
||||
test("EXPECT true returned", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("EXPECT Unused to be subtracted by 1", () => {
|
||||
expect(userEffect.Unused).toBe(0);
|
||||
});
|
||||
|
||||
test("EXPECT WhenExpires to be set", () => {
|
||||
expect(userEffect.WhenExpires).toBe(now);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IsEffectActive", () => {
|
||||
describe("GIVEN WhenExpires is null", () => {
|
||||
let result: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
result = userEffect.IsEffectActive();
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN WhenExpires is defined", () => {
|
||||
describe("AND WhenExpires is in the past", () => {
|
||||
let result: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
userEffect.WhenExpires = new Date(now.getTime() - 100);
|
||||
|
||||
result = userEffect.IsEffectActive();
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AND WhenExpires is in the future", () => {
|
||||
let result: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
userEffect.WhenExpires = new Date(now.getTime() + 100);
|
||||
|
||||
result = userEffect.IsEffectActive();
|
||||
});
|
||||
|
||||
test("EXPECT true returned", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
380
tests/helpers/EffectHelper.test.ts
Normal file
380
tests/helpers/EffectHelper.test.ts
Normal file
|
@ -0,0 +1,380 @@
|
|||
import {ActionRowBuilder, ButtonBuilder, EmbedBuilder} from "discord.js";
|
||||
import UserEffect from "../../src/database/entities/app/UserEffect";
|
||||
import EffectHelper from "../../src/helpers/EffectHelper";
|
||||
|
||||
describe("AddEffectToUserInventory", () => {
|
||||
describe("GIVEN effect is in database", () => {
|
||||
const effectMock = {
|
||||
AddUnused: jest.fn(),
|
||||
Save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock);
|
||||
|
||||
await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
|
||||
});
|
||||
|
||||
test("EXPECT database to be fetched", () => {
|
||||
expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledTimes(1);
|
||||
expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledWith("userId", "name");
|
||||
});
|
||||
|
||||
test("EXPECT effect to be updated", () => {
|
||||
expect(effectMock.AddUnused).toHaveBeenCalledTimes(1);
|
||||
expect(effectMock.AddUnused).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
test("EXPECT effect to be saved", () => {
|
||||
expect(effectMock.Save).toHaveBeenCalledTimes(1);
|
||||
expect(effectMock.Save).toHaveBeenCalledWith(UserEffect, effectMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect is not in database", () => {
|
||||
beforeAll(async () => {
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
|
||||
UserEffect.prototype.Save = jest.fn();
|
||||
|
||||
await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
|
||||
});
|
||||
|
||||
test("EXPECT effect to be saved", () => {
|
||||
expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1);
|
||||
expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("UseEffect", () => {
|
||||
describe("GIVEN effect is in database", () => {
|
||||
describe("GIVEN now is before effect.WhenExpires", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
// nowMock < whenExpires
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpires = new Date(2024, 11, 3, 14, 0);
|
||||
|
||||
const userEffect = {
|
||||
Unused: 1,
|
||||
WhenExpires: whenExpires,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.UseEffect("userId", "name", new Date());
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN currently used effect is inactive", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
// nowMock > whenExpires
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpires = new Date(2024, 11, 3, 13, 0);
|
||||
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
const userEffect = {
|
||||
Unused: 1,
|
||||
WhenExpires: whenExpires,
|
||||
UseEffect: jest.fn(),
|
||||
Save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT UseEffect to be called", () => {
|
||||
expect(userEffect.UseEffect).toHaveReturnedTimes(1);
|
||||
expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT effect to be saved", () => {
|
||||
expect(userEffect.Save).toHaveBeenCalledTimes(1);
|
||||
expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
|
||||
});
|
||||
|
||||
test("EXPECT true returned", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect.WhenExpires is null", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
// nowMock > whenExpires
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
const userEffect = {
|
||||
Unused: 1,
|
||||
WhenExpires: null,
|
||||
UseEffect: jest.fn(),
|
||||
Save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT UseEffect to be called", () => {
|
||||
expect(userEffect.UseEffect).toHaveBeenCalledTimes(1);
|
||||
expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT effect to be saved", () => {
|
||||
expect(userEffect.Save).toHaveBeenCalledTimes(1);
|
||||
expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect);
|
||||
});
|
||||
|
||||
test("EXPECT true returned", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect is not in database", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
// nowMock > whenExpires
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
|
||||
|
||||
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect.Unused is 0", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
// nowMock > whenExpires
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpiresNew = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
const userEffect = {
|
||||
Unused: 0,
|
||||
WhenExpires: null,
|
||||
UseEffect: jest.fn(),
|
||||
Save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew);
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HasEffect", () => {
|
||||
describe("GIVEN effect is in database", () => {
|
||||
describe("GIVEN effect.WhenExpires is defined", () => {
|
||||
describe("GIVEN now is before effect.WhenExpires", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
const nowMock = new Date(2024, 11, 3, 13, 30);
|
||||
const whenExpires = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
const userEffect = {
|
||||
WhenExpires: whenExpires,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.HasEffect("userId", "name");
|
||||
});
|
||||
|
||||
test("EXPECT true returned", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN now is after effect.WhenExpires", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
const nowMock = new Date(2024, 11, 3, 16, 30);
|
||||
const whenExpires = new Date(2024, 11, 3, 15, 0);
|
||||
|
||||
const userEffect = {
|
||||
WhenExpires: whenExpires,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setSystemTime(nowMock);
|
||||
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.HasEffect("userId", "name");
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect.WhenExpires is undefined", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
const userEffect = {
|
||||
WhenExpires: undefined,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect);
|
||||
|
||||
result = await EffectHelper.HasEffect("userId", "name");
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN effect is not in database", () => {
|
||||
let result: boolean | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
|
||||
|
||||
result = await EffectHelper.HasEffect("userId", "name");
|
||||
});
|
||||
|
||||
test("EXPECT false returned", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GenerateEffectEmbed", () => {
|
||||
beforeEach(async () => {
|
||||
UserEffect.FetchAllByUserIdPaginated = jest.fn()
|
||||
.mockResolvedValue([
|
||||
[],
|
||||
0,
|
||||
]);
|
||||
|
||||
await EffectHelper.GenerateEffectEmbed("userId", 1);
|
||||
});
|
||||
|
||||
test("EXPECT UserEffect.FetchAllByUserIdPaginated to be called", () => {
|
||||
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledTimes(1);
|
||||
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledWith("userId", 0, 10);
|
||||
});
|
||||
|
||||
describe("GIVEN there are no effects returned", () => {
|
||||
let result: {
|
||||
embed: EmbedBuilder,
|
||||
row: ActionRowBuilder<ButtonBuilder>,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
UserEffect.FetchAllByUserIdPaginated = jest.fn()
|
||||
.mockResolvedValue([
|
||||
[],
|
||||
0,
|
||||
]);
|
||||
|
||||
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
|
||||
});
|
||||
|
||||
test("EXPECT result returned", () => {
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GIVEN there are effects returned", () => {
|
||||
let result: {
|
||||
embed: EmbedBuilder,
|
||||
row: ActionRowBuilder<ButtonBuilder>,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
UserEffect.FetchAllByUserIdPaginated = jest.fn()
|
||||
.mockResolvedValue([
|
||||
[
|
||||
{
|
||||
Name: "name",
|
||||
Unused: 1,
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
|
||||
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
|
||||
});
|
||||
|
||||
test("EXPECT result returned", () => {
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("AND it is the first page", () => {
|
||||
beforeEach(async () => {
|
||||
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
|
||||
});
|
||||
|
||||
test("EXPECT Previous button to be disabled", () => {
|
||||
const button = result.row.components[0].data as unknown as {
|
||||
label: string,
|
||||
disabled: boolean
|
||||
};
|
||||
|
||||
expect(button).toBeDefined();
|
||||
expect(button.label).toBe("Previous");
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AND it is the last page", () => {
|
||||
beforeEach(async () => {
|
||||
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
|
||||
});
|
||||
|
||||
test("EXPECT Next button to be disabled", () => {
|
||||
const button = result.row.components[1].data as unknown as {
|
||||
label: string,
|
||||
disabled: boolean
|
||||
};
|
||||
|
||||
expect(button).toBeDefined();
|
||||
expect(button.label).toBe("Next");
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
71
tests/helpers/__snapshots__/EffectHelper.test.ts.snap
Normal file
71
tests/helpers/__snapshots__/EffectHelper.test.ts.snap
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GenerateEffectEmbed GIVEN there are effects returned EXPECT result returned 1`] = `
|
||||
{
|
||||
"embed": {
|
||||
"color": 3166394,
|
||||
"description": "name x1",
|
||||
"footer": {
|
||||
"icon_url": undefined,
|
||||
"text": "Page 1 of 1",
|
||||
},
|
||||
"title": "Effects",
|
||||
},
|
||||
"row": {
|
||||
"components": [
|
||||
{
|
||||
"custom_id": "effects list 0",
|
||||
"disabled": true,
|
||||
"emoji": undefined,
|
||||
"label": "Previous",
|
||||
"style": 1,
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"custom_id": "effects list 2",
|
||||
"disabled": true,
|
||||
"emoji": undefined,
|
||||
"label": "Next",
|
||||
"style": 1,
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"type": 1,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`GenerateEffectEmbed GIVEN there are no effects returned EXPECT result returned 1`] = `
|
||||
{
|
||||
"embed": {
|
||||
"color": 3166394,
|
||||
"description": "*none*",
|
||||
"footer": {
|
||||
"icon_url": undefined,
|
||||
"text": "Page 1 of 1",
|
||||
},
|
||||
"title": "Effects",
|
||||
},
|
||||
"row": {
|
||||
"components": [
|
||||
{
|
||||
"custom_id": "effects list 0",
|
||||
"disabled": true,
|
||||
"emoji": undefined,
|
||||
"label": "Previous",
|
||||
"style": 1,
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"custom_id": "effects list 2",
|
||||
"disabled": true,
|
||||
"emoji": undefined,
|
||||
"label": "Next",
|
||||
"style": 1,
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"type": 1,
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -1,66 +0,0 @@
|
|||
import {CoreClient} from "../src/client/client";
|
||||
import Registry from "../src/registry";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
describe("RegisterCommands", () => {
|
||||
test("EXPECT every command in the commands folder to be registered", () => {
|
||||
const registeredCommands: string[] = [];
|
||||
|
||||
CoreClient.RegisterCommand = jest.fn().mockImplementation((name: string) => {
|
||||
registeredCommands.push(name);
|
||||
});
|
||||
|
||||
Registry.RegisterCommands();
|
||||
|
||||
const commandFiles = getFilesInDirectory(path.join(process.cwd(), "src", "commands"))
|
||||
.filter(x => x.endsWith(".ts"));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
expect(registeredCommands).toContain(file.split("/").pop()!.split(".")[0]);
|
||||
}
|
||||
|
||||
expect(commandFiles.length).toBe(registeredCommands.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RegisterButtonEvents", () => {
|
||||
test("EXEPCT every button event in the button events folder to be registered", () => {
|
||||
const registeredButtonEvents: string[] = [];
|
||||
|
||||
CoreClient.RegisterButtonEvent = jest.fn().mockImplementation((name: string) => {
|
||||
registeredButtonEvents.push(name);
|
||||
});
|
||||
|
||||
Registry.RegisterButtonEvents();
|
||||
|
||||
const eventFiles = getFilesInDirectory(path.join(process.cwd(), "src", "buttonEvents"))
|
||||
.filter(x => x.endsWith(".ts"));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
expect(registeredButtonEvents).toContain(file.split("/").pop()!.split(".")[0].toLowerCase());
|
||||
}
|
||||
|
||||
expect(eventFiles.length).toBe(registeredButtonEvents.length);
|
||||
});
|
||||
});
|
||||
|
||||
function getFilesInDirectory(dir: string): string[] {
|
||||
let results: string[] = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
|
||||
list.forEach(file => {
|
||||
file = path.join(dir, file);
|
||||
const stat = fs.statSync(file);
|
||||
|
||||
if (stat && stat.isDirectory()) {
|
||||
/* recurse into a subdirectory */
|
||||
results = results.concat(getFilesInDirectory(file));
|
||||
} else {
|
||||
/* is a file */
|
||||
results.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
Loading…
Reference in a new issue