From c6f5f9bcdef577b6046d34839f2055b4123da69a Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:23:05 +0700 Subject: [PATCH 1/7] chore: apply changes from #9 --- bin/registercmds.ts | 8 +-- package-lock.json | 1 + src/commands/AttackCommand.ts | 11 ++-- src/commands/ChestsCommand.ts | 11 ++-- src/commands/CollectionCommand.ts | 20 +++--- src/commands/ExploreCommand.ts | 11 ++-- src/commands/FleeCommand.ts | 11 ++-- src/commands/GuideCommand.ts | 13 ++-- src/commands/HelpCommand.ts | 25 ++++---- src/commands/InventoryCommand.ts | 11 ++-- src/commands/LeaderboardCommand.ts | 20 +++--- src/commands/LookupCommand.ts | 17 ++--- src/commands/MarketCommand.ts | 11 ++-- src/commands/NetworkCommand.ts | 22 +++---- src/commands/ProfileCommand.ts | 13 ++-- src/commands/RegisterCommand.ts | 16 +++-- src/commands/RestCommand.ts | 11 ++-- src/commands/TasksCommand.ts | 13 ++-- src/commands/TestCommand.ts | 16 +++-- src/commands/TravelCommand.ts | 11 ++-- src/commands/VoteCommand.ts | 16 +++-- src/components/buttons/AttackButton.ts | 5 +- src/components/buttons/BulkCollectButton.ts | 5 +- src/components/buttons/BulkDismantleButton.ts | 5 +- src/components/buttons/BulkSellButton.ts | 5 +- src/components/buttons/ChestBuyButton.ts | 5 +- src/components/buttons/ChestOpenButton.ts | 5 +- src/components/buttons/ChestStartButton.ts | 5 +- src/components/buttons/CollectButton.ts | 5 +- src/components/buttons/ConsumeButton.ts | 5 +- src/components/buttons/DismantleButton.ts | 5 +- src/components/buttons/EmbedAttackButton.ts | 5 +- src/components/buttons/EmbedFleeButton.ts | 5 +- src/components/buttons/EnhanceButton.ts | 5 +- src/components/buttons/EquipButton.ts | 5 +- src/components/buttons/ExploreAgainButton.ts | 5 +- src/components/buttons/ExploreButton.ts | 5 +- src/components/buttons/FleeButton.ts | 5 +- src/components/buttons/GuideNavButton.ts | 7 +-- src/components/buttons/LockButton.ts | 5 +- src/components/buttons/MarketBuyButton.ts | 5 +- src/components/buttons/MarketCancelButton.ts | 5 +- src/components/buttons/MarketNextButton.ts | 5 +- src/components/buttons/MarketPrevButton.ts | 5 +- .../buttons/MarketRedirectButton.ts | 5 +- .../buttons/MarketSellPageButton.ts | 5 +- src/components/buttons/ReforgeButton.ts | 5 +- .../buttons/RegisterAcceptButton.ts | 5 +- .../buttons/RegisterDeclineButton.ts | 5 +- src/components/buttons/RestButton.ts | 5 +- src/components/buttons/SellButton.ts | 5 +- src/components/buttons/SkillPointsButton.ts | 5 +- src/components/buttons/TaskClaimButton.ts | 5 +- src/components/buttons/TasksTabButton.ts | 5 +- src/components/menus/InvSelectMenu.ts | 9 +-- src/components/menus/MarketSellMenu.ts | 5 +- src/components/menus/ReforgeSelectMenu.ts | 5 +- src/components/menus/TravelSelectMenu.ts | 5 +- src/components/menus/UnequipMenu.ts | 10 +-- src/components/modals/BulkCollectModal.ts | 5 +- src/components/modals/BulkDismantleModal.ts | 5 +- src/components/modals/BulkSellModal.ts | 5 +- src/components/modals/CollectModal.ts | 5 +- src/components/modals/ConsumeModal.ts | 5 +- src/components/modals/MarketSellModal.ts | 5 +- src/components/modals/SellModal.ts | 5 +- src/components/modals/SkillPointsModal.ts | 5 +- src/events/ClientReadyEvent.ts | 9 ++- src/events/GuildCreateEvent.ts | 9 ++- src/events/InteractionCreateEvent.ts | 9 ++- src/handlers/ButtonHandler.ts | 4 +- src/handlers/EventHandler.ts | 6 +- src/handlers/ModalSubmitHandler.ts | 2 +- src/handlers/SelectMenuHandler.ts | 4 +- src/handlers/SlashCommandHandler.ts | 5 +- src/interfaces/ICooldown.ts | 3 - src/structures/Button.ts | 29 ++++++--- src/structures/Event.ts | 23 ++++--- src/structures/ModalSubmit.ts | 29 ++++++--- src/structures/SelectMenu.ts | 29 ++++++--- src/structures/SlashCommand.ts | 63 ++++++++++--------- 81 files changed, 338 insertions(+), 415 deletions(-) delete mode 100644 src/interfaces/ICooldown.ts diff --git a/bin/registercmds.ts b/bin/registercmds.ts index 007b08e..55f37af 100644 --- a/bin/registercmds.ts +++ b/bin/registercmds.ts @@ -15,12 +15,10 @@ for (const file of commandFiles) { const command = require(fullPath); const Module = new command.default(); if (!(Module instanceof SlashCommand)) continue; - const isGlobal = Module.isGlobalCommand(); - const commandData = Module.getData(); - if (isGlobal) { - globalCommandArray.push(commandData.toJSON()); + if (Module.isGlobalCommand) { + globalCommandArray.push(Module.data.toJSON()); } else { - commandArray.push(commandData.toJSON()); + commandArray.push(Module.data.toJSON()); } } diff --git a/package-lock.json b/package-lock.json index af40a17..bc45ca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "dfo-discord-bot", "version": "1.0.0", + "license": "Apache-2.0", "dependencies": { "@napi-rs/canvas": "^0.1.97", "discord-hybrid-sharding": "^3.0.1", diff --git a/src/commands/AttackCommand.ts b/src/commands/AttackCommand.ts index 0395a2b..86a3a20 100644 --- a/src/commands/AttackCommand.ts +++ b/src/commands/AttackCommand.ts @@ -8,7 +8,13 @@ import Routes from "../utilities/Routes"; export default class AttackCommand extends SlashCommand { constructor() { - super('attack', 'Attack the enemy in your encounter', 'Gaming'); + super({ + name: "attack", + description: "Attack the enemy in your encounter", + category: "Gaming", + cooldown: 1.8, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -34,7 +40,4 @@ export default class AttackCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 1.8; } } \ No newline at end of file diff --git a/src/commands/ChestsCommand.ts b/src/commands/ChestsCommand.ts index e1c1442..8060d3b 100644 --- a/src/commands/ChestsCommand.ts +++ b/src/commands/ChestsCommand.ts @@ -11,7 +11,13 @@ import type { IChestSlot } from "../interfaces/IGameJSON"; export default class ChestsCommand extends SlashCommand { constructor() { - super('chests', 'View and manage your chest vault', 'Gaming'); + super({ + name: "chests", + description: "View and manage your chest vault", + category: "Gaming", + cooldown: 5, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -107,9 +113,6 @@ export default class ChestsCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } function chunkArray(arr: T[], size: number): T[][] { diff --git a/src/commands/CollectionCommand.ts b/src/commands/CollectionCommand.ts index 8ce3f10..9f0f0f3 100644 --- a/src/commands/CollectionCommand.ts +++ b/src/commands/CollectionCommand.ts @@ -9,9 +9,15 @@ import { formatError } from "../utilities/ErrorMessages"; export default class CollectionCommand extends SlashCommand { constructor() { - super('collection', 'View your or another player\'s item collection', 'General'); - - this.data.addUserOption((o) => + super({ + name: "collection", + description: "View your or another player's item collection", + category: "General", + cooldown: 5, + isGlobalCommand: true + }); + + this.builder.addUserOption((o) => o.setName('user') .setDescription('Select a user') .setRequired(false) @@ -93,12 +99,4 @@ export default class CollectionCommand extends SlashCommand { await paginator.start(interaction); } - - public isGlobalCommand(): boolean { - return true; - } - - public cooldown(): number { - return 5; - } } diff --git a/src/commands/ExploreCommand.ts b/src/commands/ExploreCommand.ts index bb4ea9f..bac4e3b 100644 --- a/src/commands/ExploreCommand.ts +++ b/src/commands/ExploreCommand.ts @@ -8,7 +8,13 @@ import Routes from "../utilities/Routes"; export default class ExploreCommand extends SlashCommand { constructor() { - super('explore', 'Explore the world and find items or enemy encounters!', 'Gaming'); + super({ + name: "explore", + description: "Explore the world and find items or enemy encounters!", + category: "Gaming", + cooldown: 7, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -39,7 +45,4 @@ export default class ExploreCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 7; } } \ No newline at end of file diff --git a/src/commands/FleeCommand.ts b/src/commands/FleeCommand.ts index 8951687..dc1281d 100644 --- a/src/commands/FleeCommand.ts +++ b/src/commands/FleeCommand.ts @@ -8,7 +8,13 @@ import Routes from "../utilities/Routes"; export default class FleeCommand extends SlashCommand { constructor() { - super('flee', 'Flee the enemy encounter you\'re in.', 'Gaming'); + super({ + name: "flee", + description: "Flee the enemy encounter you're in.", + category: "Gaming", + cooldown: 2, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -34,7 +40,4 @@ export default class FleeCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 2; } } \ No newline at end of file diff --git a/src/commands/GuideCommand.ts b/src/commands/GuideCommand.ts index 683716a..babb4b0 100644 --- a/src/commands/GuideCommand.ts +++ b/src/commands/GuideCommand.ts @@ -121,8 +121,14 @@ const SECTION_ORDER = ['basics', 'combat', 'workshop', 'economy', 'tasks', 'zone export default class GuideCommand extends SlashCommand { constructor() { - super('guide', 'View the DFO game guide', 'General'); - this.data.addStringOption((o) => + super({ + name: "guide", + description: "View the DFO game guide", + category: "General", + cooldown: 3, + isGlobalCommand: true + }); + this.builder.addStringOption((o) => o.setName('section') .setDescription('Jump to a specific section') .setRequired(false) @@ -176,7 +182,4 @@ export default class GuideCommand extends SlashCommand { components: navRow.components.length > 0 ? [navRow] : [], }); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index e2cd422..d3cd1af 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -19,7 +19,13 @@ const CATEGORY_COLORS: Record = { export default class HelpCommand extends SlashCommand { constructor() { - super('help', 'View all available commands and how to get started', 'General'); + super({ + name: "help", + description: "View all available commands and how to get started", + category: "General", + cooldown: 3, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -31,12 +37,11 @@ export default class HelpCommand extends SlashCommand { const categories = new Map(); for (const command of commands.values()) { - const cat = command.getCategory(); // Hide developer commands from regular users - if (cat === 'Developer') continue; + if (command.category === 'Developer') continue; - if (!categories.has(cat)) categories.set(cat, []); - categories.get(cat)!.push(command); + if (!categories.has(command.category)) categories.set(command.category, []); + categories.get(command.category)!.push(command); } const pages: EmbedBuilder[] = []; @@ -66,7 +71,7 @@ export default class HelpCommand extends SlashCommand { let description = ''; for (const cmd of cmds) { - description += `**\`/${cmd.getName()}\`** — ${cmd.getDescription()}\n`; + description += `**\`/${cmd.name}\`** — ${cmd.description}\n`; } const embed = new EmbedBuilder() @@ -84,12 +89,4 @@ export default class HelpCommand extends SlashCommand { await paginator.start(interaction); } - - public isGlobalCommand(): boolean { - return true; - } - - public cooldown(): number { - return 3; - } } \ No newline at end of file diff --git a/src/commands/InventoryCommand.ts b/src/commands/InventoryCommand.ts index 7692b9d..b509e05 100644 --- a/src/commands/InventoryCommand.ts +++ b/src/commands/InventoryCommand.ts @@ -15,7 +15,13 @@ import ImageService from "../utilities/ImageService"; export default class InventoryCommand extends SlashCommand { constructor() { - super('inventory', 'View your inventory and manage items', 'General'); + super({ + name: "inventory", + description: "View your inventory and manage items", + category: "General", + cooldown: 5, + isGlobalCommand: true + }); // No options — the select menu handles item selection } @@ -124,7 +130,4 @@ export default class InventoryCommand extends SlashCommand { await paginator.start(interaction); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/commands/LeaderboardCommand.ts b/src/commands/LeaderboardCommand.ts index de99709..6b52b7b 100644 --- a/src/commands/LeaderboardCommand.ts +++ b/src/commands/LeaderboardCommand.ts @@ -46,9 +46,15 @@ const STAT_DISPLAY: Record = { export default class LeaderboardCommand extends SlashCommand { constructor() { - super('leaderboard', 'View the top players', 'General'); - - this.data.addStringOption((o) => + super({ + name: "leaderboard", + description: "View the top players", + category: "General", + cooldown: 10, + isGlobalCommand: true + }); + + this.builder.addStringOption((o) => o.setName('stat') .setDescription('Which stat to rank by') .setChoices(STAT_OPTIONS) @@ -106,12 +112,4 @@ export default class LeaderboardCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { - return true; - } - - public cooldown(): number { - return 10; - } } \ No newline at end of file diff --git a/src/commands/LookupCommand.ts b/src/commands/LookupCommand.ts index a693cb1..55d1ae8 100644 --- a/src/commands/LookupCommand.ts +++ b/src/commands/LookupCommand.ts @@ -18,10 +18,16 @@ const typeOptions = [ export default class LookupCommand extends SlashCommand { constructor() { - super('lookup', 'Lookup specific objects in the game', 'Moderator'); - - this.data.addStringOption((o) => o.setName('type').setDescription('Select a type').setChoices(typeOptions).setRequired(true)); - this.data.addIntegerOption((o) => o.setName('id').setDescription('Enter an id to lookup. Use -1 for all').setMinValue(-1).setRequired(true).setAutocomplete(true)); + super({ + name: "lookup", + description: "Lookup specific objects in the game", + category: "Moderator", + cooldown: 3, + isGlobalCommand: false + }); + + this.builder.addStringOption((o) => o.setName('type').setDescription('Select a type').setChoices(typeOptions).setRequired(true)); + this.builder.addIntegerOption((o) => o.setName('id').setDescription('Enter an id to lookup. Use -1 for all').setMinValue(-1).setRequired(true).setAutocomplete(true)); } /** @@ -159,7 +165,4 @@ export default class LookupCommand extends SlashCommand { break; } } - - public isGlobalCommand(): boolean { return false; } - public cooldown(): number { return 3; } } \ No newline at end of file diff --git a/src/commands/MarketCommand.ts b/src/commands/MarketCommand.ts index 3b43512..e7d938b 100644 --- a/src/commands/MarketCommand.ts +++ b/src/commands/MarketCommand.ts @@ -40,7 +40,13 @@ export const SELL_PAGE_SIZE = 25; export default class MarketCommand extends SlashCommand { constructor() { - super('market', 'Browse and trade on the Global Market', 'Gaming'); + super({ + name: "market", + description: "Browse and trade on the Global Market", + category: "Gaming", + cooldown: 5, + isGlobalCommand: true + }); this.data .addSubcommand((sub) => @@ -144,9 +150,6 @@ export default class MarketCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } /** diff --git a/src/commands/NetworkCommand.ts b/src/commands/NetworkCommand.ts index ab167b1..6c1ea3c 100644 --- a/src/commands/NetworkCommand.ts +++ b/src/commands/NetworkCommand.ts @@ -36,10 +36,16 @@ const options = [ export default class NetworkCommand extends SlashCommand { constructor() { - super('network', 'Track all clusters and guilds the bot is connected to', 'Moderator'); - - this.data.addStringOption((o) => o.setName('type').setDescription('Select a view type').setChoices(options).setRequired(true)); - this.data.addStringOption((o) => o.setName('id').setDescription("Enter a Cluster ID or Guild ID. Use 'all' to view everything.").setRequired(false)); + super({ + name: "network", + description: "Track all clusters and guilds the bot is connected to", + category: "Moderator", + cooldown: 5, + isGlobalCommand: false + }); + + this.builder.addStringOption((o) => o.setName('type').setDescription('Select a view type').setChoices(options).setRequired(true)); + this.builder.addStringOption((o) => o.setName('id').setDescription("Enter a Cluster ID or Guild ID. Use 'all' to view everything.").setRequired(false)); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -226,12 +232,4 @@ export default class NetworkCommand extends SlashCommand { })); } } - - public isGlobalCommand(): boolean { - return false; - } - - public cooldown(): number { - return 5; - } } diff --git a/src/commands/ProfileCommand.ts b/src/commands/ProfileCommand.ts index db566aa..14bfd16 100644 --- a/src/commands/ProfileCommand.ts +++ b/src/commands/ProfileCommand.ts @@ -11,8 +11,14 @@ import ImageService from "../utilities/ImageService"; export default class ProfileCommand extends SlashCommand { constructor() { - super('profile', 'View your or another player\'s profile', 'General'); - this.data.addUserOption((o) => o.setName('user').setDescription('Select a user').setRequired(false)); + super({ + name: "profile", + description: "View your or another player's profile", + category: "General", + cooldown: 5, + isGlobalCommand: true + }); + this.builder.addUserOption((o) => o.setName('user').setDescription('Select a user').setRequired(false)); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -78,7 +84,4 @@ export default class ProfileCommand extends SlashCommand { components, }); } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } \ No newline at end of file diff --git a/src/commands/RegisterCommand.ts b/src/commands/RegisterCommand.ts index 8b69576..3176825 100644 --- a/src/commands/RegisterCommand.ts +++ b/src/commands/RegisterCommand.ts @@ -3,7 +3,13 @@ import SlashCommand from "../structures/SlashCommand"; export default class RegisterCommand extends SlashCommand { constructor() { - super('register', 'Register new user data with the bot', 'General'); + super({ + name: "register", + description: "Register new user data with the bot", + category: "General", + cooldown: 5, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -42,12 +48,4 @@ export default class RegisterCommand extends SlashCommand { await interaction.reply({ embeds: [embed], components: [row], flags: MessageFlags.Ephemeral }); } - - public isGlobalCommand(): boolean { - return true; - } - - public cooldown(): number { - return 5; - } } \ No newline at end of file diff --git a/src/commands/RestCommand.ts b/src/commands/RestCommand.ts index bc6967a..3bf8e45 100644 --- a/src/commands/RestCommand.ts +++ b/src/commands/RestCommand.ts @@ -6,7 +6,13 @@ import Routes from "../utilities/Routes"; export default class RestCommand extends SlashCommand { constructor() { - super('rest', 'Rest at the inn to restore HP (costs gold)', 'Gaming'); + super({ + name: "rest", + description: "Rest at the inn to restore HP (costs gold)", + category: "Gaming", + cooldown: 5, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -44,7 +50,4 @@ export default class RestCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/commands/TasksCommand.ts b/src/commands/TasksCommand.ts index 41909be..a34912b 100644 --- a/src/commands/TasksCommand.ts +++ b/src/commands/TasksCommand.ts @@ -12,8 +12,14 @@ import type { ITaskJSON } from "../interfaces/IGameJSON"; export default class TasksCommand extends SlashCommand { constructor() { - super('tasks', 'View your active tasks and claim rewards', 'Gaming'); - this.data.addStringOption((o) => + super({ + name: "tasks", + description: "View your active tasks and claim rewards", + category: "Gaming", + cooldown: 5, + isGlobalCommand: true + }); + this.builder.addStringOption((o) => o.setName('period') .setDescription('Task period to view') .setRequired(false) @@ -104,7 +110,4 @@ export default class TasksCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/commands/TestCommand.ts b/src/commands/TestCommand.ts index 35f11c9..4c0d5eb 100644 --- a/src/commands/TestCommand.ts +++ b/src/commands/TestCommand.ts @@ -4,7 +4,13 @@ import logger from "../utilities/Logger"; export default class TestCommand extends SlashCommand { constructor() { - super('test', 'dev command', 'Developer'); + super({ + name: "test", + description: "dev command", + category: "Developer", + cooldown: 5, + isGlobalCommand: false + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -28,12 +34,4 @@ export default class TestCommand extends SlashCommand { await interaction.editReply({ content: `\`${factId}\`\n${text}` }); } - - public isGlobalCommand(): boolean { - return false; - } - - public cooldown(): number { - return 5; - } } \ No newline at end of file diff --git a/src/commands/TravelCommand.ts b/src/commands/TravelCommand.ts index 1f83c26..ba5a89c 100644 --- a/src/commands/TravelCommand.ts +++ b/src/commands/TravelCommand.ts @@ -11,7 +11,13 @@ import ImageService from "../utilities/ImageService"; export default class TravelCommand extends SlashCommand { constructor() { - super('travel', 'View the zone map and travel to a different zone', 'Gaming'); + super({ + name: "travel", + description: "View the zone map and travel to a different zone", + category: "Gaming", + cooldown: 5, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -70,7 +76,4 @@ export default class TravelCommand extends SlashCommand { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isGlobalCommand(): boolean { return true; } - public cooldown(): number { return 5; } } \ No newline at end of file diff --git a/src/commands/VoteCommand.ts b/src/commands/VoteCommand.ts index 0a5123d..cd05d7b 100644 --- a/src/commands/VoteCommand.ts +++ b/src/commands/VoteCommand.ts @@ -3,7 +3,13 @@ import SlashCommand from "../structures/SlashCommand"; export default class VoteCommand extends SlashCommand { constructor() { - super('vote', 'Support DFO by voting on top.gg!', 'General'); + super({ + name: "vote", + description: "Support DFO by voting on top.gg!", + category: "General", + cooldown: 5, + isGlobalCommand: true + }); } public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { @@ -33,12 +39,4 @@ export default class VoteCommand extends SlashCommand { await interaction.reply({ embeds: [embed], components: [row] }); } - - public isGlobalCommand(): boolean { - return true; - } - - public cooldown(): number { - return 5; - } } \ No newline at end of file diff --git a/src/components/buttons/AttackButton.ts b/src/components/buttons/AttackButton.ts index 88d0d59..e5c7787 100644 --- a/src/components/buttons/AttackButton.ts +++ b/src/components/buttons/AttackButton.ts @@ -7,7 +7,7 @@ import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class AttackButton extends Button { - constructor() { super('attack'); } + constructor() { super({ customId: "attack", cooldown: 1.8, isAuthorOnly: false }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -32,7 +32,4 @@ export default class AttackButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isAuthorOnly(): boolean { return false; } - public cooldown(): number { return 1.8; } } \ No newline at end of file diff --git a/src/components/buttons/BulkCollectButton.ts b/src/components/buttons/BulkCollectButton.ts index a948ec8..76bc089 100644 --- a/src/components/buttons/BulkCollectButton.ts +++ b/src/components/buttons/BulkCollectButton.ts @@ -8,7 +8,7 @@ import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; const ITEMS_PER_PAGE = 15; export default class BulkCollectButton extends Button { - constructor() { super('bulk_collect'); } + constructor() { super({ customId: "bulk_collect", cooldown: 3, isAuthorOnly: true }); } // customId format: bulk_collect: public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { @@ -71,7 +71,4 @@ export default class BulkCollectButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/BulkDismantleButton.ts b/src/components/buttons/BulkDismantleButton.ts index a49f072..1b6077d 100644 --- a/src/components/buttons/BulkDismantleButton.ts +++ b/src/components/buttons/BulkDismantleButton.ts @@ -8,7 +8,7 @@ import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; const ITEMS_PER_PAGE = 15; export default class BulkDismantleButton extends Button { - constructor() { super('bulk_dismantle'); } + constructor() { super({ customId: "bulk_dismantle", cooldown: 3, isAuthorOnly: true }); } // customId format: bulk_dismantle: public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { @@ -71,7 +71,4 @@ export default class BulkDismantleButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/BulkSellButton.ts b/src/components/buttons/BulkSellButton.ts index 1dc0b7d..a986c67 100644 --- a/src/components/buttons/BulkSellButton.ts +++ b/src/components/buttons/BulkSellButton.ts @@ -8,7 +8,7 @@ import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; const ITEMS_PER_PAGE = 15; export default class BulkSellButton extends Button { - constructor() { super('bulk_sell'); } + constructor() { super({ customId: "bulk_sell", cooldown: 3, isAuthorOnly: true }); } // customId format: bulk_sell: public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { @@ -74,7 +74,4 @@ export default class BulkSellButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/ChestBuyButton.ts b/src/components/buttons/ChestBuyButton.ts index 8884bce..1f37c19 100644 --- a/src/components/buttons/ChestBuyButton.ts +++ b/src/components/buttons/ChestBuyButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class ChestBuyButton extends Button { constructor() { - super('chest_buy'); + super({ customId: "chest_buy", cooldown: 3, isAuthorOnly: true }); } // customId format: chest_buy: @@ -40,7 +40,4 @@ export default class ChestBuyButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/ChestOpenButton.ts b/src/components/buttons/ChestOpenButton.ts index ec90506..b5fd091 100644 --- a/src/components/buttons/ChestOpenButton.ts +++ b/src/components/buttons/ChestOpenButton.ts @@ -11,7 +11,7 @@ const RARITY_EMOJIS: Record = { export default class ChestOpenButton extends Button { constructor() { - super('chest_open'); + super({ customId: "chest_open", cooldown: 3, isAuthorOnly: true }); } // customId format: chest_open: @@ -64,7 +64,4 @@ export default class ChestOpenButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/ChestStartButton.ts b/src/components/buttons/ChestStartButton.ts index befc8a9..0a11c23 100644 --- a/src/components/buttons/ChestStartButton.ts +++ b/src/components/buttons/ChestStartButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class ChestStartButton extends Button { constructor() { - super('chest_start'); + super({ customId: "chest_start", cooldown: 2, isAuthorOnly: true }); } // customId format: chest_start: @@ -40,7 +40,4 @@ export default class ChestStartButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/CollectButton.ts b/src/components/buttons/CollectButton.ts index f7b4877..13c152f 100644 --- a/src/components/buttons/CollectButton.ts +++ b/src/components/buttons/CollectButton.ts @@ -3,7 +3,7 @@ import Button from "../../structures/Button"; export default class CollectButton extends Button { constructor() { - super('collect'); + super({ customId: "collect", cooldown: 2, isAuthorOnly: true }); } // customId format: collect:: @@ -22,7 +22,4 @@ export default class CollectButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/ConsumeButton.ts b/src/components/buttons/ConsumeButton.ts index dfedf63..5a7ada0 100644 --- a/src/components/buttons/ConsumeButton.ts +++ b/src/components/buttons/ConsumeButton.ts @@ -3,7 +3,7 @@ import Button from "../../structures/Button"; export default class ConsumeButton extends Button { constructor() { - super('consume'); + super({ customId: "consume", cooldown: 2, isAuthorOnly: true }); } // customId format: consume:: @@ -22,7 +22,4 @@ export default class ConsumeButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/DismantleButton.ts b/src/components/buttons/DismantleButton.ts index cca69bf..d27ae60 100644 --- a/src/components/buttons/DismantleButton.ts +++ b/src/components/buttons/DismantleButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class DismantleButton extends Button { constructor() { - super('dismantle'); + super({ customId: "dismantle", cooldown: 3, isAuthorOnly: true }); } // customId format: dismantle::: @@ -56,7 +56,4 @@ export default class DismantleButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/EmbedAttackButton.ts b/src/components/buttons/EmbedAttackButton.ts index e62486f..60c464e 100644 --- a/src/components/buttons/EmbedAttackButton.ts +++ b/src/components/buttons/EmbedAttackButton.ts @@ -7,7 +7,7 @@ import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class EmbedAttackButton extends Button { - constructor() { super('embedAttack'); } + constructor() { super({ customId: "embedAttack", cooldown: 1.8, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferUpdate(); @@ -32,7 +32,4 @@ export default class EmbedAttackButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 1.8; } } \ No newline at end of file diff --git a/src/components/buttons/EmbedFleeButton.ts b/src/components/buttons/EmbedFleeButton.ts index 43a4ba2..c1f6e11 100644 --- a/src/components/buttons/EmbedFleeButton.ts +++ b/src/components/buttons/EmbedFleeButton.ts @@ -7,7 +7,7 @@ import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class EmbedFleeButton extends Button { - constructor() { super('embedFlee'); } + constructor() { super({ customId: "embedFlee", cooldown: 1.8, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferUpdate(); @@ -32,7 +32,4 @@ export default class EmbedFleeButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 1.8; } } \ No newline at end of file diff --git a/src/components/buttons/EnhanceButton.ts b/src/components/buttons/EnhanceButton.ts index 2d4271e..fe5da8b 100644 --- a/src/components/buttons/EnhanceButton.ts +++ b/src/components/buttons/EnhanceButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class EnhanceButton extends Button { constructor() { - super('enhance'); + super({ customId: "enhance", cooldown: 3, isAuthorOnly: true }); } // customId format: enhance:: @@ -58,7 +58,4 @@ export default class EnhanceButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/EquipButton.ts b/src/components/buttons/EquipButton.ts index 18d296b..034bc1f 100644 --- a/src/components/buttons/EquipButton.ts +++ b/src/components/buttons/EquipButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class EquipButton extends Button { constructor() { - super('equip'); + super({ customId: "equip", cooldown: 3, isAuthorOnly: true }); } // customId format: equip:: @@ -39,7 +39,4 @@ export default class EquipButton extends Button { await interaction.editReply({ files: [], components: [], content: formatError(err.message, err.code), embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/ExploreAgainButton.ts b/src/components/buttons/ExploreAgainButton.ts index 3470b35..8e328e1 100644 --- a/src/components/buttons/ExploreAgainButton.ts +++ b/src/components/buttons/ExploreAgainButton.ts @@ -8,7 +8,7 @@ import { IStepJSON } from "../../interfaces/IStepJSON"; export default class ExploreAgainButton extends Button { constructor() { - super('explore_again'); + super({ customId: "explore_again", cooldown: 7, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { @@ -38,7 +38,4 @@ export default class ExploreAgainButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 7; } } diff --git a/src/components/buttons/ExploreButton.ts b/src/components/buttons/ExploreButton.ts index b779536..3cb8a04 100644 --- a/src/components/buttons/ExploreButton.ts +++ b/src/components/buttons/ExploreButton.ts @@ -7,7 +7,7 @@ import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class ExploreButton extends Button { - constructor() { super('explore'); } + constructor() { super({ customId: "explore", cooldown: 7, isAuthorOnly: false }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -37,7 +37,4 @@ export default class ExploreButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isAuthorOnly(): boolean { return false; } - public cooldown(): number { return 7; } } \ No newline at end of file diff --git a/src/components/buttons/FleeButton.ts b/src/components/buttons/FleeButton.ts index 33f8fc0..0e2dee4 100644 --- a/src/components/buttons/FleeButton.ts +++ b/src/components/buttons/FleeButton.ts @@ -7,7 +7,7 @@ import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class FleeButton extends Button { - constructor() { super('flee'); } + constructor() { super({ customId: "flee", cooldown: 1.8, isAuthorOnly: false }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -32,7 +32,4 @@ export default class FleeButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } - - public isAuthorOnly(): boolean { return false; } - public cooldown(): number { return 1.8; } } \ No newline at end of file diff --git a/src/components/buttons/GuideNavButton.ts b/src/components/buttons/GuideNavButton.ts index 6f15628..5c72411 100644 --- a/src/components/buttons/GuideNavButton.ts +++ b/src/components/buttons/GuideNavButton.ts @@ -18,7 +18,7 @@ const SECTION_ORDER = ['basics', 'combat', 'workshop', 'economy', 'tasks', 'zone export default class GuideNavButton extends Button { constructor() { - super('guide_nav'); + super({ customId: "guide_nav", cooldown: 1, isAuthorOnly: false }); } // customId format: guide_nav: @@ -31,8 +31,5 @@ export default class GuideNavButton extends Button { content: `📖 Use \`/guide ${section}\` to view the **${SECTIONS[section]?.title ?? section}** section.`, ephemeral: true, }); - } - - public isAuthorOnly(): boolean { return false; } // Anyone can navigate the guide - public cooldown(): number { return 1; } + } // Anyone can navigate the guide } diff --git a/src/components/buttons/LockButton.ts b/src/components/buttons/LockButton.ts index e8ba871..85d8627 100644 --- a/src/components/buttons/LockButton.ts +++ b/src/components/buttons/LockButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class LockButton extends Button { constructor() { - super('lock'); + super({ customId: "lock", cooldown: 2, isAuthorOnly: true }); } // customId format: lock:: @@ -46,7 +46,4 @@ export default class LockButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/MarketBuyButton.ts b/src/components/buttons/MarketBuyButton.ts index c975550..1bbcacc 100644 --- a/src/components/buttons/MarketBuyButton.ts +++ b/src/components/buttons/MarketBuyButton.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class MarketBuyButton extends Button { - constructor() { super('mkt_buy'); } + constructor() { super({ customId: "mkt_buy", cooldown: 3, isAuthorOnly: false }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -35,7 +35,4 @@ export default class MarketBuyButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return false; } - public cooldown(): number { return 3; } } \ No newline at end of file diff --git a/src/components/buttons/MarketCancelButton.ts b/src/components/buttons/MarketCancelButton.ts index 27f79d7..1d28df4 100644 --- a/src/components/buttons/MarketCancelButton.ts +++ b/src/components/buttons/MarketCancelButton.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class MarketCancelButton extends Button { - constructor() { super('mkt_cancel'); } + constructor() { super({ customId: "mkt_cancel", cooldown: 3, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -34,7 +34,4 @@ export default class MarketCancelButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } \ No newline at end of file diff --git a/src/components/buttons/MarketNextButton.ts b/src/components/buttons/MarketNextButton.ts index e72a2a8..682f878 100644 --- a/src/components/buttons/MarketNextButton.ts +++ b/src/components/buttons/MarketNextButton.ts @@ -3,13 +3,10 @@ import Button from "../../structures/Button"; import { handleMarketPage } from "./MarketPrevButton"; export default class MarketNextButton extends Button { - constructor() { super('mkt_next'); } + constructor() { super({ customId: "mkt_next", cooldown: 2, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferUpdate(); await handleMarketPage(interaction, args, 1); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } \ No newline at end of file diff --git a/src/components/buttons/MarketPrevButton.ts b/src/components/buttons/MarketPrevButton.ts index 353f7fd..2b8eaae 100644 --- a/src/components/buttons/MarketPrevButton.ts +++ b/src/components/buttons/MarketPrevButton.ts @@ -6,15 +6,12 @@ import Routes from "../../utilities/Routes"; import MarketImageBuilder, { type MarketListing, type MarketPageConfig } from "../../utilities/MarketImageBuilder"; export default class MarketPrevButton extends Button { - constructor() { super('mkt_prev'); } + constructor() { super({ customId: "mkt_prev", cooldown: 2, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferUpdate(); await handleMarketPage(interaction, args, -1); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } /** diff --git a/src/components/buttons/MarketRedirectButton.ts b/src/components/buttons/MarketRedirectButton.ts index 4662de2..d6663ea 100644 --- a/src/components/buttons/MarketRedirectButton.ts +++ b/src/components/buttons/MarketRedirectButton.ts @@ -3,7 +3,7 @@ import Button from "../../structures/Button"; export default class MarketRedirectButton extends Button { constructor() { - super('market_redirect'); + super({ customId: "market_redirect", cooldown: 2, isAuthorOnly: true }); } // customId format: market_redirect: @@ -15,7 +15,4 @@ export default class MarketRedirectButton extends Button { flags: MessageFlags.Ephemeral, }); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/MarketSellPageButton.ts b/src/components/buttons/MarketSellPageButton.ts index 10a64b9..38d6a9c 100644 --- a/src/components/buttons/MarketSellPageButton.ts +++ b/src/components/buttons/MarketSellPageButton.ts @@ -5,7 +5,7 @@ import { buildSellPage } from "../../commands/MarketCommand"; export default class MarketSellPageButton extends Button { constructor() { - super('mkt_sell_page'); + super({ customId: "mkt_sell_page", cooldown: 2, isAuthorOnly: true }); } // customId format: mkt_sell_page: @@ -21,7 +21,4 @@ export default class MarketSellPageButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), components: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/ReforgeButton.ts b/src/components/buttons/ReforgeButton.ts index dc5f80e..641f3e9 100644 --- a/src/components/buttons/ReforgeButton.ts +++ b/src/components/buttons/ReforgeButton.ts @@ -7,7 +7,7 @@ import Button from "../../structures/Button"; export default class ReforgeButton extends Button { constructor() { - super('reforge'); + super({ customId: "reforge", cooldown: 3, isAuthorOnly: true }); } // customId format: reforge:: @@ -49,7 +49,4 @@ export default class ReforgeButton extends Button { ephemeral: true, }); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/RegisterAcceptButton.ts b/src/components/buttons/RegisterAcceptButton.ts index 0c9854a..6177a6b 100644 --- a/src/components/buttons/RegisterAcceptButton.ts +++ b/src/components/buttons/RegisterAcceptButton.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class RegisterAcceptButton extends Button { - constructor() { super('register_accept'); } + constructor() { super({ customId: "register_accept", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferUpdate(); @@ -76,7 +76,4 @@ export default class RegisterAcceptButton extends Button { }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } \ No newline at end of file diff --git a/src/components/buttons/RegisterDeclineButton.ts b/src/components/buttons/RegisterDeclineButton.ts index 0dd8765..8155073 100644 --- a/src/components/buttons/RegisterDeclineButton.ts +++ b/src/components/buttons/RegisterDeclineButton.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, Client } from "discord.js"; import Button from "../../structures/Button"; export default class RegisterDeclineButton extends Button { - constructor() { super('register_decline'); } + constructor() { super({ customId: "register_decline", cooldown: 3, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client): Promise { await interaction.deferUpdate(); @@ -13,7 +13,4 @@ export default class RegisterDeclineButton extends Button { components: [], }); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } \ No newline at end of file diff --git a/src/components/buttons/RestButton.ts b/src/components/buttons/RestButton.ts index 17a8429..6e5afb4 100644 --- a/src/components/buttons/RestButton.ts +++ b/src/components/buttons/RestButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class RestButton extends Button { constructor() { - super('rest'); + super({ customId: "rest", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { @@ -37,7 +37,4 @@ export default class RestButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/components/buttons/SellButton.ts b/src/components/buttons/SellButton.ts index 24ce3d8..6c3febf 100644 --- a/src/components/buttons/SellButton.ts +++ b/src/components/buttons/SellButton.ts @@ -3,7 +3,7 @@ import Button from "../../structures/Button"; export default class SellButton extends Button { constructor() { - super('sell'); + super({ customId: "sell", cooldown: 2, isAuthorOnly: true }); } // customId format: sell:: @@ -22,7 +22,4 @@ export default class SellButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/buttons/SkillPointsButton.ts b/src/components/buttons/SkillPointsButton.ts index 84d9721..4bb95ba 100644 --- a/src/components/buttons/SkillPointsButton.ts +++ b/src/components/buttons/SkillPointsButton.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, Client, LabelBuilder, ModalBuilder, TextDisplayBuild import Button from "../../structures/Button"; export default class SkillPointsButton extends Button { - constructor() { super('skillpoints'); } + constructor() { super({ customId: "skillpoints", cooldown: 3, isAuthorOnly: true }); } public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { const availablePoints = parseInt(args?.[0] ?? '0', 10); @@ -42,7 +42,4 @@ export default class SkillPointsButton extends Button { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } \ No newline at end of file diff --git a/src/components/buttons/TaskClaimButton.ts b/src/components/buttons/TaskClaimButton.ts index 1c36bda..da7ddc2 100644 --- a/src/components/buttons/TaskClaimButton.ts +++ b/src/components/buttons/TaskClaimButton.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class TaskClaimButton extends Button { constructor() { - super('task_claim'); + super({ customId: "task_claim", cooldown: 3, isAuthorOnly: true }); } // customId format: task_claim:: @@ -57,7 +57,4 @@ export default class TaskClaimButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/buttons/TasksTabButton.ts b/src/components/buttons/TasksTabButton.ts index 796073e..0245f76 100644 --- a/src/components/buttons/TasksTabButton.ts +++ b/src/components/buttons/TasksTabButton.ts @@ -8,7 +8,7 @@ import type { ITaskJSON } from "../../interfaces/IGameJSON"; export default class TasksTabButton extends Button { constructor() { - super('tasks_tab'); + super({ customId: "tasks_tab", cooldown: 3, isAuthorOnly: true }); } // customId format: tasks_tab: @@ -78,7 +78,4 @@ export default class TasksTabButton extends Button { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/menus/InvSelectMenu.ts b/src/components/menus/InvSelectMenu.ts index 4a11b0b..8975d11 100644 --- a/src/components/menus/InvSelectMenu.ts +++ b/src/components/menus/InvSelectMenu.ts @@ -9,7 +9,7 @@ import type { IPlayerJSON } from "../../interfaces/IPlayerJSON"; export default class InvSelectMenu extends SelectMenu { constructor() { - super('inv_select'); + super({ customId: "inv_select", cooldown: 3, isAuthorOnly: true }); } // customId format: inv_select: — value is the _id of the selected item @@ -34,8 +34,8 @@ export default class InvSelectMenu extends SelectMenu { return; } - const inventory: IInventoryItem[] = body.data.inventory || []; - const player: IPlayerJSON = body.data.player; + const inventory: IInventoryItem[] = body.builder.inventory || []; + const player: IPlayerJSON = body.builder.player; // Find the exact item by _id const item = inventory.find((inv: IInventoryItem) => inv._id === docId); @@ -52,7 +52,4 @@ export default class InvSelectMenu extends SelectMenu { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/menus/MarketSellMenu.ts b/src/components/menus/MarketSellMenu.ts index e6ca3e6..e199dfb 100644 --- a/src/components/menus/MarketSellMenu.ts +++ b/src/components/menus/MarketSellMenu.ts @@ -4,7 +4,7 @@ import ItemManager from "../../managers/ItemManager"; export default class MarketSellMenu extends SelectMenu { constructor() { - super('mkt_sell_select'); + super({ customId: "mkt_sell_select", cooldown: 3, isAuthorOnly: true }); } // select value format: docId:itemId:maxQuantity @@ -52,7 +52,4 @@ export default class MarketSellMenu extends SelectMenu { await interaction.showModal(modal); } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/menus/ReforgeSelectMenu.ts b/src/components/menus/ReforgeSelectMenu.ts index 1249c46..ddfa221 100644 --- a/src/components/menus/ReforgeSelectMenu.ts +++ b/src/components/menus/ReforgeSelectMenu.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class ReforgeSelectMenu extends SelectMenu { constructor() { - super('reforge_select'); + super({ customId: "reforge_select", cooldown: 3, isAuthorOnly: true }); } // customId format: reforge_select:: @@ -78,7 +78,4 @@ export default class ReforgeSelectMenu extends SelectMenu { await interaction.editReply({ content: formatError(err.message, err.code), components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/menus/TravelSelectMenu.ts b/src/components/menus/TravelSelectMenu.ts index 2bc06f7..f457fe2 100644 --- a/src/components/menus/TravelSelectMenu.ts +++ b/src/components/menus/TravelSelectMenu.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; import { getZone } from "../../utilities/ZoneData"; export default class TravelSelectMenu extends SelectMenu { - constructor() { super('travel_select'); } + constructor() { super({ customId: "travel_select", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: AnySelectMenuInteraction, client: Client): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -40,7 +40,4 @@ export default class TravelSelectMenu extends SelectMenu { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } \ No newline at end of file diff --git a/src/components/menus/UnequipMenu.ts b/src/components/menus/UnequipMenu.ts index 7c091e2..312eaf3 100644 --- a/src/components/menus/UnequipMenu.ts +++ b/src/components/menus/UnequipMenu.ts @@ -9,7 +9,7 @@ import { EquipmentSlot } from "../../interfaces/IItemJSON"; export default class UnequipMenu extends SelectMenu { constructor() { - super('unequip'); + super({ customId: "unequip", cooldown: 2, isAuthorOnly: true }); } public async execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise { @@ -66,12 +66,4 @@ export default class UnequipMenu extends SelectMenu { return; } } - - public isAuthorOnly(): boolean { - return true; - } - - public cooldown(): number { - return 2; - } } diff --git a/src/components/modals/BulkCollectModal.ts b/src/components/modals/BulkCollectModal.ts index 413d1cd..c3a904a 100644 --- a/src/components/modals/BulkCollectModal.ts +++ b/src/components/modals/BulkCollectModal.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class BulkCollectModal extends ModalSubmit { - constructor() { super('bulk_collect_modal'); } + constructor() { super({ customId: "bulk_collect_modal", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -50,7 +50,4 @@ export default class BulkCollectModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code), embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/components/modals/BulkDismantleModal.ts b/src/components/modals/BulkDismantleModal.ts index afc8828..78f23a9 100644 --- a/src/components/modals/BulkDismantleModal.ts +++ b/src/components/modals/BulkDismantleModal.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class BulkDismantleModal extends ModalSubmit { - constructor() { super('bulk_dismantle_modal'); } + constructor() { super({ customId: "bulk_dismantle_modal", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -50,7 +50,4 @@ export default class BulkDismantleModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/components/modals/BulkSellModal.ts b/src/components/modals/BulkSellModal.ts index 50813eb..93ab095 100644 --- a/src/components/modals/BulkSellModal.ts +++ b/src/components/modals/BulkSellModal.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class BulkSellModal extends ModalSubmit { - constructor() { super('bulk_sell_modal'); } + constructor() { super({ customId: "bulk_sell_modal", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -51,7 +51,4 @@ export default class BulkSellModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public cooldown(): number { return 5; } - public isAuthorOnly(): boolean { return true; } } diff --git a/src/components/modals/CollectModal.ts b/src/components/modals/CollectModal.ts index e521745..19d7df9 100644 --- a/src/components/modals/CollectModal.ts +++ b/src/components/modals/CollectModal.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class CollectModal extends ModalSubmit { constructor() { - super('collect'); + super({ customId: "collect", cooldown: 2, isAuthorOnly: true }); } // customId format: collect: @@ -40,7 +40,4 @@ export default class CollectModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code), components: [], files: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 2; } } diff --git a/src/components/modals/ConsumeModal.ts b/src/components/modals/ConsumeModal.ts index 5ba5d3f..8a5921e 100644 --- a/src/components/modals/ConsumeModal.ts +++ b/src/components/modals/ConsumeModal.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class ConsumeModal extends ModalSubmit { constructor() { - super('consume'); + super({ customId: "consume", cooldown: 3, isAuthorOnly: true }); } // customId format: consume: @@ -40,7 +40,4 @@ export default class ConsumeModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/modals/MarketSellModal.ts b/src/components/modals/MarketSellModal.ts index 3bbbac2..a777a23 100644 --- a/src/components/modals/MarketSellModal.ts +++ b/src/components/modals/MarketSellModal.ts @@ -7,7 +7,7 @@ import ItemManager from "../../managers/ItemManager"; export default class MarketSellModal extends ModalSubmit { constructor() { - super('mkt_sell_modal'); + super({ customId: "mkt_sell_modal", cooldown: 5, isAuthorOnly: true }); } // customId format: mkt_sell_modal:: @@ -77,7 +77,4 @@ export default class MarketSellModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } diff --git a/src/components/modals/SellModal.ts b/src/components/modals/SellModal.ts index fbba15e..e378a45 100644 --- a/src/components/modals/SellModal.ts +++ b/src/components/modals/SellModal.ts @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; export default class SellModal extends ModalSubmit { constructor() { - super('sell'); + super({ customId: "sell", cooldown: 3, isAuthorOnly: true }); } // customId format: sell: @@ -43,7 +43,4 @@ export default class SellModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 3; } } diff --git a/src/components/modals/SkillPointsModal.ts b/src/components/modals/SkillPointsModal.ts index 8520ac1..83a36a1 100644 --- a/src/components/modals/SkillPointsModal.ts +++ b/src/components/modals/SkillPointsModal.ts @@ -5,7 +5,7 @@ import { formatError } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; export default class SkillPointsModal extends ModalSubmit { - constructor() { super('skillpoints_modal'); } + constructor() { super({ customId: "skillpoints_modal", cooldown: 5, isAuthorOnly: true }); } public async execute(interaction: ModalSubmitInteraction, client: Client): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -67,7 +67,4 @@ export default class SkillPointsModal extends ModalSubmit { await interaction.editReply({ content: formatError(err.message, err.code) }); } } - - public isAuthorOnly(): boolean { return true; } - public cooldown(): number { return 5; } } \ No newline at end of file diff --git a/src/events/ClientReadyEvent.ts b/src/events/ClientReadyEvent.ts index cbf9a04..a3f41fc 100644 --- a/src/events/ClientReadyEvent.ts +++ b/src/events/ClientReadyEvent.ts @@ -7,7 +7,10 @@ import PresenceManager from "../managers/PresenceManager"; export default class ClientReadyEvent extends Event { constructor() { - super('clientReady'); + super({ + name: 'clientReady', + isOnce: true + }); } public async execute(client: Client) { @@ -33,8 +36,4 @@ export default class ClientReadyEvent extends Event { await PresenceManager.init(client); }, delayMs); } - - public isOnce(): boolean { - return true; - } } diff --git a/src/events/GuildCreateEvent.ts b/src/events/GuildCreateEvent.ts index 0b8780e..07fd227 100644 --- a/src/events/GuildCreateEvent.ts +++ b/src/events/GuildCreateEvent.ts @@ -4,7 +4,10 @@ import logger from "../utilities/Logger"; export default class GuildCreateEvent extends Event { constructor() { - super(Events.GuildCreate); + super({ + name: Events.GuildCreate, + isOnce: false + }); } public async execute(guild: Guild, client: Client): Promise { @@ -78,8 +81,4 @@ export default class GuildCreateEvent extends Event { logger.info(`Joined a new guild! ${guild.name} (${guild.id})`); } - - public isOnce(): boolean { - return false; - } } \ No newline at end of file diff --git a/src/events/InteractionCreateEvent.ts b/src/events/InteractionCreateEvent.ts index ed44d89..1b64988 100644 --- a/src/events/InteractionCreateEvent.ts +++ b/src/events/InteractionCreateEvent.ts @@ -10,7 +10,10 @@ import { ApiError } from "../utilities/ApiClient"; export default class InteractionCreateEvent extends Event { constructor() { - super('interactionCreate'); + super({ + name: 'interactionCreate', + isOnce: false + }); } private async handleError(interaction: BaseInteraction, err: any) { @@ -59,8 +62,4 @@ export default class InteractionCreateEvent extends Event { await this.handleError(interaction, err); } } - - public isOnce(): boolean { - return false; - } } \ No newline at end of file diff --git a/src/handlers/ButtonHandler.ts b/src/handlers/ButtonHandler.ts index dc972ce..6e48791 100644 --- a/src/handlers/ButtonHandler.ts +++ b/src/handlers/ButtonHandler.ts @@ -45,7 +45,7 @@ export default class ButtonHandler { return; } - if (button.isAuthorOnly() && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; + if (button.isAuthorOnly && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; let key = `b-${id}-${interaction.user.id}`; if (customId === 'startNewDay') key = `adventure-${interaction.user.id}`; @@ -56,7 +56,7 @@ export default class ButtonHandler { } await button.execute(interaction, client, target); - CooldownManager.addCooldown(key, button.cooldown()); + CooldownManager.addCooldown(key, button.cooldown); logger.button(`${interaction.user.username} (${interaction.user.id}) used '${customId}'`); } catch (err) { throw err; diff --git a/src/handlers/EventHandler.ts b/src/handlers/EventHandler.ts index 7b66910..d9c0b00 100644 --- a/src/handlers/EventHandler.ts +++ b/src/handlers/EventHandler.ts @@ -21,10 +21,10 @@ export default class EventHandler { event = new event.default(); if (!(event instanceof Event)) continue; - if (event.isOnce()) { - this.client.once(event.getName(), (...args: any[]) => event.execute(...args, this.client)); + if (event.isOnce) { + this.client.once(event.name, (...args: any[]) => event.execute(...args, this.client)); } else { - this.client.on(event.getName(), (...args: any[]) => event.execute(...args, this.client)); + this.client.on(event.name, (...args: any[]) => event.execute(...args, this.client)); } } } diff --git a/src/handlers/ModalSubmitHandler.ts b/src/handlers/ModalSubmitHandler.ts index 7b10af1..8a517f4 100644 --- a/src/handlers/ModalSubmitHandler.ts +++ b/src/handlers/ModalSubmitHandler.ts @@ -47,7 +47,7 @@ export default class ModalSubmitHandler { if (CooldownManager.onCooldown(key)) return; await modal.execute(interaction, client, target); - CooldownManager.addCooldown(key, modal.cooldown()); + CooldownManager.addCooldown(key, modal.cooldown); } catch (err) { throw err; } diff --git a/src/handlers/SelectMenuHandler.ts b/src/handlers/SelectMenuHandler.ts index 56a1409..d6352e6 100644 --- a/src/handlers/SelectMenuHandler.ts +++ b/src/handlers/SelectMenuHandler.ts @@ -42,14 +42,14 @@ export default class SelectMenuHandler { const menu = this._cache.get(id); if (!menu) throw new Error(`No executable data could be found for menu with ID: ${customId}`); - if (menu.isAuthorOnly() && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; + if (menu.isAuthorOnly && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; const key = `s-${customId}-${interaction.user.id}`; if (CooldownManager.onCooldown(key)) return; await menu.execute(interaction, client, target); - CooldownManager.addCooldown(key, menu.cooldown()); + CooldownManager.addCooldown(key, menu.cooldown); } catch (err) { throw err; } diff --git a/src/handlers/SlashCommandHandler.ts b/src/handlers/SlashCommandHandler.ts index 135349f..848299b 100644 --- a/src/handlers/SlashCommandHandler.ts +++ b/src/handlers/SlashCommandHandler.ts @@ -22,8 +22,7 @@ export default class SlashCommandHandler { let command = require(join(filePath, file)); command = new command.default(); if (!(command instanceof SlashCommand)) continue; - const commandData = command.getData(); - this._cache.set(commandData.name, command); + this._cache.set(command.data.name, command); } logger.info(`[SlashCommandHandler] Cached a total of ${this._cache.size} commands`); @@ -49,7 +48,7 @@ export default class SlashCommandHandler { await command.execute(interaction, client); - CooldownManager.addCooldown(key, command.cooldown()); + CooldownManager.addCooldown(key, command.cooldown); logger.command(`/${name} | ${interaction.user.username} (${interaction.user.id}) | ${interaction.guild?.name ?? 'DM'} | ${Date.now() - startTime}ms`); } catch (err) { throw err; diff --git a/src/interfaces/ICooldown.ts b/src/interfaces/ICooldown.ts deleted file mode 100644 index 0ceea96..0000000 --- a/src/interfaces/ICooldown.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface ICooldown { - cooldown(): number; -} \ No newline at end of file diff --git a/src/structures/Button.ts b/src/structures/Button.ts index bfdb262..6406aec 100644 --- a/src/structures/Button.ts +++ b/src/structures/Button.ts @@ -1,17 +1,30 @@ import IExecutable from "../interfaces/IExecutable"; -import ICooldown from "../interfaces/ICooldown"; import { ButtonInteraction, Client } from "discord.js"; -export default abstract class Button implements IExecutable, ICooldown { - public customId: string; +export interface ButtonOptions { + customId: string; + cooldown: number; + isAuthorOnly: boolean; +} - constructor(customId: string) { - this.customId = customId; +export default abstract class Button implements IExecutable { + private readonly options: ButtonOptions; + + constructor(options: ButtonOptions) { + this.options = options; } - public abstract isAuthorOnly(): boolean; + public get customId(): string { + return this.options.customId; + } - public abstract execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise; + public get cooldown(): number { + return this.options.cooldown; + } - public abstract cooldown(): number; + public get isAuthorOnly(): boolean { + return this.options.isAuthorOnly; + } + + public abstract execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise; } \ No newline at end of file diff --git a/src/structures/Event.ts b/src/structures/Event.ts index 923870d..b5427f1 100644 --- a/src/structures/Event.ts +++ b/src/structures/Event.ts @@ -1,17 +1,24 @@ import IExecutable from "../interfaces/IExecutable"; +export interface EventOptions { + name: string; + isOnce: boolean; +} + export default abstract class Event implements IExecutable { - protected name: string; + private readonly options: EventOptions; - constructor(name: string) { - this.name = name; + constructor(options: EventOptions) { + this.options = options; } - public abstract execute(...args: any[]): Promise; - - public abstract isOnce(): boolean; + public get name(): string { + return this.options.name; + } - public getName(): string { - return this.name; + public get isOnce(): boolean { + return this.options.isOnce; } + + public abstract execute(...args: any[]): Promise; } \ No newline at end of file diff --git a/src/structures/ModalSubmit.ts b/src/structures/ModalSubmit.ts index 120a3e5..e4fc492 100644 --- a/src/structures/ModalSubmit.ts +++ b/src/structures/ModalSubmit.ts @@ -1,17 +1,30 @@ import { ModalSubmitInteraction, Client } from "discord.js"; import IExecutable from "../interfaces/IExecutable"; -import ICooldown from "../interfaces/ICooldown"; -export default abstract class ModalSubmit implements IExecutable, ICooldown { - public customId: string; +export interface ModalSubmitOptions { + customId: string; + cooldown: number; + isAuthorOnly: boolean; +} - constructor(customId: string) { - this.customId = customId; +export default abstract class ModalSubmit implements IExecutable { + private readonly options: ModalSubmitOptions; + + constructor(options: ModalSubmitOptions) { + this.options = options; } - public abstract execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise; + public get customId(): string { + return this.options.customId; + } - public abstract isAuthorOnly(): boolean; + public get cooldown(): number { + return this.options.cooldown; + } - public abstract cooldown(): number; + public get isAuthorOnly(): boolean { + return this.options.isAuthorOnly; + } + + public abstract execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise; } \ No newline at end of file diff --git a/src/structures/SelectMenu.ts b/src/structures/SelectMenu.ts index 1da62cc..f3e3d02 100644 --- a/src/structures/SelectMenu.ts +++ b/src/structures/SelectMenu.ts @@ -1,17 +1,30 @@ import { AnySelectMenuInteraction, Client } from "discord.js"; import IExecutable from "../interfaces/IExecutable"; -import ICooldown from "../interfaces/ICooldown"; -export default abstract class SelectMenu implements IExecutable, ICooldown { - public customId: string; +export interface SelectMenuOptions { + customId: string; + cooldown: number; + isAuthorOnly: boolean; +} - constructor(customId: string) { - this.customId = customId; +export default abstract class SelectMenu implements IExecutable { + private readonly options: SelectMenuOptions; + + constructor(options: SelectMenuOptions) { + this.options = options; } - public abstract isAuthorOnly(): boolean; + public get customId(): string { + return this.options.customId; + } - public abstract execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise; + public get cooldown(): number { + return this.options.cooldown; + } - public abstract cooldown(): number; + public get isAuthorOnly(): boolean { + return this.options.isAuthorOnly; + } + + public abstract execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise; } \ No newline at end of file diff --git a/src/structures/SlashCommand.ts b/src/structures/SlashCommand.ts index a5012d1..a92dd59 100644 --- a/src/structures/SlashCommand.ts +++ b/src/structures/SlashCommand.ts @@ -1,44 +1,51 @@ import { AutocompleteInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; import IExecutable from "../interfaces/IExecutable"; -import ICooldown from "../interfaces/ICooldown"; import { Client } from "discord.js"; -export default abstract class SlashCommand implements IExecutable, ICooldown { - protected name: string; - protected description: string; - protected category: string; - protected data: SlashCommandBuilder; - - constructor(name: string, description: string, category: string) { - this.name = name; - this.description = description; - this.category = category; - this.data = new SlashCommandBuilder() - .setName(name) - .setDescription(description); +export interface SlashCommandOptions { + name: string; + description: string; + category: string; + cooldown: number; + isGlobalCommand: boolean; +} + +export default abstract class SlashCommand implements IExecutable { + private readonly options: SlashCommandOptions; + protected builder: SlashCommandBuilder; + + constructor(options: SlashCommandOptions) { + this.options = options; + this.builder = new SlashCommandBuilder() + .setName(options.name) + .setDescription(options.description); } - public abstract execute(interaction: ChatInputCommandInteraction, client: Client): Promise; - - public abstract cooldown(): number; - - public abstract isGlobalCommand(): boolean; + public get name(): string { + return this.options.name; + } - public async autocomplete?(interaction: AutocompleteInteraction, client: Client): Promise; + public get description(): string { + return this.options.description; + } - public getData(): SlashCommandBuilder { - return this.data; + public get category(): string { + return this.options.category; } - public getName(): string { - return this.name; + public get cooldown(): number { + return this.options.cooldown; } - public getDescription(): string { - return this.description; + public get isGlobalCommand(): boolean { + return this.options.isGlobalCommand; } - public getCategory(): string { - return this.category; + public get data(): SlashCommandBuilder { + return this.builder; } + + public abstract execute(interaction: ChatInputCommandInteraction, client: Client): Promise; + + public async autocomplete?(interaction: AutocompleteInteraction, client: Client): Promise; } \ No newline at end of file From 5f004970c1ee67497776dd60745c95a17fd14581 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:08:31 +0700 Subject: [PATCH 2/7] ci: add eslint and apply automatic eslint fixes --- eslint.config.mjs | 197 +++ package-lock.json | 1273 ++++++++++++++++- package.json | 6 + src/commands/AttackCommand.ts | 4 +- src/commands/ChestsCommand.ts | 6 +- src/commands/CollectionCommand.ts | 21 +- src/commands/ExploreCommand.ts | 6 +- src/commands/FleeCommand.ts | 4 +- src/commands/GuideCommand.ts | 49 +- src/commands/HelpCommand.ts | 6 +- src/commands/InventoryCommand.ts | 12 +- src/commands/LeaderboardCommand.ts | 27 +- src/commands/LookupCommand.ts | 184 +-- src/commands/MarketCommand.ts | 45 +- src/commands/NetworkCommand.ts | 241 ++-- src/commands/ProfileCommand.ts | 12 +- src/commands/RegisterCommand.ts | 4 +- src/commands/RestCommand.ts | 8 +- src/commands/TasksCommand.ts | 25 +- src/commands/TestCommand.ts | 2 +- src/commands/TravelCommand.ts | 13 +- src/commands/VoteCommand.ts | 4 +- src/components/buttons/AttackButton.ts | 4 +- src/components/buttons/BulkCollectButton.ts | 4 +- src/components/buttons/BulkDismantleButton.ts | 2 +- src/components/buttons/BulkSellButton.ts | 2 +- src/components/buttons/ChestBuyButton.ts | 6 +- src/components/buttons/ChestOpenButton.ts | 8 +- src/components/buttons/ChestStartButton.ts | 6 +- src/components/buttons/CollectButton.ts | 5 +- src/components/buttons/ConsumeButton.ts | 5 +- src/components/buttons/DismantleButton.ts | 8 +- src/components/buttons/EmbedAttackButton.ts | 4 +- src/components/buttons/EmbedFleeButton.ts | 4 +- src/components/buttons/EnhanceButton.ts | 6 +- src/components/buttons/EquipButton.ts | 4 +- src/components/buttons/ExploreAgainButton.ts | 6 +- src/components/buttons/ExploreButton.ts | 6 +- src/components/buttons/FleeButton.ts | 4 +- src/components/buttons/GuideNavButton.ts | 6 +- src/components/buttons/LockButton.ts | 8 +- src/components/buttons/MarketBuyButton.ts | 4 +- src/components/buttons/MarketCancelButton.ts | 4 +- src/components/buttons/MarketNextButton.ts | 2 +- src/components/buttons/MarketPrevButton.ts | 6 +- .../buttons/MarketRedirectButton.ts | 4 +- .../buttons/MarketSellPageButton.ts | 2 +- src/components/buttons/ReforgeButton.ts | 8 +- .../buttons/RegisterAcceptButton.ts | 28 +- .../buttons/RegisterDeclineButton.ts | 4 +- src/components/buttons/RestButton.ts | 6 +- src/components/buttons/SellButton.ts | 5 +- src/components/buttons/SkillPointsButton.ts | 2 +- src/components/buttons/TaskClaimButton.ts | 6 +- src/components/buttons/TasksTabButton.ts | 8 +- src/components/menus/InvSelectMenu.ts | 2 +- src/components/menus/MarketSellMenu.ts | 26 +- src/components/menus/ReforgeSelectMenu.ts | 8 +- src/components/menus/TravelSelectMenu.ts | 4 +- src/components/menus/UnequipMenu.ts | 12 +- src/components/modals/BulkCollectModal.ts | 4 +- src/components/modals/BulkDismantleModal.ts | 4 +- src/components/modals/BulkSellModal.ts | 4 +- src/components/modals/CollectModal.ts | 4 +- src/components/modals/ConsumeModal.ts | 4 +- src/components/modals/MarketSellModal.ts | 10 +- src/components/modals/SellModal.ts | 6 +- src/components/modals/SkillPointsModal.ts | 6 +- src/events/ClientReadyEvent.ts | 12 +- src/events/GuildCreateEvent.ts | 16 +- src/events/InteractionCreateEvent.ts | 4 +- src/handlers/ButtonHandler.ts | 5 +- src/handlers/EventHandler.ts | 4 +- src/handlers/ModalSubmitHandler.ts | 5 +- src/handlers/SelectMenuHandler.ts | 5 +- src/handlers/SlashCommandHandler.ts | 5 +- src/index.ts | 56 +- src/interfaces/ICollectionJSON.ts | 2 +- src/interfaces/ICombatJSON.ts | 6 +- src/interfaces/IExecutable.ts | 2 +- src/interfaces/IGameJSON.ts | 2 +- src/interfaces/IInventoryJSON.ts | 2 +- src/interfaces/IItemJSON.ts | 2 +- src/interfaces/IItemsJSON.ts | 4 +- src/interfaces/IPlayerJSON.ts | 6 +- src/interfaces/IStepJSON.ts | 4 +- src/managers/CooldownManager.ts | 10 +- src/managers/ItemManager.ts | 4 +- src/managers/PresenceManager.ts | 8 +- src/structures/Button.ts | 4 +- src/structures/Event.ts | 2 +- src/structures/ModalSubmit.ts | 4 +- src/structures/SelectMenu.ts | 4 +- src/structures/SlashCommand.ts | 6 +- src/structures/containers/AttackContainer.ts | 24 +- src/structures/containers/ExploreContainer.ts | 42 +- .../containers/ItemLookupContainer.ts | 20 +- .../containers/NPCLookupContainer.ts | 8 +- src/structures/containers/ProfileContainer.ts | 54 +- .../containers/ScenarioLookupContainer.ts | 17 +- src/utilities/AdventureImageBuilder.ts | 522 +++---- src/utilities/ApiClient.ts | 4 +- src/utilities/ChestsImageBuilder.ts | 6 +- src/utilities/CombatResponseBuilder.ts | 10 +- src/utilities/ErrorMessages.ts | 34 +- src/utilities/ImageService.ts | 18 +- src/utilities/ImageWorker.ts | 76 +- src/utilities/InventoryImageBuilder.ts | 218 +-- src/utilities/ItemImageBuilder.ts | 176 +-- src/utilities/ItemViewBuilder.ts | 12 +- src/utilities/LeaderboardImageBuilder.ts | 8 +- src/utilities/Logger.ts | 16 +- src/utilities/MarketImageBuilder.ts | 6 +- src/utilities/PaginatorBuilder.ts | 18 +- src/utilities/PlayerGuard.ts | 2 +- src/utilities/ProfileImageBuilder.ts | 196 +-- src/utilities/Routes.ts | 2 +- src/utilities/TasksImageBuilder.ts | 10 +- src/utilities/TravelImageBuilder.ts | 12 +- src/utilities/WorkerPool.ts | 6 +- src/utilities/ZoneData.ts | 4 +- 121 files changed, 2765 insertions(+), 1370 deletions(-) create mode 100644 eslint.config.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..dd3df47 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,197 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import stylistic from '@stylistic/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import globals from 'globals' + +export default [ + { + files: ['src/**/*.ts'], + + plugins: { + '@typescript-eslint': typescriptEslint, + '@stylistic': stylistic + }, + + languageOptions: { + globals: { + ...globals.node + }, + + parser: tsParser, + ecmaVersion: 13, + sourceType: 'module', + + parserOptions: { + ecmaFeatures: { + impliedStrict: true + }, + + project: ['./tsconfig.json'] + } + }, + + rules: { + '@typescript-eslint/array-type': [ + 'error', + { + default: 'array', + readonly: 'array' + } + ], + + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-tslint-comment': 'error', + '@typescript-eslint/class-literal-property-style': ['error', 'fields'], + '@stylistic/comma-dangle': ['error'], + '@typescript-eslint/consistent-indexed-object-style': ['error', 'record'], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + fixStyle: 'inline-type-imports', + }, + ], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@stylistic/keyword-spacing': 'error', + '@stylistic/member-delimiter-style': 'error', + '@typescript-eslint/method-signature-style': ['error', 'property'], + '@typescript-eslint/no-array-constructor': 'error', + '@typescript-eslint/no-dupe-class-members': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-extra-non-null-assertion': 'error', + '@stylistic/no-extra-parens': 'error', + '@stylistic/no-extra-semi': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-loop-func': 'error', + '@typescript-eslint/no-loss-of-precision': 'error', + + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksConditionals: true, + checksVoidReturn: false + } + ], + + '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/only-throw-error': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-useless-empty-export': 'error', + '@typescript-eslint/non-nullable-type-assertion-style': 'error', + '@stylistic/object-curly-spacing': ['error', 'always'], + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-literal-enum-member': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-reduce-type-parameter': 'error', + '@typescript-eslint/prefer-return-this-type': 'error', + '@typescript-eslint/require-array-sort-compare': 'error', + '@typescript-eslint/restrict-plus-operands': 'error', + '@stylistic/semi': ['error', 'always'], + '@stylistic/semi-spacing': 'error', + '@stylistic/space-before-blocks': 'error', + '@stylistic/type-annotation-spacing': 'error', + + '@typescript-eslint/typedef': [ + 'error', + { + memberVariableDeclaration: true, + parameter: true, + propertyDeclaration: true, + variableDeclaration: false + } + ], + + '@typescript-eslint/unified-signatures': 'error', + 'block-scoped-var': 'error', + 'block-spacing': 'error', + camelcase: 'error', + 'class-methods-use-this': 'error', + 'comma-style': 'error', + 'default-case-last': 'error', + 'dot-notation': 'error', + 'generator-star-spacing': 'error', + 'getter-return': 'error', + 'implicit-arrow-linebreak': 'error', + indent: ['error', 2], + 'key-spacing': 'error', + 'new-parens': 'error', + 'no-alert': 'error', + 'no-class-assign': 'error', + 'no-const-assign': 'error', + 'no-constructor-return': 'error', + 'no-control-regex': 'error', + 'no-delete-var': 'error', + 'no-dupe-args': 'error', + 'no-dupe-else-if': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-else-return': 'error', + 'no-empty': 'error', + 'no-empty-pattern': 'error', + 'no-eval': 'error', + 'no-ex-assign': 'error', + 'no-extend-native': 'error', + 'no-fallthrough': 'error', + 'no-func-assign': 'error', + 'no-global-assign': 'error', + 'no-import-assign': 'error', + 'no-inner-declarations': 'error', + 'no-invalid-regexp': 'error', + 'no-label-var': 'error', + 'no-lonely-if': 'error', + 'no-mixed-operators': 'error', + 'no-mixed-spaces-and-tabs': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-symbol': 'error', + 'no-new-wrappers': 'error', + 'no-nonoctal-decimal-escape': 'error', + 'no-obj-calls': 'error', + 'no-prototype-builtins': 'error', + + 'no-return-assign': 'error', + 'no-self-assign': 'error', + 'no-setter-return': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-undef-init': 'error', + 'no-unneeded-ternary': 'error', + 'no-unreachable': 'error', + 'no-unsafe-optional-chaining': 'error', + 'no-unused-labels': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-escape': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-whitespace-before-property': 'error', + 'no-with': 'error', + 'object-shorthand': 'error', + 'operator-assignment': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'prefer-exponentiation-operator': 'error', + 'prefer-object-has-own': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'require-yield': 'error', + strict: 'error', + 'switch-colon-spacing': 'error', + 'template-curly-spacing': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + 'yield-star-spacing': 'error' + } + } +] diff --git a/package-lock.json b/package-lock.json index bc45ca5..ece3528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,13 @@ "pino": "^10.2.1" }, "devDependencies": { + "@stylistic/eslint-plugin": "^5.10.0", "@types/mongoose": "^5.11.96", "@types/node": "^20.11.24", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^10.1.0", + "globals": "^17.4.0", "pino-pretty": "^13.1.3", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", @@ -168,6 +173,204 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -493,6 +696,40 @@ "npm": ">=7.0.0" } }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -521,6 +758,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mongoose": { "version": "5.11.96", "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.96.tgz", @@ -578,6 +836,278 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", @@ -601,6 +1131,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", @@ -614,6 +1154,23 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -750,6 +1307,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -760,6 +1332,31 @@ "node": "*" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -839,9 +1436,267 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/fast-copy": { @@ -857,6 +1712,20 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -864,6 +1733,19 @@ "dev": true, "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -877,6 +1759,44 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -944,6 +1864,19 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -964,6 +1897,26 @@ "dev": true, "license": "MIT" }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1045,6 +1998,13 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -1055,6 +2015,27 @@ "node": ">=10" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/kareem": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", @@ -1064,6 +2045,46 @@ "node": ">=18.0.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1235,6 +2256,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1264,6 +2292,66 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1274,6 +2362,16 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1356,6 +2454,16 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -1481,6 +2589,42 @@ ], "license": "BSD-3-Clause" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/sift": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", @@ -1583,6 +2727,54 @@ "node": ">=20" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1618,6 +2810,19 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", @@ -1732,6 +2937,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1761,6 +2979,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -1790,6 +3018,32 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1837,6 +3091,19 @@ "engines": { "node": ">=6" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 9637ea3..2c75454 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "dist/index.js", "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "lint": "eslint", "build": "tsc && node -e \"const fs=require('fs'); ['commands', 'events', 'components/buttons', 'components/menus', 'components/modals'].forEach(d => fs.mkdirSync('./dist/' + d, { recursive: true }));\"", "start": "node dist/index.js" }, @@ -18,8 +19,13 @@ "pino": "^10.2.1" }, "devDependencies": { + "@stylistic/eslint-plugin": "^5.10.0", "@types/mongoose": "^5.11.96", "@types/node": "^20.11.24", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^10.1.0", + "globals": "^17.4.0", "pino-pretty": "^13.1.3", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", diff --git a/src/commands/AttackCommand.ts b/src/commands/AttackCommand.ts index 86a3a20..77803d4 100644 --- a/src/commands/AttackCommand.ts +++ b/src/commands/AttackCommand.ts @@ -1,6 +1,6 @@ -import { ChatInputCommandInteraction, Client } from "discord.js"; +import { type ChatInputCommandInteraction, type Client } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { ICombatJSON } from "../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../interfaces/ICombatJSON"; import { apiFetch } from "../utilities/ApiClient"; import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../utilities/ErrorMessages"; diff --git a/src/commands/ChestsCommand.ts b/src/commands/ChestsCommand.ts index 8060d3b..4313a23 100644 --- a/src/commands/ChestsCommand.ts +++ b/src/commands/ChestsCommand.ts @@ -1,6 +1,6 @@ import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - ChatInputCommandInteraction, Client, EmbedBuilder, + type ChatInputCommandInteraction, type Client, EmbedBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; @@ -54,7 +54,7 @@ export default class ChestsCommand extends SlashCommand { maxSlots, divinePity, pityThreshold, - totalOpened, + totalOpened }); const attachment = new AttachmentBuilder(imageBuffer, { name: 'chests.png' }); @@ -103,7 +103,7 @@ export default class ChestsCommand extends SlashCommand { new ButtonBuilder() .setCustomId('chest_buy:Rare') .setLabel('🔵 Buy Rare') - .setStyle(ButtonStyle.Secondary), + .setStyle(ButtonStyle.Secondary) ); components.push(shopRow); } diff --git a/src/commands/CollectionCommand.ts b/src/commands/CollectionCommand.ts index 9f0f0f3..b272fe6 100644 --- a/src/commands/CollectionCommand.ts +++ b/src/commands/CollectionCommand.ts @@ -1,6 +1,6 @@ -import { ChatInputCommandInteraction, Client, EmbedBuilder } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, EmbedBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { ICollectionJSON } from "../interfaces/ICollectionJSON"; +import { type ICollectionJSON } from "../interfaces/ICollectionJSON"; import ItemManager from "../managers/ItemManager"; import PaginatorBuilder from "../utilities/PaginatorBuilder"; import Routes from "../utilities/Routes"; @@ -17,10 +17,9 @@ export default class CollectionCommand extends SlashCommand { isGlobalCommand: true }); - this.builder.addUserOption((o) => - o.setName('user') - .setDescription('Select a user') - .setRequired(false) + this.builder.addUserOption((o) => o.setName('user') + .setDescription('Select a user') + .setRequired(false) ); } @@ -47,11 +46,11 @@ export default class CollectionCommand extends SlashCommand { // Safely parse the "Map" from JSON into an array of [itemId, quantity] let collectionItems: [string, number][] = []; if (collection?.items) { - if (typeof collection.items === 'object' && !Array.isArray(collection.items)) { - collectionItems = Object.entries(collection.items); - } else if (collection.items instanceof Map) { - collectionItems = Array.from(collection.items.entries()); - } + if (typeof collection.items === 'object' && !Array.isArray(collection.items)) { + collectionItems = Object.entries(collection.items); + } else if (collection.items instanceof Map) { + collectionItems = Array.from(collection.items.entries()); + } } if (collectionItems.length === 0) { diff --git a/src/commands/ExploreCommand.ts b/src/commands/ExploreCommand.ts index bac4e3b..e727843 100644 --- a/src/commands/ExploreCommand.ts +++ b/src/commands/ExploreCommand.ts @@ -1,6 +1,6 @@ -import { ChatInputCommandInteraction, Client } from "discord.js"; +import { type ChatInputCommandInteraction, type Client } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { IStepJSON } from "../interfaces/IStepJSON"; +import { type IStepJSON } from "../interfaces/IStepJSON"; import { apiFetch } from "../utilities/ApiClient"; import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../utilities/ErrorMessages"; @@ -22,7 +22,7 @@ export default class ExploreCommand extends SlashCommand { const res = await apiFetch(Routes.explore(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id }), + body: JSON.stringify({ discordId: interaction.user.id }) }); const data = await res.json() as IStepJSON; diff --git a/src/commands/FleeCommand.ts b/src/commands/FleeCommand.ts index dc1281d..b0263bc 100644 --- a/src/commands/FleeCommand.ts +++ b/src/commands/FleeCommand.ts @@ -1,6 +1,6 @@ -import { ChatInputCommandInteraction, Client } from "discord.js"; +import { type ChatInputCommandInteraction, type Client } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { ICombatJSON } from "../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../interfaces/ICombatJSON"; import { apiFetch } from "../utilities/ApiClient"; import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../utilities/ErrorMessages"; diff --git a/src/commands/GuideCommand.ts b/src/commands/GuideCommand.ts index babb4b0..eb0089c 100644 --- a/src/commands/GuideCommand.ts +++ b/src/commands/GuideCommand.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; const SECTIONS: Record = { @@ -18,8 +18,8 @@ const SECTIONS: Record HP regenerates passively — 10% of max HP every 5 minutes.', '> Use `/rest` to heal instantly at an inn (costs gold).', '> Consumable items can also restore HP.', - '> HP is fully restored on level up.', - ].join('\n'), + '> HP is fully restored on level up.' + ].join('\n') }, combat: { title: 'Combat & Enemies', @@ -35,8 +35,8 @@ const SECTIONS: Record Crit Chance (cap: 75%) • Life Steal (cap: 25%) • Dodge (cap: 75%)', - '> Gold Find (cap: 100%) • XP Bonus (cap: 100%) • Thorns (flat damage)', - ].join('\n'), + '> Gold Find (cap: 100%) • XP Bonus (cap: 100%) • Thorns (flat damage)' + ].join('\n') }, workshop: { title: 'Workshop — Enhance, Reforge, Dismantle', @@ -57,8 +57,8 @@ const SECTIONS: Record Enhanced items return 50% of the embers invested in them.', - '> Embers are used for enhancement and other upgrades.', - ].join('\n'), + '> Embers are used for enhancement and other upgrades.' + ].join('\n') }, economy: { title: 'Economy & Gold Sinks', @@ -80,8 +80,8 @@ const SECTIONS: Record ⚠️ Permanent action! Items are removed from inventory.', '> Hit milestones for gold, XP, embers, and chests.', - '> Modified items cannot be collected.', - ].join('\n'), + '> Modified items cannot be collected.' + ].join('\n') }, tasks: { title: 'Tasks & Chests', @@ -96,8 +96,8 @@ const SECTIONS: Record Earn chests from exploring, milestones, or buy from the shop.', '> Some chests unlock instantly; others take time.', '> Open chests for items, gold, and embers.', - '> **Divine Pity**: After opening many chests without a Divine drop, one is guaranteed.', - ].join('\n'), + '> **Divine Pity**: After opening many chests without a Divine drop, one is guaranteed.' + ].join('\n') }, zones: { title: 'Zones & Travel', @@ -112,9 +112,9 @@ const SECTIONS: Record **Toll Cost** — Gold charged per step (Zones 7+)', '', 'Higher zones have tougher enemies but better rewards and rarer drops.', - 'Zone XP multipliers are capped at 3.0× to prevent runaway leveling.', - ].join('\n'), - }, + 'Zone XP multipliers are capped at 3.0× to prevent runaway leveling.' + ].join('\n') + } }; const SECTION_ORDER = ['basics', 'combat', 'workshop', 'economy', 'tasks', 'zones']; @@ -128,16 +128,15 @@ export default class GuideCommand extends SlashCommand { cooldown: 3, isGlobalCommand: true }); - this.builder.addStringOption((o) => - o.setName('section') - .setDescription('Jump to a specific section') - .setRequired(false) - .addChoices( - ...SECTION_ORDER.map(key => ({ - name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, - value: key, - })) - ) + this.builder.addStringOption((o) => o.setName('section') + .setDescription('Jump to a specific section') + .setRequired(false) + .addChoices( + ...SECTION_ORDER.map(key => ({ + name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, + value: key + })) + ) ); } @@ -179,7 +178,7 @@ export default class GuideCommand extends SlashCommand { await interaction.reply({ embeds: [embed], - components: navRow.components.length > 0 ? [navRow] : [], + components: navRow.components.length > 0 ? [navRow] : [] }); } } diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index d3cd1af..b557fcc 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Client, EmbedBuilder, Colors } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, EmbedBuilder, Colors } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import SlashCommandHandler from "../handlers/SlashCommandHandler"; import PaginatorBuilder from "../utilities/PaginatorBuilder"; @@ -7,14 +7,14 @@ const CATEGORY_ICONS: Record = { 'General': '📋', 'Gaming': '⚔️', 'Moderator': '🛡️', - 'Developer': '🔧', + 'Developer': '🔧' }; const CATEGORY_COLORS: Record = { 'General': 0x3b82f6, 'Gaming': 0xef4444, 'Moderator': 0xf59e0b, - 'Developer': 0x6b7280, + 'Developer': 0x6b7280 }; export default class HelpCommand extends SlashCommand { diff --git a/src/commands/InventoryCommand.ts b/src/commands/InventoryCommand.ts index b509e05..1bbfadc 100644 --- a/src/commands/InventoryCommand.ts +++ b/src/commands/InventoryCommand.ts @@ -1,11 +1,11 @@ import { - ChatInputCommandInteraction, Client, EmbedBuilder, AttachmentBuilder, + type ChatInputCommandInteraction, type Client, EmbedBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, + StringSelectMenuOptionBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { IInventoryItem } from "../interfaces/IInventoryJSON"; -import { IPlayerJSON } from "../interfaces/IPlayerJSON"; +import { type IInventoryItem } from "../interfaces/IInventoryJSON"; +import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; import PaginatorBuilder from "../utilities/PaginatorBuilder"; import Routes from "../utilities/Routes"; import { apiFetch } from "../utilities/ApiClient"; @@ -30,7 +30,7 @@ export default class InventoryCommand extends SlashCommand { const res = await apiFetch(Routes.inventory(interaction.user.id)); - const { success, data, error }: { success: boolean, data: any, error?: string } = await res.json(); + const { success, data, error }: { success: boolean; data: any; error?: string } = await res.json(); if (res.status === 400 || res.status === 401 || res.status === 404 || res.status === 500) { await interaction.editReply({ content: formatError(error ?? 'Unknown error') }); @@ -113,7 +113,7 @@ export default class InventoryCommand extends SlashCommand { new ButtonBuilder() .setCustomId(`bulk_dismantle:${i}`) .setLabel(`🔥 Bulk Dismantle (${eligibleCount})`) - .setStyle(ButtonStyle.Danger), + .setStyle(ButtonStyle.Danger) ) ); } diff --git a/src/commands/LeaderboardCommand.ts b/src/commands/LeaderboardCommand.ts index 6b52b7b..c0f9ed8 100644 --- a/src/commands/LeaderboardCommand.ts +++ b/src/commands/LeaderboardCommand.ts @@ -1,4 +1,4 @@ -import { AttachmentBuilder, ChatInputCommandInteraction, Client, EmbedBuilder } from "discord.js"; +import { AttachmentBuilder, type ChatInputCommandInteraction, type Client, EmbedBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; import Routes from "../utilities/Routes"; @@ -10,7 +10,7 @@ const STAT_OPTIONS = [ { name: 'Level', value: 'level' }, { name: 'Gold', value: 'coins' }, { name: 'Enemies Defeated', value: 'enemiesDefeated' }, - { name: 'Days Explored', value: 'daysPassed' }, + { name: 'Days Explored', value: 'daysPassed' } ]; const STAT_DISPLAY: Record = { @@ -19,29 +19,29 @@ const STAT_DISPLAY: Record = { stat: 'Level', emoji: '⭐', accentColor: '#eab308', - accentColorDim: '#eab30825', + accentColorDim: '#eab30825' }, 'coins': { title: 'Leaderboard — Gold', stat: 'Gold', emoji: '🪙', accentColor: '#f59e0b', - accentColorDim: '#f59e0b25', + accentColorDim: '#f59e0b25' }, 'enemiesDefeated': { title: 'Leaderboard — Enemies Defeated', stat: 'Enemies Defeated', emoji: '💀', accentColor: '#ef4444', - accentColorDim: '#ef444425', + accentColorDim: '#ef444425' }, 'daysPassed': { title: 'Leaderboard — Days Explored', stat: 'Days Explored', emoji: '📅', accentColor: '#3b82f6', - accentColorDim: '#3b82f625', - }, + accentColorDim: '#3b82f625' + } }; export default class LeaderboardCommand extends SlashCommand { @@ -54,11 +54,10 @@ export default class LeaderboardCommand extends SlashCommand { isGlobalCommand: true }); - this.builder.addStringOption((o) => - o.setName('stat') - .setDescription('Which stat to rank by') - .setChoices(STAT_OPTIONS) - .setRequired(false) + this.builder.addStringOption((o) => o.setName('stat') + .setDescription('Which stat to rank by') + .setChoices(STAT_OPTIONS) + .setRequired(false) ); } @@ -66,7 +65,7 @@ export default class LeaderboardCommand extends SlashCommand { await interaction.deferReply(); const stat = interaction.options.getString('stat', false) ?? 'level'; - const config = STAT_DISPLAY[stat] ?? STAT_DISPLAY['level']; + const config = STAT_DISPLAY[stat] ?? STAT_DISPLAY.level; try { const res = await apiFetch(Routes.leaderboard(stat)); @@ -96,7 +95,7 @@ export default class LeaderboardCommand extends SlashCommand { return { username: player.username, value, - level: player.level ?? 1, + level: player.level ?? 1 }; }); diff --git a/src/commands/LookupCommand.ts b/src/commands/LookupCommand.ts index 55d1ae8..e408bae 100644 --- a/src/commands/LookupCommand.ts +++ b/src/commands/LookupCommand.ts @@ -1,19 +1,19 @@ -import { AutocompleteInteraction, ChatInputCommandInteraction, Client, Colors, EmbedBuilder, MessageFlags } from "discord.js"; +import { type AutocompleteInteraction, type ChatInputCommandInteraction, type Client, Colors, EmbedBuilder, MessageFlags } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import ItemManager from "../managers/ItemManager"; import PaginatorBuilder from "../utilities/PaginatorBuilder"; import ItemLookupContainer from "../structures/containers/ItemLookupContainer"; import { apiFetch } from "../utilities/ApiClient"; import Routes from "../utilities/Routes"; -import { IScenarioJSON } from "../interfaces/IScenarioJSON"; +import { type IScenarioJSON } from "../interfaces/IScenarioJSON"; import ScenarioLookupContainer from "../structures/containers/ScenarioLookupContainer"; -import { INPCJSON } from "../interfaces/INPCJSON"; +import { type INPCJSON } from "../interfaces/INPCJSON"; import NPCLookupContainer from "../structures/containers/NPCLookupContainer"; const typeOptions = [ { name: 'Item', value: 'item' }, { name: 'Scenario', value: 'scenario' }, - { name: 'NPC', value: 'npc' }, + { name: 'NPC', value: 'npc' } ]; export default class LookupCommand extends SlashCommand { @@ -64,105 +64,105 @@ export default class LookupCommand extends SlashCommand { const id = interaction.options.getInteger('id', true); switch (choice) { - case 'item': - if (id === -1) { - const items = Array.from(ItemManager.cache.values()); - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { - const chunk = items.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - - for (const item of chunk) { - descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; - descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; - if (item.affixes) { - let textToAdd = ''; - for (const affix of item.affixes) { - textToAdd += affix.type === 'THORNS' - ? ` **${affix.type}:** \`${affix.value.toLocaleString()}\` |` - : ` **${affix.type}:** \`${affix.value.toLocaleString()}%\` |`; - } - if (textToAdd !== '') descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; - else descriptionText += '\n'; + case 'item': + if (id === -1) { + const items = Array.from(ItemManager.cache.values()); + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { + const chunk = items.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + + for (const item of chunk) { + descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; + descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; + if (item.affixes) { + let textToAdd = ''; + for (const affix of item.affixes) { + textToAdd += affix.type === 'THORNS' + ? ` **${affix.type}:** \`${affix.value.toLocaleString()}\` |` + : ` **${affix.type}:** \`${affix.value.toLocaleString()}%\` |`; } + if (textToAdd !== '') descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; + else descriptionText += '\n'; } - - pages.push(new EmbedBuilder().setColor(Colors.Green).setTitle('Item Manager').setDescription(descriptionText)); } - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); - return; - } else { - const item = ItemManager.get(id); - if (!item) { - await interaction.editReply({ content: 'No item with that id exists!' }); - return; - } - await interaction.editReply({ components: [new ItemLookupContainer(item).build()], flags: MessageFlags.IsComponentsV2 }); + pages.push(new EmbedBuilder().setColor(Colors.Green).setTitle('Item Manager').setDescription(descriptionText)); } - break; - - case 'scenario': - if (id === -1) { - const res = await apiFetch(Routes.scenarios()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const scenario of chunk) { - descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? scenario.description.substring(0, 125) + '...' : scenario.description}\`\`\`\n`; - descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; - } - pages.push(new EmbedBuilder().setTitle('Scenario Manager').setDescription(descriptionText)); - } - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); + await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); + + } else { + const item = ItemManager.get(id); + if (!item) { + await interaction.editReply({ content: 'No item with that id exists!' }); return; - } else { - const res = await apiFetch(Routes.scenario(id)); - if (res.status === 404) { await interaction.editReply({ content: 'No scenario was found for this id!' }); return; } - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON } = await res.json(); - await interaction.editReply({ components: [new ScenarioLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); } - break; - - case 'npc': - if (id === -1) { - const res = await apiFetch(Routes.npcs()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: INPCJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const npc of chunk) { - descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; - descriptionText += `└ ${npc.description.length > 128 ? npc.description.substring(0, 125) + '...' : npc.description}\n\n`; - } - pages.push(new EmbedBuilder().setTitle('NPC Manager').setDescription(descriptionText)); + await interaction.editReply({ components: [new ItemLookupContainer(item).build()], flags: MessageFlags.IsComponentsV2 }); + } + break; + + case 'scenario': + if (id === -1) { + const res = await apiFetch(Routes.scenarios()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const scenario of chunk) { + descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? `${scenario.description.substring(0, 125)}...` : scenario.description}\`\`\`\n`; + descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; } + pages.push(new EmbedBuilder().setTitle('Scenario Manager').setDescription(descriptionText)); + } - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); - return; - } else { - const res = await apiFetch(Routes.npc(id)); - if (!res.ok) throw new Error('API Error!'); - const { success, data }: { success: boolean, data: INPCJSON } = await res.json(); - if (!success) { await interaction.editReply({ content: 'No NPC was found for the provided ID!' }); return; } - await interaction.editReply({ components: [new NPCLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); + await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); + + } else { + const res = await apiFetch(Routes.scenario(id)); + if (res.status === 404) { await interaction.editReply({ content: 'No scenario was found for this id!' }); return; } + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON } = await res.json(); + await interaction.editReply({ components: [new ScenarioLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); + } + break; + + case 'npc': + if (id === -1) { + const res = await apiFetch(Routes.npcs()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: INPCJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const npc of chunk) { + descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; + descriptionText += `└ ${npc.description.length > 128 ? `${npc.description.substring(0, 125)}...` : npc.description}\n\n`; + } + pages.push(new EmbedBuilder().setTitle('NPC Manager').setDescription(descriptionText)); } - break; + + await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); + + } else { + const res = await apiFetch(Routes.npc(id)); + if (!res.ok) throw new Error('API Error!'); + const { success, data }: { success: boolean; data: INPCJSON } = await res.json(); + if (!success) { await interaction.editReply({ content: 'No NPC was found for the provided ID!' }); return; } + await interaction.editReply({ components: [new NPCLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); + } + break; } } } \ No newline at end of file diff --git a/src/commands/MarketCommand.ts b/src/commands/MarketCommand.ts index e7d938b..c05629a 100644 --- a/src/commands/MarketCommand.ts +++ b/src/commands/MarketCommand.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - ChatInputCommandInteraction, Client, EmbedBuilder, MessageFlags, - StringSelectMenuBuilder, StringSelectMenuOptionBuilder, + type ChatInputCommandInteraction, type Client, EmbedBuilder, MessageFlags, + StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; @@ -17,7 +17,7 @@ const RARITY_CHOICES = [ { name: 'Common', value: 'Common' }, { name: 'Uncommon', value: 'Uncommon' }, { name: 'Rare', value: 'Rare' }, { name: 'Elite', value: 'Elite' }, { name: 'Epic', value: 'Epic' }, { name: 'Legendary', value: 'Legendary' }, - { name: 'Divine', value: 'Divine' }, { name: 'Exotic', value: 'Exotic' }, + { name: 'Divine', value: 'Divine' }, { name: 'Exotic', value: 'Exotic' } ]; const SORT_CHOICES = [ @@ -27,13 +27,13 @@ const SORT_CHOICES = [ { name: 'Level: High → Low', value: 'level_desc' }, { name: 'Highest ATK', value: 'atk_desc' }, { name: 'Highest DEF', value: 'def_desc' }, - { name: 'Highest HP', value: 'hp_desc' }, + { name: 'Highest HP', value: 'hp_desc' } ]; const TYPE_CHOICES = [ { name: 'All', value: 'All' }, { name: 'Weapon', value: 'Weapon' }, { name: 'Armor', value: 'Armor' }, - { name: 'Accessory', value: 'Accessory' }, { name: 'Consumable', value: 'Consumable' }, + { name: 'Accessory', value: 'Accessory' }, { name: 'Consumable', value: 'Consumable' } ]; export const SELL_PAGE_SIZE = 25; @@ -49,21 +49,18 @@ export default class MarketCommand extends SlashCommand { }); this.data - .addSubcommand((sub) => - sub.setName('browse') - .setDescription('Browse items for sale on the Global Market') - .addStringOption((o) => o.setName('search').setDescription('Search by item name').setRequired(false)) - .addStringOption((o) => o.setName('rarity').setDescription('Filter by rarity').setChoices(RARITY_CHOICES).setRequired(false)) - .addStringOption((o) => o.setName('type').setDescription('Filter by item type').setChoices(TYPE_CHOICES).setRequired(false)) - .addStringOption((o) => o.setName('sort').setDescription('Sort order').setChoices(SORT_CHOICES).setRequired(false)) + .addSubcommand((sub) => sub.setName('browse') + .setDescription('Browse items for sale on the Global Market') + .addStringOption((o) => o.setName('search').setDescription('Search by item name').setRequired(false)) + .addStringOption((o) => o.setName('rarity').setDescription('Filter by rarity').setChoices(RARITY_CHOICES).setRequired(false)) + .addStringOption((o) => o.setName('type').setDescription('Filter by item type').setChoices(TYPE_CHOICES).setRequired(false)) + .addStringOption((o) => o.setName('sort').setDescription('Sort order').setChoices(SORT_CHOICES).setRequired(false)) ) - .addSubcommand((sub) => - sub.setName('listings') - .setDescription('View your active market listings') + .addSubcommand((sub) => sub.setName('listings') + .setDescription('View your active market listings') ) - .addSubcommand((sub) => - sub.setName('sell') - .setDescription('Select an item from your inventory to list on the market') + .addSubcommand((sub) => sub.setName('sell') + .setDescription('Select an item from your inventory to list on the market') ); } @@ -72,9 +69,9 @@ export default class MarketCommand extends SlashCommand { const discordId = interaction.user.id; switch (sub) { - case 'browse': return this.handleBrowse(interaction, discordId); - case 'listings': return this.handleListings(interaction, discordId); - case 'sell': return this.handleSell(interaction, discordId); + case 'browse': return this.handleBrowse(interaction, discordId); + case 'listings': return this.handleListings(interaction, discordId); + case 'sell': return this.handleSell(interaction, discordId); } } @@ -191,7 +188,7 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ if (!def) continue; const enhTag = inv.enhanceLevel > 0 ? ` +${inv.enhanceLevel}` : ''; - const modTag = (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) ? ' ✨' : ''; + const modTag = inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides ? ' ✨' : ''; const value = def.value || 0; options.push( @@ -231,7 +228,7 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ .setCustomId(`mkt_sell_page:${safePage + 1}`) .setLabel('Next ▶') .setStyle(ButtonStyle.Secondary) - .setDisabled(safePage >= totalPages - 1), + .setDisabled(safePage >= totalPages - 1) ); components.push(navRow); } @@ -296,7 +293,7 @@ function buildMarketButtons( .setCustomId(`mkt_next:${config.page}:${filterKey}:${mode}`) .setLabel('Next ▶') .setStyle(ButtonStyle.Secondary) - .setDisabled(config.page >= config.totalPages), + .setDisabled(config.page >= config.totalPages) ); rows.push(navRow); } diff --git a/src/commands/NetworkCommand.ts b/src/commands/NetworkCommand.ts index 6c1ea3c..3112a0f 100644 --- a/src/commands/NetworkCommand.ts +++ b/src/commands/NetworkCommand.ts @@ -1,6 +1,6 @@ import { - ChatInputCommandInteraction, - Client, + type ChatInputCommandInteraction, + type Client, Colors, EmbedBuilder, MessageFlags, @@ -31,7 +31,7 @@ interface GuildInfo { const options = [ { name: 'Overview', value: 'overview' }, { name: 'Cluster', value: 'shard' }, - { name: 'Guild', value: 'guild' }, + { name: 'Guild', value: 'guild' } ]; export default class NetworkCommand extends SlashCommand { @@ -55,121 +55,121 @@ export default class NetworkCommand extends SlashCommand { const id = interaction.options.getString('id') || 'all'; switch (choice) { - case 'overview': { - const clusters = await this.getClusters(client); - const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); - const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); - const avgPing = clusters.reduce((acc, s) => acc + s.ping, 0) / clusters.length; - const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); - - const container = new ContainerBuilder() - .setAccentColor(Colors.Blurple) - .addTextDisplayComponents(text => text.setContent(`# 🌐 Global Network Overview`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` - )); - - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); - break; - } - - case 'shard': { - const clusters = await this.getClusters(client); + case 'overview': { + const clusters = await this.getClusters(client); + const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); + const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); + const avgPing = clusters.reduce((acc, s) => acc + s.ping, 0) / clusters.length; + const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); + + const container = new ContainerBuilder() + .setAccentColor(Colors.Blurple) + .addTextDisplayComponents(text => text.setContent(`# 🌐 Global Network Overview`)) + .addSeparatorComponents(sep => sep.setDivider(true)) + .addTextDisplayComponents(text => text.setContent( + `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` + )); + + await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); + break; + } - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + case 'shard': { + const clusters = await this.getClusters(client); - for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { - const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - for (const cluster of chunk) { - descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; - descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; - } + for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { + const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; - pages.push(new EmbedBuilder().setColor(Colors.Blue).setTitle('Network Manager: Clusters').setDescription(descriptionText)); + for (const cluster of chunk) { + descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; + descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; } - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); + pages.push(new EmbedBuilder().setColor(Colors.Blue).setTitle('Network Manager: Clusters').setDescription(descriptionText)); + } - await paginator.start(interaction); + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + + await paginator.start(interaction); + + } else { + const targetCluster = clusters.find(s => s.id.toString() === id); + if (!targetCluster) { + await interaction.editReply({ content: `❌ No cluster could be found with the ID: \`${id}\`` }); return; - } else { - const targetCluster = clusters.find(s => s.id.toString() === id); - if (!targetCluster) { - await interaction.editReply({ content: `❌ No cluster could be found with the ID: \`${id}\`` }); - return; - } + } - const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); - const uptimeHours = Math.floor(uptimeMins / 60); + const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); + const uptimeHours = Math.floor(uptimeMins / 60); - const container = new ContainerBuilder() - .setAccentColor(Colors.Blue) - .addTextDisplayComponents(text => text.setContent(`# 💎 Cluster #${targetCluster.id}`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` - )); + const container = new ContainerBuilder() + .setAccentColor(Colors.Blue) + .addTextDisplayComponents(text => text.setContent(`# 💎 Cluster #${targetCluster.id}`)) + .addSeparatorComponents(sep => sep.setDivider(true)) + .addTextDisplayComponents(text => text.setContent( + `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` + )); - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); - } - break; + await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); } + break; + } - case 'guild': { - const guilds = await this.getGuilds(client); - - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + case 'guild': { + const guilds = await this.getGuilds(client); - for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { - const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - for (const guild of chunk) { - descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; - descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; - } + for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { + const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; - pages.push(new EmbedBuilder().setColor(Colors.Purple).setTitle('Network Manager: Guilds').setDescription(descriptionText)); + for (const guild of chunk) { + descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; + descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; } - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); + pages.push(new EmbedBuilder().setColor(Colors.Purple).setTitle('Network Manager: Guilds').setDescription(descriptionText)); + } - await paginator.start(interaction); + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + + await paginator.start(interaction); + + } else { + const targetGuild = guilds.find(g => g.id === id); + if (!targetGuild) { + await interaction.editReply({ content: `❌ No guild could be found with the ID: \`${id}\`` }); return; - } else { - const targetGuild = guilds.find(g => g.id === id); - if (!targetGuild) { - await interaction.editReply({ content: `❌ No guild could be found with the ID: \`${id}\`` }); - return; - } + } - const joinedTimestamp = targetGuild.joinedAt ? `` : 'Unknown'; + const joinedTimestamp = targetGuild.joinedAt ? `` : 'Unknown'; - const container = new ContainerBuilder() - .setAccentColor(Colors.Purple) - .addTextDisplayComponents(text => text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` - )); + const container = new ContainerBuilder() + .setAccentColor(Colors.Purple) + .addTextDisplayComponents(text => text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`)) + .addSeparatorComponents(sep => sep.setDivider(true)) + .addTextDisplayComponents(text => text.setContent( + `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` + )); - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); - } - break; + await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); } + break; + } } } @@ -191,17 +191,17 @@ export default class NetworkCommand extends SlashCommand { uptime: c.uptime })); return results; - } else { - return [{ - id: 0, - shards: [0], - shardCount: 1, - ping: client.ws.ping, - guilds: client.guilds.cache.size, - users: client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), - uptime: client.uptime - }]; - } + } + return [{ + id: 0, + shards: [0], + shardCount: 1, + ping: client.ws.ping, + guilds: client.guilds.cache.size, + users: client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), + uptime: client.uptime + }]; + } /** @@ -210,26 +210,25 @@ export default class NetworkCommand extends SlashCommand { private async getGuilds(client: Client): Promise { const cluster = (client as any).cluster; if (cluster) { - const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => - c.guilds.cache.map((g: any) => ({ - id: g.id, - name: g.name, - memberCount: g.memberCount, - clusterId: c.cluster?.id ?? 0, - ownerId: g.ownerId, - joinedAt: g.joinedTimestamp - })) - ); - return results.flat(); - } else { - return client.guilds.cache.map(g => ({ + const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => c.guilds.cache.map((g: any) => ({ id: g.id, name: g.name, memberCount: g.memberCount, - clusterId: 0, + clusterId: c.cluster?.id ?? 0, ownerId: g.ownerId, joinedAt: g.joinedTimestamp - })); - } + })) + ); + return results.flat(); + } + return client.guilds.cache.map(g => ({ + id: g.id, + name: g.name, + memberCount: g.memberCount, + clusterId: 0, + ownerId: g.ownerId, + joinedAt: g.joinedTimestamp + })); + } } diff --git a/src/commands/ProfileCommand.ts b/src/commands/ProfileCommand.ts index 14bfd16..923dd4b 100644 --- a/src/commands/ProfileCommand.ts +++ b/src/commands/ProfileCommand.ts @@ -1,12 +1,12 @@ -import { ChatInputCommandInteraction, Client, AttachmentBuilder, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, AttachmentBuilder, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; -import { IPlayerJSON } from "../interfaces/IPlayerJSON"; -import { IInventoryItem } from "../interfaces/IInventoryJSON"; -import { ICollectionJSON } from "../interfaces/ICollectionJSON"; +import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; +import { type IInventoryItem } from "../interfaces/IInventoryJSON"; +import { type ICollectionJSON } from "../interfaces/ICollectionJSON"; import Routes from "../utilities/Routes"; import { apiFetch } from "../utilities/ApiClient"; import { formatError } from "../utilities/ErrorMessages"; -import { EquipmentSlot } from "../interfaces/IItemJSON"; +import { type EquipmentSlot } from "../interfaces/IItemJSON"; import ImageService from "../utilities/ImageService"; export default class ProfileCommand extends SlashCommand { @@ -81,7 +81,7 @@ export default class ProfileCommand extends SlashCommand { await interaction.editReply({ files: [profileAttachment], - components, + components }); } } \ No newline at end of file diff --git a/src/commands/RegisterCommand.ts b/src/commands/RegisterCommand.ts index 3176825..97c0115 100644 --- a/src/commands/RegisterCommand.ts +++ b/src/commands/RegisterCommand.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; export default class RegisterCommand extends SlashCommand { @@ -43,7 +43,7 @@ export default class RegisterCommand extends SlashCommand { .setLabel('Privacy Policy & ToS') .setStyle(ButtonStyle.Link) .setURL('https://capi.gg/legal') - .setEmoji('📜'), + .setEmoji('📜') ); await interaction.reply({ embeds: [embed], components: [row], flags: MessageFlags.Ephemeral }); diff --git a/src/commands/RestCommand.ts b/src/commands/RestCommand.ts index 3bf8e45..c05b070 100644 --- a/src/commands/RestCommand.ts +++ b/src/commands/RestCommand.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Client } from "discord.js"; +import { type ChatInputCommandInteraction, type Client } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; import { formatError } from "../utilities/ErrorMessages"; @@ -22,7 +22,7 @@ export default class RestCommand extends SlashCommand { // Go straight to POST — the endpoint returns appropriate errors for full HP, no gold, etc. const res = await apiFetch(Routes.rest(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id }), + body: JSON.stringify({ discordId: interaction.user.id }) }); const result = await res.json(); @@ -43,8 +43,8 @@ export default class RestCommand extends SlashCommand { ``, `❤️ Restored **${result.healedAmount?.toLocaleString() ?? '???'} HP** → ${result.newHp?.toLocaleString() ?? '???'} / ${result.maxHp?.toLocaleString() ?? '???'}`, `🪙 Cost: **${result.goldSpent?.toLocaleString() ?? '???'}** Gold`, - `💰 Balance: **${result.newBalance?.toLocaleString() ?? '???'}** Gold`, - ].join('\n'), + `💰 Balance: **${result.newBalance?.toLocaleString() ?? '???'}** Gold` + ].join('\n') }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code) }); diff --git a/src/commands/TasksCommand.ts b/src/commands/TasksCommand.ts index a34912b..665684c 100644 --- a/src/commands/TasksCommand.ts +++ b/src/commands/TasksCommand.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - ChatInputCommandInteraction, Client, EmbedBuilder, StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, + type ChatInputCommandInteraction, type Client, EmbedBuilder, type StringSelectMenuBuilder, + StringSelectMenuOptionBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; @@ -19,15 +19,14 @@ export default class TasksCommand extends SlashCommand { cooldown: 5, isGlobalCommand: true }); - this.builder.addStringOption((o) => - o.setName('period') - .setDescription('Task period to view') - .setRequired(false) - .addChoices( - { name: 'Daily', value: 'daily' }, - { name: 'Weekly', value: 'weekly' }, - { name: 'Monthly', value: 'monthly' }, - ) + this.builder.addStringOption((o) => o.setName('period') + .setDescription('Task period to view') + .setRequired(false) + .addChoices( + { name: 'Daily', value: 'daily' }, + { name: 'Weekly', value: 'weekly' }, + { name: 'Monthly', value: 'monthly' } + ) ); } @@ -60,7 +59,7 @@ export default class TasksCommand extends SlashCommand { const imageBuffer = await ImageService.tasks(tasks, { period, resetIn, - playerEmbers, + playerEmbers }); const attachment = new AttachmentBuilder(imageBuffer, { name: 'tasks.png' }); @@ -101,7 +100,7 @@ export default class TasksCommand extends SlashCommand { .setCustomId(`tasks_tab:monthly`) .setLabel('Monthly') .setStyle(period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary) - .setDisabled(period === 'monthly'), + .setDisabled(period === 'monthly') ); components.push(periodRow); diff --git a/src/commands/TestCommand.ts b/src/commands/TestCommand.ts index 4c0d5eb..92ecd45 100644 --- a/src/commands/TestCommand.ts +++ b/src/commands/TestCommand.ts @@ -1,4 +1,4 @@ -import { ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, ContainerBuilder, MessageFlags } from "discord.js"; +import { ButtonBuilder, ButtonStyle, ChannelType, type ChatInputCommandInteraction, type Client, ContainerBuilder, MessageFlags } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import logger from "../utilities/Logger"; diff --git a/src/commands/TravelCommand.ts b/src/commands/TravelCommand.ts index ba5a89c..9dac36d 100644 --- a/src/commands/TravelCommand.ts +++ b/src/commands/TravelCommand.ts @@ -1,6 +1,6 @@ import { - ActionRowBuilder, AttachmentBuilder, ChatInputCommandInteraction, - Client, EmbedBuilder, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder + ActionRowBuilder, AttachmentBuilder, type ChatInputCommandInteraction, + type Client, EmbedBuilder, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; import { apiFetch } from "../utilities/ApiClient"; @@ -54,11 +54,10 @@ export default class TravelCommand extends SlashCommand { const components: ActionRowBuilder[] = []; if (accessible.length > 0) { - const options = accessible.map(zone => - new StringSelectMenuOptionBuilder() - .setLabel(zone.name) - .setDescription(`Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat`) - .setValue(String(zone.id)) + const options = accessible.map(zone => new StringSelectMenuOptionBuilder() + .setLabel(zone.name) + .setDescription(`Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat`) + .setValue(String(zone.id)) ); const selectMenu = new StringSelectMenuBuilder() diff --git a/src/commands/VoteCommand.ts b/src/commands/VoteCommand.ts index cd05d7b..32544a9 100644 --- a/src/commands/VoteCommand.ts +++ b/src/commands/VoteCommand.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import SlashCommand from "../structures/SlashCommand"; export default class VoteCommand extends SlashCommand { @@ -34,7 +34,7 @@ export default class VoteCommand extends SlashCommand { .setLabel('Play on Web') .setStyle(ButtonStyle.Link) .setURL('https://capi.gg/dfo') - .setEmoji('🌐'), + .setEmoji('🌐') ); await interaction.reply({ embeds: [embed], components: [row] }); diff --git a/src/components/buttons/AttackButton.ts b/src/components/buttons/AttackButton.ts index e5c7787..c1a57f9 100644 --- a/src/components/buttons/AttackButton.ts +++ b/src/components/buttons/AttackButton.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, Client, MessageFlags } from "discord.js"; +import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; import Button from "../../structures/Button"; -import { ICombatJSON } from "../../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../../interfaces/ICombatJSON"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; diff --git a/src/components/buttons/BulkCollectButton.ts b/src/components/buttons/BulkCollectButton.ts index 76bc089..3686d15 100644 --- a/src/components/buttons/BulkCollectButton.ts +++ b/src/components/buttons/BulkCollectButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; +import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; import Button from "../../structures/Button"; import ItemManager from "../../managers/ItemManager"; import { apiFetch } from "../../utilities/ApiClient"; @@ -29,7 +29,7 @@ export default class BulkCollectButton extends Button { }); if (eligible.length === 0) { - await interaction.reply({ content: '❌ No eligible items to collect on this page.', flags: MessageFlags.Ephemeral}); + await interaction.reply({ content: '❌ No eligible items to collect on this page.', flags: MessageFlags.Ephemeral }); return; } diff --git a/src/components/buttons/BulkDismantleButton.ts b/src/components/buttons/BulkDismantleButton.ts index 1b6077d..7232553 100644 --- a/src/components/buttons/BulkDismantleButton.ts +++ b/src/components/buttons/BulkDismantleButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; +import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; import Button from "../../structures/Button"; import ItemManager from "../../managers/ItemManager"; import { apiFetch } from "../../utilities/ApiClient"; diff --git a/src/components/buttons/BulkSellButton.ts b/src/components/buttons/BulkSellButton.ts index a986c67..90ebe61 100644 --- a/src/components/buttons/BulkSellButton.ts +++ b/src/components/buttons/BulkSellButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; +import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; import Button from "../../structures/Button"; import ItemManager from "../../managers/ItemManager"; import { apiFetch } from "../../utilities/ApiClient"; diff --git a/src/components/buttons/ChestBuyButton.ts b/src/components/buttons/ChestBuyButton.ts index 1f37c19..85f4b2f 100644 --- a/src/components/buttons/ChestBuyButton.ts +++ b/src/components/buttons/ChestBuyButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -22,7 +22,7 @@ export default class ChestBuyButton extends Button { try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'buy', tier }), + body: JSON.stringify({ discordId: interaction.user.id, action: 'buy', tier }) }); const body = await res.json(); @@ -34,7 +34,7 @@ export default class ChestBuyButton extends Button { await interaction.editReply({ content: `🛒 **Purchased a ${tier} Chest!**\n🪙 Cost: **${body.goldCost?.toLocaleString() ?? '???'}** gold\n💰 Balance: **${body.newBalance?.toLocaleString() ?? '???'}** gold\n\nRun \`/chests\` to view your vault.`, - files: [], components: [], embeds: [], + files: [], components: [], embeds: [] }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); diff --git a/src/components/buttons/ChestOpenButton.ts b/src/components/buttons/ChestOpenButton.ts index b5fd091..a23552b 100644 --- a/src/components/buttons/ChestOpenButton.ts +++ b/src/components/buttons/ChestOpenButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -6,7 +6,7 @@ import Routes from "../../utilities/Routes"; const RARITY_EMOJIS: Record = { Common: '⬜', Uncommon: '🟩', Rare: '🟦', Elite: '🟧', - Epic: '🟪', Legendary: '🟡', Divine: '💎', Exotic: '💜', + Epic: '🟪', Legendary: '🟡', Divine: '💎', Exotic: '💜' }; export default class ChestOpenButton extends Button { @@ -27,7 +27,7 @@ export default class ChestOpenButton extends Button { try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'open', chestId }), + body: JSON.stringify({ discordId: interaction.user.id, action: 'open', chestId }) }); const body = await res.json(); @@ -40,7 +40,7 @@ export default class ChestOpenButton extends Button { const loot = body.loot; const lines = [ `🎉 **Chest Opened!**`, - ``, + `` ]; if (loot.isPity) { diff --git a/src/components/buttons/ChestStartButton.ts b/src/components/buttons/ChestStartButton.ts index 0a11c23..476d6eb 100644 --- a/src/components/buttons/ChestStartButton.ts +++ b/src/components/buttons/ChestStartButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -22,7 +22,7 @@ export default class ChestStartButton extends Button { try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'start', chestId }), + body: JSON.stringify({ discordId: interaction.user.id, action: 'start', chestId }) }); const body = await res.json(); @@ -34,7 +34,7 @@ export default class ChestStartButton extends Button { await interaction.editReply({ content: `⏳ **Chest unlocking!** It will be ready to open in **${body.unlockTime ?? 'a while'}**.\n\nRun \`/chests\` again later to open it.`, - files: [], components: [], + files: [], components: [] }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); diff --git a/src/components/buttons/CollectButton.ts b/src/components/buttons/CollectButton.ts index 13c152f..9db9495 100644 --- a/src/components/buttons/CollectButton.ts +++ b/src/components/buttons/CollectButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, ModalBuilder, TextInputStyle } from "discord.js"; +import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; import Button from "../../structures/Button"; export default class CollectButton extends Button { @@ -15,8 +15,7 @@ export default class CollectButton extends Button { .setCustomId(`collect:${docId}`) .setTitle('⚠️ Collect Item (Permanent)') .addLabelComponents( - (label) => - label.setLabel('Amount').setDescription(`⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})`) + (label) => label.setLabel('Amount').setDescription(`⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})`) .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short).setPlaceholder(maxQty)) ); diff --git a/src/components/buttons/ConsumeButton.ts b/src/components/buttons/ConsumeButton.ts index 5a7ada0..a18c57b 100644 --- a/src/components/buttons/ConsumeButton.ts +++ b/src/components/buttons/ConsumeButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, ModalBuilder, TextInputStyle } from "discord.js"; +import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; import Button from "../../structures/Button"; export default class ConsumeButton extends Button { @@ -15,8 +15,7 @@ export default class ConsumeButton extends Button { .setTitle('Consume Item') .setCustomId(`consume:${docId}`) .addLabelComponents( - (label) => - label.setLabel('Amount').setDescription(`Enter amount to consume (Max: ${maxQty})`) + (label) => label.setLabel('Amount').setDescription(`Enter amount to consume (Max: ${maxQty})`) .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short)) ); diff --git a/src/components/buttons/DismantleButton.ts b/src/components/buttons/DismantleButton.ts index d27ae60..85c1a40 100644 --- a/src/components/buttons/DismantleButton.ts +++ b/src/components/buttons/DismantleButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -32,8 +32,8 @@ export default class DismantleButton extends Button { discordId: interaction.user.id, itemId, inventoryId: docId, - amount, - }), + amount + }) }); const body = await res.json(); @@ -48,7 +48,7 @@ export default class DismantleButton extends Button { `🔥 **${body.message}**`, ``, `🔥 Embers gained: **+${body.embersGained?.toLocaleString() ?? '???'}**`, - `🔥 Total embers: **${body.newEmbers?.toLocaleString() ?? '???'}**`, + `🔥 Total embers: **${body.newEmbers?.toLocaleString() ?? '???'}**` ].join('\n'), files: [], components: [], embeds: [] }); diff --git a/src/components/buttons/EmbedAttackButton.ts b/src/components/buttons/EmbedAttackButton.ts index 60c464e..ad8fe0b 100644 --- a/src/components/buttons/EmbedAttackButton.ts +++ b/src/components/buttons/EmbedAttackButton.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; -import { ICombatJSON } from "../../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../../interfaces/ICombatJSON"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; diff --git a/src/components/buttons/EmbedFleeButton.ts b/src/components/buttons/EmbedFleeButton.ts index c1f6e11..adda376 100644 --- a/src/components/buttons/EmbedFleeButton.ts +++ b/src/components/buttons/EmbedFleeButton.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; -import { ICombatJSON } from "../../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../../interfaces/ICombatJSON"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; diff --git a/src/components/buttons/EnhanceButton.ts b/src/components/buttons/EnhanceButton.ts index fe5da8b..0d7e7aa 100644 --- a/src/components/buttons/EnhanceButton.ts +++ b/src/components/buttons/EnhanceButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -24,7 +24,7 @@ export default class EnhanceButton extends Button { try { const res = await apiFetch(Routes.enhance(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }), + body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }) }); const body = await res.json(); @@ -38,7 +38,7 @@ export default class EnhanceButton extends Button { const lines = [ `⬆️ **Enhancement ${result.succeeded ? 'Succeeded' : 'Failed'}!**`, ``, - `📦 **${result.itemName}** → +${result.newLevel}`, + `📦 **${result.itemName}** → +${result.newLevel}` ]; if (result.succeeded) { diff --git a/src/components/buttons/EquipButton.ts b/src/components/buttons/EquipButton.ts index 034bc1f..c7a78ed 100644 --- a/src/components/buttons/EquipButton.ts +++ b/src/components/buttons/EquipButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -24,7 +24,7 @@ export default class EquipButton extends Button { try { const res = await apiFetch(Routes.equip(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }), + body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }) }); const { success, error, message } = await res.json(); diff --git a/src/components/buttons/ExploreAgainButton.ts b/src/components/buttons/ExploreAgainButton.ts index 8e328e1..453b244 100644 --- a/src/components/buttons/ExploreAgainButton.ts +++ b/src/components/buttons/ExploreAgainButton.ts @@ -1,10 +1,10 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; import Routes from "../../utilities/Routes"; -import { IStepJSON } from "../../interfaces/IStepJSON"; +import { type IStepJSON } from "../../interfaces/IStepJSON"; export default class ExploreAgainButton extends Button { constructor() { @@ -17,7 +17,7 @@ export default class ExploreAgainButton extends Button { try { const res = await apiFetch(Routes.explore(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id }), + body: JSON.stringify({ discordId: interaction.user.id }) }); const data = await res.json() as IStepJSON; diff --git a/src/components/buttons/ExploreButton.ts b/src/components/buttons/ExploreButton.ts index 3cb8a04..b53f089 100644 --- a/src/components/buttons/ExploreButton.ts +++ b/src/components/buttons/ExploreButton.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, Client, MessageFlags } from "discord.js"; +import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; import Button from "../../structures/Button"; -import { IStepJSON } from "../../interfaces/IStepJSON"; +import { type IStepJSON } from "../../interfaces/IStepJSON"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; @@ -14,7 +14,7 @@ export default class ExploreButton extends Button { const res = await apiFetch(Routes.explore(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id }), + body: JSON.stringify({ discordId: interaction.user.id }) }); const data = await res.json() as IStepJSON; diff --git a/src/components/buttons/FleeButton.ts b/src/components/buttons/FleeButton.ts index 0e2dee4..6326566 100644 --- a/src/components/buttons/FleeButton.ts +++ b/src/components/buttons/FleeButton.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, Client, MessageFlags } from "discord.js"; +import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; import Button from "../../structures/Button"; -import { ICombatJSON } from "../../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../../interfaces/ICombatJSON"; import { apiFetch } from "../../utilities/ApiClient"; import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; diff --git a/src/components/buttons/GuideNavButton.ts b/src/components/buttons/GuideNavButton.ts index 5c72411..a6e7129 100644 --- a/src/components/buttons/GuideNavButton.ts +++ b/src/components/buttons/GuideNavButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; const SECTIONS: Record = { @@ -7,7 +7,7 @@ const SECTIONS: Record 1) { rows.push(new ActionRowBuilder().setComponents( new ButtonBuilder().setCustomId(`mkt_prev:${config.page}:${filterKey}:${mode}`).setLabel('◀ Prev').setStyle(ButtonStyle.Secondary).setDisabled(config.page <= 1), - new ButtonBuilder().setCustomId(`mkt_next:${config.page}:${filterKey}:${mode}`).setLabel('Next ▶').setStyle(ButtonStyle.Secondary).setDisabled(config.page >= config.totalPages), + new ButtonBuilder().setCustomId(`mkt_next:${config.page}:${filterKey}:${mode}`).setLabel('Next ▶').setStyle(ButtonStyle.Secondary).setDisabled(config.page >= config.totalPages) )); } diff --git a/src/components/buttons/MarketRedirectButton.ts b/src/components/buttons/MarketRedirectButton.ts index d6663ea..303debe 100644 --- a/src/components/buttons/MarketRedirectButton.ts +++ b/src/components/buttons/MarketRedirectButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, MessageFlags } from "discord.js"; +import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; import Button from "../../structures/Button"; export default class MarketRedirectButton extends Button { @@ -12,7 +12,7 @@ export default class MarketRedirectButton extends Button { await interaction.reply({ content: `📢 **Modified items cannot be vendor-sold.**\n\nUse \`/market sell item:${itemId} quantity:1 price:\` to list this item on the Global Market.\n\nAlternatively, you can **🔥 Dismantle** it for Embers.`, - flags: MessageFlags.Ephemeral, + flags: MessageFlags.Ephemeral }); } } diff --git a/src/components/buttons/MarketSellPageButton.ts b/src/components/buttons/MarketSellPageButton.ts index 38d6a9c..df2e64b 100644 --- a/src/components/buttons/MarketSellPageButton.ts +++ b/src/components/buttons/MarketSellPageButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { formatError } from "../../utilities/ErrorMessages"; import { buildSellPage } from "../../commands/MarketCommand"; diff --git a/src/components/buttons/ReforgeButton.ts b/src/components/buttons/ReforgeButton.ts index 641f3e9..100101e 100644 --- a/src/components/buttons/ReforgeButton.ts +++ b/src/components/buttons/ReforgeButton.ts @@ -1,7 +1,7 @@ import { - ButtonInteraction, Client, ActionRowBuilder, + type ButtonInteraction, type Client, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, - MessageFlags, + MessageFlags } from "discord.js"; import Button from "../../structures/Button"; @@ -38,7 +38,7 @@ export default class ReforgeButton extends Button { new StringSelectMenuOptionBuilder() .setLabel('Full Reforge') .setDescription('Reroll both stats and affixes (costs more)') - .setValue('full'), + .setValue('full') ); const row = new ActionRowBuilder().setComponents(selectMenu); @@ -46,7 +46,7 @@ export default class ReforgeButton extends Button { await interaction.reply({ content: '🔄 **Select Reforge Type**\nChoose what to reroll on this item:', components: [row], - ephemeral: true, + ephemeral: true }); } } diff --git a/src/components/buttons/RegisterAcceptButton.ts b/src/components/buttons/RegisterAcceptButton.ts index 6177a6b..f608fed 100644 --- a/src/components/buttons/RegisterAcceptButton.ts +++ b/src/components/buttons/RegisterAcceptButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, Colors, ContainerBuilder, MessageFlags } from "discord.js"; +import { type ButtonInteraction, type Client, Colors, ContainerBuilder, MessageFlags } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -17,14 +17,14 @@ export default class RegisterAcceptButton extends Button { try { const res = await apiFetch(Routes.registerPlayer(), { method: 'POST', - body: JSON.stringify({ discordId, username, avatar }), + body: JSON.stringify({ discordId, username, avatar }) }); if (res.status === 409) { await interaction.editReply({ content: '✅ **You\'re already registered!** Use `/profile` to see your character.', embeds: [], - components: [], + components: [] }); return; } @@ -35,7 +35,7 @@ export default class RegisterAcceptButton extends Button { await interaction.editReply({ content: formatError(body.error ?? 'Registration failed.'), embeds: [], - components: [], + components: [] }); return; } @@ -43,36 +43,32 @@ export default class RegisterAcceptButton extends Button { const container = new ContainerBuilder().setAccentColor(Colors.Green); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('## ⚔️ Character Created!'), - (textDisplay) => - textDisplay.setContent(`Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.`), - (textDisplay) => - textDisplay.setContent( - '**Get started:**\n' + + (textDisplay) => textDisplay.setContent('## ⚔️ Character Created!'), + (textDisplay) => textDisplay.setContent(`Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.`), + (textDisplay) => textDisplay.setContent( + '**Get started:**\n' + '> `/explore` — Venture into the world\n' + '> `/profile` — View your character\n' + '> `/help` — See all commands' - ), + ) ); container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer') ); await interaction.editReply({ embeds: [], components: [container], - flags: MessageFlags.IsComponentsV2, + flags: MessageFlags.IsComponentsV2 }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code), embeds: [], - components: [], + components: [] }); } } diff --git a/src/components/buttons/RegisterDeclineButton.ts b/src/components/buttons/RegisterDeclineButton.ts index 8155073..14ac9f9 100644 --- a/src/components/buttons/RegisterDeclineButton.ts +++ b/src/components/buttons/RegisterDeclineButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; export default class RegisterDeclineButton extends Button { @@ -10,7 +10,7 @@ export default class RegisterDeclineButton extends Button { await interaction.editReply({ content: '👋 **No problem!** No data has been stored. You can run `/register` again anytime if you change your mind.', embeds: [], - components: [], + components: [] }); } } \ No newline at end of file diff --git a/src/components/buttons/RestButton.ts b/src/components/buttons/RestButton.ts index 6e5afb4..8a912c0 100644 --- a/src/components/buttons/RestButton.ts +++ b/src/components/buttons/RestButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -15,7 +15,7 @@ export default class RestButton extends Button { try { const res = await apiFetch(Routes.rest(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id }), + body: JSON.stringify({ discordId: interaction.user.id }) }); const result = await res.json(); @@ -29,7 +29,7 @@ export default class RestButton extends Button { content: [ `🏨 **Rested at the Inn**`, `❤️ Restored **${result.healedAmount.toLocaleString()} HP** → ${result.newHp.toLocaleString()} / ${result.maxHp.toLocaleString()}`, - `🪙 Cost: **${result.goldSpent.toLocaleString()}** Gold • 💰 Balance: **${result.newBalance.toLocaleString()}** Gold`, + `🪙 Cost: **${result.goldSpent.toLocaleString()}** Gold • 💰 Balance: **${result.newBalance.toLocaleString()}** Gold` ].join('\n'), files: [], components: [], embeds: [] }); diff --git a/src/components/buttons/SellButton.ts b/src/components/buttons/SellButton.ts index 6c3febf..b74ab98 100644 --- a/src/components/buttons/SellButton.ts +++ b/src/components/buttons/SellButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, ModalBuilder, TextInputStyle } from "discord.js"; +import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; import Button from "../../structures/Button"; export default class SellButton extends Button { @@ -15,8 +15,7 @@ export default class SellButton extends Button { .setCustomId(`sell:${docId}`) .setTitle('Sell Item') .addLabelComponents( - (label) => - label.setLabel('Amount').setDescription(`Enter amount to sell. (Max: ${maxQty})`) + (label) => label.setLabel('Amount').setDescription(`Enter amount to sell. (Max: ${maxQty})`) .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short)) ); diff --git a/src/components/buttons/SkillPointsButton.ts b/src/components/buttons/SkillPointsButton.ts index 4bb95ba..f486b2a 100644 --- a/src/components/buttons/SkillPointsButton.ts +++ b/src/components/buttons/SkillPointsButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, LabelBuilder, ModalBuilder, TextDisplayBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import { type ButtonInteraction, type Client, LabelBuilder, ModalBuilder, TextDisplayBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../../structures/Button"; export default class SkillPointsButton extends Button { diff --git a/src/components/buttons/TaskClaimButton.ts b/src/components/buttons/TaskClaimButton.ts index da7ddc2..83ee88a 100644 --- a/src/components/buttons/TaskClaimButton.ts +++ b/src/components/buttons/TaskClaimButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client } from "discord.js"; +import { type ButtonInteraction, type Client } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -24,7 +24,7 @@ export default class TaskClaimButton extends Button { try { const res = await apiFetch(Routes.tasks(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'claim', taskId, period }), + body: JSON.stringify({ discordId: interaction.user.id, action: 'claim', taskId, period }) }); const body = await res.json(); @@ -37,7 +37,7 @@ export default class TaskClaimButton extends Button { const reward = body.reward; const lines = [ `✅ **Task Claimed!**`, - ``, + `` ]; if (reward) { diff --git a/src/components/buttons/TasksTabButton.ts b/src/components/buttons/TasksTabButton.ts index 0245f76..57154ff 100644 --- a/src/components/buttons/TasksTabButton.ts +++ b/src/components/buttons/TasksTabButton.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Client, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, AttachmentBuilder } from "discord.js"; +import { type ButtonInteraction, type Client, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, AttachmentBuilder } from "discord.js"; import Button from "../../structures/Button"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -39,7 +39,7 @@ export default class TasksTabButton extends Button { const imageBuffer = await ImageService.tasks(tasks, { period, resetIn, - playerEmbers, + playerEmbers }); const attachment = new AttachmentBuilder(imageBuffer, { name: 'tasks.png' }); @@ -69,11 +69,11 @@ export default class TasksTabButton extends Button { new ActionRowBuilder().setComponents( new ButtonBuilder().setCustomId('tasks_tab:daily').setLabel('Daily').setStyle(period === 'daily' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'daily'), new ButtonBuilder().setCustomId('tasks_tab:weekly').setLabel('Weekly').setStyle(period === 'weekly' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'weekly'), - new ButtonBuilder().setCustomId('tasks_tab:monthly').setLabel('Monthly').setStyle(period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'monthly'), + new ButtonBuilder().setCustomId('tasks_tab:monthly').setLabel('Monthly').setStyle(period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'monthly') ) ); - await interaction.editReply({ embeds: [embed], files: [attachment], components: components }); + await interaction.editReply({ embeds: [embed], files: [attachment], components }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code) }); } diff --git a/src/components/menus/InvSelectMenu.ts b/src/components/menus/InvSelectMenu.ts index 8975d11..8dcb102 100644 --- a/src/components/menus/InvSelectMenu.ts +++ b/src/components/menus/InvSelectMenu.ts @@ -1,4 +1,4 @@ -import { AnySelectMenuInteraction, Client } from "discord.js"; +import { type AnySelectMenuInteraction, type Client } from "discord.js"; import SelectMenu from "../../structures/SelectMenu"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; diff --git a/src/components/menus/MarketSellMenu.ts b/src/components/menus/MarketSellMenu.ts index e199dfb..c1c92d1 100644 --- a/src/components/menus/MarketSellMenu.ts +++ b/src/components/menus/MarketSellMenu.ts @@ -1,4 +1,4 @@ -import { AnySelectMenuInteraction, Client, MessageFlags, ModalBuilder, TextInputStyle } from "discord.js"; +import { type AnySelectMenuInteraction, type Client, MessageFlags, ModalBuilder, TextInputStyle } from "discord.js"; import SelectMenu from "../../structures/SelectMenu"; import ItemManager from "../../managers/ItemManager"; @@ -30,23 +30,19 @@ export default class MarketSellMenu extends SelectMenu { .setCustomId(`mkt_sell_modal:${docId}:${itemId}`) .setTitle(`🏪 List: ${itemName.slice(0, 30)}`) .addLabelComponents( - (label) => - label.setLabel('Quantity').setDescription(`How many to list (Max: ${maxQty})`) - .setTextInputComponent((ti) => - ti.setCustomId('quantity') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`1 - ${maxQty}`) + (label) => label.setLabel('Quantity').setDescription(`How many to list (Max: ${maxQty})`) + .setTextInputComponent((ti) => ti.setCustomId('quantity') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`1 - ${maxQty}`) ) ) .addLabelComponents( - (label) => - label.setLabel('Price per unit (gold)').setDescription(`Suggested: ${baseValue.toLocaleString()}g (base value)`) - .setTextInputComponent((ti) => - ti.setCustomId('price') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`e.g. ${baseValue || 100}`) + (label) => label.setLabel('Price per unit (gold)').setDescription(`Suggested: ${baseValue.toLocaleString()}g (base value)`) + .setTextInputComponent((ti) => ti.setCustomId('price') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`e.g. ${baseValue || 100}`) ) ); diff --git a/src/components/menus/ReforgeSelectMenu.ts b/src/components/menus/ReforgeSelectMenu.ts index ddfa221..cf4b90a 100644 --- a/src/components/menus/ReforgeSelectMenu.ts +++ b/src/components/menus/ReforgeSelectMenu.ts @@ -1,4 +1,4 @@ -import { AnySelectMenuInteraction, Client } from "discord.js"; +import { type AnySelectMenuInteraction, type Client } from "discord.js"; import SelectMenu from "../../structures/SelectMenu"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -29,8 +29,8 @@ export default class ReforgeSelectMenu extends SelectMenu { discordId: interaction.user.id, itemId, inventoryId: docId, - reforgeType, - }), + reforgeType + }) }); const body = await res.json(); @@ -45,7 +45,7 @@ export default class ReforgeSelectMenu extends SelectMenu { `📦 **${body.itemName}**`, `🪙 Cost: **${body.goldSpent?.toLocaleString() ?? '???'}** gold`, `💰 Balance: **${body.newBalance?.toLocaleString() ?? '???'}** gold`, - ``, + `` ]; // Show stat comparison if stats were reforged diff --git a/src/components/menus/TravelSelectMenu.ts b/src/components/menus/TravelSelectMenu.ts index f457fe2..9e234c4 100644 --- a/src/components/menus/TravelSelectMenu.ts +++ b/src/components/menus/TravelSelectMenu.ts @@ -1,4 +1,4 @@ -import { AnySelectMenuInteraction, Client, MessageFlags } from "discord.js"; +import { type AnySelectMenuInteraction, type Client, MessageFlags } from "discord.js"; import SelectMenu from "../../structures/SelectMenu"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -23,7 +23,7 @@ export default class TravelSelectMenu extends SelectMenu { try { const res = await apiFetch(Routes.travel(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, zoneId }), + body: JSON.stringify({ discordId: interaction.user.id, zoneId }) }); const body = await res.json(); diff --git a/src/components/menus/UnequipMenu.ts b/src/components/menus/UnequipMenu.ts index 312eaf3..a92bfb2 100644 --- a/src/components/menus/UnequipMenu.ts +++ b/src/components/menus/UnequipMenu.ts @@ -1,11 +1,11 @@ -import { ActionRowBuilder, AnySelectMenuInteraction, AttachmentBuilder, Client, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; +import { ActionRowBuilder, type AnySelectMenuInteraction, AttachmentBuilder, type Client, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import SelectMenu from "../../structures/SelectMenu"; import Routes from "../../utilities/Routes"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; -import { IPlayerJSON } from "../../interfaces/IPlayerJSON"; +import { type IPlayerJSON } from "../../interfaces/IPlayerJSON"; import ImageService from "../../utilities/ImageService"; -import { EquipmentSlot } from "../../interfaces/IItemJSON"; +import { type EquipmentSlot } from "../../interfaces/IItemJSON"; export default class UnequipMenu extends SelectMenu { constructor() { @@ -24,7 +24,7 @@ export default class UnequipMenu extends SelectMenu { body: JSON.stringify({ discordId, slot }) }); - const { success, error, player }: { success?: boolean, error?: string, player?: IPlayerJSON } = await res.json(); + const { success, error, player }: { success?: boolean; error?: string; player?: IPlayerJSON } = await res.json(); if (res.status === 400 || res.status === 401 || res.status === 404 || res.status === 500) { await interaction.editReply({ content: formatError(error ?? `Unequip failed (Code: ${res.status})`), files: [], components: [], embeds: [] }); @@ -59,11 +59,11 @@ export default class UnequipMenu extends SelectMenu { await interaction.editReply({ files: [profileAttachment], - components: extraMenu, + components: extraMenu }); } else { await interaction.editReply({ content: 'Unknown error!', components: [], embeds: [], files: [] }); - return; + } } } diff --git a/src/components/modals/BulkCollectModal.ts b/src/components/modals/BulkCollectModal.ts index c3a904a..75d6c01 100644 --- a/src/components/modals/BulkCollectModal.ts +++ b/src/components/modals/BulkCollectModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client, MessageFlags } from "discord.js"; +import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -33,7 +33,7 @@ export default class BulkCollectModal extends ModalSubmit { try { const res = await apiFetch(Routes.bulkCollect(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, items }), + body: JSON.stringify({ discordId: interaction.user.id, items }) }); const body = await res.json(); diff --git a/src/components/modals/BulkDismantleModal.ts b/src/components/modals/BulkDismantleModal.ts index 78f23a9..c2db119 100644 --- a/src/components/modals/BulkDismantleModal.ts +++ b/src/components/modals/BulkDismantleModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client, MessageFlags } from "discord.js"; +import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -33,7 +33,7 @@ export default class BulkDismantleModal extends ModalSubmit { try { const res = await apiFetch(Routes.bulkDismantle(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, items }), + body: JSON.stringify({ discordId: interaction.user.id, items }) }); const body = await res.json(); diff --git a/src/components/modals/BulkSellModal.ts b/src/components/modals/BulkSellModal.ts index 93ab095..109aece 100644 --- a/src/components/modals/BulkSellModal.ts +++ b/src/components/modals/BulkSellModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client, MessageFlags } from "discord.js"; +import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -34,7 +34,7 @@ export default class BulkSellModal extends ModalSubmit { try { const res = await apiFetch(Routes.bulkSell(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, items }), + body: JSON.stringify({ discordId: interaction.user.id, items }) }); const body = await res.json(); diff --git a/src/components/modals/CollectModal.ts b/src/components/modals/CollectModal.ts index 19d7df9..2850c9e 100644 --- a/src/components/modals/CollectModal.ts +++ b/src/components/modals/CollectModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client } from "discord.js"; +import { type ModalSubmitInteraction, type Client } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -25,7 +25,7 @@ export default class CollectModal extends ModalSubmit { try { const res = await apiFetch(Routes.collectionAdd(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }), + body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) }); const { success, message, error } = await res.json(); diff --git a/src/components/modals/ConsumeModal.ts b/src/components/modals/ConsumeModal.ts index 8a5921e..b891134 100644 --- a/src/components/modals/ConsumeModal.ts +++ b/src/components/modals/ConsumeModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client } from "discord.js"; +import { type ModalSubmitInteraction, type Client } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -25,7 +25,7 @@ export default class ConsumeModal extends ModalSubmit { try { const res = await apiFetch(Routes.consume(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }), + body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) }); const { success, message, error } = await res.json(); diff --git a/src/components/modals/MarketSellModal.ts b/src/components/modals/MarketSellModal.ts index a777a23..d460e0d 100644 --- a/src/components/modals/MarketSellModal.ts +++ b/src/components/modals/MarketSellModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client, MessageFlags } from "discord.js"; +import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -46,8 +46,8 @@ export default class MarketSellModal extends ModalSubmit { itemId, inventoryId: docId, quantity, - pricePerUnit, - }), + pricePerUnit + }) }); const body = await res.json(); @@ -70,8 +70,8 @@ export default class MarketSellModal extends ModalSubmit { `🪙 Price: **${pricePerUnit.toLocaleString()}** gold each`, `💰 Total if sold: **${totalGold.toLocaleString()}** gold (5% tax applies)`, ``, - `Use \`/market listings\` to view or cancel your listings.`, - ].join('\n'), + `Use \`/market listings\` to view or cancel your listings.` + ].join('\n') }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code) }); diff --git a/src/components/modals/SellModal.ts b/src/components/modals/SellModal.ts index e378a45..4abb7cd 100644 --- a/src/components/modals/SellModal.ts +++ b/src/components/modals/SellModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client } from "discord.js"; +import { type ModalSubmitInteraction, type Client } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -25,7 +25,7 @@ export default class SellModal extends ModalSubmit { try { const res = await apiFetch(Routes.sell(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }), + body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) }); const { success, message, newBalance, error } = await res.json(); @@ -37,7 +37,7 @@ export default class SellModal extends ModalSubmit { await interaction.editReply({ content: `${message}\n💰 New Balance: **${newBalance?.toLocaleString() ?? '???'}** gold`, - components: [], files: [], embeds: [], + components: [], files: [], embeds: [] }); } catch (err: any) { await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); diff --git a/src/components/modals/SkillPointsModal.ts b/src/components/modals/SkillPointsModal.ts index 83a36a1..17a9f15 100644 --- a/src/components/modals/SkillPointsModal.ts +++ b/src/components/modals/SkillPointsModal.ts @@ -1,4 +1,4 @@ -import { ModalSubmitInteraction, Client, MessageFlags } from "discord.js"; +import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; import ModalSubmit from "../../structures/ModalSubmit"; import { apiFetch } from "../../utilities/ApiClient"; import { formatError } from "../../utilities/ErrorMessages"; @@ -34,7 +34,7 @@ export default class SkillPointsModal extends ModalSubmit { if (atkAmount > 0) { const res = await apiFetch(Routes.allocate(), { method: 'POST', - body: JSON.stringify({ discordId, stat: 'atk', amount: atkAmount }), + body: JSON.stringify({ discordId, stat: 'atk', amount: atkAmount }) }); const body = await res.json(); @@ -49,7 +49,7 @@ export default class SkillPointsModal extends ModalSubmit { if (defAmount > 0) { const res = await apiFetch(Routes.allocate(), { method: 'POST', - body: JSON.stringify({ discordId, stat: 'def', amount: defAmount }), + body: JSON.stringify({ discordId, stat: 'def', amount: defAmount }) }); const body = await res.json(); diff --git a/src/events/ClientReadyEvent.ts b/src/events/ClientReadyEvent.ts index a3f41fc..bb224c6 100644 --- a/src/events/ClientReadyEvent.ts +++ b/src/events/ClientReadyEvent.ts @@ -1,4 +1,4 @@ -import { Client } from "discord.js"; +import { type Client } from "discord.js"; import Event from "../structures/Event"; import logger from "../utilities/Logger"; import ItemManager from "../managers/ItemManager"; @@ -26,14 +26,14 @@ export default class ClientReadyEvent extends Event { WorkerPool.init(); // Stagger API requests by cluster ID to prevent slamming capi.gg - const delayMs = 1500 + (clusterId * 2500); + const delayMs = 1500 + clusterId * 2500; setTimeout(async () => { - logger.info(`[Cluster ${clusterId}] Initiating staggered ItemManager sync...`); - await ItemManager.refresh(); + logger.info(`[Cluster ${clusterId}] Initiating staggered ItemManager sync...`); + await ItemManager.refresh(); - // Start rotating presence after items are loaded - await PresenceManager.init(client); + // Start rotating presence after items are loaded + await PresenceManager.init(client); }, delayMs); } } diff --git a/src/events/GuildCreateEvent.ts b/src/events/GuildCreateEvent.ts index 07fd227..9097531 100644 --- a/src/events/GuildCreateEvent.ts +++ b/src/events/GuildCreateEvent.ts @@ -1,4 +1,4 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, Colors, ContainerBuilder, EmbedBuilder, Events, Guild, MessageFlags, TextChannel } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type Client, Colors, ContainerBuilder, EmbedBuilder, Events, type Guild, MessageFlags, type TextChannel } from "discord.js"; import Event from "../structures/Event"; import logger from "../utilities/Logger"; @@ -17,15 +17,11 @@ export default class GuildCreateEvent extends Event { if (logChannel && guild) { const container = new ContainerBuilder().setAccentColor(Colors.Green) .addSectionComponents( - (section) => - section.setThumbnailAccessory((t) => t.setURL(guild.iconURL() ?? client.user?.avatarURL()!)) + (section) => section.setThumbnailAccessory((t) => t.setURL(guild.iconURL() ?? client.user?.avatarURL()!)) .addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('## I Joined A New Server!'), - (textDisplay) => - textDisplay.setContent(`Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.`), - (textDisplay) => - textDisplay.setContent(`-# ID: \`${guild.id}\``) + (textDisplay) => textDisplay.setContent('## I Joined A New Server!'), + (textDisplay) => textDisplay.setContent(`Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.`), + (textDisplay) => textDisplay.setContent(`-# ID: \`${guild.id}\``) ) ); @@ -70,7 +66,7 @@ export default class GuildCreateEvent extends Event { .setLabel('Support Server') .setStyle(ButtonStyle.Link) .setURL('https://discord.gg/3MJkKkh99q') - .setEmoji('💬'), + .setEmoji('💬') ); await targetChannel.send({ embeds: [embed], components: [row] }); diff --git a/src/events/InteractionCreateEvent.ts b/src/events/InteractionCreateEvent.ts index 1b64988..3182776 100644 --- a/src/events/InteractionCreateEvent.ts +++ b/src/events/InteractionCreateEvent.ts @@ -1,4 +1,4 @@ -import { BaseInteraction, InteractionReplyOptions, MessageFlags, Client } from "discord.js"; +import { type BaseInteraction, type InteractionReplyOptions, MessageFlags, type Client } from "discord.js"; import Event from "../structures/Event"; import SlashCommandHandler from "../handlers/SlashCommandHandler"; import logger from "../utilities/Logger"; @@ -23,7 +23,7 @@ export default class InteractionCreateEvent extends Event { if (!interaction.isRepliable()) return; // Use themed error messages for API errors, fallback for unknown errors - const message = (err instanceof ApiError) + const message = err instanceof ApiError ? formatError(err.message, err.code) : formatError(err.message || String(err)); diff --git a/src/handlers/ButtonHandler.ts b/src/handlers/ButtonHandler.ts index 6e48791..cd8d81a 100644 --- a/src/handlers/ButtonHandler.ts +++ b/src/handlers/ButtonHandler.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, Collection, MessageFlags, Client } from "discord.js"; +import { type ButtonInteraction, Collection, MessageFlags, type Client } from "discord.js"; import { readdirSync } from 'fs'; import { join } from "path"; import Button from "../structures/Button"; @@ -10,8 +10,7 @@ export default class ButtonHandler { private static _cache: Collection = new Collection(); public static load(): void { - const buttonFiles = readdirSync(filePath).filter(file => - (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const buttonFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') ); if (buttonFiles.length < 1) { diff --git a/src/handlers/EventHandler.ts b/src/handlers/EventHandler.ts index d9c0b00..17efc70 100644 --- a/src/handlers/EventHandler.ts +++ b/src/handlers/EventHandler.ts @@ -1,6 +1,6 @@ import { readdirSync } from 'fs'; import Event from '../structures/Event'; -import { Client } from 'discord.js'; +import { type Client } from 'discord.js'; import { join } from 'path'; const filePath = join(__dirname, '../events'); @@ -14,7 +14,7 @@ export default class EventHandler { } private initialize(): void { - const eventFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts'))); + const eventFiles = readdirSync(filePath).filter(file => file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts')); for (const file of eventFiles) { let event = require(join(filePath, file)); diff --git a/src/handlers/ModalSubmitHandler.ts b/src/handlers/ModalSubmitHandler.ts index 8a517f4..b21e4e0 100644 --- a/src/handlers/ModalSubmitHandler.ts +++ b/src/handlers/ModalSubmitHandler.ts @@ -1,5 +1,5 @@ import ModalSubmit from "../structures/ModalSubmit"; -import { ModalSubmitInteraction, Collection, Client } from "discord.js"; +import { type ModalSubmitInteraction, Collection, type Client } from "discord.js"; import { readdirSync } from "fs"; import { join } from "path"; import logger from "../utilities/Logger"; @@ -10,8 +10,7 @@ export default class ModalSubmitHandler { private static _cache: Collection = new Collection(); public static load(): void { - const modalFiles = readdirSync(filePath).filter(file => - (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const modalFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') ); if (modalFiles.length < 1) { diff --git a/src/handlers/SelectMenuHandler.ts b/src/handlers/SelectMenuHandler.ts index d6352e6..ab7c94c 100644 --- a/src/handlers/SelectMenuHandler.ts +++ b/src/handlers/SelectMenuHandler.ts @@ -1,5 +1,5 @@ import SelectMenu from "../structures/SelectMenu"; -import { AnySelectMenuInteraction, Collection, Client } from "discord.js"; +import { type AnySelectMenuInteraction, Collection, type Client } from "discord.js"; import { readdirSync } from 'fs'; import logger from "../utilities/Logger"; import CooldownManager from "../managers/CooldownManager"; @@ -10,8 +10,7 @@ export default class SelectMenuHandler { private static _cache: Collection = new Collection(); public static load(): void { - const menuFiles = readdirSync(filePath).filter(file => - (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const menuFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') ); if (menuFiles.length < 1) { diff --git a/src/handlers/SlashCommandHandler.ts b/src/handlers/SlashCommandHandler.ts index 848299b..5bf29d0 100644 --- a/src/handlers/SlashCommandHandler.ts +++ b/src/handlers/SlashCommandHandler.ts @@ -1,4 +1,4 @@ -import { AutocompleteInteraction, ChatInputCommandInteraction, Collection, MessageFlags, Client } from "discord.js"; +import { type AutocompleteInteraction, type ChatInputCommandInteraction, Collection, MessageFlags, type Client } from "discord.js"; import { readdirSync } from 'fs'; import { join } from "path"; import SlashCommand from "../structures/SlashCommand"; @@ -14,8 +14,7 @@ export default class SlashCommandHandler { } public static load(): void { - const commandFiles = readdirSync(filePath).filter(file => - (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const commandFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') ); for (const file of commandFiles) { diff --git a/src/index.ts b/src/index.ts index fbffe12..8f9f5fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,41 +22,41 @@ const isCompiled = __filename.endsWith('.js'); const botFile = path.join(__dirname, isCompiled ? 'bot.js' : 'bot.ts'); const manager = new ClusterManager(botFile, { - token: process.env.BOT_TOKEN, - totalShards: 'auto', // Let Discord decide shard count - shardsPerClusters: 2, // 2 internal shards per cluster process - totalClusters: 'auto', // Auto-calculate cluster count - mode: 'process', // Each cluster is a separate process - respawn: true, - restarts: { - max: 10, // Max restarts per cluster - interval: 60000 * 60, // Reset restart counter every hour - }, - queue: { - auto: true, // Automatically manage spawn queue - timeout: 60000, // 60s timeout per cluster spawn - }, - execArgv: isCompiled ? [] : ['-r', 'ts-node/register'], + token: process.env.BOT_TOKEN, + totalShards: 'auto', // Let Discord decide shard count + shardsPerClusters: 2, // 2 internal shards per cluster process + totalClusters: 'auto', // Auto-calculate cluster count + mode: 'process', // Each cluster is a separate process + respawn: true, + restarts: { + max: 10, // Max restarts per cluster + interval: 60000 * 60 // Reset restart counter every hour + }, + queue: { + auto: true, // Automatically manage spawn queue + timeout: 60000 // 60s timeout per cluster spawn + }, + execArgv: isCompiled ? [] : ['-r', 'ts-node/register'] }); manager.on('clusterCreate', (cluster) => { - logger.info(`[System] Launched Cluster #${cluster.id} (Shards: ${cluster.shardList.join(', ')})`); + logger.info(`[System] Launched Cluster #${cluster.id} (Shards: ${cluster.shardList.join(', ')})`); - cluster.on('error', (err) => { - logger.error(`[Cluster #${cluster.id}] Error: ${err}`); - }); + cluster.on('error', (err) => { + logger.error(`[Cluster #${cluster.id}] Error: ${err}`); + }); - cluster.on('death', () => { - logger.error(`[Cluster #${cluster.id}] Died. Respawning...`); - }); + cluster.on('death', () => { + logger.error(`[Cluster #${cluster.id}] Died. Respawning...`); + }); - cluster.on('message', (message: any) => { - if (message && typeof message === 'object' && 'type' in message && message.type === 'log') { - logger.info(`[Cluster #${cluster.id}] ${message.content}`); - } - }); + cluster.on('message', (message: any) => { + if (message && typeof message === 'object' && 'type' in message && message.type === 'log') { + logger.info(`[Cluster #${cluster.id}] ${message.content}`); + } + }); }); manager.spawn().catch(err => { - logger.error(`[System] Failed to spawn clusters: ${err}`); + logger.error(`[System] Failed to spawn clusters: ${err}`); }); diff --git a/src/interfaces/ICollectionJSON.ts b/src/interfaces/ICollectionJSON.ts index 1035cfc..5911284 100644 --- a/src/interfaces/ICollectionJSON.ts +++ b/src/interfaces/ICollectionJSON.ts @@ -1,6 +1,6 @@ export interface ICollectionJSON { userId: string; - items: Map, + items: Map; totalItemsCollected: number; uniqueItemsFound: number; createdAt: Date; diff --git a/src/interfaces/ICombatJSON.ts b/src/interfaces/ICombatJSON.ts index 87a48f1..7906869 100644 --- a/src/interfaces/ICombatJSON.ts +++ b/src/interfaces/ICombatJSON.ts @@ -1,5 +1,5 @@ -import { IEnemyJSON } from "./IEnemyJSON"; -import { IItemJSON } from "./IItemJSON"; +import { type IEnemyJSON } from "./IEnemyJSON"; +import { type IItemJSON } from "./IItemJSON"; export interface ICombatJSON { success: boolean; @@ -15,4 +15,4 @@ export interface ICombatJSON { levelsGained: number; }; error?: string; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/interfaces/IExecutable.ts b/src/interfaces/IExecutable.ts index 3e433c8..3523885 100644 --- a/src/interfaces/IExecutable.ts +++ b/src/interfaces/IExecutable.ts @@ -1,3 +1,3 @@ export default interface IExecutable { - execute(...args: any[]): Promise; + execute: (...args: any[]) => Promise; } \ No newline at end of file diff --git a/src/interfaces/IGameJSON.ts b/src/interfaces/IGameJSON.ts index 841ab49..009fdca 100644 --- a/src/interfaces/IGameJSON.ts +++ b/src/interfaces/IGameJSON.ts @@ -58,7 +58,7 @@ export interface IChestOpenResult { success: boolean; message: string; loot: { - items: Array<{ name: string; rarity: string; level: number; type: string }>; + items: { name: string; rarity: string; level: number; type: string }[]; gold: number; embers: number; isPity: boolean; diff --git a/src/interfaces/IInventoryJSON.ts b/src/interfaces/IInventoryJSON.ts index 89f68b7..68937e4 100644 --- a/src/interfaces/IInventoryJSON.ts +++ b/src/interfaces/IInventoryJSON.ts @@ -6,7 +6,7 @@ export interface IInventoryItem { isLocked: boolean; enhanceLevel: number; // 0 = base, 1-10 = enhanced statOverrides: { atk: number; def: number; hp: number } | null; - affixOverrides: Array<{ type: string; value: number }> | null; + affixOverrides: { type: string; value: number }[] | null; petLevel: number; createdAt: Date; updatedAt: Date; diff --git a/src/interfaces/IItemJSON.ts b/src/interfaces/IItemJSON.ts index 055c218..f67fe55 100644 --- a/src/interfaces/IItemJSON.ts +++ b/src/interfaces/IItemJSON.ts @@ -53,7 +53,7 @@ export interface IItemJSON { def: number; hp: number; }; - affixes?: Array<{ type: ItemAffixes, value: number }>; + affixes?: { type: ItemAffixes; value: number }[]; action: { effect: EffectType; amount: number; diff --git a/src/interfaces/IItemsJSON.ts b/src/interfaces/IItemsJSON.ts index 3fea040..63bd79e 100644 --- a/src/interfaces/IItemsJSON.ts +++ b/src/interfaces/IItemsJSON.ts @@ -1,7 +1,7 @@ -import { IItemJSON } from "./IItemJSON"; +import { type IItemJSON } from "./IItemJSON"; export interface IItemsJSON { success: boolean; count: number; data: IItemJSON[]; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/interfaces/IPlayerJSON.ts b/src/interfaces/IPlayerJSON.ts index 3a16b40..b837006 100644 --- a/src/interfaces/IPlayerJSON.ts +++ b/src/interfaces/IPlayerJSON.ts @@ -1,4 +1,4 @@ -import { IEnemyJSON } from "./IEnemyJSON"; +import { type IEnemyJSON } from "./IEnemyJSON"; export type Privilege = | 'Member' @@ -36,12 +36,12 @@ export interface IPlayerJSON { collections: { uniqueClaimed: number; totalClaimed: number; - } + }; discordRoleData?: { accessToken: any; refreshToken: any; expiresAt: any; - } + }; activeEncounter: IEnemyJSON | null; cooldowns: { step: Date; diff --git a/src/interfaces/IStepJSON.ts b/src/interfaces/IStepJSON.ts index 192c1d1..e73b3c7 100644 --- a/src/interfaces/IStepJSON.ts +++ b/src/interfaces/IStepJSON.ts @@ -1,5 +1,5 @@ -import { IEnemyJSON } from "./IEnemyJSON"; -import { IItemJSON } from "./IItemJSON"; +import { type IEnemyJSON } from "./IEnemyJSON"; +import { type IItemJSON } from "./IItemJSON"; export interface IStepJSON { success: boolean; diff --git a/src/managers/CooldownManager.ts b/src/managers/CooldownManager.ts index 0a01872..f0c94fb 100644 --- a/src/managers/CooldownManager.ts +++ b/src/managers/CooldownManager.ts @@ -11,10 +11,10 @@ export default class CooldownManager { if (expiration > Date.now()) { return true; - } else { - this._cache.delete(key); - return false; - } + } + this._cache.delete(key); + return false; + } public static getExpiration(key: string): number { @@ -27,7 +27,7 @@ export default class CooldownManager { public static addCooldown(key: string, durationInSeconds: number): void { if (this.onCooldown(key)) return; - const expiresAt = Date.now() + (durationInSeconds * 1000); + const expiresAt = Date.now() + durationInSeconds * 1000; this._cache.set(key, expiresAt); } diff --git a/src/managers/ItemManager.ts b/src/managers/ItemManager.ts index e07f10c..f6e7718 100644 --- a/src/managers/ItemManager.ts +++ b/src/managers/ItemManager.ts @@ -1,5 +1,5 @@ import { Collection } from "discord.js"; -import { IItemJSON } from "../interfaces/IItemJSON"; +import { type IItemJSON } from "../interfaces/IItemJSON"; import logger from "../utilities/Logger"; import Routes from "../utilities/Routes"; import 'dotenv/config'; @@ -30,7 +30,7 @@ export default class ItemManager { try { const res = await fetch(Routes.items(), { headers: Routes.HEADERS(), - signal: AbortSignal.timeout(FETCH_TIMEOUT), + signal: AbortSignal.timeout(FETCH_TIMEOUT) }); if (!res.ok) { diff --git a/src/managers/PresenceManager.ts b/src/managers/PresenceManager.ts index 0a617f6..cf733a2 100644 --- a/src/managers/PresenceManager.ts +++ b/src/managers/PresenceManager.ts @@ -1,4 +1,4 @@ -import { ActivityType, Client } from 'discord.js'; +import { ActivityType, type Client } from 'discord.js'; import logger from '../utilities/Logger'; import Routes from '../utilities/Routes'; import ItemManager from './ItemManager'; @@ -23,7 +23,7 @@ export default class PresenceManager { private static stats: GameStats = { players: 0, items: 0, - scenarios: 0, + scenarios: 0 }; public static async init(client: Client): Promise { @@ -80,14 +80,14 @@ export default class PresenceManager { { type: ActivityType.Watching, name: `${this.totalGuilds.toLocaleString()} servers` }, { type: ActivityType.Watching, name: `${this.stats.items.toLocaleString()} items` }, { type: ActivityType.Watching, name: `${this.stats.scenarios.toLocaleString()} scenarios` }, - { type: ActivityType.Playing, name: `capi.gg` }, + { type: ActivityType.Playing, name: `capi.gg` } ]; const current = activities[this.rotationIndex % activities.length]; this.client.user.setPresence({ activities: [{ name: current.name, type: current.type }], - status: 'online', + status: 'online' }); this.rotationIndex++; diff --git a/src/structures/Button.ts b/src/structures/Button.ts index 6406aec..1131150 100644 --- a/src/structures/Button.ts +++ b/src/structures/Button.ts @@ -1,5 +1,5 @@ -import IExecutable from "../interfaces/IExecutable"; -import { ButtonInteraction, Client } from "discord.js"; +import type IExecutable from "../interfaces/IExecutable"; +import { type ButtonInteraction, type Client } from "discord.js"; export interface ButtonOptions { customId: string; diff --git a/src/structures/Event.ts b/src/structures/Event.ts index b5427f1..806e27f 100644 --- a/src/structures/Event.ts +++ b/src/structures/Event.ts @@ -1,4 +1,4 @@ -import IExecutable from "../interfaces/IExecutable"; +import type IExecutable from "../interfaces/IExecutable"; export interface EventOptions { name: string; diff --git a/src/structures/ModalSubmit.ts b/src/structures/ModalSubmit.ts index e4fc492..505bab0 100644 --- a/src/structures/ModalSubmit.ts +++ b/src/structures/ModalSubmit.ts @@ -1,5 +1,5 @@ -import { ModalSubmitInteraction, Client } from "discord.js"; -import IExecutable from "../interfaces/IExecutable"; +import { type ModalSubmitInteraction, type Client } from "discord.js"; +import type IExecutable from "../interfaces/IExecutable"; export interface ModalSubmitOptions { customId: string; diff --git a/src/structures/SelectMenu.ts b/src/structures/SelectMenu.ts index f3e3d02..00426b1 100644 --- a/src/structures/SelectMenu.ts +++ b/src/structures/SelectMenu.ts @@ -1,5 +1,5 @@ -import { AnySelectMenuInteraction, Client } from "discord.js"; -import IExecutable from "../interfaces/IExecutable"; +import { type AnySelectMenuInteraction, type Client } from "discord.js"; +import type IExecutable from "../interfaces/IExecutable"; export interface SelectMenuOptions { customId: string; diff --git a/src/structures/SlashCommand.ts b/src/structures/SlashCommand.ts index a92dd59..bcc8564 100644 --- a/src/structures/SlashCommand.ts +++ b/src/structures/SlashCommand.ts @@ -1,6 +1,6 @@ -import { AutocompleteInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; -import IExecutable from "../interfaces/IExecutable"; -import { Client } from "discord.js"; +import { type AutocompleteInteraction, type ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; +import type IExecutable from "../interfaces/IExecutable"; +import { type Client } from "discord.js"; export interface SlashCommandOptions { name: string; diff --git a/src/structures/containers/AttackContainer.ts b/src/structures/containers/AttackContainer.ts index 164d23d..dbbf066 100644 --- a/src/structures/containers/AttackContainer.ts +++ b/src/structures/containers/AttackContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { ICombatJSON } from "../../interfaces/ICombatJSON"; +import { type ICombatJSON } from "../../interfaces/ICombatJSON"; export default class AttackContainer { private data: ICombatJSON; @@ -11,39 +11,34 @@ export default class AttackContainer { public build(): ContainerBuilder { const container = new ContainerBuilder(); - container.setAccentColor(this.data.victory ? 0x10b981 : (this.data.combatEnded ? 0x6b7280 : 0xef4444)); + container.setAccentColor(this.data.victory ? 0x10b981 : this.data.combatEnded ? 0x6b7280 : 0xef4444); const cleanFlavorText = this.data.flavorText.replace(/\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, '**$1**'); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(cleanFlavorText) + (textDisplay) => textDisplay.setContent(cleanFlavorText) ); if (!this.data.combatEnded && this.data.enemy) { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\``), - (textDisplay) => - textDisplay.setContent(`-# Use /attack to strike again!`) + (textDisplay) => textDisplay.setContent(`**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\``), + (textDisplay) => textDisplay.setContent(`-# Use /attack to strike again!`) ); } if (this.data.victory && this.data.rewards) { container.addSeparatorComponents((s) => s); - let rewardText = []; + const rewardText = []; if (this.data.rewards.xp) rewardText.push(`✨ +${this.data.rewards.xp.toLocaleString()} XP`); if (this.data.rewards.gold) rewardText.push(`🪙 +${this.data.rewards.gold.toLocaleString()} Gold`); if (this.data.rewards.item) rewardText.push(`🎒 Looted: **${this.data.rewards.item.name}**`); for (const reward of rewardText) { container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(reward) + (textDisplay) => textDisplay.setContent(reward) ); } } @@ -51,8 +46,7 @@ export default class AttackContainer { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ExploreContainer.ts b/src/structures/containers/ExploreContainer.ts index a192ae0..1c860ea 100644 --- a/src/structures/containers/ExploreContainer.ts +++ b/src/structures/containers/ExploreContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { IStepJSON } from "../../interfaces/IStepJSON"; +import { type IStepJSON } from "../../interfaces/IStepJSON"; export default class ExploreContainer { private data: IStepJSON; @@ -16,33 +16,25 @@ export default class ExploreContainer { const cleanFlavorText = this.data.flavorText.replace(/\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, '**$1**'); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(cleanFlavorText), - (textDisplay) => - textDisplay.setContent(`-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\``) + (textDisplay) => textDisplay.setContent(cleanFlavorText), + (textDisplay) => textDisplay.setContent(`-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\``) ); if (this.data.enemy) { const enemy = this.data.enemy; container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\``), - (textDisplay) => - textDisplay.setContent(`**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`-# Use the /attack command to fight`) + (textDisplay) => textDisplay.setContent(`**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\``), + (textDisplay) => textDisplay.setContent(`**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`-# Use the /attack command to fight`) ); container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -52,7 +44,7 @@ export default class ExploreContainer { const experience = stats.exp ?? 0; const expRequired = stats.expRequired ?? 1; - let rewardText = []; + const rewardText = []; if (this.data.rewards.xp) rewardText.push(`✨ +${this.data.rewards.xp} XP`); if (this.data.rewards.gold) rewardText.push(`🪙 +${this.data.rewards.gold} Gold`); if (this.data.rewards.item) rewardText.push(`🎒 Found: **${this.data.rewards.item.name}** (${this.data.rewards.item.rarity})`); @@ -61,23 +53,20 @@ export default class ExploreContainer { if (rewardText.length >= 1) { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\``) - ) + (textDisplay) => textDisplay.setContent(`-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\``) + ); } for (const text of rewardText) { container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(text) + (textDisplay) => textDisplay.setContent(text) ); } container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -86,8 +75,7 @@ export default class ExploreContainer { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ItemLookupContainer.ts b/src/structures/containers/ItemLookupContainer.ts index f5c90c2..0e2ddfa 100644 --- a/src/structures/containers/ItemLookupContainer.ts +++ b/src/structures/containers/ItemLookupContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { IItemJSON, RARITY_COLORS } from "../../interfaces/IItemJSON"; +import { type IItemJSON, RARITY_COLORS } from "../../interfaces/IItemJSON"; export default class ItemLookupContainer { private data: IItemJSON; @@ -12,21 +12,16 @@ export default class ItemLookupContainer { const container = new ContainerBuilder().setAccentColor(RARITY_COLORS[this.data.rarity]); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), - (textDisplay) => - textDisplay.setContent(`-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*`), - (textDisplay) => - textDisplay.setContent(`*${this.data.description}*`), - (textDisplay) => - textDisplay.setContent(`-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\``) + (textDisplay) => textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), + (textDisplay) => textDisplay.setContent(`-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*`), + (textDisplay) => textDisplay.setContent(`*${this.data.description}*`), + (textDisplay) => textDisplay.setContent(`-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\``) ); if (this.data.affixes) { for (const affix of this.data.affixes) { container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\``) + (textDisplay) => textDisplay.setContent(`**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\``) ); } } @@ -34,8 +29,7 @@ export default class ItemLookupContainer { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/NPCLookupContainer.ts b/src/structures/containers/NPCLookupContainer.ts index 6303f6a..f1f672b 100644 --- a/src/structures/containers/NPCLookupContainer.ts +++ b/src/structures/containers/NPCLookupContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { INPCJSON } from "../../interfaces/INPCJSON"; +import { type INPCJSON } from "../../interfaces/INPCJSON"; export default class NPCLookupContainer { private data: INPCJSON; @@ -12,10 +12,8 @@ export default class NPCLookupContainer { const container = new ContainerBuilder(); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), - (textDisplay) => - textDisplay.setContent(this.data.description) + (textDisplay) => textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), + (textDisplay) => textDisplay.setContent(this.data.description) ); return container; diff --git a/src/structures/containers/ProfileContainer.ts b/src/structures/containers/ProfileContainer.ts index 727a6bb..ac82119 100644 --- a/src/structures/containers/ProfileContainer.ts +++ b/src/structures/containers/ProfileContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { IPlayerJSON } from "../../interfaces/IPlayerJSON"; +import { type IPlayerJSON } from "../../interfaces/IPlayerJSON"; export default class ProfileContainer { private data: IPlayerJSON; @@ -12,53 +12,37 @@ export default class ProfileContainer { const container = new ContainerBuilder(); container.addSectionComponents( - (section) => - section.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Username:** \`${this.data.username}\``), - (textDisplay) => - textDisplay.setContent(`**Level:** \`${this.data.level.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**Experience:** \`${this.data.experience.toLocaleString()}\``) - ).setThumbnailAccessory( - (tb) => - tb.setURL(`https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png`) - ) + (section) => section.addTextDisplayComponents( + (textDisplay) => textDisplay.setContent(`**Username:** \`${this.data.username}\``), + (textDisplay) => textDisplay.setContent(`**Level:** \`${this.data.level.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Experience:** \`${this.data.experience.toLocaleString()}\``) + ).setThumbnailAccessory( + (tb) => tb.setURL(`https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png`) + ) ); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), - (textDisplay) => - textDisplay.setContent(`**Coins:** \`${this.data.coins.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\``), - (textDisplay) => - textDisplay.setContent(`**ATK:** \`${this.data.stats.atk.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**DEF:** \`${this.data.stats.def.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), + (textDisplay) => textDisplay.setContent(`**Coins:** \`${this.data.coins.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\``), + (textDisplay) => textDisplay.setContent(`**ATK:** \`${this.data.stats.atk.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**DEF:** \`${this.data.stats.def.toLocaleString()}\``) ); container.addSeparatorComponents((separator) => separator); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\``) + (textDisplay) => textDisplay.setContent(`**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\``) ); container.addSeparatorComponents((separator) => separator); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ScenarioLookupContainer.ts b/src/structures/containers/ScenarioLookupContainer.ts index 1dd8e4b..e434ccf 100644 --- a/src/structures/containers/ScenarioLookupContainer.ts +++ b/src/structures/containers/ScenarioLookupContainer.ts @@ -1,5 +1,5 @@ import { ContainerBuilder } from "discord.js"; -import { IScenarioJSON } from "../../interfaces/IScenarioJSON"; +import { type IScenarioJSON } from "../../interfaces/IScenarioJSON"; export default class ScenarioLookupContainer { private data: IScenarioJSON; @@ -12,21 +12,16 @@ export default class ScenarioLookupContainer { const container = new ContainerBuilder(); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('## Scenario Viewer'), - (textDisplay) => - textDisplay.setContent(this.data.description), - (textDisplay) => - textDisplay.setContent(`-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\``), - (textDisplay) => - textDisplay.setContent(`-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\``) + (textDisplay) => textDisplay.setContent('## Scenario Viewer'), + (textDisplay) => textDisplay.setContent(this.data.description), + (textDisplay) => textDisplay.setContent(`-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\``), + (textDisplay) => textDisplay.setContent(`-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\``) ); container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/utilities/AdventureImageBuilder.ts b/src/utilities/AdventureImageBuilder.ts index 1f54413..0d914bc 100644 --- a/src/utilities/AdventureImageBuilder.ts +++ b/src/utilities/AdventureImageBuilder.ts @@ -1,6 +1,6 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; -import { IStepJSON } from '../interfaces/IStepJSON'; -import { ICombatJSON } from '../interfaces/ICombatJSON'; +import { type IStepJSON } from '../interfaces/IStepJSON'; +import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { join } from 'path'; // Load OS-agnostic emoji font @@ -14,187 +14,187 @@ export default class AdventureImageBuilder { let currentY = startY; for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - - // 1. Block-Level Modifiers (Headers, Quotes, Lists, Subtext) - let isQuote = false; - let isSubtext = false; - let headerLevel = 0; - let listPrefix = ''; - let lineIndent = 0; - - if (line.startsWith('>>> ')) { isQuote = true; line = line.substring(4); } - else if (line.startsWith('> ')) { isQuote = true; line = line.substring(2); } - - if (line.startsWith('-# ')) { isSubtext = true; line = line.substring(3); } - else if (line.startsWith('### ')) { headerLevel = 3; line = line.substring(4); } - else if (line.startsWith('## ')) { headerLevel = 2; line = line.substring(3); } - else if (line.startsWith('# ')) { headerLevel = 1; line = line.substring(2); } - - const listMatch = line.match(/^(\s*[-*+]\s|\s*\d+\.\s)/); - if (listMatch) { - listPrefix = listMatch[0].trim(); - line = line.substring(listMatch[0].length); - lineIndent = 25; // Push list items inward - } - - if (isQuote) lineIndent = 20; - - // 2. Inline Markdown to Pseudo-HTML conversion - let parsedLine = line - .replace(/\|\|([^|]+)\|\|/g, '$1') // Strip Spoilers - .replace(/<@!?\d+>/g, '@User') // Format Mentions - .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // Bold Italic - .replace(/\*\*(.+?)\*\*/g, '$1') // Bold - .replace(/\*(.+?)\*/g, '$1') // Italic - .replace(/__(.+?)__/g, '$1') // Underline - .replace(/~~(.+?)~~/g, '$1') // Strikethrough - .replace(/`([^`]+)`/g, '$1') // Inline Code - .replace(/\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, '$1'); // Hex Colors - - // 3. Tokenize by our custom tags - const tokens = parsedLine.split(/(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g).filter(Boolean); - - let currentState = { - bold: headerLevel > 0, - italic: isQuote || isSubtext, - underline: false, - strike: false, - code: false, - color: isSubtext ? '#6b7280' : (isQuote ? '#9ca3af' : defaultColor) - }; - - const wordObjects: { word: string, state: any }[] = []; - - // Inject the bullet point/number if it's a list - if (listPrefix) { - wordObjects.push({ word: listPrefix + ' ', state: { ...currentState, color: '#10b981', bold: true } }); - } - - // Apply state toggles and chunk text into words - for (const token of tokens) { - if (token === '') { currentState.bold = true; currentState.italic = true; } - else if (token === '') { currentState.bold = false; currentState.italic = false; } - else if (token === '') currentState.bold = true; - else if (token === '') currentState.bold = false; - else if (token === '') currentState.italic = true; - else if (token === '') currentState.italic = false; - else if (token === '') currentState.underline = true; - else if (token === '') currentState.underline = false; - else if (token === '') currentState.strike = true; - else if (token === '') currentState.strike = false; - else if (token === '') { currentState.code = true; currentState.color = '#6ee7b7'; } - else if (token === '') { currentState.code = false; currentState.color = isSubtext ? '#6b7280' : (isQuote ? '#9ca3af' : defaultColor); } - else if (token.startsWith('') { currentState.color = isSubtext ? '#6b7280' : (isQuote ? '#9ca3af' : defaultColor); } - else { - const textWords = token.split(' '); - for (let w = 0; w < textWords.length; w++) { - const wordStr = textWords[w] + (w < textWords.length - 1 ? ' ' : ''); - if (wordStr.length > 0) { - wordObjects.push({ word: wordStr, state: { ...currentState } }); - } - } + let line = lines[i]; + + // 1. Block-Level Modifiers (Headers, Quotes, Lists, Subtext) + let isQuote = false; + let isSubtext = false; + let headerLevel = 0; + let listPrefix = ''; + let lineIndent = 0; + + if (line.startsWith('>>> ')) { isQuote = true; line = line.substring(4); } + else if (line.startsWith('> ')) { isQuote = true; line = line.substring(2); } + + if (line.startsWith('-# ')) { isSubtext = true; line = line.substring(3); } + else if (line.startsWith('### ')) { headerLevel = 3; line = line.substring(4); } + else if (line.startsWith('## ')) { headerLevel = 2; line = line.substring(3); } + else if (line.startsWith('# ')) { headerLevel = 1; line = line.substring(2); } + + const listMatch = line.match(/^(\s*[-*+]\s|\s*\d+\.\s)/); + if (listMatch) { + listPrefix = listMatch[0].trim(); + line = line.substring(listMatch[0].length); + lineIndent = 25; // Push list items inward + } + + if (isQuote) lineIndent = 20; + + // 2. Inline Markdown to Pseudo-HTML conversion + const parsedLine = line + .replace(/\|\|([^|]+)\|\|/g, '$1') // Strip Spoilers + .replace(/<@!?\d+>/g, '@User') // Format Mentions + .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // Bold Italic + .replace(/\*\*(.+?)\*\*/g, '$1') // Bold + .replace(/\*(.+?)\*/g, '$1') // Italic + .replace(/__(.+?)__/g, '$1') // Underline + .replace(/~~(.+?)~~/g, '$1') // Strikethrough + .replace(/`([^`]+)`/g, '$1') // Inline Code + .replace(/\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, '$1'); // Hex Colors + + // 3. Tokenize by our custom tags + const tokens = parsedLine.split(/(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g).filter(Boolean); + + const currentState = { + bold: headerLevel > 0, + italic: isQuote || isSubtext, + underline: false, + strike: false, + code: false, + color: isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor + }; + + const wordObjects: { word: string; state: any }[] = []; + + // Inject the bullet point/number if it's a list + if (listPrefix) { + wordObjects.push({ word: `${listPrefix} `, state: { ...currentState, color: '#10b981', bold: true } }); + } + + // Apply state toggles and chunk text into words + for (const token of tokens) { + if (token === '') { currentState.bold = true; currentState.italic = true; } + else if (token === '') { currentState.bold = false; currentState.italic = false; } + else if (token === '') currentState.bold = true; + else if (token === '') currentState.bold = false; + else if (token === '') currentState.italic = true; + else if (token === '') currentState.italic = false; + else if (token === '') currentState.underline = true; + else if (token === '') currentState.underline = false; + else if (token === '') currentState.strike = true; + else if (token === '') currentState.strike = false; + else if (token === '') { currentState.code = true; currentState.color = '#6ee7b7'; } + else if (token === '') { currentState.code = false; currentState.color = isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor; } + else if (token.startsWith('') { currentState.color = isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor; } + else { + const textWords = token.split(' '); + for (let w = 0; w < textWords.length; w++) { + const wordStr = textWords[w] + (w < textWords.length - 1 ? ' ' : ''); + if (wordStr.length > 0) { + wordObjects.push({ word: wordStr, state: { ...currentState } }); } + } } + } - const getFont = (state: any) => { - let weight = state.bold ? 'bold ' : ''; - let style = state.italic ? 'italic ' : ''; - let size = 22; // Base size + const getFont = (state: any) => { + const weight = state.bold ? 'bold ' : ''; + const style = state.italic ? 'italic ' : ''; + let size = 22; // Base size - if (headerLevel === 1) size = 32; - else if (headerLevel === 2) size = 28; - else if (headerLevel === 3) size = 24; - else if (isSubtext) size = 16; - else if (state.code) size = 20; - - let family = state.code ? 'monospace' : 'sans-serif'; - if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; - - return `${style}${weight}${size}px ${family}`; - }; - - let lineWords: any[] = []; - let currentLineWidth = 0; - const startYOfParagraph = currentY; - - let increment = baseLineHeight; - if (headerLevel === 1) increment = 44; - else if (headerLevel === 2) increment = 38; - else if (headerLevel === 3) increment = 32; - else if (isSubtext) increment = 22; - - if (wordObjects.length === 0) { - currentY += baseLineHeight; - continue; - } - - // Draws a full wrapped line to the canvas - const flushLine = () => { - if (lineWords.length === 0) return; + if (headerLevel === 1) size = 32; + else if (headerLevel === 2) size = 28; + else if (headerLevel === 3) size = 24; + else if (isSubtext) size = 16; + else if (state.code) size = 20; + + let family = state.code ? 'monospace' : 'sans-serif'; + if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; + + return `${style}${weight}${size}px ${family}`; + }; + + let lineWords: any[] = []; + let currentLineWidth = 0; + const startYOfParagraph = currentY; + + let increment = baseLineHeight; + if (headerLevel === 1) increment = 44; + else if (headerLevel === 2) increment = 38; + else if (headerLevel === 3) increment = 32; + else if (isSubtext) increment = 22; + + if (wordObjects.length === 0) { + currentY += baseLineHeight; + continue; + } + + // Draws a full wrapped line to the canvas + const flushLine = () => { + if (lineWords.length === 0) return; - if (draw) { - let drawX = startX + lineIndent; - for (const lw of lineWords) { - ctx.font = getFont(lw.state); - ctx.fillStyle = lw.state.color; + if (draw) { + let drawX = startX + lineIndent; + for (const lw of lineWords) { + ctx.font = getFont(lw.state); + ctx.fillStyle = lw.state.color; - const m = ctx.measureText(lw.word); + const m = ctx.measureText(lw.word); - if (lw.state.code) { - ctx.fillStyle = '#ffffff1a'; - ctx.fillRect(drawX, currentY - (increment * 0.7), m.width, increment); - ctx.fillStyle = lw.state.color; - } - - ctx.fillText(lw.word, drawX, currentY); - - // Underlines and Strikethroughs - if (lw.state.underline) { - ctx.fillRect(drawX, currentY + 4, m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); - } - if (lw.state.strike) { - ctx.fillRect(drawX, currentY - (increment * 0.3), m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); - } - - drawX += m.width; - } + if (lw.state.code) { + ctx.fillStyle = '#ffffff1a'; + ctx.fillRect(drawX, currentY - increment * 0.7, m.width, increment); + ctx.fillStyle = lw.state.color; } - currentY += increment; - lineWords = []; - currentLineWidth = 0; - }; - - // Measure & Wrap loop - for (let w = 0; w < wordObjects.length; w++) { - const wObj = wordObjects[w]; - ctx.font = getFont(wObj.state); - let metrics = ctx.measureText(wObj.word); - - if (currentLineWidth + metrics.width > maxWidth - lineIndent && lineWords.length > 0) { - flushLine(); - // Strip leading space on wrap - if (wObj.word.startsWith(' ')) { - wObj.word = wObj.word.substring(1); - metrics = ctx.measureText(wObj.word); - } + + ctx.fillText(lw.word, drawX, currentY); + + // Underlines and Strikethroughs + if (lw.state.underline) { + ctx.fillRect(drawX, currentY + 4, m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); + } + if (lw.state.strike) { + ctx.fillRect(drawX, currentY - increment * 0.3, m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); } - lineWords.push(wObj); - currentLineWidth += metrics.width; + drawX += m.width; + } + } + currentY += increment; + lineWords = []; + currentLineWidth = 0; + }; + + // Measure & Wrap loop + for (let w = 0; w < wordObjects.length; w++) { + const wObj = wordObjects[w]; + ctx.font = getFont(wObj.state); + let metrics = ctx.measureText(wObj.word); + + if (currentLineWidth + metrics.width > maxWidth - lineIndent && lineWords.length > 0) { + flushLine(); + // Strip leading space on wrap + if (wObj.word.startsWith(' ')) { + wObj.word = wObj.word.substring(1); + metrics = ctx.measureText(wObj.word); + } } - flushLine(); + lineWords.push(wObj); + currentLineWidth += metrics.width; + } - // Draw Block Quote bar across the entire paragraph block - if (isQuote && draw) { - ctx.fillStyle = '#10b98180'; - ctx.fillRect(startX, startYOfParagraph - (increment * 0.7), 4, currentY - startYOfParagraph); - } + flushLine(); - if (headerLevel > 0) currentY += 10; - else currentY += baseLineHeight * 0.3; // Paragraph margin + // Draw Block Quote bar across the entire paragraph block + if (isQuote && draw) { + ctx.fillStyle = '#10b98180'; + ctx.fillRect(startX, startYOfParagraph - increment * 0.7, 4, currentY - startYOfParagraph); + } + + if (headerLevel > 0) currentY += 10; + else currentY += baseLineHeight * 0.3; // Paragraph margin } return currentY - startY; @@ -207,20 +207,20 @@ export default class AdventureImageBuilder { const enemyStats = data.enemy; const scenarioMeta = { - id: (data as IStepJSON).scenarioId || '0', - author: (data as IStepJSON).scenarioAuthor || 'SYSTEM' + id: (data as IStepJSON).scenarioId || '0', + author: (data as IStepJSON).scenarioAuthor || 'SYSTEM' }; const pStats = data.playerStats || {}; const level = pStats.level ?? 1; const mappedStats = { - hp: Math.floor(pStats.stats?.hp ?? pStats.hp ?? 0), - maxHp: pStats.maxHp ?? 100, - level: level, - exp: Math.floor(pStats.experience ?? pStats.exp ?? 0), - gold: pStats.coins ?? pStats.gold ?? 0, - expRequired: pStats.expRequired ?? Math.floor(50 * Math.pow(Math.max(1, level), 1.3)), - activeBonuses: pStats.activeBonuses || {} + hp: Math.floor(pStats.stats?.hp ?? pStats.hp ?? 0), + maxHp: pStats.maxHp ?? 100, + level, + exp: Math.floor(pStats.experience ?? pStats.exp ?? 0), + gold: pStats.coins ?? pStats.gold ?? 0, + expRequired: pStats.expRequired ?? Math.floor(50 * Math.max(1, level)**1.3), + activeBonuses: pStats.activeBonuses || {} }; const inCombat = !!enemyStats; @@ -240,7 +240,7 @@ export default class AdventureImageBuilder { let extraHeight = 0; const baseTextSpace = 160; if (requiredTextHeight > baseTextSpace) { - extraHeight = requiredTextHeight - baseTextSpace + 20; // Stretch canvas + extraHeight = requiredTextHeight - baseTextSpace + 20; // Stretch canvas } // --- DYNAMIC CANVAS SIZING --- @@ -261,14 +261,14 @@ export default class AdventureImageBuilder { ctx.strokeStyle = '#ffffff05'; ctx.lineWidth = 1; for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); } // 2. Header ctx.fillStyle = themeColor; ctx.font = 'bold 36px sans-serif'; ctx.textAlign = 'center'; - const headerText = isDead ? 'SYSTEM FAILURE' : (inCombat ? 'COMBAT ENGAGED' : 'ADVENTURE'); + const headerText = isDead ? 'SYSTEM FAILURE' : inCombat ? 'COMBAT ENGAGED' : 'ADVENTURE'; ctx.fillText(headerText, canvas.width / 2, 60); // 3. Terminal Window @@ -300,7 +300,7 @@ export default class AdventureImageBuilder { ctx.fillStyle = '#6b7280'; ctx.font = '12px monospace'; ctx.textAlign = 'left'; - ctx.fillText(inCombat ? 'combat_protocol.exe' : (isDead ? 'system_dump.log' : 'adventure_logs.sh'), termX + 80, termY + 20); + ctx.fillText(inCombat ? 'combat_protocol.exe' : isDead ? 'system_dump.log' : 'adventure_logs.sh', termX + 80, termY + 20); ctx.fillStyle = '#ffffff0a'; ctx.fillRect(termX, termY + termH - 25, termW, 25); @@ -310,7 +310,7 @@ export default class AdventureImageBuilder { ctx.textAlign = 'right'; ctx.fillText(`Author: ${scenarioMeta.author}`, termX + termW - 15, termY + termH - 8); - const textColor = isDead ? '#fca5a5' : (inCombat ? '#fca5a5' : themeColor); + const textColor = isDead ? '#fca5a5' : inCombat ? '#fca5a5' : themeColor; ctx.fillStyle = textColor; ctx.font = '22px monospace'; ctx.textAlign = 'left'; @@ -323,34 +323,34 @@ export default class AdventureImageBuilder { // 4. Enemy Stats if (inCombat && enemyStats && !isDead) { - ctx.fillStyle = '#450a0a'; - ctx.strokeStyle = '#ef44444d'; - ctx.beginPath(); ctx.roundRect(termX, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#ef4444b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('ATK', termX + 30, yOffset + 18); - ctx.fillStyle = '#f87171'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); - - ctx.fillStyle = '#172554'; - ctx.strokeStyle = '#3b82f64d'; - ctx.beginPath(); ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#3b82f6b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('DEF', termX + termW - 30, yOffset + 18); - ctx.fillStyle = '#60a5fa'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); - - const eBarX = termX + 75; - const eBarW = termW - 150; - ctx.fillStyle = '#f87171'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(enemyStats.name, eBarX, yOffset + 15); - ctx.textAlign = 'right'; - ctx.fillText(`${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, eBarX + eBarW, yOffset + 15); - - ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); ctx.fill(); - const eHpPercent = Math.max(0, Math.min(enemyStats.currentHp / enemyStats.maxHp, 1)); - ctx.fillStyle = '#dc2626'; - ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); ctx.fill(); - - yOffset += 75; + ctx.fillStyle = '#450a0a'; + ctx.strokeStyle = '#ef44444d'; + ctx.beginPath(); ctx.roundRect(termX, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); + ctx.fillStyle = '#ef4444b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('ATK', termX + 30, yOffset + 18); + ctx.fillStyle = '#f87171'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); + + ctx.fillStyle = '#172554'; + ctx.strokeStyle = '#3b82f64d'; + ctx.beginPath(); ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); + ctx.fillStyle = '#3b82f6b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('DEF', termX + termW - 30, yOffset + 18); + ctx.fillStyle = '#60a5fa'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); + + const eBarX = termX + 75; + const eBarW = termW - 150; + ctx.fillStyle = '#f87171'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(enemyStats.name, eBarX, yOffset + 15); + ctx.textAlign = 'right'; + ctx.fillText(`${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, eBarX + eBarW, yOffset + 15); + + ctx.fillStyle = '#ffffff1a'; + ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); ctx.fill(); + const eHpPercent = Math.max(0, Math.min(enemyStats.currentHp / enemyStats.maxHp, 1)); + ctx.fillStyle = '#dc2626'; + ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); ctx.fill(); + + yOffset += 75; } // 5. Player Stats @@ -388,30 +388,30 @@ export default class AdventureImageBuilder { // 6. Active Bonuses if (hasBonuses) { - let pillX = termX; + let pillX = termX; - const drawBonusPill = (label: string, value: string, bgColor: string, borderColor: string, textColor: string) => { - ctx.font = 'bold 10px sans-serif'; - const text = `${label}: ${value}`; - const textWidth = ctx.measureText(text).width; + const drawBonusPill = (label: string, value: string, bgColor: string, borderColor: string, textColor: string) => { + ctx.font = 'bold 10px sans-serif'; + const text = `${label}: ${value}`; + const textWidth = ctx.measureText(text).width; - ctx.fillStyle = bgColor; - ctx.strokeStyle = borderColor; - ctx.beginPath(); ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); ctx.fill(); ctx.stroke(); + ctx.fillStyle = bgColor; + ctx.strokeStyle = borderColor; + ctx.beginPath(); ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); ctx.fill(); ctx.stroke(); - ctx.fillStyle = textColor; - ctx.textAlign = 'center'; - ctx.fillText(text, pillX + (textWidth + 16) / 2, yOffset + 14); + ctx.fillStyle = textColor; + ctx.textAlign = 'center'; + ctx.fillText(text, pillX + (textWidth + 16) / 2, yOffset + 14); - pillX += textWidth + 24; - }; + pillX += textWidth + 24; + }; - if (b.critChance > 5) drawBonusPill('Crit', `${b.critChance}%`, '#713f1233', '#eab30833', '#facc15'); - if (b.lifeSteal > 0) drawBonusPill('Vamp', `${b.lifeSteal}%`, '#450a0a33', '#ef444433', '#f87171'); - if (b.dodge > 0) drawBonusPill('Dodge', `${b.dodge}%`, '#17255433', '#3b82f633', '#93c5fd'); - if (b.thorns > 0) drawBonusPill('Thorns', `${b.thorns}`, '#7c2d1233', '#f9731633', '#fb923c'); + if (b.critChance > 5) drawBonusPill('Crit', `${b.critChance}%`, '#713f1233', '#eab30833', '#facc15'); + if (b.lifeSteal > 0) drawBonusPill('Vamp', `${b.lifeSteal}%`, '#450a0a33', '#ef444433', '#f87171'); + if (b.dodge > 0) drawBonusPill('Dodge', `${b.dodge}%`, '#17255433', '#3b82f633', '#93c5fd'); + if (b.thorns > 0) drawBonusPill('Thorns', `${b.thorns}`, '#7c2d1233', '#f9731633', '#fb923c'); - yOffset += 35; + yOffset += 35; } // 7. Footer (Gold) @@ -426,45 +426,45 @@ export default class AdventureImageBuilder { // 8. TOAST NOTIFICATIONS (Rewards) const rewards = (data as any).rewards; if (rewards) { - const toasts: { msg: string, color: string, icon: string }[] = []; + const toasts: { msg: string; color: string; icon: string }[] = []; - if (rewards.xp) toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); - if (rewards.gold) toasts.push({ msg: `+${rewards.gold} Gold`, color: '#eab308', icon: '🪙' }); - if (rewards.levelsGained > 0) toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); - if (rewards.item) { - const RARITY_COLORS: Record = { Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff', Exotic: '#ff00cc' }; - const itemColor = RARITY_COLORS[rewards.item.rarity] || '#ffffff'; - toasts.push({ msg: rewards.item.name, color: itemColor, icon: '🎒' }); - } - - let toastY = 30; - for (const toast of toasts) { - ctx.font = 'bold 14px sans-serif'; - const msgWidth = ctx.measureText(toast.msg).width; - const toastW = msgWidth + 60; - const toastH = 40; + if (rewards.xp) toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); + if (rewards.gold) toasts.push({ msg: `+${rewards.gold} Gold`, color: '#eab308', icon: '🪙' }); + if (rewards.levelsGained > 0) toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); + if (rewards.item) { + const RARITY_COLORS: Record = { Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff', Exotic: '#ff00cc' }; + const itemColor = RARITY_COLORS[rewards.item.rarity] || '#ffffff'; + toasts.push({ msg: rewards.item.name, color: itemColor, icon: '🎒' }); + } + + let toastY = 30; + for (const toast of toasts) { + ctx.font = 'bold 14px sans-serif'; + const msgWidth = ctx.measureText(toast.msg).width; + const toastW = msgWidth + 60; + const toastH = 40; - ctx.fillStyle = '#0a0a0ae6'; - ctx.beginPath(); ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); ctx.fill(); + ctx.fillStyle = '#0a0a0ae6'; + ctx.beginPath(); ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = `${toast.color}40`; - ctx.stroke(); + ctx.lineWidth = 1; + ctx.strokeStyle = `${toast.color}40`; + ctx.stroke(); - ctx.fillStyle = toast.color; - ctx.fillRect(0, toastY, 4, toastH); + ctx.fillStyle = toast.color; + ctx.fillRect(0, toastY, 4, toastH); - ctx.font = '16px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(toast.icon, 24, toastY + 26); + ctx.font = '16px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(toast.icon, 24, toastY + 26); - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(toast.msg, 44, toastY + 25); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(toast.msg, 44, toastY + 25); - toastY += toastH + 10; - } + toastY += toastH + 10; + } } return canvas.toBuffer('image/png'); diff --git a/src/utilities/ApiClient.ts b/src/utilities/ApiClient.ts index d6393b1..153dc22 100644 --- a/src/utilities/ApiClient.ts +++ b/src/utilities/ApiClient.ts @@ -48,9 +48,9 @@ export async function apiFetch(url: string, options?: RequestInit): Promise = { Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', + Divine: '#00e5ff' }; const TIER_EMOJIS: Record = { Common: '📦', Uncommon: '🟢', Rare: '🔵', Elite: '🟠', - Epic: '🟣', Legendary: '⭐', Divine: '💎', + Epic: '🟣', Legendary: '⭐', Divine: '💎' }; export interface ChestsPageConfig { @@ -156,7 +156,7 @@ export default class ChestsImageBuilder { } else if (chest.status === 'unlocking') { const remainSec = Math.max(0, Math.floor(chest.remainingMs / 1000)); const h = Math.floor(remainSec / 3600); - const m = Math.floor((remainSec % 3600) / 60); + const m = Math.floor(remainSec % 3600 / 60); const s = remainSec % 60; const timeStr = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; diff --git a/src/utilities/CombatResponseBuilder.ts b/src/utilities/CombatResponseBuilder.ts index 5b56eaa..fb71b01 100644 --- a/src/utilities/CombatResponseBuilder.ts +++ b/src/utilities/CombatResponseBuilder.ts @@ -1,6 +1,6 @@ import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; -import { ICombatJSON } from '../interfaces/ICombatJSON'; -import { IStepJSON } from '../interfaces/IStepJSON'; +import { type ICombatJSON } from '../interfaces/ICombatJSON'; +import { type IStepJSON } from '../interfaces/IStepJSON'; import ImageService from './ImageService'; export interface CombatResponse { @@ -32,8 +32,8 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis const playerStats = isStepData.playerStats; const showCombatButtons = - (isCombatData.combatEnded === false) || - (isStepData.combatTrigger === true); + isCombatData.combatEnded === false || + isStepData.combatTrigger === true; if (showCombatButtons) { const row = new ActionRowBuilder().setComponents( @@ -44,7 +44,7 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis new ButtonBuilder() .setCustomId('embedFlee') .setLabel('Flee') - .setStyle(ButtonStyle.Secondary), + .setStyle(ButtonStyle.Secondary) ); components.push(row); } else { diff --git a/src/utilities/ErrorMessages.ts b/src/utilities/ErrorMessages.ts index d13d7a8..e6ba440 100644 --- a/src/utilities/ErrorMessages.ts +++ b/src/utilities/ErrorMessages.ts @@ -5,27 +5,27 @@ const ERROR_MAP: Record = { // API error codes (structured) - 'PLAYER_NOT_FOUND': '📜 **Adventurer not found!** Begin your journey with .', - 'IN_COMBAT': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', - 'INCAPACITATED': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', - 'NO_ACTIVE_COMBAT': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', - 'API_UNAVAILABLE': '🔧 **The realm is under maintenance.** The game server is temporarily unreachable. Please try again in a moment.', - 'API_TIMEOUT': '⏳ **The winds of fate are slow today.** The game server took too long to respond. Please try again.', - 'API_NETWORK_ERROR': '🌐 **Lost connection to the realm.** Could not reach the game server. Please try again later.', + 'PLAYER_NOT_FOUND': '📜 **Adventurer not found!** Begin your journey with .', + 'IN_COMBAT': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', + 'INCAPACITATED': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', + 'NO_ACTIVE_COMBAT': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', + 'API_UNAVAILABLE': '🔧 **The realm is under maintenance.** The game server is temporarily unreachable. Please try again in a moment.', + 'API_TIMEOUT': '⏳ **The winds of fate are slow today.** The game server took too long to respond. Please try again.', + 'API_NETWORK_ERROR': '🌐 **Lost connection to the realm.** Could not reach the game server. Please try again later.', // Raw API error strings (legacy matching) - 'You are incapacitated.': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', + 'You are incapacitated.': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', 'You are incapacitated. Wait for regeneration.': '💀 **You have fallen!** Wait for your health to regenerate before venturing out.', - 'No active combat found.': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', - 'You are currently in combat!': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', - 'Player not found': '📜 **Adventurer not found!** Begin your journey with `/register`.', - 'Player load failed': '📜 **Adventurer not found!** Begin your journey with `/register`.', + 'No active combat found.': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', + 'You are currently in combat!': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', + 'Player not found': '📜 **Adventurer not found!** Begin your journey with `/register`.', + 'Player load failed': '📜 **Adventurer not found!** Begin your journey with `/register`.', 'You need to create player data in order to explore!': '📜 **Adventurer not found!** Begin your journey with `/register`.', - 'Item not found': '🔍 **That item doesn\'t exist.** Check the ID and try again.', - 'Not enough items': '🎒 **Not enough items!** You don\'t have that many in your inventory.', - 'Cannot sell a locked item.': '🔒 **This item is locked!** Unlock it first before selling.', - 'This item cannot be consumed': '❌ **This item can\'t be consumed.** Only consumable items have effects.', - 'You already have player data!': '✅ **You\'re already registered!** Use `/profile` to see your character.', + 'Item not found': '🔍 **That item doesn\'t exist.** Check the ID and try again.', + 'Not enough items': '🎒 **Not enough items!** You don\'t have that many in your inventory.', + 'Cannot sell a locked item.': '🔒 **This item is locked!** Unlock it first before selling.', + 'This item cannot be consumed': '❌ **This item can\'t be consumed.** Only consumable items have effects.', + 'You already have player data!': '✅ **You\'re already registered!** Use `/profile` to see your character.' }; /** diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index 820da33..ca6186b 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -1,10 +1,10 @@ -import { User } from 'discord.js'; -import { IStepJSON } from '../interfaces/IStepJSON'; -import { ICombatJSON } from '../interfaces/ICombatJSON'; -import { IPlayerJSON } from '../interfaces/IPlayerJSON'; -import { IItemJSON } from '../interfaces/IItemJSON'; -import { IInventoryItem } from '../interfaces/IInventoryJSON'; -import { ITaskJSON, IChestSlot } from '../interfaces/IGameJSON'; +import { type User } from 'discord.js'; +import { type IStepJSON } from '../interfaces/IStepJSON'; +import { type ICombatJSON } from '../interfaces/ICombatJSON'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import { type IItemJSON } from '../interfaces/IItemJSON'; +import { type IInventoryItem } from '../interfaces/IInventoryJSON'; +import { type ITaskJSON, type IChestSlot } from '../interfaces/IGameJSON'; import ItemManager from '../managers/ItemManager'; import WorkerPool from './WorkerPool'; import type { LeaderboardEntry, LeaderboardConfig } from './LeaderboardImageBuilder'; @@ -34,7 +34,7 @@ export default class ImageService { return WorkerPool.run('profile', { player, avatarUrl: discordUser.displayAvatarURL({ extension: 'png', size: 256 }), - itemCache: this.serializeItemCache(), + itemCache: this.serializeItemCache() }); } @@ -42,7 +42,7 @@ export default class ImageService { return WorkerPool.run('inventory', { chunk, player, - itemCache: this.serializeItemCache(), + itemCache: this.serializeItemCache() }); } diff --git a/src/utilities/ImageWorker.ts b/src/utilities/ImageWorker.ts index 32e6f28..bf2d2f5 100644 --- a/src/utilities/ImageWorker.ts +++ b/src/utilities/ImageWorker.ts @@ -18,52 +18,52 @@ parentPort.on('message', async (msg: { builderName: string; payload: any }) => { let buffer: Buffer; switch (msg.builderName) { - case 'adventure': - buffer = await AdventureImageBuilder.build(msg.payload.data); - break; + case 'adventure': + buffer = await AdventureImageBuilder.build(msg.payload.data); + break; - case 'profile': - buffer = await ProfileImageBuilder.build( - msg.payload.player, - msg.payload.avatarUrl, - msg.payload.itemCache - ); - break; + case 'profile': + buffer = await ProfileImageBuilder.build( + msg.payload.player, + msg.payload.avatarUrl, + msg.payload.itemCache + ); + break; - case 'inventory': - buffer = await InventoryImageBuilder.build( - msg.payload.chunk, - msg.payload.player, - msg.payload.itemCache - ); - break; + case 'inventory': + buffer = await InventoryImageBuilder.build( + msg.payload.chunk, + msg.payload.player, + msg.payload.itemCache + ); + break; - case 'item': - buffer = await ItemImageBuilder.build(msg.payload.item); - break; + case 'item': + buffer = await ItemImageBuilder.build(msg.payload.item); + break; - case 'leaderboard': - buffer = await LeaderboardImageBuilder.build(msg.payload.entries, msg.payload.config); - break; + case 'leaderboard': + buffer = await LeaderboardImageBuilder.build(msg.payload.entries, msg.payload.config); + break; - case 'market': - buffer = await MarketImageBuilder.build(msg.payload.listings, msg.payload.config); - break; + case 'market': + buffer = await MarketImageBuilder.build(msg.payload.listings, msg.payload.config); + break; - case 'travel': - buffer = await TravelImageBuilder.build(msg.payload.playerLevel, msg.payload.currentZoneId); - break; + case 'travel': + buffer = await TravelImageBuilder.build(msg.payload.playerLevel, msg.payload.currentZoneId); + break; - case 'tasks': - buffer = await TasksImageBuilder.build(msg.payload.tasks, msg.payload.config); - break; + case 'tasks': + buffer = await TasksImageBuilder.build(msg.payload.tasks, msg.payload.config); + break; - case 'chests': - buffer = await ChestsImageBuilder.build(msg.payload.chests, msg.payload.config); - break; + case 'chests': + buffer = await ChestsImageBuilder.build(msg.payload.chests, msg.payload.config); + break; - default: - throw new Error(`Unknown builder: ${msg.builderName}`); + default: + throw new Error(`Unknown builder: ${msg.builderName}`); } const arrayBuffer = new ArrayBuffer(buffer.byteLength); @@ -76,7 +76,7 @@ parentPort.on('message', async (msg: { builderName: string; payload: any }) => { } catch (err: any) { parentPort!.postMessage({ success: false, - error: err.message ?? String(err), + error: err.message ?? String(err) }); } }); diff --git a/src/utilities/InventoryImageBuilder.ts b/src/utilities/InventoryImageBuilder.ts index b5586ab..8e08af2 100644 --- a/src/utilities/InventoryImageBuilder.ts +++ b/src/utilities/InventoryImageBuilder.ts @@ -1,32 +1,32 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; -import { IInventoryItem } from '../interfaces/IInventoryJSON'; -import { IPlayerJSON } from '../interfaces/IPlayerJSON'; -import { IItemJSON } from '../interfaces/IItemJSON'; +import { type IInventoryItem } from '../interfaces/IInventoryJSON'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import { type IItemJSON } from '../interfaces/IItemJSON'; import ItemManager from '../managers/ItemManager'; import { join } from 'path'; try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', + Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', + Divine: '#00e5ff', Exotic: '#ff00cc' }; const SLOT_ICONS: Record = { - 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', - 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', - 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' + 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', + 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', + 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' }; const CATEGORY_ICONS: Record = { - 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', - 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' + 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', + 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' }; function getItemIcon(item: any) { - if (item.slot && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; - return CATEGORY_ICONS[item.type] || '📦'; + if (item.slot && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; + return CATEGORY_ICONS[item.type] || '📦'; } export default class InventoryImageBuilder { @@ -84,107 +84,107 @@ export default class InventoryImageBuilder { const gapY = 20; for (let i = 0; i < chunk.length; i++) { - const invEntry = chunk[i]; - const itemData = getItem(invEntry.itemId); + const invEntry = chunk[i]; + const itemData = getItem(invEntry.itemId); - const col = i % 5; - const row = Math.floor(i / 5); - const boxX = startX + col * (boxW + gapX); - const boxY = startY + row * (boxH + gapY); - - // Box BG - ctx.fillStyle = '#00000066'; - ctx.beginPath(); - ctx.roundRect(boxX, boxY, boxW, boxH, 12); - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = '#ffffff1a'; - ctx.stroke(); + const col = i % 5; + const row = Math.floor(i / 5); + const boxX = startX + col * (boxW + gapX); + const boxY = startY + row * (boxH + gapY); + + // Box BG + ctx.fillStyle = '#00000066'; + ctx.beginPath(); + ctx.roundRect(boxX, boxY, boxW, boxH, 12); + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = '#ffffff1a'; + ctx.stroke(); + + if (itemData) { + const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; + const enhanceLevel = invEntry.enhanceLevel || 0; + + // Top Left: Lock Icon + if (invEntry.isLocked) { + ctx.fillStyle = '#ffffff'; + ctx.font = '14px "NotoEmoji", sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('🔒', boxX + 10, boxY + 25); + } - if (itemData) { - const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - const enhanceLevel = invEntry.enhanceLevel || 0; - - // Top Left: Lock Icon - if (invEntry.isLocked) { - ctx.fillStyle = '#ffffff'; - ctx.font = '14px "NotoEmoji", sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText('🔒', boxX + 10, boxY + 25); - } - - // Top Left (after lock): Enhancement Badge - if (enhanceLevel > 0) { - const badgeX = invEntry.isLocked ? boxX + 30 : boxX + 8; - const badgeText = `+${enhanceLevel}`; - ctx.font = 'bold 10px sans-serif'; - const badgeW = ctx.measureText(badgeText).width + 8; + // Top Left (after lock): Enhancement Badge + if (enhanceLevel > 0) { + const badgeX = invEntry.isLocked ? boxX + 30 : boxX + 8; + const badgeText = `+${enhanceLevel}`; + ctx.font = 'bold 10px sans-serif'; + const badgeW = ctx.measureText(badgeText).width + 8; - ctx.fillStyle = '#92400e88'; // amber-900/50 - ctx.beginPath(); - ctx.roundRect(badgeX, boxY + 10, badgeW, 16, 3); - ctx.fill(); - ctx.strokeStyle = '#f59e0b66'; - ctx.lineWidth = 1; - ctx.stroke(); + ctx.fillStyle = '#92400e88'; // amber-900/50 + ctx.beginPath(); + ctx.roundRect(badgeX, boxY + 10, badgeW, 16, 3); + ctx.fill(); + ctx.strokeStyle = '#f59e0b66'; + ctx.lineWidth = 1; + ctx.stroke(); - ctx.fillStyle = '#fbbf24'; - ctx.textAlign = 'center'; - ctx.fillText(badgeText, badgeX + badgeW / 2, boxY + 22); - } - - // Top Right: Quantity Pill - ctx.fillStyle = '#00000099'; - ctx.beginPath(); - ctx.roundRect(boxX + boxW - 40, boxY + 10, 30, 18, 4); - ctx.fill(); - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(`x${invEntry.quantity}`, boxX + boxW - 25, boxY + 23); - - // Center: Emoji - ctx.fillStyle = '#ffffff'; - ctx.font = '45px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(getItemIcon(itemData), boxX + boxW / 2, boxY + 85); - - // Bottom Panel BG - ctx.fillStyle = '#00000099'; - ctx.beginPath(); - ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); - ctx.fill(); - - // Item Name (with +level suffix if enhanced) - ctx.fillStyle = color; - ctx.font = 'bold 12px sans-serif'; - const displayName = enhanceLevel > 0 ? `${itemData.name} +${enhanceLevel}` : itemData.name; - ctx.fillText(displayName, boxX + boxW / 2, boxY + 132, boxW - 10); - - // Type & Level - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.fillText(`${itemData.type.toUpperCase()} | LVL ${itemData.level}`, boxX + boxW / 2, boxY + 148); - - // Value - ctx.fillStyle = '#eab308'; - ctx.font = '10px sans-serif'; - const totalValue = Math.floor((itemData.value || 0) * invEntry.quantity); - ctx.fillText(`${totalValue.toLocaleString()}g`, boxX + boxW / 2, boxY + 164); - - // Bottom Rarity Border - ctx.beginPath(); - ctx.moveTo(boxX + 10, boxY + boxH); - ctx.lineTo(boxX + boxW - 10, boxY + boxH); - ctx.lineWidth = 4; - ctx.strokeStyle = color; - ctx.stroke(); - } else { - ctx.fillStyle = '#374151'; - ctx.font = 'italic 12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('Unknown Item', boxX + boxW / 2, boxY + boxH / 2); + ctx.fillStyle = '#fbbf24'; + ctx.textAlign = 'center'; + ctx.fillText(badgeText, badgeX + badgeW / 2, boxY + 22); } + + // Top Right: Quantity Pill + ctx.fillStyle = '#00000099'; + ctx.beginPath(); + ctx.roundRect(boxX + boxW - 40, boxY + 10, 30, 18, 4); + ctx.fill(); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`x${invEntry.quantity}`, boxX + boxW - 25, boxY + 23); + + // Center: Emoji + ctx.fillStyle = '#ffffff'; + ctx.font = '45px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(getItemIcon(itemData), boxX + boxW / 2, boxY + 85); + + // Bottom Panel BG + ctx.fillStyle = '#00000099'; + ctx.beginPath(); + ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); + ctx.fill(); + + // Item Name (with +level suffix if enhanced) + ctx.fillStyle = color; + ctx.font = 'bold 12px sans-serif'; + const displayName = enhanceLevel > 0 ? `${itemData.name} +${enhanceLevel}` : itemData.name; + ctx.fillText(displayName, boxX + boxW / 2, boxY + 132, boxW - 10); + + // Type & Level + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.fillText(`${itemData.type.toUpperCase()} | LVL ${itemData.level}`, boxX + boxW / 2, boxY + 148); + + // Value + ctx.fillStyle = '#eab308'; + ctx.font = '10px sans-serif'; + const totalValue = Math.floor((itemData.value || 0) * invEntry.quantity); + ctx.fillText(`${totalValue.toLocaleString()}g`, boxX + boxW / 2, boxY + 164); + + // Bottom Rarity Border + ctx.beginPath(); + ctx.moveTo(boxX + 10, boxY + boxH); + ctx.lineTo(boxX + boxW - 10, boxY + boxH); + ctx.lineWidth = 4; + ctx.strokeStyle = color; + ctx.stroke(); + } else { + ctx.fillStyle = '#374151'; + ctx.font = 'italic 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Unknown Item', boxX + boxW / 2, boxY + boxH / 2); + } } return canvas.toBuffer('image/png'); diff --git a/src/utilities/ItemImageBuilder.ts b/src/utilities/ItemImageBuilder.ts index 5573145..c36d825 100644 --- a/src/utilities/ItemImageBuilder.ts +++ b/src/utilities/ItemImageBuilder.ts @@ -1,36 +1,36 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; -import { IItemJSON } from '../interfaces/IItemJSON'; +import { type IItemJSON } from '../interfaces/IItemJSON'; import { join } from 'path'; try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', + Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', + Divine: '#00e5ff', Exotic: '#ff00cc' }; const SLOT_ICONS: Record = { - 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', - 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', - 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' + 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', + 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', + 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' }; const CATEGORY_ICONS: Record = { - 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', - 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' + 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', + 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' }; function getItemIcon(item: IItemJSON) { - if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; - return CATEGORY_ICONS[item.type] || '📦'; + if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; + return CATEGORY_ICONS[item.type] || '📦'; } export default class ItemImageBuilder { public static async build(item: IItemJSON): Promise { const affixesCount = item.affixes?.length || 0; const enhanceLevel = (item as any).enhanceLevel || 0; - const canvasHeight = affixesCount > 0 ? 430 + (affixesCount * 45) : 400; + const canvasHeight = affixesCount > 0 ? 430 + affixesCount * 45 : 400; const canvas = createCanvas(600, canvasHeight); const ctx = canvas.getContext('2d'); @@ -75,8 +75,8 @@ export default class ItemImageBuilder { let enhText = ''; let enhWidth = 0; if (enhanceLevel > 0) { - enhText = `+${enhanceLevel} ENHANCED`; - enhWidth = ctx.measureText(enhText).width + 20; + enhText = `+${enhanceLevel} ENHANCED`; + enhWidth = ctx.measureText(enhText).width + 20; } const totalBadgeWidth = rWidth + tWidth + (enhWidth > 0 ? enhWidth + 10 : 0) + 10; @@ -106,15 +106,15 @@ export default class ItemImageBuilder { // Enhancement Badge (amber) if (enhanceLevel > 0) { - currentX += tWidth + 10; - ctx.fillStyle = '#92400e44'; // amber-900/25 - ctx.strokeStyle = '#f59e0b66'; // amber-500/40 - ctx.beginPath(); - ctx.roundRect(currentX, 155, enhWidth, 24, 4); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = '#fbbf24'; // amber-400 - ctx.fillText(enhText, currentX + enhWidth / 2, 171); + currentX += tWidth + 10; + ctx.fillStyle = '#92400e44'; // amber-900/25 + ctx.strokeStyle = '#f59e0b66'; // amber-500/40 + ctx.beginPath(); + ctx.roundRect(currentX, 155, enhWidth, 24, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = '#fbbf24'; // amber-400 + ctx.fillText(enhText, currentX + enhWidth / 2, 171); } // 4. Description @@ -126,85 +126,85 @@ export default class ItemImageBuilder { // 5. Stats or Consumable Effect let yOffset = 260; if (item.type === 'Consumable') { - ctx.fillStyle = '#713f1233'; - ctx.strokeStyle = '#eab3084d'; + ctx.fillStyle = '#713f1233'; + ctx.strokeStyle = '#eab3084d'; + ctx.beginPath(); + ctx.roundRect(50, yOffset, 500, 70, 8); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#eab308'; + ctx.font = 'bold 10px sans-serif'; + ctx.fillText('EFFECT', canvas.width / 2, yOffset + 25); + + let effectText = 'Unknown Effect'; + let effectColor = '#ffffff'; + if (item.action?.effect === 'HEAL_HP') { effectText = `Restores ${item.action.amount} HP`; effectColor = '#4ade80'; } + else if (item.action?.effect === 'GRANT_XP') { effectText = `Grants ${item.action.amount} XP`; effectColor = '#c084fc'; } + else if (item.action?.effect === 'GRANT_GOLD') { effectText = `Grants ${item.action.amount} Gold`; effectColor = '#fbbf24'; } + + ctx.fillStyle = effectColor; + ctx.font = 'bold 20px sans-serif'; + ctx.fillText(effectText, canvas.width / 2, yOffset + 50); + + } else if (item.stats) { + const boxW = 150; + const gap = 20; + const statX = (canvas.width - (boxW * 3 + gap * 2)) / 2; + + const drawStatBox = (x: number, label: string, val: number, valColor: string) => { + ctx.fillStyle = '#ffffff0a'; + ctx.strokeStyle = '#ffffff1a'; ctx.beginPath(); - ctx.roundRect(50, yOffset, 500, 70, 8); + ctx.roundRect(x, yOffset, boxW, 70, 8); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#eab308'; + ctx.fillStyle = '#6b7280'; ctx.font = 'bold 10px sans-serif'; - ctx.fillText('EFFECT', canvas.width / 2, yOffset + 25); - - let effectText = 'Unknown Effect'; - let effectColor = '#ffffff'; - if (item.action?.effect === 'HEAL_HP') { effectText = `Restores ${item.action.amount} HP`; effectColor = '#4ade80'; } - else if (item.action?.effect === 'GRANT_XP') { effectText = `Grants ${item.action.amount} XP`; effectColor = '#c084fc'; } - else if (item.action?.effect === 'GRANT_GOLD') { effectText = `Grants ${item.action.amount} Gold`; effectColor = '#fbbf24'; } + ctx.textAlign = 'center'; + ctx.fillText(label, x + boxW / 2, yOffset + 25); - ctx.fillStyle = effectColor; - ctx.font = 'bold 20px sans-serif'; - ctx.fillText(effectText, canvas.width / 2, yOffset + 50); + ctx.fillStyle = valColor; + ctx.font = 'bold 24px monospace'; + ctx.fillText(val.toString(), x + boxW / 2, yOffset + 55); + }; - } else if (item.stats) { - const boxW = 150; - const gap = 20; - const statX = (canvas.width - ((boxW * 3) + (gap * 2))) / 2; - - const drawStatBox = (x: number, label: string, val: number, valColor: string) => { - ctx.fillStyle = '#ffffff0a'; - ctx.strokeStyle = '#ffffff1a'; - ctx.beginPath(); - ctx.roundRect(x, yOffset, boxW, 70, 8); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#6b7280'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(label, x + boxW / 2, yOffset + 25); - - ctx.fillStyle = valColor; - ctx.font = 'bold 24px monospace'; - ctx.fillText(val.toString(), x + boxW / 2, yOffset + 55); - }; - - drawStatBox(statX, 'ATK', item.stats.atk || 0, '#f87171'); - drawStatBox(statX + boxW + gap, 'DEF', item.stats.def || 0, '#60a5fa'); - drawStatBox(statX + (boxW + gap) * 2, 'HP', item.stats.hp || 0, '#4ade80'); + drawStatBox(statX, 'ATK', item.stats.atk || 0, '#f87171'); + drawStatBox(statX + boxW + gap, 'DEF', item.stats.def || 0, '#60a5fa'); + drawStatBox(statX + (boxW + gap) * 2, 'HP', item.stats.hp || 0, '#4ade80'); } // 6. Affixes if (affixesCount > 0) { - yOffset += 110; + yOffset += 110; - ctx.fillStyle = '#c084fc'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('SPECIAL EFFECTS', canvas.width / 2, yOffset); + ctx.fillStyle = '#c084fc'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('SPECIAL EFFECTS', canvas.width / 2, yOffset); - yOffset += 15; + yOffset += 15; item.affixes!.forEach(affix => { - ctx.fillStyle = '#581c8733'; - ctx.strokeStyle = '#a855f733'; - ctx.beginPath(); - ctx.roundRect(150, yOffset, 300, 32, 4); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#e9d5ff'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); - - const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px monospace'; - ctx.textAlign = 'right'; - ctx.fillText(valText, 435, yOffset + 22); - - yOffset += 40; + ctx.fillStyle = '#581c8733'; + ctx.strokeStyle = '#a855f733'; + ctx.beginPath(); + ctx.roundRect(150, yOffset, 300, 32, 4); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#e9d5ff'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); + + const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px monospace'; + ctx.textAlign = 'right'; + ctx.fillText(valText, 435, yOffset + 22); + + yOffset += 40; }); } diff --git a/src/utilities/ItemViewBuilder.ts b/src/utilities/ItemViewBuilder.ts index c316e89..dfd1897 100644 --- a/src/utilities/ItemViewBuilder.ts +++ b/src/utilities/ItemViewBuilder.ts @@ -1,9 +1,9 @@ -import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; -import { IInventoryItem } from "../interfaces/IInventoryJSON"; +import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, type EmbedBuilder } from "discord.js"; +import { type IInventoryItem } from "../interfaces/IInventoryJSON"; import ItemManager from "../managers/ItemManager"; import ImageService from "./ImageService"; -import { IPlayerJSON } from "../interfaces/IPlayerJSON"; -import { IItemJSON } from "../interfaces/IItemJSON"; +import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; +import { type IItemJSON } from "../interfaces/IItemJSON"; export interface ItemViewResponse { embeds: EmbedBuilder[]; @@ -40,7 +40,7 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): const hasSlot = hydratedItem.slot !== 'None'; const isConsumable = hydratedItem.type === 'Consumable'; const isLocked = item.isLocked; - const isModified = (item.enhanceLevel > 0) || !!item.statOverrides || !!item.affixOverrides; + const isModified = item.enhanceLevel > 0 || !!item.statOverrides || !!item.affixOverrides; const docId = item._id; // MongoDB document _id for variant targeting // === ROW 1: Equip / Consume + Lock === @@ -68,7 +68,7 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): // === ROW 2: Sell + Collect === // Modified items can't be vendor-sold or collected let sellText = `🪙 Sell (${Math.floor(hydratedItem.value * item.quantity).toLocaleString()}g)`; - let sellDisabled = isConsumable || isLocked; + const sellDisabled = isConsumable || isLocked; let collectText = 'Add to Collection'; let collectDisabled = isConsumable || isLocked; diff --git a/src/utilities/LeaderboardImageBuilder.ts b/src/utilities/LeaderboardImageBuilder.ts index e83e98b..3c6fdf4 100644 --- a/src/utilities/LeaderboardImageBuilder.ts +++ b/src/utilities/LeaderboardImageBuilder.ts @@ -28,11 +28,11 @@ const CANVAS_WIDTH = 800; export default class LeaderboardImageBuilder { public static async build(entries: LeaderboardEntry[], config: LeaderboardConfig): Promise { const rowCount = Math.min(entries.length, 10); - const canvasHeight = HEADER_HEIGHT + (rowCount * ROW_HEIGHT) + FOOTER_HEIGHT + PADDING; + const canvasHeight = HEADER_HEIGHT + rowCount * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - (PADDING * 2); + const contentWidth = CANVAS_WIDTH - PADDING * 2; // --- 1. Background --- ctx.fillStyle = '#0a0a0a'; @@ -75,7 +75,7 @@ export default class LeaderboardImageBuilder { for (let i = 0; i < rowCount; i++) { const entry = entries[i]; - const rowY = startY + (i * ROW_HEIGHT); + const rowY = startY + i * ROW_HEIGHT; const isTop3 = i < 3; // Row background — alternating subtle stripes @@ -126,7 +126,7 @@ export default class LeaderboardImageBuilder { } // --- 4. Footer --- - const footerY = startY + (rowCount * ROW_HEIGHT) + 15; + const footerY = startY + rowCount * ROW_HEIGHT + 15; ctx.textAlign = 'center'; ctx.fillStyle = '#374151'; diff --git a/src/utilities/Logger.ts b/src/utilities/Logger.ts index ad834b2..28d946f 100644 --- a/src/utilities/Logger.ts +++ b/src/utilities/Logger.ts @@ -18,12 +18,12 @@ const additionalLevels = { dev: 35, command: 34, player: 33, - button: 32, + button: 32 }; // --- File destination (reopenable for rotation) --- // minLength: 0 ensures writes flush quickly — prevents sonic-boom "not ready" on exit -let fileDestination = pino.destination({ dest: LOG_FILE, sync: false, minLength: 0 }); +const fileDestination = pino.destination({ dest: LOG_FILE, sync: false, minLength: 0 }); /** * Rotates log files when bot.log exceeds MAX_SIZE_BYTES. @@ -60,7 +60,7 @@ function rotateIfNeeded(): void { const logger = pino( { customLevels: additionalLevels, - level: 'debug', + level: 'debug' }, pino.multistream([ { @@ -74,14 +74,14 @@ const logger = pino( levelFirst: true, customLevels: 'dev:35,command:34,player:33,button:32', customColors: 'dev:magenta,command:magenta,player:magenta,button:magenta', - useOnlyCustomProps: false, - }, - }), + useOnlyCustomProps: false + } + }) }, { level: 'debug', - stream: fileDestination, - }, + stream: fileDestination + } ]) ); diff --git a/src/utilities/MarketImageBuilder.ts b/src/utilities/MarketImageBuilder.ts index 7d54da4..30ae43d 100644 --- a/src/utilities/MarketImageBuilder.ts +++ b/src/utilities/MarketImageBuilder.ts @@ -61,7 +61,7 @@ export default class MarketImageBuilder { const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - (PADDING * 2); + const contentWidth = CANVAS_WIDTH - PADDING * 2; // --- Background --- ctx.fillStyle = '#0a0a0a'; @@ -75,7 +75,7 @@ export default class MarketImageBuilder { const accentColor = config.mode === 'my_listings' ? '#3b82f6' : '#10b981'; const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); - headerGrad.addColorStop(0, accentColor + '25'); + headerGrad.addColorStop(0, `${accentColor}25`); headerGrad.addColorStop(1, '#0a0a0a00'); ctx.fillStyle = headerGrad; ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); @@ -110,7 +110,7 @@ export default class MarketImageBuilder { for (let i = 0; i < rowCount; i++) { const listing = listings[i]; const item = listing.item; - const rowY = startY + (i * ROW_HEIGHT); + const rowY = startY + i * ROW_HEIGHT; const rarityColor = RARITY_COLORS[item.rarity] || '#ffffff'; const displayIndex = i + 1; diff --git a/src/utilities/PaginatorBuilder.ts b/src/utilities/PaginatorBuilder.ts index baffa25..b09edd5 100644 --- a/src/utilities/PaginatorBuilder.ts +++ b/src/utilities/PaginatorBuilder.ts @@ -3,11 +3,11 @@ import { ButtonBuilder, ButtonStyle, EmbedBuilder, - CommandInteraction, - MessageComponentInteraction, + type CommandInteraction, + type MessageComponentInteraction, ComponentType, - AttachmentBuilder, - MessageActionRowComponentBuilder + type AttachmentBuilder, + type MessageActionRowComponentBuilder } from 'discord.js'; export default class PaginatorBuilder { @@ -92,7 +92,7 @@ export default class PaginatorBuilder { const embed = EmbedBuilder.from(originalEmbed); const currentFooter = originalEmbed.data.footer?.text || ''; return embed.setFooter({ - text: (currentFooter ? `${currentFooter} | Page ${index + 1} of ${this.pages.length}` : `Page ${index + 1} of ${this.pages.length}`) + ' | ⚔️ DFO Cross-Platform', + text: `${currentFooter ? `${currentFooter} | Page ${index + 1} of ${this.pages.length}` : `Page ${index + 1} of ${this.pages.length}`} | ⚔️ DFO Cross-Platform`, iconURL: originalEmbed.data.footer?.icon_url }); }; @@ -130,10 +130,10 @@ export default class PaginatorBuilder { collector.on('collect', async (i) => { collector.resetTimer(); switch (i.customId) { - case 'page_first': currentPage = 0; break; - case 'page_prev': currentPage = Math.max(0, currentPage - 1); break; - case 'page_next': currentPage = Math.min(this.pages.length - 1, currentPage + 1); break; - case 'page_last': currentPage = this.pages.length - 1; break; + case 'page_first': currentPage = 0; break; + case 'page_prev': currentPage = Math.max(0, currentPage - 1); break; + case 'page_next': currentPage = Math.min(this.pages.length - 1, currentPage + 1); break; + case 'page_last': currentPage = this.pages.length - 1; break; } await i.update({ embeds: [getEmbed(currentPage)], diff --git a/src/utilities/PlayerGuard.ts b/src/utilities/PlayerGuard.ts index 71dc2e6..cfe18f9 100644 --- a/src/utilities/PlayerGuard.ts +++ b/src/utilities/PlayerGuard.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, ButtonInteraction, MessageFlags } from 'discord.js'; +import { type ChatInputCommandInteraction, type ButtonInteraction, MessageFlags } from 'discord.js'; import Routes from './Routes'; import { apiFetch } from './ApiClient'; diff --git a/src/utilities/ProfileImageBuilder.ts b/src/utilities/ProfileImageBuilder.ts index 2a0bb06..86f47fe 100644 --- a/src/utilities/ProfileImageBuilder.ts +++ b/src/utilities/ProfileImageBuilder.ts @@ -1,7 +1,7 @@ import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas'; -import { IPlayerJSON } from '../interfaces/IPlayerJSON'; -import { IItemJSON } from '../interfaces/IItemJSON'; -import { User } from 'discord.js'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import { type IItemJSON } from '../interfaces/IItemJSON'; +import { type User } from 'discord.js'; import ItemManager from '../managers/ItemManager'; import { join } from 'path'; @@ -32,8 +32,8 @@ export default class ProfileImageBuilder { const ctx = canvas.getContext('2d'); // --- Svelte Logic Conversions --- - const xpToNext = Math.floor(50 * Math.pow((player.level || 1), 1.3)); - const xpProgress = Math.min((player.experience / xpToNext), 1); + const xpToNext = Math.floor(50 * (player.level || 1)**1.3); + const xpProgress = Math.min(player.experience / xpToNext, 1); const totalAtk = player.stats?.atk || 0; const totalDef = player.stats?.def || 0; const currentHp = Math.floor(player.stats?.hp || 0); @@ -92,18 +92,18 @@ export default class ProfileImageBuilder { // --- 4. Stats Grid --- const drawGridBox = (x: number, y: number, label: string, value: string, borderColor: string, valueColor: string) => { - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(x, y, 180, 70); - ctx.fillStyle = borderColor; - ctx.fillRect(x, y, 4, 70); + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(x, y, 180, 70); + ctx.fillStyle = borderColor; + ctx.fillRect(x, y, 4, 70); - ctx.fillStyle = '#6b7280'; - ctx.font = '12px sans-serif'; - ctx.fillText(label.toUpperCase(), x + 15, y + 25); + ctx.fillStyle = '#6b7280'; + ctx.font = '12px sans-serif'; + ctx.fillText(label.toUpperCase(), x + 15, y + 25); - ctx.fillStyle = valueColor; - ctx.font = 'bold 24px monospace'; - ctx.fillText(value, x + 15, y + 55); + ctx.fillStyle = valueColor; + ctx.font = 'bold 24px monospace'; + ctx.fillText(value, x + 15, y + 55); }; drawGridBox(180, 130, 'Level', player.level.toString(), '#eab308', '#ffffff'); @@ -129,7 +129,7 @@ export default class ProfileImageBuilder { ctx.fillStyle = '#ffffff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(`${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, barX + (barWidth / 2), barY + 16); + ctx.fillText(`${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, barX + barWidth / 2, barY + 16); ctx.textAlign = 'left'; // --- 6. Combat Stats Panel --- @@ -146,7 +146,7 @@ export default class ProfileImageBuilder { ctx.fillText(`${currentHp} / ${maxHp}`, 730, panelY + 30); ctx.textAlign = 'left'; - const hpProgress = Math.min((currentHp / maxHp), 1); + const hpProgress = Math.min(currentHp / maxHp, 1); ctx.fillStyle = '#1f2937'; ctx.beginPath(); ctx.roundRect(60, panelY + 45, 670, 12, 6); @@ -157,18 +157,18 @@ export default class ProfileImageBuilder { ctx.fill(); const drawStatBox = (x: number, y: number, label: string, value: string, color: string) => { - ctx.fillStyle = '#00000066'; - ctx.beginPath(); - ctx.roundRect(x, y, 325, 50, 6); - ctx.fill(); - - ctx.fillStyle = '#6b7280'; - ctx.font = 'bold 14px sans-serif'; - ctx.fillText(label, x + 15, y + 30); - - ctx.fillStyle = color; - ctx.font = 'bold 22px monospace'; - ctx.fillText(value, x + 100, y + 33); + ctx.fillStyle = '#00000066'; + ctx.beginPath(); + ctx.roundRect(x, y, 325, 50, 6); + ctx.fill(); + + ctx.fillStyle = '#6b7280'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText(label, x + 15, y + 30); + + ctx.fillStyle = color; + ctx.font = 'bold 22px monospace'; + ctx.fillText(value, x + 100, y + 33); }; drawStatBox(60, panelY + 70, 'ATK', totalAtk.toString(), '#f87171'); @@ -193,15 +193,15 @@ export default class ProfileImageBuilder { // Grid Settings (4 columns, 3 rows) const equipSlots = [ - { key: 'Head', icon: '⛑️' }, { key: 'Necklace', icon: '📿' }, { key: 'Chest', icon: '👕' }, { key: 'MainHand', icon: '⚔️' }, - { key: 'Legs', icon: '👖' }, { key: 'OffHand', icon: '🛡️' }, { key: 'Hands', icon: '🧤' }, { key: 'RingA', icon: '💍' }, - { key: 'Feet', icon: '👢' }, { key: 'RingB', icon: '💍' }, { key: 'Pet', icon: '🐾' }, { key: 'Special', icon: '✨' } + { key: 'Head', icon: '⛑️' }, { key: 'Necklace', icon: '📿' }, { key: 'Chest', icon: '👕' }, { key: 'MainHand', icon: '⚔️' }, + { key: 'Legs', icon: '👖' }, { key: 'OffHand', icon: '🛡️' }, { key: 'Hands', icon: '🧤' }, { key: 'RingA', icon: '💍' }, + { key: 'Feet', icon: '👢' }, { key: 'RingB', icon: '💍' }, { key: 'Pet', icon: '🐾' }, { key: 'Special', icon: '✨' } ]; const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', + Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', + Divine: '#00e5ff', Exotic: '#ff00cc' }; const gridStartX = 60; @@ -212,71 +212,71 @@ export default class ProfileImageBuilder { const gapY = 15; for (let i = 0; i < equipSlots.length; i++) { - const slot = equipSlots[i]; - const col = i % 4; - const row = Math.floor(i / 4); - - const boxX = gridStartX + col * (boxWidth + gapX); - const boxY = gridStartY + row * (boxHeight + gapY); - - // Fetch item data if equipped - const equippedRef = player.equipment ? (player.equipment as any)[slot.key] : null; - let itemData = null; - if (equippedRef && equippedRef.itemId) { - itemData = getItem(equippedRef.itemId) ?? null; - } - - // Box BG & Outline - ctx.fillStyle = '#00000066'; // bg-black/40 - ctx.beginPath(); - ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 8); - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = '#ffffff11'; - ctx.stroke(); - - ctx.textAlign = 'center'; - - // --- UPDATED EMOJI RENDERING --- - ctx.fillStyle = '#ffffff66'; // Muted icon - // Use the NotoEmoji font we registered above - ctx.font = '20px "NotoEmoji", sans-serif'; - ctx.fillText(slot.icon, boxX + boxWidth / 2, boxY + 30); + const slot = equipSlots[i]; + const col = i % 4; + const row = Math.floor(i / 4); + + const boxX = gridStartX + col * (boxWidth + gapX); + const boxY = gridStartY + row * (boxHeight + gapY); + + // Fetch item data if equipped + const equippedRef = player.equipment ? (player.equipment as any)[slot.key] : null; + let itemData = null; + if (equippedRef?.itemId) { + itemData = getItem(equippedRef.itemId) ?? null; + } + + // Box BG & Outline + ctx.fillStyle = '#00000066'; // bg-black/40 + ctx.beginPath(); + ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 8); + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = '#ffffff11'; + ctx.stroke(); + + ctx.textAlign = 'center'; + + // --- UPDATED EMOJI RENDERING --- + ctx.fillStyle = '#ffffff66'; // Muted icon + // Use the NotoEmoji font we registered above + ctx.font = '20px "NotoEmoji", sans-serif'; + ctx.fillText(slot.icon, boxX + boxWidth / 2, boxY + 30); + + // Slot Key + ctx.fillStyle = '#4b5563'; // text-gray-600 + // Instantly reset the font back to standard sans-serif for regular text + ctx.font = 'bold 10px sans-serif'; + ctx.fillText(slot.key.toUpperCase(), boxX + boxWidth / 2, boxY + 45); + + if (itemData) { + const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; + + // Truncate name if it's too long (using canvas maxWidth param) + ctx.fillStyle = color; + ctx.font = 'bold 13px sans-serif'; + ctx.fillText(itemData.name, boxX + boxWidth / 2, boxY + 65, boxWidth - 10); - // Slot Key - ctx.fillStyle = '#4b5563'; // text-gray-600 - // Instantly reset the font back to standard sans-serif for regular text - ctx.font = 'bold 10px sans-serif'; - ctx.fillText(slot.key.toUpperCase(), boxX + boxWidth / 2, boxY + 45); + // Item Level + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.fillText(`Lvl ${itemData.level}`, boxX + boxWidth / 2, boxY + 80); - if (itemData) { - const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - - // Truncate name if it's too long (using canvas maxWidth param) - ctx.fillStyle = color; - ctx.font = 'bold 13px sans-serif'; - ctx.fillText(itemData.name, boxX + boxWidth / 2, boxY + 65, boxWidth - 10); - - // Item Level - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.fillText(`Lvl ${itemData.level}`, boxX + boxWidth / 2, boxY + 80); - - // Bottom Border (matches rarity) - ctx.beginPath(); - ctx.moveTo(boxX + 15, boxY + boxHeight); - ctx.lineTo(boxX + boxWidth - 15, boxY + boxHeight); - ctx.lineWidth = 3; - ctx.strokeStyle = color; - ctx.stroke(); - } else { - // Empty State - ctx.fillStyle = '#374151'; // text-gray-700 - ctx.font = 'italic 12px sans-serif'; - ctx.fillText('Empty', boxX + boxWidth / 2, boxY + 70); - } - - ctx.textAlign = 'left'; // Reset alignment for next loop iteration + // Bottom Border (matches rarity) + ctx.beginPath(); + ctx.moveTo(boxX + 15, boxY + boxHeight); + ctx.lineTo(boxX + boxWidth - 15, boxY + boxHeight); + ctx.lineWidth = 3; + ctx.strokeStyle = color; + ctx.stroke(); + } else { + // Empty State + ctx.fillStyle = '#374151'; // text-gray-700 + ctx.font = 'italic 12px sans-serif'; + ctx.fillText('Empty', boxX + boxWidth / 2, boxY + 70); + } + + ctx.textAlign = 'left'; // Reset alignment for next loop iteration } return canvas.toBuffer('image/png'); diff --git a/src/utilities/Routes.ts b/src/utilities/Routes.ts index 726cbfa..8229908 100644 --- a/src/utilities/Routes.ts +++ b/src/utilities/Routes.ts @@ -136,7 +136,7 @@ export default class Routes { // ========== MARKET ========== - public static marketBrowse(discordId: string, params?: { page?: number, search?: string, rarity?: string, type?: string, sort?: string }): string { + public static marketBrowse(discordId: string, params?: { page?: number; search?: string; rarity?: string; type?: string; sort?: string }): string { const base = `https://capi.gg/api/market?discordId=${discordId}&limit=8`; const qs = new URLSearchParams(); if (params?.page) qs.set('page', String(params.page)); diff --git a/src/utilities/TasksImageBuilder.ts b/src/utilities/TasksImageBuilder.ts index c95347d..49535e6 100644 --- a/src/utilities/TasksImageBuilder.ts +++ b/src/utilities/TasksImageBuilder.ts @@ -5,9 +5,9 @@ import type { ITaskJSON } from '../interfaces/IGameJSON'; try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} const PERIOD_COLORS: Record = { - daily: { bg: '#064e3b33', border: '#10b98144', text: '#34d399' }, - weekly: { bg: '#1e1b4b33', border: '#6366f144', text: '#818cf8' }, - monthly: { bg: '#4a044e33', border: '#c026d344', text: '#e879f9' }, + daily: { bg: '#064e3b33', border: '#10b98144', text: '#34d399' }, + weekly: { bg: '#1e1b4b33', border: '#6366f144', text: '#818cf8' }, + monthly: { bg: '#4a044e33', border: '#c026d344', text: '#e879f9' } }; export interface TasksPageConfig { @@ -21,7 +21,7 @@ export default class TasksImageBuilder { const rowH = 90; const headerH = 100; const footerH = 40; - const canvasH = headerH + (tasks.length * rowH) + footerH + 20; + const canvasH = headerH + tasks.length * rowH + footerH + 20; const canvas = createCanvas(800, Math.max(300, canvasH)); const ctx = canvas.getContext('2d'); @@ -87,7 +87,7 @@ export default class TasksImageBuilder { let y = headerH; for (const task of tasks) { - const pct = Math.min(100, Math.floor((task.progress / task.target) * 100)); + const pct = Math.min(100, Math.floor(task.progress / task.target * 100)); const isComplete = task.progress >= task.target; const isClaimed = task.claimed; diff --git a/src/utilities/TravelImageBuilder.ts b/src/utilities/TravelImageBuilder.ts index d26ab2d..cc24768 100644 --- a/src/utilities/TravelImageBuilder.ts +++ b/src/utilities/TravelImageBuilder.ts @@ -6,7 +6,7 @@ try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji const RARITY_COLORS: Record = { Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', - Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff', + Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff' }; const TIER_COLORS: Record = { @@ -15,7 +15,7 @@ const TIER_COLORS: Record = { 'The Hero': '#e67e22', 'The Ascendant': '#9b59b6', 'The Cosmic': '#00e5ff', - 'Beyond': '#ff00cc', + 'Beyond': '#ff00cc' }; const ROW_HEIGHT = 52; @@ -43,11 +43,11 @@ export default class TravelImageBuilder { totalRows += zones.length; } - const canvasHeight = HEADER_HEIGHT + (tierCount * TIER_HEADER_HEIGHT) + (totalRows * ROW_HEIGHT) + FOOTER_HEIGHT + PADDING; + const canvasHeight = HEADER_HEIGHT + tierCount * TIER_HEADER_HEIGHT + totalRows * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - (PADDING * 2); + const contentWidth = CANVAS_WIDTH - PADDING * 2; // --- Background --- ctx.fillStyle = '#0a0a0a'; @@ -87,7 +87,7 @@ export default class TravelImageBuilder { const tierColor = TIER_COLORS[tierName] || '#ffffff'; // Tier header - ctx.fillStyle = tierColor + '15'; + ctx.fillStyle = `${tierColor}15`; ctx.fillRect(PADDING, y, contentWidth, TIER_HEADER_HEIGHT - 2); ctx.fillStyle = tierColor; @@ -148,7 +148,7 @@ export default class TravelImageBuilder { ctx.fillText(`Lvl ${zone.levelReq}+`, PADDING + contentWidth - 100, y + 22); // Rarity cap pill - ctx.fillStyle = rarityColor + '20'; + ctx.fillStyle = `${rarityColor}20`; ctx.font = '10px sans-serif'; const pillText = zone.rarityCap; const pillWidth = ctx.measureText(pillText).width + 14; diff --git a/src/utilities/WorkerPool.ts b/src/utilities/WorkerPool.ts index f95c2c7..6e2d2d8 100644 --- a/src/utilities/WorkerPool.ts +++ b/src/utilities/WorkerPool.ts @@ -33,7 +33,7 @@ export default class WorkerPool { for (let i = 0; i < poolSize; i++) { const worker = new Worker(workerFile, { // If running raw TypeScript, we need ts-node to compile the worker file - execArgv: isCompiled ? [] : ['-r', 'ts-node/register'], + execArgv: isCompiled ? [] : ['-r', 'ts-node/register'] }); worker.on('error', (err) => { @@ -107,7 +107,7 @@ export default class WorkerPool { worker.postMessage({ builderName: task.builderName, - payload: task.payload, + payload: task.payload }); } @@ -143,7 +143,7 @@ export default class WorkerPool { return { total: this.workers.length, available: this.available.length, - queued: this.queue.length, + queued: this.queue.length }; } } \ No newline at end of file diff --git a/src/utilities/ZoneData.ts b/src/utilities/ZoneData.ts index c61a762..7cc1d96 100644 --- a/src/utilities/ZoneData.ts +++ b/src/utilities/ZoneData.ts @@ -20,7 +20,7 @@ const TIER_NAMES: Record = { 2: 'The Adventurer', 3: 'The Hero', 4: 'The Ascendant', - 5: 'The Cosmic', + 5: 'The Cosmic' }; function getTier(zoneId: number): string { @@ -52,7 +52,7 @@ export const ZONES: ZoneInfo[] = [ { id: 17, name: 'Nebula of Souls', description: 'The spirits of the ancients watch your every step.', levelReq: 600, tier: getTier(17), rarityCap: 'Divine', combatChance: 40, tollCost: 650 }, { id: 18, name: 'Black Hole Horizon', description: 'Light cannot escape. Hope struggles to survive.', levelReq: 700, tier: getTier(18), rarityCap: 'Divine', combatChance: 50, tollCost: 800 }, { id: 19, name: "Creation's Forge", description: 'Where worlds are made and destroyed.', levelReq: 800, tier: getTier(19), rarityCap: 'Divine', combatChance: 55, tollCost: 1000 }, - { id: 20, name: 'The Absolute', description: 'The end of all things. The beginning of eternity.', levelReq: 900, tier: getTier(20), rarityCap: 'Divine', combatChance: 60, tollCost: 1200 }, + { id: 20, name: 'The Absolute', description: 'The end of all things. The beginning of eternity.', levelReq: 900, tier: getTier(20), rarityCap: 'Divine', combatChance: 60, tollCost: 1200 } ]; export function getZone(id: number): ZoneInfo | undefined { From 5d109e2bcf09d3fc2d4264fe4cef802fb0c6bff8 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:21:48 +0700 Subject: [PATCH 3/7] ci: add prettier and apply automatic styling --- .prettierrc.json | 5 + bin/deletecmd.ts | 8 +- bin/registercmds.ts | 27 +- eslint.config.mjs | 3 +- package-lock.json | 17 + package.json | 2 + src/bot.ts | 22 +- src/commands/AttackCommand.ts | 29 +- src/commands/ChestsCommand.ts | 68 ++- src/commands/CollectionCommand.ts | 60 ++- src/commands/ExploreCommand.ts | 37 +- src/commands/FleeCommand.ts | 27 +- src/commands/GuideCommand.ts | 68 ++- src/commands/HelpCommand.ts | 61 +-- src/commands/InventoryCommand.ts | 72 ++- src/commands/LeaderboardCommand.ts | 71 ++- src/commands/LookupCommand.ts | 294 +++++++----- src/commands/MarketCommand.ts | 267 ++++++++--- src/commands/NetworkCommand.ts | 331 +++++++------ src/commands/ProfileCommand.ts | 86 +++- src/commands/RegisterCommand.ts | 47 +- src/commands/RestCommand.ts | 38 +- src/commands/TasksCommand.ts | 103 +++-- src/commands/TestCommand.ts | 29 +- src/commands/TravelCommand.ts | 92 ++-- src/commands/VoteCommand.ts | 28 +- src/components/buttons/AttackButton.ts | 27 +- src/components/buttons/BulkCollectButton.ts | 50 +- src/components/buttons/BulkDismantleButton.ts | 46 +- src/components/buttons/BulkSellButton.ts | 56 ++- src/components/buttons/ChestBuyButton.ts | 49 +- src/components/buttons/ChestOpenButton.ts | 77 +++- src/components/buttons/ChestStartButton.ts | 48 +- src/components/buttons/CollectButton.ts | 33 +- src/components/buttons/ConsumeButton.ts | 30 +- src/components/buttons/DismantleButton.ts | 43 +- src/components/buttons/EmbedAttackButton.ts | 27 +- src/components/buttons/EmbedFleeButton.ts | 27 +- src/components/buttons/EnhanceButton.ts | 61 ++- src/components/buttons/EquipButton.ts | 52 ++- src/components/buttons/ExploreAgainButton.ts | 45 +- src/components/buttons/ExploreButton.ts | 35 +- src/components/buttons/FleeButton.ts | 27 +- src/components/buttons/GuideNavButton.ts | 32 +- src/components/buttons/LockButton.ts | 43 +- src/components/buttons/MarketBuyButton.ts | 40 +- src/components/buttons/MarketCancelButton.ts | 35 +- src/components/buttons/MarketNextButton.ts | 18 +- src/components/buttons/MarketPrevButton.ts | 105 ++++- .../buttons/MarketRedirectButton.ts | 12 +- .../buttons/MarketSellPageButton.ts | 21 +- src/components/buttons/ReforgeButton.ts | 31 +- .../buttons/RegisterAcceptButton.ts | 54 ++- .../buttons/RegisterDeclineButton.ts | 18 +- src/components/buttons/RestButton.ts | 36 +- src/components/buttons/SellButton.ts | 30 +- src/components/buttons/SkillPointsButton.ts | 29 +- src/components/buttons/TaskClaimButton.ts | 72 ++- src/components/buttons/TasksTabButton.ts | 88 +++- src/components/menus/InvSelectMenu.ts | 52 ++- src/components/menus/MarketSellMenu.ts | 59 ++- src/components/menus/ReforgeSelectMenu.ts | 57 ++- src/components/menus/TravelSelectMenu.ts | 35 +- src/components/menus/UnequipMenu.ts | 87 +++- src/components/modals/BulkCollectModal.ts | 73 ++- src/components/modals/BulkDismantleModal.ts | 67 ++- src/components/modals/BulkSellModal.ts | 62 ++- src/components/modals/CollectModal.ts | 52 ++- src/components/modals/ConsumeModal.ts | 49 +- src/components/modals/MarketSellModal.ts | 49 +- src/components/modals/SellModal.ts | 46 +- src/components/modals/SkillPointsModal.ts | 46 +- src/events/ClientReadyEvent.ts | 24 +- src/events/GuildCreateEvent.ts | 81 +++- src/events/InteractionCreateEvent.ts | 61 ++- src/handlers/ButtonHandler.ts | 60 ++- src/handlers/EventHandler.ts | 15 +- src/handlers/ModalSubmitHandler.ts | 44 +- src/handlers/SelectMenuHandler.ts | 48 +- src/handlers/SlashCommandHandler.ts | 53 ++- src/index.ts | 29 +- src/interfaces/ICollectionJSON.ts | 14 +- src/interfaces/ICombatJSON.ts | 6 +- src/interfaces/IEnemyJSON.ts | 2 +- src/interfaces/IExecutable.ts | 2 +- src/interfaces/IGameJSON.ts | 12 +- src/interfaces/IInventoryJSON.ts | 6 +- src/interfaces/IItemJSON.ts | 21 +- src/interfaces/IItemsJSON.ts | 4 +- src/interfaces/INPCJSON.ts | 2 +- src/interfaces/IPlayerJSON.ts | 14 +- src/interfaces/IScenarioJSON.ts | 2 +- src/interfaces/IStepJSON.ts | 10 +- src/managers/CooldownManager.ts | 7 +- src/managers/ItemManager.ts | 24 +- src/managers/PresenceManager.ts | 35 +- src/structures/Button.ts | 12 +- src/structures/Event.ts | 4 +- src/structures/ModalSubmit.ts | 12 +- src/structures/SelectMenu.ts | 12 +- src/structures/SlashCommand.ts | 22 +- src/structures/containers/AttackContainer.ts | 49 +- src/structures/containers/ExploreContainer.ts | 75 ++- .../containers/ItemLookupContainer.ts | 33 +- .../containers/NPCLookupContainer.ts | 9 +- src/structures/containers/ProfileContainer.ts | 85 +++- .../containers/ScenarioLookupContainer.ts | 20 +- src/utilities/AdventureImageBuilder.ts | 435 ++++++++++++++---- src/utilities/ApiClient.ts | 36 +- src/utilities/ChestsImageBuilder.ts | 48 +- src/utilities/CombatResponseBuilder.ts | 31 +- src/utilities/ErrorMessages.ts | 70 ++- src/utilities/ImageService.ts | 41 +- src/utilities/ImageWorker.ts | 96 ++-- src/utilities/InventoryImageBuilder.ts | 76 ++- src/utilities/ItemImageBuilder.ts | 131 ++++-- src/utilities/ItemViewBuilder.ts | 91 +++- src/utilities/LeaderboardImageBuilder.ts | 44 +- src/utilities/Logger.ts | 25 +- src/utilities/MarketImageBuilder.ts | 96 +++- src/utilities/PaginatorBuilder.ts | 74 ++- src/utilities/PlayerGuard.ts | 13 +- src/utilities/ProfileImageBuilder.ts | 133 ++++-- src/utilities/Routes.ts | 18 +- src/utilities/TasksImageBuilder.ts | 59 ++- src/utilities/TravelImageBuilder.ts | 51 +- src/utilities/WorkerPool.ts | 21 +- src/utilities/ZoneData.ts | 227 ++++++++- 128 files changed, 4803 insertions(+), 1945 deletions(-) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..38bc8e0 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "none" +} diff --git a/bin/deletecmd.ts b/bin/deletecmd.ts index 5228192..9721991 100644 --- a/bin/deletecmd.ts +++ b/bin/deletecmd.ts @@ -1,6 +1,6 @@ require('dotenv').config(); -import { Routes, REST } from "discord.js"; -import logger from "../src/utilities/Logger"; +import { Routes, REST } from 'discord.js'; +import logger from '../src/utilities/Logger'; const cmdToDelete = '1478498588362277006'; const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!); @@ -8,7 +8,7 @@ const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!); try { logger.info(`Attempting to delete the command /${cmdToDelete} ...`); rest.delete(Routes.applicationCommand(process.env.APP_ID!, cmdToDelete)); - logger.info('... Successfully deleted the command') + logger.info('... Successfully deleted the command'); } catch (err) { throw err; -} \ No newline at end of file +} diff --git a/bin/registercmds.ts b/bin/registercmds.ts index 55f37af..1ee01e1 100644 --- a/bin/registercmds.ts +++ b/bin/registercmds.ts @@ -1,8 +1,8 @@ -import { Routes, REST } from "discord.js"; -import { readdirSync } from "fs"; +import { Routes, REST } from 'discord.js'; +import { readdirSync } from 'fs'; import path from 'path'; -import SlashCommand from "../src/structures/SlashCommand"; -import logger from "../src/utilities/Logger"; +import SlashCommand from '../src/structures/SlashCommand'; +import logger from '../src/utilities/Logger'; require('dotenv').config(); const commandDirectory = './src/commands/'; @@ -25,17 +25,26 @@ for (const file of commandFiles) { const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!); try { - logger.info(`Attempting to register ${commandArray.length} server commands...`); - rest.put(Routes.applicationGuildCommands(process.env.APP_ID!, process.env.GUILD_ID!), { body: commandArray }); + logger.info( + `Attempting to register ${commandArray.length} server commands...` + ); + rest.put( + Routes.applicationGuildCommands(process.env.APP_ID!, process.env.GUILD_ID!), + { body: commandArray } + ); logger.info('Successfully registered commands!'); } catch (err) { throw err; } try { - logger.info(`Attempting to register ${globalCommandArray.length} global commands...`); - rest.put(Routes.applicationCommands(process.env.APP_ID!), { body: globalCommandArray }); + logger.info( + `Attempting to register ${globalCommandArray.length} global commands...` + ); + rest.put(Routes.applicationCommands(process.env.APP_ID!), { + body: globalCommandArray + }); logger.info('Successfully registered commands!'); } catch (err) { throw err; -} \ No newline at end of file +} diff --git a/eslint.config.mjs b/eslint.config.mjs index dd3df47..c82d623 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import globals from 'globals' export default [ { - files: ['src/**/*.ts'], + files: ['src/**/*.ts', 'bin/*.ts'], plugins: { '@typescript-eslint': typescriptEslint, @@ -130,7 +130,6 @@ export default [ 'no-dupe-keys': 'error', 'no-duplicate-case': 'error', 'no-else-return': 'error', - 'no-empty': 'error', 'no-empty-pattern': 'error', 'no-eval': 'error', 'no-ex-assign': 'error', diff --git a/package-lock.json b/package-lock.json index ece3528..c815ccd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint": "^10.1.0", "globals": "^17.4.0", "pino-pretty": "^13.1.3", + "prettier": "^3.8.1", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.9.3" @@ -2464,6 +2465,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", diff --git a/package.json b/package.json index 2c75454..794e1a8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", "lint": "eslint", + "prettier": "prettier **/*.ts --write", "build": "tsc && node -e \"const fs=require('fs'); ['commands', 'events', 'components/buttons', 'components/menus', 'components/modals'].forEach(d => fs.mkdirSync('./dist/' + d, { recursive: true }));\"", "start": "node dist/index.js" }, @@ -27,6 +28,7 @@ "eslint": "^10.1.0", "globals": "^17.4.0", "pino-pretty": "^13.1.3", + "prettier": "^3.8.1", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.9.3" diff --git a/src/bot.ts b/src/bot.ts index fd9184f..98632b4 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,18 +13,20 @@ limitations under the License. */ -import logger, { flushAndClose } from "./utilities/Logger"; -import { Client, GatewayIntentBits } from "discord.js"; +import logger, { flushAndClose } from './utilities/Logger'; +import { Client, GatewayIntentBits } from 'discord.js'; import 'dotenv/config'; -import EventHandler from "./handlers/EventHandler"; -import SlashCommandHandler from "./handlers/SlashCommandHandler"; -import ButtonHandler from "./handlers/ButtonHandler"; -import SelectMenuHandler from "./handlers/SelectMenuHandler"; -import ModalSubmitHandler from "./handlers/ModalSubmitHandler"; -import WorkerPool from "./utilities/WorkerPool"; -import PresenceManager from "./managers/PresenceManager"; +import EventHandler from './handlers/EventHandler'; +import SlashCommandHandler from './handlers/SlashCommandHandler'; +import ButtonHandler from './handlers/ButtonHandler'; +import SelectMenuHandler from './handlers/SelectMenuHandler'; +import ModalSubmitHandler from './handlers/ModalSubmitHandler'; +import WorkerPool from './utilities/WorkerPool'; +import PresenceManager from './managers/PresenceManager'; -const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] +}); (async () => { try { diff --git a/src/commands/AttackCommand.ts b/src/commands/AttackCommand.ts index 77803d4..172a6b8 100644 --- a/src/commands/AttackCommand.ts +++ b/src/commands/AttackCommand.ts @@ -1,23 +1,26 @@ -import { type ChatInputCommandInteraction, type Client } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type ICombatJSON } from "../interfaces/ICombatJSON"; -import { apiFetch } from "../utilities/ApiClient"; -import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; +import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type ICombatJSON } from '../interfaces/ICombatJSON'; +import { apiFetch } from '../utilities/ApiClient'; +import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; export default class AttackCommand extends SlashCommand { constructor() { super({ - name: "attack", - description: "Attack the enemy in your encounter", - category: "Gaming", + name: 'attack', + description: 'Attack the enemy in your encounter', + category: 'Gaming', cooldown: 1.8, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const res = await apiFetch(Routes.combat(), { @@ -30,7 +33,7 @@ export default class AttackCommand extends SlashCommand { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -40,4 +43,4 @@ export default class AttackCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/commands/ChestsCommand.ts b/src/commands/ChestsCommand.ts index 4313a23..719d803 100644 --- a/src/commands/ChestsCommand.ts +++ b/src/commands/ChestsCommand.ts @@ -1,26 +1,34 @@ import { - ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - type ChatInputCommandInteraction, type Client, EmbedBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; -import ImageService from "../utilities/ImageService"; -import type { IChestSlot } from "../interfaces/IGameJSON"; + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; +import ImageService from '../utilities/ImageService'; +import type { IChestSlot } from '../interfaces/IGameJSON'; export default class ChestsCommand extends SlashCommand { constructor() { super({ - name: "chests", - description: "View and manage your chest vault", - category: "Gaming", + name: 'chests', + description: 'View and manage your chest vault', + category: 'Gaming', cooldown: 5, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const discordId = interaction.user.id; @@ -30,16 +38,22 @@ export default class ChestsCommand extends SlashCommand { const playerBody = await res.json(); if (!res.ok) { - await interaction.editReply({ content: formatError(playerBody.error ?? 'Failed to load player') }); + await interaction.editReply({ + content: formatError(playerBody.error ?? 'Failed to load player') + }); return; } // Fetch chest data via the chests endpoint (GET with discordId) - const chestRes = await apiFetch(`${Routes.chests()}?discordId=${discordId}`); + const chestRes = await apiFetch( + `${Routes.chests()}?discordId=${discordId}` + ); const chestBody = await chestRes.json(); if (!chestRes.ok) { - await interaction.editReply({ content: formatError(chestBody.error ?? 'Failed to load chests') }); + await interaction.editReply({ + content: formatError(chestBody.error ?? 'Failed to load chests') + }); return; } @@ -57,13 +71,19 @@ export default class ChestsCommand extends SlashCommand { totalOpened }); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'chests.png' }); - const embed = new EmbedBuilder().setColor(0xeab308).setImage('attachment://chests.png'); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'chests.png' + }); + const embed = new EmbedBuilder() + .setColor(0xeab308) + .setImage('attachment://chests.png'); const components: ActionRowBuilder[] = []; // Action buttons for each chest (max 2 rows of 4) - const actionable = chests.filter(c => c.status === 'ready' || c.status === 'locked'); + const actionable = chests.filter( + (c) => c.status === 'ready' || c.status === 'locked' + ); const chunks = chunkArray(actionable, 4); for (const chunk of chunks.slice(0, 2)) { @@ -108,9 +128,15 @@ export default class ChestsCommand extends SlashCommand { components.push(shopRow); } - await interaction.editReply({ embeds: [embed], files: [attachment], components }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/commands/CollectionCommand.ts b/src/commands/CollectionCommand.ts index b272fe6..501175b 100644 --- a/src/commands/CollectionCommand.ts +++ b/src/commands/CollectionCommand.ts @@ -1,42 +1,53 @@ -import { type ChatInputCommandInteraction, type Client, EmbedBuilder } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type ICollectionJSON } from "../interfaces/ICollectionJSON"; -import ItemManager from "../managers/ItemManager"; -import PaginatorBuilder from "../utilities/PaginatorBuilder"; -import Routes from "../utilities/Routes"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; +import { + type ChatInputCommandInteraction, + type Client, + EmbedBuilder +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type ICollectionJSON } from '../interfaces/ICollectionJSON'; +import ItemManager from '../managers/ItemManager'; +import PaginatorBuilder from '../utilities/PaginatorBuilder'; +import Routes from '../utilities/Routes'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; export default class CollectionCommand extends SlashCommand { constructor() { super({ - name: "collection", + name: 'collection', description: "View your or another player's item collection", - category: "General", + category: 'General', cooldown: 5, isGlobalCommand: true }); - this.builder.addUserOption((o) => o.setName('user') - .setDescription('Select a user') - .setRequired(false) + this.builder.addUserOption((o) => + o.setName('user').setDescription('Select a user').setRequired(false) ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); - - const targetUser = interaction.options.getUser('user', false) ?? interaction.user; + + const targetUser = + interaction.options.getUser('user', false) ?? interaction.user; const res = await apiFetch(Routes.player(targetUser.id)); if (res.status === 404) { - await interaction.editReply({ content: `${targetUser.username} hasn't made any DFO player data!` }); + await interaction.editReply({ + content: `${targetUser.username} hasn't made any DFO player data!` + }); return; } if (!res.ok) { - await interaction.editReply({ content: formatError('Failed to load player data') }); + await interaction.editReply({ + content: formatError('Failed to load player data') + }); return; } @@ -46,7 +57,10 @@ export default class CollectionCommand extends SlashCommand { // Safely parse the "Map" from JSON into an array of [itemId, quantity] let collectionItems: [string, number][] = []; if (collection?.items) { - if (typeof collection.items === 'object' && !Array.isArray(collection.items)) { + if ( + typeof collection.items === 'object' && + !Array.isArray(collection.items) + ) { collectionItems = Object.entries(collection.items); } else if (collection.items instanceof Map) { collectionItems = Array.from(collection.items.entries()); @@ -54,7 +68,9 @@ export default class CollectionCommand extends SlashCommand { } if (collectionItems.length === 0) { - await interaction.editReply({ content: `📖 **${targetUser.username}** hasn't discovered any items yet.` }); + await interaction.editReply({ + content: `📖 **${targetUser.username}** hasn't discovered any items yet.` + }); return; } @@ -67,13 +83,13 @@ export default class CollectionCommand extends SlashCommand { for (let i = 0; i < collectionItems.length; i += ITEMS_PER_PAGE) { const chunk = collectionItems.slice(i, i + ITEMS_PER_PAGE); - + let descriptionText = statsHeader; for (const [itemIdString, quantity] of chunk) { const itemId = parseInt(itemIdString, 10); const itemData = ItemManager.get(itemId); - + if (itemData) { descriptionText += `✨ **${itemData.name}** (x${quantity})\n`; descriptionText += `└ *${itemData.rarity} ${itemData.type}*\n\n`; diff --git a/src/commands/ExploreCommand.ts b/src/commands/ExploreCommand.ts index e727843..77e6a28 100644 --- a/src/commands/ExploreCommand.ts +++ b/src/commands/ExploreCommand.ts @@ -1,23 +1,26 @@ -import { type ChatInputCommandInteraction, type Client } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type IStepJSON } from "../interfaces/IStepJSON"; -import { apiFetch } from "../utilities/ApiClient"; -import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; +import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type IStepJSON } from '../interfaces/IStepJSON'; +import { apiFetch } from '../utilities/ApiClient'; +import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; export default class ExploreCommand extends SlashCommand { constructor() { super({ - name: "explore", - description: "Explore the world and find items or enemy encounters!", - category: "Gaming", + name: 'explore', + description: 'Explore the world and find items or enemy encounters!', + category: 'Gaming', cooldown: 7, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const res = await apiFetch(Routes.explore(), { @@ -25,15 +28,19 @@ export default class ExploreCommand extends SlashCommand { body: JSON.stringify({ discordId: interaction.user.id }) }); - const data = await res.json() as IStepJSON; + const data = (await res.json()) as IStepJSON; if (res.status === 429) { - await interaction.editReply({ content: formatCooldown('step', data.cooldownRemaining) }); + await interaction.editReply({ + content: formatCooldown('step', data.cooldownRemaining) + }); return; } if (res.status === 404) { - await interaction.editReply({ content: formatError('', 'PLAYER_NOT_FOUND') }); + await interaction.editReply({ + content: formatError('', 'PLAYER_NOT_FOUND') + }); return; } @@ -45,4 +52,4 @@ export default class ExploreCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/commands/FleeCommand.ts b/src/commands/FleeCommand.ts index b0263bc..9b2e1b5 100644 --- a/src/commands/FleeCommand.ts +++ b/src/commands/FleeCommand.ts @@ -1,23 +1,26 @@ -import { type ChatInputCommandInteraction, type Client } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type ICombatJSON } from "../interfaces/ICombatJSON"; -import { apiFetch } from "../utilities/ApiClient"; -import { buildCombatResponse } from "../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; +import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type ICombatJSON } from '../interfaces/ICombatJSON'; +import { apiFetch } from '../utilities/ApiClient'; +import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; export default class FleeCommand extends SlashCommand { constructor() { super({ - name: "flee", + name: 'flee', description: "Flee the enemy encounter you're in.", - category: "Gaming", + category: 'Gaming', cooldown: 2, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const res = await apiFetch(Routes.combat(), { @@ -30,7 +33,7 @@ export default class FleeCommand extends SlashCommand { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -40,4 +43,4 @@ export default class FleeCommand extends SlashCommand { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/commands/GuideCommand.ts b/src/commands/GuideCommand.ts index eb0089c..8314209 100644 --- a/src/commands/GuideCommand.ts +++ b/src/commands/GuideCommand.ts @@ -1,12 +1,22 @@ -import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; +import { + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; -const SECTIONS: Record = { +const SECTIONS: Record< + string, + { title: string; emoji: string; content: string } +> = { basics: { title: 'Getting Started', emoji: '📖', content: [ - '**Welcome to Dragon\'s Fall Online!**', + "**Welcome to Dragon's Fall Online!**", '', '`/register` — Create your character', '`/explore` — Take a step in your current zone. You may find gold, items, or enemies!', @@ -42,15 +52,15 @@ const SECTIONS: Record +1 to +5: Guaranteed success', '> +6 to +10: Decreasing success chance (80% → 20%)', '> Failed attempts consume resources. High-level failures may destroy the item.', '> Enhanced items become unique variants — they split from stacks.', '', - '🔄 **Reforge** — Reroll an item\'s stats and/or affixes. Costs gold.', + "🔄 **Reforge** — Reroll an item's stats and/or affixes. Costs gold.", '> Stats: Reroll ATK/DEF/HP values', '> Affixes: Reroll special effects', '> Full: Reroll everything (costs more)', @@ -117,30 +127,42 @@ const SECTIONS: Record o.setName('section') - .setDescription('Jump to a specific section') - .setRequired(false) - .addChoices( - ...SECTION_ORDER.map(key => ({ - name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, - value: key - })) - ) + this.builder.addStringOption((o) => + o + .setName('section') + .setDescription('Jump to a specific section') + .setRequired(false) + .addChoices( + ...SECTION_ORDER.map((key) => ({ + name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, + value: key + })) + ) ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { const section = interaction.options.getString('section') ?? 'basics'; const data = SECTIONS[section] || SECTIONS.basics; @@ -148,7 +170,9 @@ export default class GuideCommand extends SlashCommand { .setColor(0x10b981) .setTitle(`${data.emoji} ${data.title}`) .setDescription(data.content) - .setFooter({ text: `DFO Guide • Use /guide
to jump to a topic` }); + .setFooter({ + text: `DFO Guide • Use /guide
to jump to a topic` + }); // Section navigation buttons const currentIdx = SECTION_ORDER.indexOf(section); diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index b557fcc..9d82a68 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -1,34 +1,42 @@ -import { type ChatInputCommandInteraction, type Client, EmbedBuilder, Colors } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import SlashCommandHandler from "../handlers/SlashCommandHandler"; -import PaginatorBuilder from "../utilities/PaginatorBuilder"; +import { + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + Colors +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import SlashCommandHandler from '../handlers/SlashCommandHandler'; +import PaginatorBuilder from '../utilities/PaginatorBuilder'; const CATEGORY_ICONS: Record = { - 'General': '📋', - 'Gaming': '⚔️', - 'Moderator': '🛡️', - 'Developer': '🔧' + General: '📋', + Gaming: '⚔️', + Moderator: '🛡️', + Developer: '🔧' }; const CATEGORY_COLORS: Record = { - 'General': 0x3b82f6, - 'Gaming': 0xef4444, - 'Moderator': 0xf59e0b, - 'Developer': 0x6b7280 + General: 0x3b82f6, + Gaming: 0xef4444, + Moderator: 0xf59e0b, + Developer: 0x6b7280 }; export default class HelpCommand extends SlashCommand { constructor() { super({ - name: "help", - description: "View all available commands and how to get started", - category: "General", + name: 'help', + description: 'View all available commands and how to get started', + category: 'General', cooldown: 3, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const commands = SlashCommandHandler.getCache(); @@ -40,7 +48,8 @@ export default class HelpCommand extends SlashCommand { // Hide developer commands from regular users if (command.category === 'Developer') continue; - if (!categories.has(command.category)) categories.set(command.category, []); + if (!categories.has(command.category)) + categories.set(command.category, []); categories.get(command.category)!.push(command); } @@ -49,16 +58,16 @@ export default class HelpCommand extends SlashCommand { // Page 1: Overview / Getting Started const overviewEmbed = new EmbedBuilder() .setColor(0x10b981) - .setTitle('⚔️ Dragon\'s Fall Online') + .setTitle("⚔️ Dragon's Fall Online") .setDescription( 'A lightweight text-based MMORPG. Collect thousands of unique items, explore endless scenarios, and watch numbers go up.\n\n' + - '**Getting Started:**\n' + - '> 1. Run `/register` to create your character\n' + - '> 2. Use `/explore` to adventure and find loot\n' + - '> 3. Check `/inventory` to manage your gear\n' + - '> 4. View `/profile` to see your stats\n\n' + - '**Links:**\n' + - '> 🌐 [Play on Web](https://capi.gg/dfo) • 🗳️ [Vote on top.gg](https://top.gg) • 💬 [Discord Server](https://discord.gg/dfo)' + '**Getting Started:**\n' + + '> 1. Run `/register` to create your character\n' + + '> 2. Use `/explore` to adventure and find loot\n' + + '> 3. Check `/inventory` to manage your gear\n' + + '> 4. View `/profile` to see your stats\n\n' + + '**Links:**\n' + + '> 🌐 [Play on Web](https://capi.gg/dfo) • 🗳️ [Vote on top.gg](https://top.gg) • 💬 [Discord Server](https://discord.gg/dfo)' ) .setThumbnail(client.user?.displayAvatarURL() ?? ''); @@ -89,4 +98,4 @@ export default class HelpCommand extends SlashCommand { await paginator.start(interaction); } -} \ No newline at end of file +} diff --git a/src/commands/InventoryCommand.ts b/src/commands/InventoryCommand.ts index 1bbfadc..0e3b79f 100644 --- a/src/commands/InventoryCommand.ts +++ b/src/commands/InventoryCommand.ts @@ -1,39 +1,59 @@ import { - type ChatInputCommandInteraction, type Client, EmbedBuilder, AttachmentBuilder, - ButtonBuilder, ButtonStyle, ActionRowBuilder, StringSelectMenuBuilder, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder, + StringSelectMenuBuilder, StringSelectMenuOptionBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type IInventoryItem } from "../interfaces/IInventoryJSON"; -import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; -import PaginatorBuilder from "../utilities/PaginatorBuilder"; -import Routes from "../utilities/Routes"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import ItemManager from "../managers/ItemManager"; -import ImageService from "../utilities/ImageService"; +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type IInventoryItem } from '../interfaces/IInventoryJSON'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import PaginatorBuilder from '../utilities/PaginatorBuilder'; +import Routes from '../utilities/Routes'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import ItemManager from '../managers/ItemManager'; +import ImageService from '../utilities/ImageService'; export default class InventoryCommand extends SlashCommand { constructor() { super({ - name: "inventory", - description: "View your inventory and manage items", - category: "General", + name: 'inventory', + description: 'View your inventory and manage items', + category: 'General', cooldown: 5, isGlobalCommand: true }); // No options — the select menu handles item selection } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const res = await apiFetch(Routes.inventory(interaction.user.id)); - const { success, data, error }: { success: boolean; data: any; error?: string } = await res.json(); - - if (res.status === 400 || res.status === 401 || res.status === 404 || res.status === 500) { - await interaction.editReply({ content: formatError(error ?? 'Unknown error') }); + const { + success, + data, + error + }: { success: boolean; data: any; error?: string } = await res.json(); + + if ( + res.status === 400 || + res.status === 401 || + res.status === 404 || + res.status === 500 + ) { + await interaction.editReply({ + content: formatError(error ?? 'Unknown error') + }); return; } @@ -41,7 +61,9 @@ export default class InventoryCommand extends SlashCommand { const player = data.player as IPlayerJSON; if (!inventory || inventory.length === 0) { - await interaction.editReply({ content: `🎒 **${interaction.user.username}**'s inventory is completely empty.` }); + await interaction.editReply({ + content: `🎒 **${interaction.user.username}**'s inventory is completely empty.` + }); return; } @@ -88,11 +110,15 @@ export default class InventoryCommand extends SlashCommand { .setMaxValues(1) .addOptions(selectOptions.slice(0, 25)); - pageRows.push(new ActionRowBuilder().setComponents(selectMenu)); + pageRows.push( + new ActionRowBuilder().setComponents( + selectMenu + ) + ); } // === BULK ACTION BUTTONS === - const eligibleCount = chunk.filter(inv => { + const eligibleCount = chunk.filter((inv) => { if (inv.isLocked) return false; const def = ItemManager.get(inv.itemId); if (!def || def.type === 'Consumable') return false; diff --git a/src/commands/LeaderboardCommand.ts b/src/commands/LeaderboardCommand.ts index c0f9ed8..06c876b 100644 --- a/src/commands/LeaderboardCommand.ts +++ b/src/commands/LeaderboardCommand.ts @@ -1,10 +1,18 @@ -import { AttachmentBuilder, type ChatInputCommandInteraction, type Client, EmbedBuilder } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import Routes from "../utilities/Routes"; -import { formatError } from "../utilities/ErrorMessages"; -import { type LeaderboardEntry, type LeaderboardConfig } from "../utilities/LeaderboardImageBuilder"; -import ImageService from "../utilities/ImageService"; +import { + AttachmentBuilder, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import Routes from '../utilities/Routes'; +import { formatError } from '../utilities/ErrorMessages'; +import { + type LeaderboardEntry, + type LeaderboardConfig +} from '../utilities/LeaderboardImageBuilder'; +import ImageService from '../utilities/ImageService'; const STAT_OPTIONS = [ { name: 'Level', value: 'level' }, @@ -14,28 +22,28 @@ const STAT_OPTIONS = [ ]; const STAT_DISPLAY: Record = { - 'level': { + level: { title: 'Leaderboard — Level', stat: 'Level', emoji: '⭐', accentColor: '#eab308', accentColorDim: '#eab30825' }, - 'coins': { + coins: { title: 'Leaderboard — Gold', stat: 'Gold', emoji: '🪙', accentColor: '#f59e0b', accentColorDim: '#f59e0b25' }, - 'enemiesDefeated': { + enemiesDefeated: { title: 'Leaderboard — Enemies Defeated', stat: 'Enemies Defeated', emoji: '💀', accentColor: '#ef4444', accentColorDim: '#ef444425' }, - 'daysPassed': { + daysPassed: { title: 'Leaderboard — Days Explored', stat: 'Days Explored', emoji: '📅', @@ -47,21 +55,26 @@ const STAT_DISPLAY: Record = { export default class LeaderboardCommand extends SlashCommand { constructor() { super({ - name: "leaderboard", - description: "View the top players", - category: "General", + name: 'leaderboard', + description: 'View the top players', + category: 'General', cooldown: 10, isGlobalCommand: true }); - this.builder.addStringOption((o) => o.setName('stat') - .setDescription('Which stat to rank by') - .setChoices(STAT_OPTIONS) - .setRequired(false) + this.builder.addStringOption((o) => + o + .setName('stat') + .setDescription('Which stat to rank by') + .setChoices(STAT_OPTIONS) + .setRequired(false) ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const stat = interaction.options.getString('stat', false) ?? 'level'; @@ -72,19 +85,23 @@ export default class LeaderboardCommand extends SlashCommand { if (!res.ok) { const body = await res.json().catch(() => ({})); - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load leaderboard') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load leaderboard') + }); return; } const { data }: { data: any[] } = await res.json(); if (!data || data.length === 0) { - await interaction.editReply({ content: '📊 **No players found yet.** Be the first to `/register`!' }); + await interaction.editReply({ + content: '📊 **No players found yet.** Be the first to `/register`!' + }); return; } // Map API data to the image builder's expected shape - const entries: LeaderboardEntry[] = data.map(player => { + const entries: LeaderboardEntry[] = data.map((player) => { let value: number; if (stat === 'enemiesDefeated' || stat === 'daysPassed') { value = player.statistics?.[stat] ?? 0; @@ -100,7 +117,9 @@ export default class LeaderboardCommand extends SlashCommand { }); const imageBuffer = await ImageService.leaderboard(entries, config); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'leaderboard.png' }); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'leaderboard.png' + }); const embed = new EmbedBuilder() .setColor(parseInt(config.accentColor.replace('#', ''), 16)) @@ -108,7 +127,9 @@ export default class LeaderboardCommand extends SlashCommand { await interaction.editReply({ embeds: [embed], files: [attachment] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/commands/LookupCommand.ts b/src/commands/LookupCommand.ts index e408bae..046b357 100644 --- a/src/commands/LookupCommand.ts +++ b/src/commands/LookupCommand.ts @@ -1,14 +1,21 @@ -import { type AutocompleteInteraction, type ChatInputCommandInteraction, type Client, Colors, EmbedBuilder, MessageFlags } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import ItemManager from "../managers/ItemManager"; -import PaginatorBuilder from "../utilities/PaginatorBuilder"; -import ItemLookupContainer from "../structures/containers/ItemLookupContainer"; -import { apiFetch } from "../utilities/ApiClient"; -import Routes from "../utilities/Routes"; -import { type IScenarioJSON } from "../interfaces/IScenarioJSON"; -import ScenarioLookupContainer from "../structures/containers/ScenarioLookupContainer"; -import { type INPCJSON } from "../interfaces/INPCJSON"; -import NPCLookupContainer from "../structures/containers/NPCLookupContainer"; +import { + type AutocompleteInteraction, + type ChatInputCommandInteraction, + type Client, + Colors, + EmbedBuilder, + MessageFlags +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import ItemManager from '../managers/ItemManager'; +import PaginatorBuilder from '../utilities/PaginatorBuilder'; +import ItemLookupContainer from '../structures/containers/ItemLookupContainer'; +import { apiFetch } from '../utilities/ApiClient'; +import Routes from '../utilities/Routes'; +import { type IScenarioJSON } from '../interfaces/IScenarioJSON'; +import ScenarioLookupContainer from '../structures/containers/ScenarioLookupContainer'; +import { type INPCJSON } from '../interfaces/INPCJSON'; +import NPCLookupContainer from '../structures/containers/NPCLookupContainer'; const typeOptions = [ { name: 'Item', value: 'item' }, @@ -19,21 +26,37 @@ const typeOptions = [ export default class LookupCommand extends SlashCommand { constructor() { super({ - name: "lookup", - description: "Lookup specific objects in the game", - category: "Moderator", + name: 'lookup', + description: 'Lookup specific objects in the game', + category: 'Moderator', cooldown: 3, isGlobalCommand: false }); - this.builder.addStringOption((o) => o.setName('type').setDescription('Select a type').setChoices(typeOptions).setRequired(true)); - this.builder.addIntegerOption((o) => o.setName('id').setDescription('Enter an id to lookup. Use -1 for all').setMinValue(-1).setRequired(true).setAutocomplete(true)); + this.builder.addStringOption((o) => + o + .setName('type') + .setDescription('Select a type') + .setChoices(typeOptions) + .setRequired(true) + ); + this.builder.addIntegerOption((o) => + o + .setName('id') + .setDescription('Enter an id to lookup. Use -1 for all') + .setMinValue(-1) + .setRequired(true) + .setAutocomplete(true) + ); } /** * Autocomplete handler (#9) — suggests items by name when type is 'item' */ - public async autocomplete(interaction: AutocompleteInteraction, client: Client): Promise { + public async autocomplete( + interaction: AutocompleteInteraction, + client: Client + ): Promise { const type = interaction.options.getString('type'); const focused = interaction.options.getFocused(true); @@ -47,9 +70,13 @@ export default class LookupCommand extends SlashCommand { // Filter by name match, return up to 25 suggestions (Discord limit) const matches = items - .filter(item => item.name.toLowerCase().includes(query) || String(item.itemId).startsWith(query)) + .filter( + (item) => + item.name.toLowerCase().includes(query) || + String(item.itemId).startsWith(query) + ) .slice(0, 25) - .map(item => ({ + .map((item) => ({ name: `[${item.itemId}] ${item.name} (${item.rarity} Lvl ${item.level})`, value: item.itemId })); @@ -57,112 +84,161 @@ export default class LookupCommand extends SlashCommand { await interaction.respond(matches); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const choice = interaction.options.getString('type', true); const id = interaction.options.getInteger('id', true); switch (choice) { - case 'item': - if (id === -1) { - const items = Array.from(ItemManager.cache.values()); - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { - const chunk = items.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - - for (const item of chunk) { - descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; - descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; - if (item.affixes) { - let textToAdd = ''; - for (const affix of item.affixes) { - textToAdd += affix.type === 'THORNS' - ? ` **${affix.type}:** \`${affix.value.toLocaleString()}\` |` - : ` **${affix.type}:** \`${affix.value.toLocaleString()}%\` |`; + case 'item': + if (id === -1) { + const items = Array.from(ItemManager.cache.values()); + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { + const chunk = items.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + + for (const item of chunk) { + descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; + descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; + if (item.affixes) { + let textToAdd = ''; + for (const affix of item.affixes) { + textToAdd += + affix.type === 'THORNS' + ? ` **${affix.type}:** \`${affix.value.toLocaleString()}\` |` + : ` **${affix.type}:** \`${affix.value.toLocaleString()}%\` |`; + } + if (textToAdd !== '') + descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; + else descriptionText += '\n'; } - if (textToAdd !== '') descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; - else descriptionText += '\n'; } + + pages.push( + new EmbedBuilder() + .setColor(Colors.Green) + .setTitle('Item Manager') + .setDescription(descriptionText) + ); } - pages.push(new EmbedBuilder().setColor(Colors.Green).setTitle('Item Manager').setDescription(descriptionText)); + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const item = ItemManager.get(id); + if (!item) { + await interaction.editReply({ + content: 'No item with that id exists!' + }); + return; + } + await interaction.editReply({ + components: [new ItemLookupContainer(item).build()], + flags: MessageFlags.IsComponentsV2 + }); } + break; + + case 'scenario': + if (id === -1) { + const res = await apiFetch(Routes.scenarios()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const scenario of chunk) { + descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? `${scenario.description.substring(0, 125)}...` : scenario.description}\`\`\`\n`; + descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; + } + pages.push( + new EmbedBuilder() + .setTitle('Scenario Manager') + .setDescription(descriptionText) + ); + } - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); - - } else { - const item = ItemManager.get(id); - if (!item) { - await interaction.editReply({ content: 'No item with that id exists!' }); - return; - } - await interaction.editReply({ components: [new ItemLookupContainer(item).build()], flags: MessageFlags.IsComponentsV2 }); - } - break; - - case 'scenario': - if (id === -1) { - const res = await apiFetch(Routes.scenarios()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const scenario of chunk) { - descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? `${scenario.description.substring(0, 125)}...` : scenario.description}\`\`\`\n`; - descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const res = await apiFetch(Routes.scenario(id)); + if (res.status === 404) { + await interaction.editReply({ + content: 'No scenario was found for this id!' + }); + return; } - pages.push(new EmbedBuilder().setTitle('Scenario Manager').setDescription(descriptionText)); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON } = await res.json(); + await interaction.editReply({ + components: [new ScenarioLookupContainer(data).build()], + flags: MessageFlags.IsComponentsV2 + }); } + break; + + case 'npc': + if (id === -1) { + const res = await apiFetch(Routes.npcs()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: INPCJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const npc of chunk) { + descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; + descriptionText += `└ ${npc.description.length > 128 ? `${npc.description.substring(0, 125)}...` : npc.description}\n\n`; + } + pages.push( + new EmbedBuilder() + .setTitle('NPC Manager') + .setDescription(descriptionText) + ); + } - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); - - } else { - const res = await apiFetch(Routes.scenario(id)); - if (res.status === 404) { await interaction.editReply({ content: 'No scenario was found for this id!' }); return; } - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON } = await res.json(); - await interaction.editReply({ components: [new ScenarioLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); - } - break; - - case 'npc': - if (id === -1) { - const res = await apiFetch(Routes.npcs()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: INPCJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const npc of chunk) { - descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; - descriptionText += `└ ${npc.description.length > 128 ? `${npc.description.substring(0, 125)}...` : npc.description}\n\n`; + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const res = await apiFetch(Routes.npc(id)); + if (!res.ok) throw new Error('API Error!'); + const { success, data }: { success: boolean; data: INPCJSON } = + await res.json(); + if (!success) { + await interaction.editReply({ + content: 'No NPC was found for the provided ID!' + }); + return; } - pages.push(new EmbedBuilder().setTitle('NPC Manager').setDescription(descriptionText)); + await interaction.editReply({ + components: [new NPCLookupContainer(data).build()], + flags: MessageFlags.IsComponentsV2 + }); } - - await new PaginatorBuilder().setPages(pages).setTargetUser(interaction.user.id).setIdleTimeout(60_000).start(interaction); - - } else { - const res = await apiFetch(Routes.npc(id)); - if (!res.ok) throw new Error('API Error!'); - const { success, data }: { success: boolean; data: INPCJSON } = await res.json(); - if (!success) { await interaction.editReply({ content: 'No NPC was found for the provided ID!' }); return; } - await interaction.editReply({ components: [new NPCLookupContainer(data).build()], flags: MessageFlags.IsComponentsV2 }); - } - break; + break; } } -} \ No newline at end of file +} diff --git a/src/commands/MarketCommand.ts b/src/commands/MarketCommand.ts index c05629a..2466731 100644 --- a/src/commands/MarketCommand.ts +++ b/src/commands/MarketCommand.ts @@ -1,23 +1,37 @@ import { - ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - type ChatInputCommandInteraction, type Client, EmbedBuilder, MessageFlags, - StringSelectMenuBuilder, StringSelectMenuOptionBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; -import { type MarketListing, type MarketPageConfig } from "../utilities/MarketImageBuilder"; -import ImageService from "../utilities/ImageService"; -import ItemManager from "../managers/ItemManager"; -import type { IInventoryItem } from "../interfaces/IInventoryJSON"; + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + MessageFlags, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; +import { + type MarketListing, + type MarketPageConfig +} from '../utilities/MarketImageBuilder'; +import ImageService from '../utilities/ImageService'; +import ItemManager from '../managers/ItemManager'; +import type { IInventoryItem } from '../interfaces/IInventoryJSON'; const RARITY_CHOICES = [ { name: 'All', value: 'All' }, - { name: 'Common', value: 'Common' }, { name: 'Uncommon', value: 'Uncommon' }, - { name: 'Rare', value: 'Rare' }, { name: 'Elite', value: 'Elite' }, - { name: 'Epic', value: 'Epic' }, { name: 'Legendary', value: 'Legendary' }, - { name: 'Divine', value: 'Divine' }, { name: 'Exotic', value: 'Exotic' } + { name: 'Common', value: 'Common' }, + { name: 'Uncommon', value: 'Uncommon' }, + { name: 'Rare', value: 'Rare' }, + { name: 'Elite', value: 'Elite' }, + { name: 'Epic', value: 'Epic' }, + { name: 'Legendary', value: 'Legendary' }, + { name: 'Divine', value: 'Divine' }, + { name: 'Exotic', value: 'Exotic' } ]; const SORT_CHOICES = [ @@ -32,8 +46,10 @@ const SORT_CHOICES = [ const TYPE_CHOICES = [ { name: 'All', value: 'All' }, - { name: 'Weapon', value: 'Weapon' }, { name: 'Armor', value: 'Armor' }, - { name: 'Accessory', value: 'Accessory' }, { name: 'Consumable', value: 'Consumable' } + { name: 'Weapon', value: 'Weapon' }, + { name: 'Armor', value: 'Armor' }, + { name: 'Accessory', value: 'Accessory' }, + { name: 'Consumable', value: 'Consumable' } ]; export const SELL_PAGE_SIZE = 25; @@ -41,41 +57,81 @@ export const SELL_PAGE_SIZE = 25; export default class MarketCommand extends SlashCommand { constructor() { super({ - name: "market", - description: "Browse and trade on the Global Market", - category: "Gaming", + name: 'market', + description: 'Browse and trade on the Global Market', + category: 'Gaming', cooldown: 5, isGlobalCommand: true }); this.data - .addSubcommand((sub) => sub.setName('browse') - .setDescription('Browse items for sale on the Global Market') - .addStringOption((o) => o.setName('search').setDescription('Search by item name').setRequired(false)) - .addStringOption((o) => o.setName('rarity').setDescription('Filter by rarity').setChoices(RARITY_CHOICES).setRequired(false)) - .addStringOption((o) => o.setName('type').setDescription('Filter by item type').setChoices(TYPE_CHOICES).setRequired(false)) - .addStringOption((o) => o.setName('sort').setDescription('Sort order').setChoices(SORT_CHOICES).setRequired(false)) + .addSubcommand((sub) => + sub + .setName('browse') + .setDescription('Browse items for sale on the Global Market') + .addStringOption((o) => + o + .setName('search') + .setDescription('Search by item name') + .setRequired(false) + ) + .addStringOption((o) => + o + .setName('rarity') + .setDescription('Filter by rarity') + .setChoices(RARITY_CHOICES) + .setRequired(false) + ) + .addStringOption((o) => + o + .setName('type') + .setDescription('Filter by item type') + .setChoices(TYPE_CHOICES) + .setRequired(false) + ) + .addStringOption((o) => + o + .setName('sort') + .setDescription('Sort order') + .setChoices(SORT_CHOICES) + .setRequired(false) + ) ) - .addSubcommand((sub) => sub.setName('listings') - .setDescription('View your active market listings') + .addSubcommand((sub) => + sub + .setName('listings') + .setDescription('View your active market listings') ) - .addSubcommand((sub) => sub.setName('sell') - .setDescription('Select an item from your inventory to list on the market') + .addSubcommand((sub) => + sub + .setName('sell') + .setDescription( + 'Select an item from your inventory to list on the market' + ) ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { const sub = interaction.options.getSubcommand(true); const discordId = interaction.user.id; switch (sub) { - case 'browse': return this.handleBrowse(interaction, discordId); - case 'listings': return this.handleListings(interaction, discordId); - case 'sell': return this.handleSell(interaction, discordId); + case 'browse': + return this.handleBrowse(interaction, discordId); + case 'listings': + return this.handleListings(interaction, discordId); + case 'sell': + return this.handleSell(interaction, discordId); } } - private async handleBrowse(interaction: ChatInputCommandInteraction, discordId: string): Promise { + private async handleBrowse( + interaction: ChatInputCommandInteraction, + discordId: string + ): Promise { await interaction.deferReply(); const search = interaction.options.getString('search') ?? ''; @@ -84,32 +140,65 @@ export default class MarketCommand extends SlashCommand { const sort = interaction.options.getString('sort') ?? 'newest'; try { - const res = await apiFetch(Routes.marketBrowse(discordId, { page: 1, search: search || undefined, rarity, type, sort })); + const res = await apiFetch( + Routes.marketBrowse(discordId, { + page: 1, + search: search || undefined, + rarity, + type, + sort + }) + ); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load market') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load market') + }); return; } const listings: MarketListing[] = body.data; const pagination = body.pagination; - const config: MarketPageConfig = { page: pagination.page, totalPages: pagination.totalPages, totalItems: pagination.totalItems, mode: 'browse' }; + const config: MarketPageConfig = { + page: pagination.page, + totalPages: pagination.totalPages, + totalItems: pagination.totalItems, + mode: 'browse' + }; const imageBuffer = await ImageService.market(listings, config); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'market.png' }); - const embed = new EmbedBuilder().setColor(0x10b981).setImage('attachment://market.png'); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'market.png' + }); + const embed = new EmbedBuilder() + .setColor(0x10b981) + .setImage('attachment://market.png'); const filterKey = `${(search || '').slice(0, 30)}|${rarity}|${type}|${sort}`; - const components = buildMarketButtons(listings, config, filterKey, 'browse'); + const components = buildMarketButtons( + listings, + config, + filterKey, + 'browse' + ); - await interaction.editReply({ embeds: [embed], files: [attachment], components }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } - private async handleListings(interaction: ChatInputCommandInteraction, discordId: string): Promise { + private async handleListings( + interaction: ChatInputCommandInteraction, + discordId: string + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); try { @@ -117,34 +206,61 @@ export default class MarketCommand extends SlashCommand { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load your listings') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load your listings') + }); return; } const listings: MarketListing[] = body.data; const pagination = body.pagination; - const config: MarketPageConfig = { page: pagination.page, totalPages: pagination.totalPages, totalItems: pagination.totalItems, mode: 'my_listings' }; + const config: MarketPageConfig = { + page: pagination.page, + totalPages: pagination.totalPages, + totalItems: pagination.totalItems, + mode: 'my_listings' + }; const imageBuffer = await ImageService.market(listings, config); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'my_listings.png' }); - const embed = new EmbedBuilder().setColor(0x3b82f6).setImage('attachment://my_listings.png'); - - const components = buildMarketButtons(listings, config, '', 'my_listings'); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'my_listings.png' + }); + const embed = new EmbedBuilder() + .setColor(0x3b82f6) + .setImage('attachment://my_listings.png'); + + const components = buildMarketButtons( + listings, + config, + '', + 'my_listings' + ); - await interaction.editReply({ embeds: [embed], files: [attachment], components }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } - private async handleSell(interaction: ChatInputCommandInteraction, discordId: string): Promise { + private async handleSell( + interaction: ChatInputCommandInteraction, + discordId: string + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); try { const result = await buildSellPage(discordId, 0); await interaction.editReply(result); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } @@ -153,7 +269,10 @@ export default class MarketCommand extends SlashCommand { * Builds a paginated sell page with select menu + prev/next buttons. * Shared by MarketCommand.handleSell and MarketSellPageButton. */ -export async function buildSellPage(discordId: string, page: number): Promise<{ content: string; components: ActionRowBuilder[] }> { +export async function buildSellPage( + discordId: string, + page: number +): Promise<{ content: string; components: ActionRowBuilder[] }> { const res = await apiFetch(Routes.inventory(discordId)); const body = await res.json(); @@ -164,7 +283,10 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ const inventory: IInventoryItem[] = body.data?.inventory || []; if (inventory.length === 0) { - return { content: '🎒 Your inventory is empty — nothing to sell!', components: [] }; + return { + content: '🎒 Your inventory is empty — nothing to sell!', + components: [] + }; } const sellable = inventory.filter((inv: IInventoryItem) => { @@ -175,12 +297,19 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ }); if (sellable.length === 0) { - return { content: '❌ No sellable items. Unlock or acquire non-consumable items first.', components: [] }; + return { + content: + '❌ No sellable items. Unlock or acquire non-consumable items first.', + components: [] + }; } const totalPages = Math.ceil(sellable.length / SELL_PAGE_SIZE); const safePage = Math.max(0, Math.min(page, totalPages - 1)); - const pageItems = sellable.slice(safePage * SELL_PAGE_SIZE, (safePage + 1) * SELL_PAGE_SIZE); + const pageItems = sellable.slice( + safePage * SELL_PAGE_SIZE, + (safePage + 1) * SELL_PAGE_SIZE + ); const options: StringSelectMenuOptionBuilder[] = []; for (const inv of pageItems) { @@ -188,13 +317,18 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ if (!def) continue; const enhTag = inv.enhanceLevel > 0 ? ` +${inv.enhanceLevel}` : ''; - const modTag = inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides ? ' ✨' : ''; + const modTag = + inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides + ? ' ✨' + : ''; const value = def.value || 0; options.push( new StringSelectMenuOptionBuilder() .setLabel(`${def.name}${enhTag} (x${inv.quantity})${modTag}`) - .setDescription(`${def.rarity} ${def.type} • Lvl ${def.level} • Base: ${value.toLocaleString()}g`) + .setDescription( + `${def.rarity} ${def.type} • Lvl ${def.level} • Base: ${value.toLocaleString()}g` + ) .setValue(`${inv._id}:${inv.itemId}:${inv.quantity}`) ); } @@ -209,7 +343,9 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ .setMaxValues(1) .addOptions(options); - components.push(new ActionRowBuilder().setComponents(selectMenu)); + components.push( + new ActionRowBuilder().setComponents(selectMenu) + ); } if (totalPages > 1) { @@ -233,9 +369,10 @@ export async function buildSellPage(discordId: string, page: number): Promise<{ components.push(navRow); } - const header = totalPages > 1 - ? `🏪 **Select an item to sell** (Page ${safePage + 1}/${totalPages} • ${sellable.length} items)` - : `🏪 **Select an item to sell** (${sellable.length} items)`; + const header = + totalPages > 1 + ? `🏪 **Select an item to sell** (Page ${safePage + 1}/${totalPages} • ${sellable.length} items)` + : `🏪 **Select an item to sell** (${sellable.length} items)`; return { content: header, components }; } diff --git a/src/commands/NetworkCommand.ts b/src/commands/NetworkCommand.ts index 3112a0f..52281ae 100644 --- a/src/commands/NetworkCommand.ts +++ b/src/commands/NetworkCommand.ts @@ -1,13 +1,13 @@ -import { - type ChatInputCommandInteraction, - type Client, - Colors, - EmbedBuilder, - MessageFlags, +import { + type ChatInputCommandInteraction, + type Client, + Colors, + EmbedBuilder, + MessageFlags, ContainerBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import PaginatorBuilder from "../utilities/PaginatorBuilder"; +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import PaginatorBuilder from '../utilities/PaginatorBuilder'; interface ClusterInfo { id: number; @@ -37,144 +37,196 @@ const options = [ export default class NetworkCommand extends SlashCommand { constructor() { super({ - name: "network", - description: "Track all clusters and guilds the bot is connected to", - category: "Moderator", + name: 'network', + description: 'Track all clusters and guilds the bot is connected to', + category: 'Moderator', cooldown: 5, isGlobalCommand: false }); - this.builder.addStringOption((o) => o.setName('type').setDescription('Select a view type').setChoices(options).setRequired(true)); - this.builder.addStringOption((o) => o.setName('id').setDescription("Enter a Cluster ID or Guild ID. Use 'all' to view everything.").setRequired(false)); + this.builder.addStringOption((o) => + o + .setName('type') + .setDescription('Select a view type') + .setChoices(options) + .setRequired(true) + ); + this.builder.addStringOption((o) => + o + .setName('id') + .setDescription( + "Enter a Cluster ID or Guild ID. Use 'all' to view everything." + ) + .setRequired(false) + ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const choice = interaction.options.getString('type', true); const id = interaction.options.getString('id') || 'all'; switch (choice) { - case 'overview': { - const clusters = await this.getClusters(client); - const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); - const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); - const avgPing = clusters.reduce((acc, s) => acc + s.ping, 0) / clusters.length; - const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); - - const container = new ContainerBuilder() - .setAccentColor(Colors.Blurple) - .addTextDisplayComponents(text => text.setContent(`# 🌐 Global Network Overview`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` - )); - - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); - break; - } + case 'overview': { + const clusters = await this.getClusters(client); + const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); + const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); + const avgPing = + clusters.reduce((acc, s) => acc + s.ping, 0) / clusters.length; + const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); - case 'shard': { - const clusters = await this.getClusters(client); + const container = new ContainerBuilder() + .setAccentColor(Colors.Blurple) + .addTextDisplayComponents((text) => + text.setContent(`# 🌐 Global Network Overview`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => + text.setContent( + `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` + ) + ); + + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); + break; + } - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + case 'shard': { + const clusters = await this.getClusters(client); - for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { - const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - for (const cluster of chunk) { - descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; - descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; - } + for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { + const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; - pages.push(new EmbedBuilder().setColor(Colors.Blue).setTitle('Network Manager: Clusters').setDescription(descriptionText)); - } + for (const cluster of chunk) { + descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; + descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; + } - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); - - await paginator.start(interaction); - - } else { - const targetCluster = clusters.find(s => s.id.toString() === id); - if (!targetCluster) { - await interaction.editReply({ content: `❌ No cluster could be found with the ID: \`${id}\`` }); - return; - } + pages.push( + new EmbedBuilder() + .setColor(Colors.Blue) + .setTitle('Network Manager: Clusters') + .setDescription(descriptionText) + ); + } - const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); - const uptimeHours = Math.floor(uptimeMins / 60); + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + + await paginator.start(interaction); + } else { + const targetCluster = clusters.find((s) => s.id.toString() === id); + if (!targetCluster) { + await interaction.editReply({ + content: `❌ No cluster could be found with the ID: \`${id}\`` + }); + return; + } - const container = new ContainerBuilder() - .setAccentColor(Colors.Blue) - .addTextDisplayComponents(text => text.setContent(`# 💎 Cluster #${targetCluster.id}`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` - )); - - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); + const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); + const uptimeHours = Math.floor(uptimeMins / 60); + + const container = new ContainerBuilder() + .setAccentColor(Colors.Blue) + .addTextDisplayComponents((text) => + text.setContent(`# 💎 Cluster #${targetCluster.id}`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => + text.setContent( + `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` + ) + ); + + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); + } + break; } - break; - } - case 'guild': { - const guilds = await this.getGuilds(client); + case 'guild': { + const guilds = await this.getGuilds(client); - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { - const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { + const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; - for (const guild of chunk) { - descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; - descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; + for (const guild of chunk) { + descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; + descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; + } + + pages.push( + new EmbedBuilder() + .setColor(Colors.Purple) + .setTitle('Network Manager: Guilds') + .setDescription(descriptionText) + ); } - pages.push(new EmbedBuilder().setColor(Colors.Purple).setTitle('Network Manager: Guilds').setDescription(descriptionText)); - } + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + + await paginator.start(interaction); + } else { + const targetGuild = guilds.find((g) => g.id === id); + if (!targetGuild) { + await interaction.editReply({ + content: `❌ No guild could be found with the ID: \`${id}\`` + }); + return; + } - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); - - await paginator.start(interaction); - - } else { - const targetGuild = guilds.find(g => g.id === id); - if (!targetGuild) { - await interaction.editReply({ content: `❌ No guild could be found with the ID: \`${id}\`` }); - return; + const joinedTimestamp = targetGuild.joinedAt + ? `` + : 'Unknown'; + + const container = new ContainerBuilder() + .setAccentColor(Colors.Purple) + .addTextDisplayComponents((text) => + text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => + text.setContent( + `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` + ) + ); + + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); } - - const joinedTimestamp = targetGuild.joinedAt ? `` : 'Unknown'; - - const container = new ContainerBuilder() - .setAccentColor(Colors.Purple) - .addTextDisplayComponents(text => text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`)) - .addSeparatorComponents(sep => sep.setDivider(true)) - .addTextDisplayComponents(text => text.setContent( - `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` - )); - - await interaction.editReply({ components: [container], flags: MessageFlags.IsComponentsV2 }); + break; } - break; - } } } // --- HELPER METHODS --- - + /** * Reaches across all clusters via hybrid sharding broadcastEval. */ @@ -187,21 +239,28 @@ export default class NetworkCommand extends SlashCommand { shardCount: c.ws.shards?.size ?? 1, ping: c.ws.ping, guilds: c.guilds.cache.size, - users: c.guilds.cache.reduce((acc: number, guild: any) => acc + guild.memberCount, 0), + users: c.guilds.cache.reduce( + (acc: number, guild: any) => acc + guild.memberCount, + 0 + ), uptime: c.uptime })); return results; - } - return [{ - id: 0, - shards: [0], - shardCount: 1, - ping: client.ws.ping, - guilds: client.guilds.cache.size, - users: client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), - uptime: client.uptime - }]; - + } + return [ + { + id: 0, + shards: [0], + shardCount: 1, + ping: client.ws.ping, + guilds: client.guilds.cache.size, + users: client.guilds.cache.reduce( + (acc, guild) => acc + guild.memberCount, + 0 + ), + uptime: client.uptime + } + ]; } /** @@ -210,18 +269,19 @@ export default class NetworkCommand extends SlashCommand { private async getGuilds(client: Client): Promise { const cluster = (client as any).cluster; if (cluster) { - const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => c.guilds.cache.map((g: any) => ({ - id: g.id, - name: g.name, - memberCount: g.memberCount, - clusterId: c.cluster?.id ?? 0, - ownerId: g.ownerId, - joinedAt: g.joinedTimestamp - })) + const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => + c.guilds.cache.map((g: any) => ({ + id: g.id, + name: g.name, + memberCount: g.memberCount, + clusterId: c.cluster?.id ?? 0, + ownerId: g.ownerId, + joinedAt: g.joinedTimestamp + })) ); return results.flat(); - } - return client.guilds.cache.map(g => ({ + } + return client.guilds.cache.map((g) => ({ id: g.id, name: g.name, memberCount: g.memberCount, @@ -229,6 +289,5 @@ export default class NetworkCommand extends SlashCommand { ownerId: g.ownerId, joinedAt: g.joinedTimestamp })); - } } diff --git a/src/commands/ProfileCommand.ts b/src/commands/ProfileCommand.ts index 923dd4b..cbf1d7c 100644 --- a/src/commands/ProfileCommand.ts +++ b/src/commands/ProfileCommand.ts @@ -1,39 +1,58 @@ -import { type ChatInputCommandInteraction, type Client, AttachmentBuilder, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; -import { type IInventoryItem } from "../interfaces/IInventoryJSON"; -import { type ICollectionJSON } from "../interfaces/ICollectionJSON"; -import Routes from "../utilities/Routes"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import { type EquipmentSlot } from "../interfaces/IItemJSON"; -import ImageService from "../utilities/ImageService"; +import { + type ChatInputCommandInteraction, + type Client, + AttachmentBuilder, + ActionRowBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + ButtonBuilder, + ButtonStyle +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import { type IInventoryItem } from '../interfaces/IInventoryJSON'; +import { type ICollectionJSON } from '../interfaces/ICollectionJSON'; +import Routes from '../utilities/Routes'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import { type EquipmentSlot } from '../interfaces/IItemJSON'; +import ImageService from '../utilities/ImageService'; export default class ProfileCommand extends SlashCommand { constructor() { super({ - name: "profile", + name: 'profile', description: "View your or another player's profile", - category: "General", + category: 'General', cooldown: 5, isGlobalCommand: true }); - this.builder.addUserOption((o) => o.setName('user').setDescription('Select a user').setRequired(false)); + this.builder.addUserOption((o) => + o.setName('user').setDescription('Select a user').setRequired(false) + ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); - const targetUser = interaction.options.getUser('user', false) ?? interaction.user; + const targetUser = + interaction.options.getUser('user', false) ?? interaction.user; const res = await apiFetch(Routes.player(targetUser.id)); if (res.status === 404) { - await interaction.editReply({ content: formatError('', 'PLAYER_NOT_FOUND') }); + await interaction.editReply({ + content: formatError('', 'PLAYER_NOT_FOUND') + }); return; } if (!res.ok) { - await interaction.editReply({ content: formatError('Failed to load profile') }); + await interaction.editReply({ + content: formatError('Failed to load profile') + }); return; } @@ -45,7 +64,9 @@ export default class ProfileCommand extends SlashCommand { // Generate the canvas profile image const imageBuffer = await ImageService.profile(player, targetUser); - const profileAttachment = new AttachmentBuilder(imageBuffer, { name: 'profile.png' }); + const profileAttachment = new AttachmentBuilder(imageBuffer, { + name: 'profile.png' + }); // Only show interactive components for your OWN profile if (targetUser.id === interaction.user.id) { @@ -53,20 +74,33 @@ export default class ProfileCommand extends SlashCommand { const options: StringSelectMenuOptionBuilder[] = []; const equipment = player.equipment; - Object.entries(equipment).forEach(entry => { + Object.entries(equipment).forEach((entry) => { const slot = entry[0] as EquipmentSlot; const itemId = entry[1]; if (itemId) { - options.push(new StringSelectMenuOptionBuilder().setLabel(slot).setValue(slot)); + options.push( + new StringSelectMenuOptionBuilder().setLabel(slot).setValue(slot) + ); } }); - const menu = new StringSelectMenuBuilder().setCustomId('unequip') - .setOptions(options.length >= 1 ? options : [new StringSelectMenuOptionBuilder().setLabel('None').setValue('None')]) + const menu = new StringSelectMenuBuilder() + .setCustomId('unequip') + .setOptions( + options.length >= 1 + ? options + : [ + new StringSelectMenuOptionBuilder() + .setLabel('None') + .setValue('None') + ] + ) .setMaxValues(1) .setPlaceholder('Unequip Slot'); - components.push(new ActionRowBuilder().setComponents(menu)); + components.push( + new ActionRowBuilder().setComponents(menu) + ); // Skill points button (only if they have unspent points) if (player.skillPoints > 0) { @@ -75,7 +109,9 @@ export default class ProfileCommand extends SlashCommand { .setLabel(`⭐ Spend Skill Points (${player.skillPoints} available)`) .setStyle(ButtonStyle.Primary); - components.push(new ActionRowBuilder().setComponents(spButton)); + components.push( + new ActionRowBuilder().setComponents(spButton) + ); } } @@ -84,4 +120,4 @@ export default class ProfileCommand extends SlashCommand { components }); } -} \ No newline at end of file +} diff --git a/src/commands/RegisterCommand.ts b/src/commands/RegisterCommand.ts index 97c0115..a33bf33 100644 --- a/src/commands/RegisterCommand.ts +++ b/src/commands/RegisterCommand.ts @@ -1,31 +1,42 @@ -import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; +import { + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + MessageFlags +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; export default class RegisterCommand extends SlashCommand { constructor() { super({ - name: "register", - description: "Register new user data with the bot", - category: "General", + name: 'register', + description: 'Register new user data with the bot', + category: 'General', cooldown: 5, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { // Show the consent prompt — registration happens when they click Accept const embed = new EmbedBuilder() .setColor(0x10b981) - .setTitle('⚔️ Welcome to Dragon\'s Fall Online') + .setTitle("⚔️ Welcome to Dragon's Fall Online") .setDescription( 'Before creating your character, please review the following:\n\n' + - '**What we store:**\n' + - '> Your Discord user ID, username, and avatar are used to create your player profile. ' + - 'Gameplay data (level, inventory, stats) is stored on our servers.\n\n' + - '**Your rights:**\n' + - '> You may request full deletion of your player data at any time by contacting the developer.\n\n' + - '**By clicking Accept**, you agree to our [Privacy Policy & Terms of Service](https://capi.gg/legal).\n\n' + - '-# You can review our full legal page at any time: https://capi.gg/legal' + '**What we store:**\n' + + '> Your Discord user ID, username, and avatar are used to create your player profile. ' + + 'Gameplay data (level, inventory, stats) is stored on our servers.\n\n' + + '**Your rights:**\n' + + '> You may request full deletion of your player data at any time by contacting the developer.\n\n' + + '**By clicking Accept**, you agree to our [Privacy Policy & Terms of Service](https://capi.gg/legal).\n\n' + + '-# You can review our full legal page at any time: https://capi.gg/legal' ) .setFooter({ text: 'DFO Cross-Platform Integration' }); @@ -46,6 +57,10 @@ export default class RegisterCommand extends SlashCommand { .setEmoji('📜') ); - await interaction.reply({ embeds: [embed], components: [row], flags: MessageFlags.Ephemeral }); + await interaction.reply({ + embeds: [embed], + components: [row], + flags: MessageFlags.Ephemeral + }); } -} \ No newline at end of file +} diff --git a/src/commands/RestCommand.ts b/src/commands/RestCommand.ts index c05b070..6f628fa 100644 --- a/src/commands/RestCommand.ts +++ b/src/commands/RestCommand.ts @@ -1,21 +1,24 @@ -import { type ChatInputCommandInteraction, type Client } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; +import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; export default class RestCommand extends SlashCommand { constructor() { super({ - name: "rest", - description: "Rest at the inn to restore HP (costs gold)", - category: "Gaming", + name: 'rest', + description: 'Rest at the inn to restore HP (costs gold)', + category: 'Gaming', cooldown: 5, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); try { @@ -29,11 +32,18 @@ export default class RestCommand extends SlashCommand { if (!res.ok || !result.success) { // Handle full HP case gracefully - if (result.error?.includes('full') || result.error?.includes('already')) { - await interaction.editReply({ content: '❤️ You are already at full HP!' }); + if ( + result.error?.includes('full') || + result.error?.includes('already') + ) { + await interaction.editReply({ + content: '❤️ You are already at full HP!' + }); return; } - await interaction.editReply({ content: formatError(result.error ?? 'Rest failed') }); + await interaction.editReply({ + content: formatError(result.error ?? 'Rest failed') + }); return; } @@ -47,7 +57,9 @@ export default class RestCommand extends SlashCommand { ].join('\n') }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/commands/TasksCommand.ts b/src/commands/TasksCommand.ts index 665684c..8ac67c2 100644 --- a/src/commands/TasksCommand.ts +++ b/src/commands/TasksCommand.ts @@ -1,36 +1,47 @@ import { - ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, - type ChatInputCommandInteraction, type Client, EmbedBuilder, type StringSelectMenuBuilder, + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + type StringSelectMenuBuilder, StringSelectMenuOptionBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; -import ImageService from "../utilities/ImageService"; -import type { ITaskJSON } from "../interfaces/IGameJSON"; +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; +import ImageService from '../utilities/ImageService'; +import type { ITaskJSON } from '../interfaces/IGameJSON'; export default class TasksCommand extends SlashCommand { constructor() { super({ - name: "tasks", - description: "View your active tasks and claim rewards", - category: "Gaming", + name: 'tasks', + description: 'View your active tasks and claim rewards', + category: 'Gaming', cooldown: 5, isGlobalCommand: true }); - this.builder.addStringOption((o) => o.setName('period') - .setDescription('Task period to view') - .setRequired(false) - .addChoices( - { name: 'Daily', value: 'daily' }, - { name: 'Weekly', value: 'weekly' }, - { name: 'Monthly', value: 'monthly' } - ) + this.builder.addStringOption((o) => + o + .setName('period') + .setDescription('Task period to view') + .setRequired(false) + .addChoices( + { name: 'Daily', value: 'daily' }, + { name: 'Weekly', value: 'weekly' }, + { name: 'Monthly', value: 'monthly' } + ) ); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const period = interaction.options.getString('period') ?? 'daily'; const discordId = interaction.user.id; @@ -40,7 +51,9 @@ export default class TasksCommand extends SlashCommand { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load tasks') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load tasks') + }); return; } @@ -53,7 +66,9 @@ export default class TasksCommand extends SlashCommand { // Convert ISO reset string to ms remaining const resetIso = resets[period]; - const resetIn = resetIso ? Math.max(0, new Date(resetIso).getTime() - Date.now()) : 0; + const resetIn = resetIso + ? Math.max(0, new Date(resetIso).getTime() - Date.now()) + : 0; // Build canvas image const imageBuffer = await ImageService.tasks(tasks, { @@ -62,15 +77,27 @@ export default class TasksCommand extends SlashCommand { playerEmbers }); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'tasks.png' }); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'tasks.png' + }); const embed = new EmbedBuilder() - .setColor(period === 'daily' ? 0x10b981 : period === 'weekly' ? 0x6366f1 : 0xc026d3) + .setColor( + period === 'daily' + ? 0x10b981 + : period === 'weekly' + ? 0x6366f1 + : 0xc026d3 + ) .setImage('attachment://tasks.png'); - const components: ActionRowBuilder[] = []; + const components: ActionRowBuilder< + ButtonBuilder | StringSelectMenuBuilder + >[] = []; // Claim buttons for completed unclaimed tasks - const claimable = tasks.filter((t: ITaskJSON) => t.progress >= t.target && !t.claimed); + const claimable = tasks.filter( + (t: ITaskJSON) => t.progress >= t.target && !t.claimed + ); if (claimable.length > 0) { const claimRow = new ActionRowBuilder(); for (const task of claimable.slice(0, 5)) { @@ -89,24 +116,36 @@ export default class TasksCommand extends SlashCommand { new ButtonBuilder() .setCustomId(`tasks_tab:daily`) .setLabel('Daily') - .setStyle(period === 'daily' ? ButtonStyle.Primary : ButtonStyle.Secondary) + .setStyle( + period === 'daily' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) .setDisabled(period === 'daily'), new ButtonBuilder() .setCustomId(`tasks_tab:weekly`) .setLabel('Weekly') - .setStyle(period === 'weekly' ? ButtonStyle.Primary : ButtonStyle.Secondary) + .setStyle( + period === 'weekly' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) .setDisabled(period === 'weekly'), new ButtonBuilder() .setCustomId(`tasks_tab:monthly`) .setLabel('Monthly') - .setStyle(period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary) + .setStyle( + period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) .setDisabled(period === 'monthly') ); components.push(periodRow); - await interaction.editReply({ embeds: [embed], files: [attachment], components: components as any }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components: components as any + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/commands/TestCommand.ts b/src/commands/TestCommand.ts index 92ecd45..7a12e73 100644 --- a/src/commands/TestCommand.ts +++ b/src/commands/TestCommand.ts @@ -1,24 +1,35 @@ -import { ButtonBuilder, ButtonStyle, ChannelType, type ChatInputCommandInteraction, type Client, ContainerBuilder, MessageFlags } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import logger from "../utilities/Logger"; +import { + ButtonBuilder, + ButtonStyle, + ChannelType, + type ChatInputCommandInteraction, + type Client, + ContainerBuilder, + MessageFlags +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import logger from '../utilities/Logger'; export default class TestCommand extends SlashCommand { constructor() { super({ - name: "test", - description: "dev command", - category: "Developer", + name: 'test', + description: 'dev command', + category: 'Developer', cooldown: 5, isGlobalCommand: false }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); const res = await fetch('https://capi.gg/api/facts/random', { headers: { - 'Authorization': `Bearer ${process.env.API_KEY}`, + Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' } }); @@ -34,4 +45,4 @@ export default class TestCommand extends SlashCommand { await interaction.editReply({ content: `\`${factId}\`\n${text}` }); } -} \ No newline at end of file +} diff --git a/src/commands/TravelCommand.ts b/src/commands/TravelCommand.ts index 9dac36d..253cc89 100644 --- a/src/commands/TravelCommand.ts +++ b/src/commands/TravelCommand.ts @@ -1,26 +1,39 @@ import { - ActionRowBuilder, AttachmentBuilder, type ChatInputCommandInteraction, - type Client, EmbedBuilder, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder -} from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; -import { apiFetch } from "../utilities/ApiClient"; -import { formatError } from "../utilities/ErrorMessages"; -import Routes from "../utilities/Routes"; -import { getAccessibleZones, getZone, type ZoneInfo } from "../utilities/ZoneData"; -import ImageService from "../utilities/ImageService"; + ActionRowBuilder, + AttachmentBuilder, + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + MessageFlags, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; +import { apiFetch } from '../utilities/ApiClient'; +import { formatError } from '../utilities/ErrorMessages'; +import Routes from '../utilities/Routes'; +import { + getAccessibleZones, + getZone, + type ZoneInfo +} from '../utilities/ZoneData'; +import ImageService from '../utilities/ImageService'; export default class TravelCommand extends SlashCommand { constructor() { super({ - name: "travel", - description: "View the zone map and travel to a different zone", - category: "Gaming", + name: 'travel', + description: 'View the zone map and travel to a different zone', + category: 'Gaming', cooldown: 5, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { await interaction.deferReply(); // Fetch player data to get current zone and level @@ -28,12 +41,16 @@ export default class TravelCommand extends SlashCommand { const res = await apiFetch(Routes.player(interaction.user.id)); if (res.status === 404) { - await interaction.editReply({ content: formatError('', 'PLAYER_NOT_FOUND') }); + await interaction.editReply({ + content: formatError('', 'PLAYER_NOT_FOUND') + }); return; } if (!res.ok) { - await interaction.editReply({ content: formatError('Failed to load player data') }); + await interaction.editReply({ + content: formatError('Failed to load player data') + }); return; } @@ -44,35 +61,56 @@ export default class TravelCommand extends SlashCommand { // Render the zone map const imageBuffer = await ImageService.travel(playerLevel, currentZoneId); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'zonemap.png' }); - const embed = new EmbedBuilder().setColor(0x10b981).setImage('attachment://zonemap.png'); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'zonemap.png' + }); + const embed = new EmbedBuilder() + .setColor(0x10b981) + .setImage('attachment://zonemap.png'); // Build the travel select menu with accessible zones (excluding current) - const accessible = getAccessibleZones(playerLevel).filter(z => z.id !== currentZoneId); + const accessible = getAccessibleZones(playerLevel).filter( + (z) => z.id !== currentZoneId + ); const currentZone = getZone(currentZoneId); const components: ActionRowBuilder[] = []; if (accessible.length > 0) { - const options = accessible.map(zone => new StringSelectMenuOptionBuilder() - .setLabel(zone.name) - .setDescription(`Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat`) - .setValue(String(zone.id)) + const options = accessible.map((zone) => + new StringSelectMenuOptionBuilder() + .setLabel(zone.name) + .setDescription( + `Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat` + ) + .setValue(String(zone.id)) ); const selectMenu = new StringSelectMenuBuilder() .setCustomId('travel_select') - .setPlaceholder(`Current: ${currentZone?.name ?? 'Unknown'} — Select destination...`) + .setPlaceholder( + `Current: ${currentZone?.name ?? 'Unknown'} — Select destination...` + ) .setMinValues(1) .setMaxValues(1) .addOptions(options.slice(0, 25)); // Discord max 25 options - components.push(new ActionRowBuilder().setComponents(selectMenu)); + components.push( + new ActionRowBuilder().setComponents( + selectMenu + ) + ); } - await interaction.editReply({ embeds: [embed], files: [attachment], components }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/commands/VoteCommand.ts b/src/commands/VoteCommand.ts index 32544a9..fb81585 100644 --- a/src/commands/VoteCommand.ts +++ b/src/commands/VoteCommand.ts @@ -1,26 +1,36 @@ -import { type ChatInputCommandInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import SlashCommand from "../structures/SlashCommand"; +import { + type ChatInputCommandInteraction, + type Client, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle +} from 'discord.js'; +import SlashCommand from '../structures/SlashCommand'; export default class VoteCommand extends SlashCommand { constructor() { super({ - name: "vote", - description: "Support DFO by voting on top.gg!", - category: "General", + name: 'vote', + description: 'Support DFO by voting on top.gg!', + category: 'General', cooldown: 5, isGlobalCommand: true }); } - public async execute(interaction: ChatInputCommandInteraction, client: Client): Promise { + public async execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { const botId = client.user?.id ?? ''; const embed = new EmbedBuilder() .setColor(0xff3366) - .setTitle('🗳️ Vote for Dragon\'s Fall Online!') + .setTitle("🗳️ Vote for Dragon's Fall Online!") .setDescription( 'Voting helps more players discover DFO and keeps the project alive.\n\n' + - 'You can vote every **12 hours** on top.gg. Thank you for your support!' + 'You can vote every **12 hours** on top.gg. Thank you for your support!' ) .setThumbnail(client.user?.displayAvatarURL() ?? ''); @@ -39,4 +49,4 @@ export default class VoteCommand extends SlashCommand { await interaction.reply({ embeds: [embed], components: [row] }); } -} \ No newline at end of file +} diff --git a/src/components/buttons/AttackButton.ts b/src/components/buttons/AttackButton.ts index c1a57f9..b24ba36 100644 --- a/src/components/buttons/AttackButton.ts +++ b/src/components/buttons/AttackButton.ts @@ -1,15 +1,20 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { type ICombatJSON } from "../../interfaces/ICombatJSON"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; +import { type ICombatJSON } from '../../interfaces/ICombatJSON'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class AttackButton extends Button { - constructor() { super({ customId: "attack", cooldown: 1.8, isAuthorOnly: false }); } + constructor() { + super({ customId: 'attack', cooldown: 1.8, isAuthorOnly: false }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const res = await apiFetch(Routes.combat(), { @@ -22,7 +27,7 @@ export default class AttackButton extends Button { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -32,4 +37,4 @@ export default class AttackButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/components/buttons/BulkCollectButton.ts b/src/components/buttons/BulkCollectButton.ts index 3686d15..84a8a41 100644 --- a/src/components/buttons/BulkCollectButton.ts +++ b/src/components/buttons/BulkCollectButton.ts @@ -1,17 +1,32 @@ -import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; -import Button from "../../structures/Button"; -import ItemManager from "../../managers/ItemManager"; -import { apiFetch } from "../../utilities/ApiClient"; -import Routes from "../../utilities/Routes"; -import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; +import { + type ButtonInteraction, + type Client, + LabelBuilder, + MessageFlags, + ModalBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + TextDisplayBuilder +} from 'discord.js'; +import Button from '../../structures/Button'; +import ItemManager from '../../managers/ItemManager'; +import { apiFetch } from '../../utilities/ApiClient'; +import Routes from '../../utilities/Routes'; +import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; export default class BulkCollectButton extends Button { - constructor() { super({ customId: "bulk_collect", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'bulk_collect', cooldown: 3, isAuthorOnly: true }); + } // customId format: bulk_collect: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const pageOffset = parseInt(args?.[0] ?? '0', 10); const res = await apiFetch(Routes.inventory(interaction.user.id)); @@ -20,16 +35,20 @@ export default class BulkCollectButton extends Button { const inventory: IInventoryItem[] = data?.inventory || []; const chunk = inventory.slice(pageOffset, pageOffset + ITEMS_PER_PAGE); - const eligible = chunk.filter(inv => { + const eligible = chunk.filter((inv) => { if (inv.isLocked) return false; - if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) return false; + if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) + return false; const def = ItemManager.get(inv.itemId); if (!def || def.type === 'Consumable') return false; return true; }); if (eligible.length === 0) { - await interaction.reply({ content: '❌ No eligible items to collect on this page.', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: '❌ No eligible items to collect on this page.', + flags: MessageFlags.Ephemeral + }); return; } @@ -57,11 +76,14 @@ export default class BulkCollectButton extends Button { const selectLabel = new LabelBuilder() .setLabel('Select items to archive') - .setDescription('Selected items move from inventory to your collection book') + .setDescription( + 'Selected items move from inventory to your collection book' + ) .setStringSelectMenuComponent(selectMenu); - const infoText = new TextDisplayBuilder() - .setContent('-# ⚠️ THIS IS PERMANENT. Items are removed from your inventory and added to your Collection Book. Modified items will be skipped. This cannot be undone.'); + const infoText = new TextDisplayBuilder().setContent( + '-# ⚠️ THIS IS PERMANENT. Items are removed from your inventory and added to your Collection Book. Modified items will be skipped. This cannot be undone.' + ); const modal = new ModalBuilder() .setCustomId('bulk_collect_modal') diff --git a/src/components/buttons/BulkDismantleButton.ts b/src/components/buttons/BulkDismantleButton.ts index 7232553..a41d26d 100644 --- a/src/components/buttons/BulkDismantleButton.ts +++ b/src/components/buttons/BulkDismantleButton.ts @@ -1,17 +1,32 @@ -import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; -import Button from "../../structures/Button"; -import ItemManager from "../../managers/ItemManager"; -import { apiFetch } from "../../utilities/ApiClient"; -import Routes from "../../utilities/Routes"; -import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; +import { + type ButtonInteraction, + type Client, + LabelBuilder, + MessageFlags, + ModalBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + TextDisplayBuilder +} from 'discord.js'; +import Button from '../../structures/Button'; +import ItemManager from '../../managers/ItemManager'; +import { apiFetch } from '../../utilities/ApiClient'; +import Routes from '../../utilities/Routes'; +import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; export default class BulkDismantleButton extends Button { - constructor() { super({ customId: "bulk_dismantle", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'bulk_dismantle', cooldown: 3, isAuthorOnly: true }); + } // customId format: bulk_dismantle: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const pageOffset = parseInt(args?.[0] ?? '0', 10); const res = await apiFetch(Routes.inventory(interaction.user.id)); @@ -20,16 +35,20 @@ export default class BulkDismantleButton extends Button { const inventory: IInventoryItem[] = data?.inventory || []; const chunk = inventory.slice(pageOffset, pageOffset + ITEMS_PER_PAGE); - const eligible = chunk.filter(inv => { + const eligible = chunk.filter((inv) => { if (inv.isLocked) return false; - if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) return false; + if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) + return false; const def = ItemManager.get(inv.itemId); if (!def || def.type === 'Consumable') return false; return true; }); if (eligible.length === 0) { - await interaction.reply({ content: '❌ No eligible items to dismantle on this page.', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: '❌ No eligible items to dismantle on this page.', + flags: MessageFlags.Ephemeral + }); return; } @@ -60,8 +79,9 @@ export default class BulkDismantleButton extends Button { .setDescription('All selected items will be destroyed for Embers') .setStringSelectMenuComponent(selectMenu); - const infoText = new TextDisplayBuilder() - .setContent('-# 🔥 Items are permanently destroyed and converted to Embers. Enhanced items return bonus embers.'); + const infoText = new TextDisplayBuilder().setContent( + '-# 🔥 Items are permanently destroyed and converted to Embers. Enhanced items return bonus embers.' + ); const modal = new ModalBuilder() .setCustomId('bulk_dismantle_modal') diff --git a/src/components/buttons/BulkSellButton.ts b/src/components/buttons/BulkSellButton.ts index 90ebe61..82b341d 100644 --- a/src/components/buttons/BulkSellButton.ts +++ b/src/components/buttons/BulkSellButton.ts @@ -1,17 +1,32 @@ -import { type ButtonInteraction, type Client, LabelBuilder, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder } from "discord.js"; -import Button from "../../structures/Button"; -import ItemManager from "../../managers/ItemManager"; -import { apiFetch } from "../../utilities/ApiClient"; -import Routes from "../../utilities/Routes"; -import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; +import { + type ButtonInteraction, + type Client, + LabelBuilder, + MessageFlags, + ModalBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, + TextDisplayBuilder +} from 'discord.js'; +import Button from '../../structures/Button'; +import ItemManager from '../../managers/ItemManager'; +import { apiFetch } from '../../utilities/ApiClient'; +import Routes from '../../utilities/Routes'; +import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; export default class BulkSellButton extends Button { - constructor() { super({ customId: "bulk_sell", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'bulk_sell', cooldown: 3, isAuthorOnly: true }); + } // customId format: bulk_sell: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const pageOffset = parseInt(args?.[0] ?? '0', 10); // Fetch inventory fresh from API @@ -22,16 +37,20 @@ export default class BulkSellButton extends Button { // Get the page chunk and filter eligible items const chunk = inventory.slice(pageOffset, pageOffset + ITEMS_PER_PAGE); - const eligible = chunk.filter(inv => { + const eligible = chunk.filter((inv) => { if (inv.isLocked) return false; - if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) return false; + if (inv.enhanceLevel > 0 || inv.statOverrides || inv.affixOverrides) + return false; const def = ItemManager.get(inv.itemId); if (!def || def.type === 'Consumable') return false; return true; }); if (eligible.length === 0) { - await interaction.reply({ content: '❌ No eligible items to sell on this page.', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: '❌ No eligible items to sell on this page.', + flags: MessageFlags.Ephemeral + }); return; } @@ -44,8 +63,10 @@ export default class BulkSellButton extends Button { options.push( new StringSelectMenuOptionBuilder() .setLabel(`${def.name} (x${inv.quantity})`) - .setDescription(`${def.rarity} ${def.type} • Sells for ${totalValue.toLocaleString()}g`) - .setValue(`${inv.itemId}-${inv.quantity}`) // Short — no _id needed for bulk sell + .setDescription( + `${def.rarity} ${def.type} • Sells for ${totalValue.toLocaleString()}g` + ) + .setValue(`${inv.itemId}-${inv.quantity}`) // Short — no _id needed for bulk sell ); } @@ -60,11 +81,14 @@ export default class BulkSellButton extends Button { const selectLabel = new LabelBuilder() .setLabel('Select items to sell') - .setDescription('All selected items will be sold for gold. Modified items are excluded.') + .setDescription( + 'All selected items will be sold for gold. Modified items are excluded.' + ) .setStringSelectMenuComponent(selectMenu); - const infoText = new TextDisplayBuilder() - .setContent('-# Locked, consumable, and modified (enhanced/reforged) items are excluded.'); + const infoText = new TextDisplayBuilder().setContent( + '-# Locked, consumable, and modified (enhanced/reforged) items are excluded.' + ); const modal = new ModalBuilder() .setCustomId('bulk_sell_modal') diff --git a/src/components/buttons/ChestBuyButton.ts b/src/components/buttons/ChestBuyButton.ts index 85f4b2f..586e1e4 100644 --- a/src/components/buttons/ChestBuyButton.ts +++ b/src/components/buttons/ChestBuyButton.ts @@ -1,43 +1,68 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class ChestBuyButton extends Button { constructor() { - super({ customId: "chest_buy", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'chest_buy', cooldown: 3, isAuthorOnly: true }); } // customId format: chest_buy: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const tier = args?.[0]; if (!tier) { - await interaction.editReply({ content: 'Error parsing chest tier!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing chest tier!', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'buy', tier }) + body: JSON.stringify({ + discordId: interaction.user.id, + action: 'buy', + tier + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to buy chest'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to buy chest'), + files: [], + components: [], + embeds: [] + }); return; } await interaction.editReply({ content: `🛒 **Purchased a ${tier} Chest!**\n🪙 Cost: **${body.goldCost?.toLocaleString() ?? '???'}** gold\n💰 Balance: **${body.newBalance?.toLocaleString() ?? '???'}** gold\n\nRun \`/chests\` to view your vault.`, - files: [], components: [], embeds: [] + files: [], + components: [], + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/ChestOpenButton.ts b/src/components/buttons/ChestOpenButton.ts index a23552b..040b1e3 100644 --- a/src/components/buttons/ChestOpenButton.ts +++ b/src/components/buttons/ChestOpenButton.ts @@ -1,47 +1,68 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; const RARITY_EMOJIS: Record = { - Common: '⬜', Uncommon: '🟩', Rare: '🟦', Elite: '🟧', - Epic: '🟪', Legendary: '🟡', Divine: '💎', Exotic: '💜' + Common: '⬜', + Uncommon: '🟩', + Rare: '🟦', + Elite: '🟧', + Epic: '🟪', + Legendary: '🟡', + Divine: '💎', + Exotic: '💜' }; export default class ChestOpenButton extends Button { constructor() { - super({ customId: "chest_open", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'chest_open', cooldown: 3, isAuthorOnly: true }); } // customId format: chest_open: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const chestId = args?.[0]; if (!chestId) { - await interaction.editReply({ content: 'Error parsing chest data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing chest data!', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'open', chestId }) + body: JSON.stringify({ + discordId: interaction.user.id, + action: 'open', + chestId + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to open chest'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to open chest'), + files: [], + components: [], + embeds: [] + }); return; } const loot = body.loot; - const lines = [ - `🎉 **Chest Opened!**`, - `` - ]; + const lines = [`🎉 **Chest Opened!**`, ``]; if (loot.isPity) { lines.push(`✨ **PITY BONUS — Guaranteed Divine item!**`); @@ -51,17 +72,31 @@ export default class ChestOpenButton extends Button { // Items for (const item of loot.items) { const emoji = RARITY_EMOJIS[item.rarity] || '📦'; - lines.push(`${emoji} **${item.name}** — ${item.rarity} ${item.type} (Lvl ${item.level})`); + lines.push( + `${emoji} **${item.name}** — ${item.rarity} ${item.type} (Lvl ${item.level})` + ); } // Gold & Embers lines.push(``); - if (loot.gold > 0) lines.push(`🪙 **+${loot.gold.toLocaleString()}** Gold`); - if (loot.embers > 0) lines.push(`🔥 **+${loot.embers.toLocaleString()}** Embers`); + if (loot.gold > 0) + lines.push(`🪙 **+${loot.gold.toLocaleString()}** Gold`); + if (loot.embers > 0) + lines.push(`🔥 **+${loot.embers.toLocaleString()}** Embers`); - await interaction.editReply({ content: lines.join('\n'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: lines.join('\n'), + files: [], + components: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/ChestStartButton.ts b/src/components/buttons/ChestStartButton.ts index 476d6eb..45b61ae 100644 --- a/src/components/buttons/ChestStartButton.ts +++ b/src/components/buttons/ChestStartButton.ts @@ -1,43 +1,67 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class ChestStartButton extends Button { constructor() { - super({ customId: "chest_start", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'chest_start', cooldown: 2, isAuthorOnly: true }); } // customId format: chest_start: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const chestId = args?.[0]; if (!chestId) { - await interaction.editReply({ content: 'Error parsing chest data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing chest data!', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.chests(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'start', chestId }) + body: JSON.stringify({ + discordId: interaction.user.id, + action: 'start', + chestId + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to start unlock'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to start unlock'), + files: [], + components: [], + embeds: [] + }); return; } await interaction.editReply({ content: `⏳ **Chest unlocking!** It will be ready to open in **${body.unlockTime ?? 'a while'}**.\n\nRun \`/chests\` again later to open it.`, - files: [], components: [] + files: [], + components: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/CollectButton.ts b/src/components/buttons/CollectButton.ts index 9db9495..e6cd951 100644 --- a/src/components/buttons/CollectButton.ts +++ b/src/components/buttons/CollectButton.ts @@ -1,22 +1,41 @@ -import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; -import Button from "../../structures/Button"; +import { + type ButtonInteraction, + type Client, + ModalBuilder, + TextInputStyle +} from 'discord.js'; +import Button from '../../structures/Button'; export default class CollectButton extends Button { constructor() { - super({ customId: "collect", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'collect', cooldown: 2, isAuthorOnly: true }); } // customId format: collect:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const docId = args?.[0]; const maxQty = args?.[1] ?? '1'; const modal = new ModalBuilder() .setCustomId(`collect:${docId}`) .setTitle('⚠️ Collect Item (Permanent)') - .addLabelComponents( - (label) => label.setLabel('Amount').setDescription(`⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})`) - .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short).setPlaceholder(maxQty)) + .addLabelComponents((label) => + label + .setLabel('Amount') + .setDescription( + `⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})` + ) + .setTextInputComponent((ti) => + ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(maxQty) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/ConsumeButton.ts b/src/components/buttons/ConsumeButton.ts index a18c57b..ddc06e0 100644 --- a/src/components/buttons/ConsumeButton.ts +++ b/src/components/buttons/ConsumeButton.ts @@ -1,22 +1,38 @@ -import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; -import Button from "../../structures/Button"; +import { + type ButtonInteraction, + type Client, + ModalBuilder, + TextInputStyle +} from 'discord.js'; +import Button from '../../structures/Button'; export default class ConsumeButton extends Button { constructor() { - super({ customId: "consume", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'consume', cooldown: 2, isAuthorOnly: true }); } // customId format: consume:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const docId = args?.[0]; const maxQty = args?.[1] ?? '1'; const modal = new ModalBuilder() .setTitle('Consume Item') .setCustomId(`consume:${docId}`) - .addLabelComponents( - (label) => label.setLabel('Amount').setDescription(`Enter amount to consume (Max: ${maxQty})`) - .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short)) + .addLabelComponents((label) => + label + .setLabel('Amount') + .setDescription(`Enter amount to consume (Max: ${maxQty})`) + .setTextInputComponent((ti) => + ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/DismantleButton.ts b/src/components/buttons/DismantleButton.ts index 85c1a40..b5b0eec 100644 --- a/src/components/buttons/DismantleButton.ts +++ b/src/components/buttons/DismantleButton.ts @@ -1,16 +1,20 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class DismantleButton extends Button { constructor() { - super({ customId: "dismantle", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'dismantle', cooldown: 3, isAuthorOnly: true }); } // customId format: dismantle::: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; @@ -18,7 +22,12 @@ export default class DismantleButton extends Button { const maxQty = parseInt(args?.[2] ?? '1', 10); if (!docId || isNaN(itemId)) { - await interaction.editReply({ content: 'Error parsing item data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing item data!', + files: [], + components: [], + embeds: [] + }); return; } @@ -39,7 +48,12 @@ export default class DismantleButton extends Button { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Dismantle failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Dismantle failed'), + files: [], + components: [], + embeds: [] + }); return; } @@ -50,10 +64,17 @@ export default class DismantleButton extends Button { `🔥 Embers gained: **+${body.embersGained?.toLocaleString() ?? '???'}**`, `🔥 Total embers: **${body.newEmbers?.toLocaleString() ?? '???'}**` ].join('\n'), - files: [], components: [], embeds: [] + files: [], + components: [], + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/EmbedAttackButton.ts b/src/components/buttons/EmbedAttackButton.ts index ad8fe0b..f7e65b4 100644 --- a/src/components/buttons/EmbedAttackButton.ts +++ b/src/components/buttons/EmbedAttackButton.ts @@ -1,15 +1,20 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { type ICombatJSON } from "../../interfaces/ICombatJSON"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { type ICombatJSON } from '../../interfaces/ICombatJSON'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class EmbedAttackButton extends Button { - constructor() { super({ customId: "embedAttack", cooldown: 1.8, isAuthorOnly: true }); } + constructor() { + super({ customId: 'embedAttack', cooldown: 1.8, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferUpdate(); const res = await apiFetch(Routes.combat(), { @@ -22,7 +27,7 @@ export default class EmbedAttackButton extends Button { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -32,4 +37,4 @@ export default class EmbedAttackButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/components/buttons/EmbedFleeButton.ts b/src/components/buttons/EmbedFleeButton.ts index adda376..3ec3efc 100644 --- a/src/components/buttons/EmbedFleeButton.ts +++ b/src/components/buttons/EmbedFleeButton.ts @@ -1,15 +1,20 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { type ICombatJSON } from "../../interfaces/ICombatJSON"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { type ICombatJSON } from '../../interfaces/ICombatJSON'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class EmbedFleeButton extends Button { - constructor() { super({ customId: "embedFlee", cooldown: 1.8, isAuthorOnly: true }); } + constructor() { + super({ customId: 'embedFlee', cooldown: 1.8, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferUpdate(); const res = await apiFetch(Routes.combat(), { @@ -22,7 +27,7 @@ export default class EmbedFleeButton extends Button { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -32,4 +37,4 @@ export default class EmbedFleeButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/components/buttons/EnhanceButton.ts b/src/components/buttons/EnhanceButton.ts index 0d7e7aa..21886fb 100644 --- a/src/components/buttons/EnhanceButton.ts +++ b/src/components/buttons/EnhanceButton.ts @@ -1,36 +1,54 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class EnhanceButton extends Button { constructor() { - super({ customId: "enhance", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'enhance', cooldown: 3, isAuthorOnly: true }); } // customId format: enhance:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; const itemId = parseInt(args?.[1] ?? '-1', 10); if (!docId || isNaN(itemId)) { - await interaction.editReply({ content: 'Error parsing item data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing item data!', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.enhance(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }) + body: JSON.stringify({ + discordId: interaction.user.id, + itemId, + inventoryId: docId + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Enhancement failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Enhancement failed'), + files: [], + components: [], + embeds: [] + }); return; } @@ -50,12 +68,27 @@ export default class EnhanceButton extends Button { } } - lines.push(``, `🪙 Gold spent: **${result.goldCost?.toLocaleString() ?? '???'}**`); - lines.push(`🔥 Embers spent: **${result.emberCost?.toLocaleString() ?? '???'}**`); + lines.push( + ``, + `🪙 Gold spent: **${result.goldCost?.toLocaleString() ?? '???'}**` + ); + lines.push( + `🔥 Embers spent: **${result.emberCost?.toLocaleString() ?? '???'}**` + ); - await interaction.editReply({ content: lines.join('\n'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: lines.join('\n'), + files: [], + components: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/EquipButton.ts b/src/components/buttons/EquipButton.ts index c7a78ed..43fb9e5 100644 --- a/src/components/buttons/EquipButton.ts +++ b/src/components/buttons/EquipButton.ts @@ -1,42 +1,70 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class EquipButton extends Button { constructor() { - super({ customId: "equip", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'equip', cooldown: 3, isAuthorOnly: true }); } // customId format: equip:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; const itemId = parseInt(args?.[1] ?? '-1', 10); if (!docId || isNaN(itemId)) { - await interaction.editReply({ files: [], components: [], content: 'Error parsing item data!', embeds: [] }); + await interaction.editReply({ + files: [], + components: [], + content: 'Error parsing item data!', + embeds: [] + }); return; } try { const res = await apiFetch(Routes.equip(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, itemId, inventoryId: docId }) + body: JSON.stringify({ + discordId: interaction.user.id, + itemId, + inventoryId: docId + }) }); const { success, error, message } = await res.json(); if (!res.ok || !success) { - await interaction.editReply({ files: [], components: [], content: formatError(error ?? 'Equip failed'), embeds: [] }); + await interaction.editReply({ + files: [], + components: [], + content: formatError(error ?? 'Equip failed'), + embeds: [] + }); return; } - await interaction.editReply({ files: [], components: [], content: message ?? 'Item equipped!', embeds: [] }); + await interaction.editReply({ + files: [], + components: [], + content: message ?? 'Item equipped!', + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ files: [], components: [], content: formatError(err.message, err.code), embeds: [] }); + await interaction.editReply({ + files: [], + components: [], + content: formatError(err.message, err.code), + embeds: [] + }); } } } diff --git a/src/components/buttons/ExploreAgainButton.ts b/src/components/buttons/ExploreAgainButton.ts index 453b244..40cfcde 100644 --- a/src/components/buttons/ExploreAgainButton.ts +++ b/src/components/buttons/ExploreAgainButton.ts @@ -1,17 +1,21 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import { type IStepJSON } from "../../interfaces/IStepJSON"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import { type IStepJSON } from '../../interfaces/IStepJSON'; export default class ExploreAgainButton extends Button { constructor() { - super({ customId: "explore_again", cooldown: 7, isAuthorOnly: true }); + super({ customId: 'explore_again', cooldown: 7, isAuthorOnly: true }); } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); try { @@ -20,22 +24,37 @@ export default class ExploreAgainButton extends Button { body: JSON.stringify({ discordId: interaction.user.id }) }); - const data = await res.json() as IStepJSON; + const data = (await res.json()) as IStepJSON; if (res.status === 429) { - await interaction.editReply({ content: formatCooldown('step', data.cooldownRemaining), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatCooldown('step', data.cooldownRemaining), + files: [], + components: [], + embeds: [] + }); return; } if (data.error) { - await interaction.editReply({ content: formatError(data.error), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(data.error), + files: [], + components: [], + embeds: [] + }); return; } const response = await buildCombatResponse(data); await interaction.editReply(response); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/ExploreButton.ts b/src/components/buttons/ExploreButton.ts index b53f089..020461f 100644 --- a/src/components/buttons/ExploreButton.ts +++ b/src/components/buttons/ExploreButton.ts @@ -1,15 +1,20 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { type IStepJSON } from "../../interfaces/IStepJSON"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; +import { type IStepJSON } from '../../interfaces/IStepJSON'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class ExploreButton extends Button { - constructor() { super({ customId: "explore", cooldown: 7, isAuthorOnly: false }); } + constructor() { + super({ customId: 'explore', cooldown: 7, isAuthorOnly: false }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const res = await apiFetch(Routes.explore(), { @@ -17,15 +22,19 @@ export default class ExploreButton extends Button { body: JSON.stringify({ discordId: interaction.user.id }) }); - const data = await res.json() as IStepJSON; + const data = (await res.json()) as IStepJSON; if (res.status === 429) { - await interaction.editReply({ content: formatCooldown('step', data.cooldownRemaining) }); + await interaction.editReply({ + content: formatCooldown('step', data.cooldownRemaining) + }); return; } if (res.status === 404) { - await interaction.editReply({ content: formatError('', 'PLAYER_NOT_FOUND') }); + await interaction.editReply({ + content: formatError('', 'PLAYER_NOT_FOUND') + }); return; } @@ -37,4 +46,4 @@ export default class ExploreButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/components/buttons/FleeButton.ts b/src/components/buttons/FleeButton.ts index 6326566..4d759aa 100644 --- a/src/components/buttons/FleeButton.ts +++ b/src/components/buttons/FleeButton.ts @@ -1,15 +1,20 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { type ICombatJSON } from "../../interfaces/ICombatJSON"; -import { apiFetch } from "../../utilities/ApiClient"; -import { buildCombatResponse } from "../../utilities/CombatResponseBuilder"; -import { formatError, formatCooldown } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; +import { type ICombatJSON } from '../../interfaces/ICombatJSON'; +import { apiFetch } from '../../utilities/ApiClient'; +import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; +import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class FleeButton extends Button { - constructor() { super({ customId: "flee", cooldown: 1.8, isAuthorOnly: false }); } + constructor() { + super({ customId: 'flee', cooldown: 1.8, isAuthorOnly: false }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const res = await apiFetch(Routes.combat(), { @@ -22,7 +27,7 @@ export default class FleeButton extends Button { return; } - const data = await res.json() as ICombatJSON; + const data = (await res.json()) as ICombatJSON; if (data.error) { await interaction.editReply({ content: formatError(data.error) }); @@ -32,4 +37,4 @@ export default class FleeButton extends Button { const response = await buildCombatResponse(data); await interaction.editReply(response); } -} \ No newline at end of file +} diff --git a/src/components/buttons/GuideNavButton.ts b/src/components/buttons/GuideNavButton.ts index a6e7129..b225dbd 100644 --- a/src/components/buttons/GuideNavButton.ts +++ b/src/components/buttons/GuideNavButton.ts @@ -1,8 +1,15 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; -const SECTIONS: Record = { - basics: { title: 'Getting Started', emoji: '📖', content: 'Use `/guide basics` to see full content.' }, +const SECTIONS: Record< + string, + { title: string; emoji: string; content: string } +> = { + basics: { + title: 'Getting Started', + emoji: '📖', + content: 'Use `/guide basics` to see full content.' + }, combat: { title: 'Combat & Enemies', emoji: '⚔️', content: '' }, workshop: { title: 'Workshop', emoji: '🔨', content: '' }, economy: { title: 'Economy & Gold Sinks', emoji: '🪙', content: '' }, @@ -14,15 +21,26 @@ const SECTIONS: Record - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const section = args?.[0] ?? 'basics'; // Re-trigger the guide command programmatically isn't possible with buttons, diff --git a/src/components/buttons/LockButton.ts b/src/components/buttons/LockButton.ts index 7b01bf3..c8c5322 100644 --- a/src/components/buttons/LockButton.ts +++ b/src/components/buttons/LockButton.ts @@ -1,23 +1,32 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class LockButton extends Button { constructor() { - super({ customId: "lock", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'lock', cooldown: 2, isAuthorOnly: true }); } // customId format: lock:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; const currentlyLocked = args?.[1] === '1'; if (!docId) { - await interaction.editReply({ content: 'Error parsing item data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing item data!', + files: [], + components: [], + embeds: [] + }); return; } @@ -34,16 +43,28 @@ export default class LockButton extends Button { const { success, isLocked, error } = await res.json(); if (!res.ok || !success) { - await interaction.editReply({ content: formatError(error ?? 'Lock failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(error ?? 'Lock failed'), + files: [], + components: [], + embeds: [] + }); return; } await interaction.editReply({ content: isLocked ? '🔒 Item locked!' : '🔓 Item unlocked!', - files: [], components: [], embeds: [] + files: [], + components: [], + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/MarketBuyButton.ts b/src/components/buttons/MarketBuyButton.ts index 53e7e57..822aec4 100644 --- a/src/components/buttons/MarketBuyButton.ts +++ b/src/components/buttons/MarketBuyButton.ts @@ -1,13 +1,19 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class MarketBuyButton extends Button { - constructor() { super({ customId: "mkt_buy", cooldown: 3, isAuthorOnly: false }); } + constructor() { + super({ customId: 'mkt_buy', cooldown: 3, isAuthorOnly: false }); + } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const listingId = args?.[0]; @@ -19,20 +25,30 @@ export default class MarketBuyButton extends Button { try { const res = await apiFetch(Routes.marketBuy(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, listingId, quantity: 1 }) + body: JSON.stringify({ + discordId: interaction.user.id, + listingId, + quantity: 1 + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Purchase failed.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Purchase failed.') + }); return; } const itemName = body.item?.name ?? 'Unknown Item'; - await interaction.editReply({ content: `🪙 **Purchase complete!** You bought **${itemName}** from the Global Market.` }); + await interaction.editReply({ + content: `🪙 **Purchase complete!** You bought **${itemName}** from the Global Market.` + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/components/buttons/MarketCancelButton.ts b/src/components/buttons/MarketCancelButton.ts index 0f096e5..d3d1dbc 100644 --- a/src/components/buttons/MarketCancelButton.ts +++ b/src/components/buttons/MarketCancelButton.ts @@ -1,13 +1,19 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class MarketCancelButton extends Button { - constructor() { super({ customId: "mkt_cancel", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'mkt_cancel', cooldown: 3, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const listingId = args?.[0]; @@ -25,13 +31,20 @@ export default class MarketCancelButton extends Button { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to cancel listing.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to cancel listing.') + }); return; } - await interaction.editReply({ content: '✅ **Listing cancelled.** Your items have been returned to your inventory.' }); + await interaction.editReply({ + content: + '✅ **Listing cancelled.** Your items have been returned to your inventory.' + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/components/buttons/MarketNextButton.ts b/src/components/buttons/MarketNextButton.ts index 78d731f..dab0e45 100644 --- a/src/components/buttons/MarketNextButton.ts +++ b/src/components/buttons/MarketNextButton.ts @@ -1,12 +1,18 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { handleMarketPage } from "./MarketPrevButton"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { handleMarketPage } from './MarketPrevButton'; export default class MarketNextButton extends Button { - constructor() { super({ customId: "mkt_next", cooldown: 2, isAuthorOnly: true }); } + constructor() { + super({ customId: 'mkt_next', cooldown: 2, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); await handleMarketPage(interaction, args, 1); } -} \ No newline at end of file +} diff --git a/src/components/buttons/MarketPrevButton.ts b/src/components/buttons/MarketPrevButton.ts index 93414ff..0a79f65 100644 --- a/src/components/buttons/MarketPrevButton.ts +++ b/src/components/buttons/MarketPrevButton.ts @@ -1,14 +1,32 @@ -import { AttachmentBuilder, type ButtonInteraction, type Client, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import MarketImageBuilder, { type MarketListing, type MarketPageConfig } from "../../utilities/MarketImageBuilder"; +import { + AttachmentBuilder, + type ButtonInteraction, + type Client, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + MessageFlags +} from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import MarketImageBuilder, { + type MarketListing, + type MarketPageConfig +} from '../../utilities/MarketImageBuilder'; export default class MarketPrevButton extends Button { - constructor() { super({ customId: "mkt_prev", cooldown: 2, isAuthorOnly: true }); } + constructor() { + super({ customId: 'mkt_prev', cooldown: 2, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); await handleMarketPage(interaction, args, -1); } @@ -18,7 +36,11 @@ export default class MarketPrevButton extends Button { * Shared pagination handler for prev/next. * CustomId format: mkt_prev:currentPage:search|rarity|type|sort:mode */ -export async function handleMarketPage(interaction: ButtonInteraction, args: string[] | null | undefined, direction: number): Promise { +export async function handleMarketPage( + interaction: ButtonInteraction, + args: string[] | null | undefined, + direction: number +): Promise { if (!args || args.length < 3) return; const currentPage = parseInt(args[0], 10); @@ -47,16 +69,27 @@ export async function handleMarketPage(interaction: ButtonInteraction, args: str const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load market') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load market') + }); return; } const listings: MarketListing[] = body.data; - const config: MarketPageConfig = { page: body.pagination.page, totalPages: body.pagination.totalPages, totalItems: body.pagination.totalItems, mode }; + const config: MarketPageConfig = { + page: body.pagination.page, + totalPages: body.pagination.totalPages, + totalItems: body.pagination.totalItems, + mode + }; const imageBuffer = await MarketImageBuilder.build(listings, config); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'market.png' }); - const embed = new EmbedBuilder().setColor(mode === 'my_listings' ? 0x3b82f6 : 0x10b981).setImage('attachment://market.png'); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'market.png' + }); + const embed = new EmbedBuilder() + .setColor(mode === 'my_listings' ? 0x3b82f6 : 0x10b981) + .setImage('attachment://market.png'); // Rebuild action + pagination buttons const rows: ActionRowBuilder[] = []; @@ -64,13 +97,23 @@ export async function handleMarketPage(interaction: ButtonInteraction, args: str if (listings.length > 0) { const isBrowse = mode === 'browse'; // Up to 2 rows of 4 action buttons - for (let rowStart = 0; rowStart < listings.length && rows.length < 2; rowStart += 4) { + for ( + let rowStart = 0; + rowStart < listings.length && rows.length < 2; + rowStart += 4 + ) { const row = new ActionRowBuilder(); - for (let j = rowStart; j < Math.min(rowStart + 4, listings.length); j++) { + for ( + let j = rowStart; + j < Math.min(rowStart + 4, listings.length); + j++ + ) { const listing = listings[j]; row.addComponents( new ButtonBuilder() - .setCustomId(`${isBrowse ? 'mkt_buy' : 'mkt_cancel'}:${listing.listingId}`) + .setCustomId( + `${isBrowse ? 'mkt_buy' : 'mkt_cancel'}:${listing.listingId}` + ) .setLabel(`${isBrowse ? '🛒' : '❌'} ${j + 1}`) .setStyle(isBrowse ? ButtonStyle.Success : ButtonStyle.Danger) ); @@ -80,14 +123,30 @@ export async function handleMarketPage(interaction: ButtonInteraction, args: str } if (config.totalPages > 1) { - rows.push(new ActionRowBuilder().setComponents( - new ButtonBuilder().setCustomId(`mkt_prev:${config.page}:${filterKey}:${mode}`).setLabel('◀ Prev').setStyle(ButtonStyle.Secondary).setDisabled(config.page <= 1), - new ButtonBuilder().setCustomId(`mkt_next:${config.page}:${filterKey}:${mode}`).setLabel('Next ▶').setStyle(ButtonStyle.Secondary).setDisabled(config.page >= config.totalPages) - )); + rows.push( + new ActionRowBuilder().setComponents( + new ButtonBuilder() + .setCustomId(`mkt_prev:${config.page}:${filterKey}:${mode}`) + .setLabel('◀ Prev') + .setStyle(ButtonStyle.Secondary) + .setDisabled(config.page <= 1), + new ButtonBuilder() + .setCustomId(`mkt_next:${config.page}:${filterKey}:${mode}`) + .setLabel('Next ▶') + .setStyle(ButtonStyle.Secondary) + .setDisabled(config.page >= config.totalPages) + ) + ); } - await interaction.editReply({ embeds: [embed], files: [attachment], components: rows }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components: rows + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } -} \ No newline at end of file +} diff --git a/src/components/buttons/MarketRedirectButton.ts b/src/components/buttons/MarketRedirectButton.ts index 303debe..1ea64be 100644 --- a/src/components/buttons/MarketRedirectButton.ts +++ b/src/components/buttons/MarketRedirectButton.ts @@ -1,13 +1,17 @@ -import { type ButtonInteraction, type Client, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; +import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; +import Button from '../../structures/Button'; export default class MarketRedirectButton extends Button { constructor() { - super({ customId: "market_redirect", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'market_redirect', cooldown: 2, isAuthorOnly: true }); } // customId format: market_redirect: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const itemId = args?.[0] ?? 'unknown'; await interaction.reply({ diff --git a/src/components/buttons/MarketSellPageButton.ts b/src/components/buttons/MarketSellPageButton.ts index df2e64b..d91952e 100644 --- a/src/components/buttons/MarketSellPageButton.ts +++ b/src/components/buttons/MarketSellPageButton.ts @@ -1,15 +1,19 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { formatError } from "../../utilities/ErrorMessages"; -import { buildSellPage } from "../../commands/MarketCommand"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { formatError } from '../../utilities/ErrorMessages'; +import { buildSellPage } from '../../commands/MarketCommand'; export default class MarketSellPageButton extends Button { constructor() { - super({ customId: "mkt_sell_page", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'mkt_sell_page', cooldown: 2, isAuthorOnly: true }); } // customId format: mkt_sell_page: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const page = parseInt(args?.[0] ?? '0', 10); @@ -18,7 +22,10 @@ export default class MarketSellPageButton extends Button { const result = await buildSellPage(interaction.user.id, page); await interaction.editReply(result); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), components: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + components: [] + }); } } } diff --git a/src/components/buttons/ReforgeButton.ts b/src/components/buttons/ReforgeButton.ts index 100101e..243d8ee 100644 --- a/src/components/buttons/ReforgeButton.ts +++ b/src/components/buttons/ReforgeButton.ts @@ -1,22 +1,32 @@ import { - type ButtonInteraction, type Client, ActionRowBuilder, - StringSelectMenuBuilder, StringSelectMenuOptionBuilder, + type ButtonInteraction, + type Client, + ActionRowBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, MessageFlags -} from "discord.js"; -import Button from "../../structures/Button"; +} from 'discord.js'; +import Button from '../../structures/Button'; export default class ReforgeButton extends Button { constructor() { - super({ customId: "reforge", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'reforge', cooldown: 3, isAuthorOnly: true }); } // customId format: reforge:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const docId = args?.[0]; const itemId = args?.[1]; if (!docId || !itemId) { - await interaction.reply({ content: 'Error parsing item data!', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'Error parsing item data!', + flags: MessageFlags.Ephemeral + }); return; } @@ -41,10 +51,13 @@ export default class ReforgeButton extends Button { .setValue('full') ); - const row = new ActionRowBuilder().setComponents(selectMenu); + const row = new ActionRowBuilder().setComponents( + selectMenu + ); await interaction.reply({ - content: '🔄 **Select Reforge Type**\nChoose what to reroll on this item:', + content: + '🔄 **Select Reforge Type**\nChoose what to reroll on this item:', components: [row], ephemeral: true }); diff --git a/src/components/buttons/RegisterAcceptButton.ts b/src/components/buttons/RegisterAcceptButton.ts index f608fed..773ab28 100644 --- a/src/components/buttons/RegisterAcceptButton.ts +++ b/src/components/buttons/RegisterAcceptButton.ts @@ -1,13 +1,24 @@ -import { type ButtonInteraction, type Client, Colors, ContainerBuilder, MessageFlags } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { + type ButtonInteraction, + type Client, + Colors, + ContainerBuilder, + MessageFlags +} from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class RegisterAcceptButton extends Button { - constructor() { super({ customId: "register_accept", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ customId: 'register_accept', cooldown: 5, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferUpdate(); const discordId = interaction.user.id; @@ -22,7 +33,8 @@ export default class RegisterAcceptButton extends Button { if (res.status === 409) { await interaction.editReply({ - content: '✅ **You\'re already registered!** Use `/profile` to see your character.', + content: + "✅ **You're already registered!** Use `/profile` to see your character.", embeds: [], components: [] }); @@ -44,19 +56,25 @@ export default class RegisterAcceptButton extends Button { container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent('## ⚔️ Character Created!'), - (textDisplay) => textDisplay.setContent(`Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.`), - (textDisplay) => textDisplay.setContent( - '**Get started:**\n' + - '> `/explore` — Venture into the world\n' + - '> `/profile` — View your character\n' + - '> `/help` — See all commands' - ) + (textDisplay) => + textDisplay.setContent( + `Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.` + ), + (textDisplay) => + textDisplay.setContent( + '**Get started:**\n' + + '> `/explore` — Venture into the world\n' + + '> `/profile` — View your character\n' + + '> `/help` — See all commands' + ) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent( + '-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer' + ) ); await interaction.editReply({ @@ -72,4 +90,4 @@ export default class RegisterAcceptButton extends Button { }); } } -} \ No newline at end of file +} diff --git a/src/components/buttons/RegisterDeclineButton.ts b/src/components/buttons/RegisterDeclineButton.ts index 14ac9f9..db46546 100644 --- a/src/components/buttons/RegisterDeclineButton.ts +++ b/src/components/buttons/RegisterDeclineButton.ts @@ -1,16 +1,22 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; export default class RegisterDeclineButton extends Button { - constructor() { super({ customId: "register_decline", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'register_decline', cooldown: 3, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client + ): Promise { await interaction.deferUpdate(); await interaction.editReply({ - content: '👋 **No problem!** No data has been stored. You can run `/register` again anytime if you change your mind.', + content: + '👋 **No problem!** No data has been stored. You can run `/register` again anytime if you change your mind.', embeds: [], components: [] }); } -} \ No newline at end of file +} diff --git a/src/components/buttons/RestButton.ts b/src/components/buttons/RestButton.ts index 8a912c0..86d42ef 100644 --- a/src/components/buttons/RestButton.ts +++ b/src/components/buttons/RestButton.ts @@ -1,15 +1,19 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class RestButton extends Button { constructor() { - super({ customId: "rest", cooldown: 5, isAuthorOnly: true }); + super({ customId: 'rest', cooldown: 5, isAuthorOnly: true }); } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); try { @@ -21,7 +25,12 @@ export default class RestButton extends Button { const result = await res.json(); if (!res.ok || !result.success) { - await interaction.editReply({ content: formatError(result.error ?? 'Rest failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(result.error ?? 'Rest failed'), + files: [], + components: [], + embeds: [] + }); return; } @@ -31,10 +40,17 @@ export default class RestButton extends Button { `❤️ Restored **${result.healedAmount.toLocaleString()} HP** → ${result.newHp.toLocaleString()} / ${result.maxHp.toLocaleString()}`, `🪙 Cost: **${result.goldSpent.toLocaleString()}** Gold • 💰 Balance: **${result.newBalance.toLocaleString()}** Gold` ].join('\n'), - files: [], components: [], embeds: [] + files: [], + components: [], + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/SellButton.ts b/src/components/buttons/SellButton.ts index b74ab98..a9f37cf 100644 --- a/src/components/buttons/SellButton.ts +++ b/src/components/buttons/SellButton.ts @@ -1,22 +1,38 @@ -import { type ButtonInteraction, type Client, ModalBuilder, TextInputStyle } from "discord.js"; -import Button from "../../structures/Button"; +import { + type ButtonInteraction, + type Client, + ModalBuilder, + TextInputStyle +} from 'discord.js'; +import Button from '../../structures/Button'; export default class SellButton extends Button { constructor() { - super({ customId: "sell", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'sell', cooldown: 2, isAuthorOnly: true }); } // customId format: sell:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const docId = args?.[0]; const maxQty = args?.[1] ?? '1'; const modal = new ModalBuilder() .setCustomId(`sell:${docId}`) .setTitle('Sell Item') - .addLabelComponents( - (label) => label.setLabel('Amount').setDescription(`Enter amount to sell. (Max: ${maxQty})`) - .setTextInputComponent((ti) => ti.setCustomId('ti1').setRequired(true).setStyle(TextInputStyle.Short)) + .addLabelComponents((label) => + label + .setLabel('Amount') + .setDescription(`Enter amount to sell. (Max: ${maxQty})`) + .setTextInputComponent((ti) => + ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/SkillPointsButton.ts b/src/components/buttons/SkillPointsButton.ts index f486b2a..d5ca154 100644 --- a/src/components/buttons/SkillPointsButton.ts +++ b/src/components/buttons/SkillPointsButton.ts @@ -1,10 +1,24 @@ -import { type ButtonInteraction, type Client, LabelBuilder, ModalBuilder, TextDisplayBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; -import Button from "../../structures/Button"; +import { + type ButtonInteraction, + type Client, + LabelBuilder, + ModalBuilder, + TextDisplayBuilder, + TextInputBuilder, + TextInputStyle +} from 'discord.js'; +import Button from '../../structures/Button'; export default class SkillPointsButton extends Button { - constructor() { super({ customId: "skillpoints", cooldown: 3, isAuthorOnly: true }); } + constructor() { + super({ customId: 'skillpoints', cooldown: 3, isAuthorOnly: true }); + } - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { const availablePoints = parseInt(args?.[0] ?? '0', 10); const atkInput = new TextInputBuilder() @@ -31,8 +45,9 @@ export default class SkillPointsButton extends Button { .setDescription('Increases your damage reduction per point') .setTextInputComponent(defInput); - const infoText = new TextDisplayBuilder() - .setContent(`-# You have **${availablePoints}** skill points available. This action is permanent.`); + const infoText = new TextDisplayBuilder().setContent( + `-# You have **${availablePoints}** skill points available. This action is permanent.` + ); const modal = new ModalBuilder() .setCustomId('skillpoints_modal') @@ -42,4 +57,4 @@ export default class SkillPointsButton extends Button { await interaction.showModal(modal); } -} \ No newline at end of file +} diff --git a/src/components/buttons/TaskClaimButton.ts b/src/components/buttons/TaskClaimButton.ts index 83ee88a..f9a9c8a 100644 --- a/src/components/buttons/TaskClaimButton.ts +++ b/src/components/buttons/TaskClaimButton.ts @@ -1,60 +1,92 @@ -import { type ButtonInteraction, type Client } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ButtonInteraction, type Client } from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class TaskClaimButton extends Button { constructor() { - super({ customId: "task_claim", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'task_claim', cooldown: 3, isAuthorOnly: true }); } // customId format: task_claim:: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const taskId = args?.[0]; const period = args?.[1] ?? 'daily'; if (!taskId) { - await interaction.editReply({ content: 'Error parsing task data!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing task data!', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.tasks(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, action: 'claim', taskId, period }) + body: JSON.stringify({ + discordId: interaction.user.id, + action: 'claim', + taskId, + period + }) }); const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to claim task'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to claim task'), + files: [], + components: [], + embeds: [] + }); return; } const reward = body.reward; - const lines = [ - `✅ **Task Claimed!**`, - `` - ]; + const lines = [`✅ **Task Claimed!**`, ``]; if (reward) { - if (reward.gold > 0) lines.push(`🪙 **+${reward.gold.toLocaleString()}** Gold`); - if (reward.xp > 0) lines.push(`✨ **+${reward.xp.toLocaleString()}** XP`); - if (reward.embers > 0) lines.push(`🔥 **+${reward.embers.toLocaleString()}** Embers`); + if (reward.gold > 0) + lines.push(`🪙 **+${reward.gold.toLocaleString()}** Gold`); + if (reward.xp > 0) + lines.push(`✨ **+${reward.xp.toLocaleString()}** XP`); + if (reward.embers > 0) + lines.push(`🔥 **+${reward.embers.toLocaleString()}** Embers`); } if (body.levelsGained > 0) { - lines.push(``, `🆙 **Gained ${body.levelsGained} Level${body.levelsGained > 1 ? 's' : ''}!**`); + lines.push( + ``, + `🆙 **Gained ${body.levelsGained} Level${body.levelsGained > 1 ? 's' : ''}!**` + ); } lines.push(``, `Run \`/tasks ${period}\` to see remaining tasks.`); - await interaction.editReply({ content: lines.join('\n'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: lines.join('\n'), + files: [], + components: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/buttons/TasksTabButton.ts b/src/components/buttons/TasksTabButton.ts index 57154ff..42892e1 100644 --- a/src/components/buttons/TasksTabButton.ts +++ b/src/components/buttons/TasksTabButton.ts @@ -1,18 +1,30 @@ -import { type ButtonInteraction, type Client, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, AttachmentBuilder } from "discord.js"; -import Button from "../../structures/Button"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import ImageService from "../../utilities/ImageService"; -import type { ITaskJSON } from "../../interfaces/IGameJSON"; +import { + type ButtonInteraction, + type Client, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + AttachmentBuilder +} from 'discord.js'; +import Button from '../../structures/Button'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import ImageService from '../../utilities/ImageService'; +import type { ITaskJSON } from '../../interfaces/IGameJSON'; export default class TasksTabButton extends Button { constructor() { - super({ customId: "tasks_tab", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'tasks_tab', cooldown: 3, isAuthorOnly: true }); } // customId format: tasks_tab: - public async execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const period = args?.[0] ?? 'daily'; @@ -23,7 +35,9 @@ export default class TasksTabButton extends Button { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load tasks') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load tasks') + }); return; } @@ -34,7 +48,9 @@ export default class TasksTabButton extends Button { // Convert ISO reset string to ms remaining const resetIso = resets[period]; - const resetIn = resetIso ? Math.max(0, new Date(resetIso).getTime() - Date.now()) : 0; + const resetIn = resetIso + ? Math.max(0, new Date(resetIso).getTime() - Date.now()) + : 0; const imageBuffer = await ImageService.tasks(tasks, { period, @@ -42,15 +58,25 @@ export default class TasksTabButton extends Button { playerEmbers }); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'tasks.png' }); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'tasks.png' + }); const embed = new EmbedBuilder() - .setColor(period === 'daily' ? 0x10b981 : period === 'weekly' ? 0x6366f1 : 0xc026d3) + .setColor( + period === 'daily' + ? 0x10b981 + : period === 'weekly' + ? 0x6366f1 + : 0xc026d3 + ) .setImage('attachment://tasks.png'); const components: ActionRowBuilder[] = []; // Claim buttons — use correct field names (id, label, claimed) - const claimable = tasks.filter((t: ITaskJSON) => t.progress >= t.target && !t.claimed); + const claimable = tasks.filter( + (t: ITaskJSON) => t.progress >= t.target && !t.claimed + ); if (claimable.length > 0) { const claimRow = new ActionRowBuilder(); for (const task of claimable.slice(0, 5)) { @@ -67,15 +93,39 @@ export default class TasksTabButton extends Button { // Period switcher components.push( new ActionRowBuilder().setComponents( - new ButtonBuilder().setCustomId('tasks_tab:daily').setLabel('Daily').setStyle(period === 'daily' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'daily'), - new ButtonBuilder().setCustomId('tasks_tab:weekly').setLabel('Weekly').setStyle(period === 'weekly' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'weekly'), - new ButtonBuilder().setCustomId('tasks_tab:monthly').setLabel('Monthly').setStyle(period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(period === 'monthly') + new ButtonBuilder() + .setCustomId('tasks_tab:daily') + .setLabel('Daily') + .setStyle( + period === 'daily' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) + .setDisabled(period === 'daily'), + new ButtonBuilder() + .setCustomId('tasks_tab:weekly') + .setLabel('Weekly') + .setStyle( + period === 'weekly' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) + .setDisabled(period === 'weekly'), + new ButtonBuilder() + .setCustomId('tasks_tab:monthly') + .setLabel('Monthly') + .setStyle( + period === 'monthly' ? ButtonStyle.Primary : ButtonStyle.Secondary + ) + .setDisabled(period === 'monthly') ) ); - await interaction.editReply({ embeds: [embed], files: [attachment], components }); + await interaction.editReply({ + embeds: [embed], + files: [attachment], + components + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/components/menus/InvSelectMenu.ts b/src/components/menus/InvSelectMenu.ts index 8dcb102..a356b6b 100644 --- a/src/components/menus/InvSelectMenu.ts +++ b/src/components/menus/InvSelectMenu.ts @@ -1,26 +1,35 @@ -import { type AnySelectMenuInteraction, type Client } from "discord.js"; -import SelectMenu from "../../structures/SelectMenu"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import { buildItemView } from "../../utilities/ItemViewBuilder"; -import type { IInventoryItem } from "../../interfaces/IInventoryJSON"; -import type { IPlayerJSON } from "../../interfaces/IPlayerJSON"; +import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import SelectMenu from '../../structures/SelectMenu'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import { buildItemView } from '../../utilities/ItemViewBuilder'; +import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; +import type { IPlayerJSON } from '../../interfaces/IPlayerJSON'; export default class InvSelectMenu extends SelectMenu { constructor() { - super({ customId: "inv_select", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'inv_select', cooldown: 3, isAuthorOnly: true }); } // customId format: inv_select: — value is the _id of the selected item - public async execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: AnySelectMenuInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = interaction.values[0]; // The MongoDB _id const discordId = interaction.user.id; if (!docId) { - await interaction.editReply({ content: 'No item selected!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'No item selected!', + files: [], + components: [], + embeds: [] + }); return; } @@ -30,7 +39,12 @@ export default class InvSelectMenu extends SelectMenu { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to load inventory'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to load inventory'), + files: [], + components: [], + embeds: [] + }); return; } @@ -41,7 +55,12 @@ export default class InvSelectMenu extends SelectMenu { const item = inventory.find((inv: IInventoryItem) => inv._id === docId); if (!item) { - await interaction.editReply({ content: 'Item not found in your inventory!', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Item not found in your inventory!', + files: [], + components: [], + embeds: [] + }); return; } @@ -49,7 +68,12 @@ export default class InvSelectMenu extends SelectMenu { const viewer = await buildItemView(player, item); await interaction.editReply({ ...viewer, embeds: viewer.embeds }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/menus/MarketSellMenu.ts b/src/components/menus/MarketSellMenu.ts index c1c92d1..bf24286 100644 --- a/src/components/menus/MarketSellMenu.ts +++ b/src/components/menus/MarketSellMenu.ts @@ -1,14 +1,24 @@ -import { type AnySelectMenuInteraction, type Client, MessageFlags, ModalBuilder, TextInputStyle } from "discord.js"; -import SelectMenu from "../../structures/SelectMenu"; -import ItemManager from "../../managers/ItemManager"; +import { + type AnySelectMenuInteraction, + type Client, + MessageFlags, + ModalBuilder, + TextInputStyle +} from 'discord.js'; +import SelectMenu from '../../structures/SelectMenu'; +import ItemManager from '../../managers/ItemManager'; export default class MarketSellMenu extends SelectMenu { constructor() { - super({ customId: "mkt_sell_select", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'mkt_sell_select', cooldown: 3, isAuthorOnly: true }); } // select value format: docId:itemId:maxQuantity - public async execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: AnySelectMenuInteraction, + client: Client, + args?: string[] | null + ): Promise { const selected = interaction.values[0]; if (!selected) return; @@ -17,7 +27,10 @@ export default class MarketSellMenu extends SelectMenu { const maxQty = parseInt(maxQtyStr, 10); if (!docId || isNaN(itemId)) { - await interaction.reply({ content: 'Error parsing item data!', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'Error parsing item data!', + flags: MessageFlags.Ephemeral + }); return; } @@ -29,20 +42,30 @@ export default class MarketSellMenu extends SelectMenu { const modal = new ModalBuilder() .setCustomId(`mkt_sell_modal:${docId}:${itemId}`) .setTitle(`🏪 List: ${itemName.slice(0, 30)}`) - .addLabelComponents( - (label) => label.setLabel('Quantity').setDescription(`How many to list (Max: ${maxQty})`) - .setTextInputComponent((ti) => ti.setCustomId('quantity') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`1 - ${maxQty}`) + .addLabelComponents((label) => + label + .setLabel('Quantity') + .setDescription(`How many to list (Max: ${maxQty})`) + .setTextInputComponent((ti) => + ti + .setCustomId('quantity') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`1 - ${maxQty}`) ) ) - .addLabelComponents( - (label) => label.setLabel('Price per unit (gold)').setDescription(`Suggested: ${baseValue.toLocaleString()}g (base value)`) - .setTextInputComponent((ti) => ti.setCustomId('price') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`e.g. ${baseValue || 100}`) + .addLabelComponents((label) => + label + .setLabel('Price per unit (gold)') + .setDescription( + `Suggested: ${baseValue.toLocaleString()}g (base value)` + ) + .setTextInputComponent((ti) => + ti + .setCustomId('price') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`e.g. ${baseValue || 100}`) ) ); diff --git a/src/components/menus/ReforgeSelectMenu.ts b/src/components/menus/ReforgeSelectMenu.ts index cf4b90a..841527d 100644 --- a/src/components/menus/ReforgeSelectMenu.ts +++ b/src/components/menus/ReforgeSelectMenu.ts @@ -1,16 +1,20 @@ -import { type AnySelectMenuInteraction, type Client } from "discord.js"; -import SelectMenu from "../../structures/SelectMenu"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import SelectMenu from '../../structures/SelectMenu'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class ReforgeSelectMenu extends SelectMenu { constructor() { - super({ customId: "reforge_select", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'reforge_select', cooldown: 3, isAuthorOnly: true }); } // customId format: reforge_select:: - public async execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: AnySelectMenuInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; @@ -18,7 +22,11 @@ export default class ReforgeSelectMenu extends SelectMenu { const reforgeType = interaction.values[0]; // 'stats' | 'affixes' | 'full' if (!docId || isNaN(itemId) || !reforgeType) { - await interaction.editReply({ content: 'Error parsing reforge data!', components: [], embeds: [] }); + await interaction.editReply({ + content: 'Error parsing reforge data!', + components: [], + embeds: [] + }); return; } @@ -36,7 +44,11 @@ export default class ReforgeSelectMenu extends SelectMenu { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Reforge failed'), components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(body.error ?? 'Reforge failed'), + components: [], + embeds: [] + }); return; } @@ -49,7 +61,11 @@ export default class ReforgeSelectMenu extends SelectMenu { ]; // Show stat comparison if stats were reforged - if (body.oldStats && body.newStats && (reforgeType === 'stats' || reforgeType === 'full')) { + if ( + body.oldStats && + body.newStats && + (reforgeType === 'stats' || reforgeType === 'full') + ) { const fmtStat = (label: string, old: number, now: number) => { const diff = now - old; const arrow = diff > 0 ? '🟢' : diff < 0 ? '🔴' : '⚪'; @@ -62,20 +78,33 @@ export default class ReforgeSelectMenu extends SelectMenu { } // Show affix comparison if affixes were reforged - if (body.newAffixes && (reforgeType === 'affixes' || reforgeType === 'full')) { + if ( + body.newAffixes && + (reforgeType === 'affixes' || reforgeType === 'full') + ) { lines.push(``, `**New Affixes:**`); if (body.newAffixes.length === 0) { lines.push('None'); } else { for (const affix of body.newAffixes) { - lines.push(`• ${affix.type.replace(/_/g, ' ')} +${affix.value}${affix.type === 'THORNS' ? '' : '%'}`); + lines.push( + `• ${affix.type.replace(/_/g, ' ')} +${affix.value}${affix.type === 'THORNS' ? '' : '%'}` + ); } } } - await interaction.editReply({ content: lines.join('\n'), components: [], embeds: [] }); + await interaction.editReply({ + content: lines.join('\n'), + components: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + components: [], + embeds: [] + }); } } } diff --git a/src/components/menus/TravelSelectMenu.ts b/src/components/menus/TravelSelectMenu.ts index 9e234c4..e092f84 100644 --- a/src/components/menus/TravelSelectMenu.ts +++ b/src/components/menus/TravelSelectMenu.ts @@ -1,14 +1,23 @@ -import { type AnySelectMenuInteraction, type Client, MessageFlags } from "discord.js"; -import SelectMenu from "../../structures/SelectMenu"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import { getZone } from "../../utilities/ZoneData"; +import { + type AnySelectMenuInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import SelectMenu from '../../structures/SelectMenu'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import { getZone } from '../../utilities/ZoneData'; export default class TravelSelectMenu extends SelectMenu { - constructor() { super({ customId: "travel_select", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ customId: 'travel_select', cooldown: 5, isAuthorOnly: true }); + } - public async execute(interaction: AnySelectMenuInteraction, client: Client): Promise { + public async execute( + interaction: AnySelectMenuInteraction, + client: Client + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const zoneId = parseInt(interaction.values[0], 10); @@ -29,7 +38,9 @@ export default class TravelSelectMenu extends SelectMenu { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Travel failed.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Travel failed.') + }); return; } @@ -37,7 +48,9 @@ export default class TravelSelectMenu extends SelectMenu { content: `🗺️ **Traveled to ${zone?.name ?? body.zoneName}!**\n\n> *${zone?.description ?? 'A new zone awaits.'}*\n\nUse \`/explore\` to begin adventuring here.` }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/components/menus/UnequipMenu.ts b/src/components/menus/UnequipMenu.ts index a92bfb2..05d2c8e 100644 --- a/src/components/menus/UnequipMenu.ts +++ b/src/components/menus/UnequipMenu.ts @@ -1,18 +1,29 @@ -import { ActionRowBuilder, type AnySelectMenuInteraction, AttachmentBuilder, type Client, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; -import SelectMenu from "../../structures/SelectMenu"; -import Routes from "../../utilities/Routes"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import { type IPlayerJSON } from "../../interfaces/IPlayerJSON"; -import ImageService from "../../utilities/ImageService"; -import { type EquipmentSlot } from "../../interfaces/IItemJSON"; +import { + ActionRowBuilder, + type AnySelectMenuInteraction, + AttachmentBuilder, + type Client, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder +} from 'discord.js'; +import SelectMenu from '../../structures/SelectMenu'; +import Routes from '../../utilities/Routes'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import { type IPlayerJSON } from '../../interfaces/IPlayerJSON'; +import ImageService from '../../utilities/ImageService'; +import { type EquipmentSlot } from '../../interfaces/IItemJSON'; export default class UnequipMenu extends SelectMenu { constructor() { - super({ customId: "unequip", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'unequip', cooldown: 2, isAuthorOnly: true }); } - public async execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: AnySelectMenuInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const slot = interaction.values[0]; @@ -24,37 +35,67 @@ export default class UnequipMenu extends SelectMenu { body: JSON.stringify({ discordId, slot }) }); - const { success, error, player }: { success?: boolean; error?: string; player?: IPlayerJSON } = await res.json(); + const { + success, + error, + player + }: { success?: boolean; error?: string; player?: IPlayerJSON } = + await res.json(); - if (res.status === 400 || res.status === 401 || res.status === 404 || res.status === 500) { - await interaction.editReply({ content: formatError(error ?? `Unequip failed (Code: ${res.status})`), files: [], components: [], embeds: [] }); + if ( + res.status === 400 || + res.status === 401 || + res.status === 404 || + res.status === 500 + ) { + await interaction.editReply({ + content: formatError(error ?? `Unequip failed (Code: ${res.status})`), + files: [], + components: [], + embeds: [] + }); return; } if (success) { const profile = await ImageService.profile(player!, interaction.user); - const profileAttachment = new AttachmentBuilder(profile, { name: 'profile.png' }); + const profileAttachment = new AttachmentBuilder(profile, { + name: 'profile.png' + }); if (player!.id === interaction.user.id) { const options: StringSelectMenuOptionBuilder[] = []; const equipment = player!.equipment; - Object.entries(equipment).forEach(entry => { + Object.entries(equipment).forEach((entry) => { const slot = entry[0] as EquipmentSlot; const itemId = entry[1]; if (itemId) { - options.push(new StringSelectMenuOptionBuilder().setLabel(slot).setValue(slot)); + options.push( + new StringSelectMenuOptionBuilder().setLabel(slot).setValue(slot) + ); } }); - const menu = new StringSelectMenuBuilder().setCustomId('unequip') - .setOptions(options.length >= 1 ? options : [new StringSelectMenuOptionBuilder().setLabel('None').setValue('None')]) + const menu = new StringSelectMenuBuilder() + .setCustomId('unequip') + .setOptions( + options.length >= 1 + ? options + : [ + new StringSelectMenuOptionBuilder() + .setLabel('None') + .setValue('None') + ] + ) .setMaxValues(1) .setPlaceholder('Unequip Slot'); - extraMenu.push(new ActionRowBuilder().setComponents(menu)); + extraMenu.push( + new ActionRowBuilder().setComponents(menu) + ); } await interaction.editReply({ @@ -62,8 +103,12 @@ export default class UnequipMenu extends SelectMenu { components: extraMenu }); } else { - await interaction.editReply({ content: 'Unknown error!', components: [], embeds: [], files: [] }); - + await interaction.editReply({ + content: 'Unknown error!', + components: [], + embeds: [], + files: [] + }); } } } diff --git a/src/components/modals/BulkCollectModal.ts b/src/components/modals/BulkCollectModal.ts index 75d6c01..7e84e2c 100644 --- a/src/components/modals/BulkCollectModal.ts +++ b/src/components/modals/BulkCollectModal.ts @@ -1,32 +1,59 @@ -import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { + type ModalSubmitInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class BulkCollectModal extends ModalSubmit { - constructor() { super({ customId: "bulk_collect_modal", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ customId: 'bulk_collect_modal', cooldown: 5, isAuthorOnly: true }); + } - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const selectedValues = interaction.fields.getStringSelectValues('bulk_collect_select'); + const selectedValues = interaction.fields.getStringSelectValues( + 'bulk_collect_select' + ); if (!selectedValues || selectedValues.length === 0) { - await interaction.editReply({ content: '❌ No items were selected.', embeds: [] }); + await interaction.editReply({ + content: '❌ No items were selected.', + embeds: [] + }); return; } - const items = selectedValues.map(val => { - const parts = val.split('-'); - if (parts.length >= 3) { - return { inventoryId: parts[0], itemId: parseInt(parts[1], 10), amount: parseInt(parts[2], 10) }; - } - return { itemId: parseInt(parts[0], 10), amount: parseInt(parts[1], 10) }; - }).filter(i => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); + const items = selectedValues + .map((val) => { + const parts = val.split('-'); + if (parts.length >= 3) { + return { + inventoryId: parts[0], + itemId: parseInt(parts[1], 10), + amount: parseInt(parts[2], 10) + }; + } + return { + itemId: parseInt(parts[0], 10), + amount: parseInt(parts[1], 10) + }; + }) + .filter((i) => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); if (items.length === 0) { - await interaction.editReply({ content: '❌ Could not parse selected items.', embeds: [] }); + await interaction.editReply({ + content: '❌ Could not parse selected items.', + embeds: [] + }); return; } @@ -39,15 +66,21 @@ export default class BulkCollectModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Bulk collect failed.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Bulk collect failed.') + }); return; } await interaction.editReply({ - content: `📖 **Bulk Collect Complete!** ${body.message}`, embeds: [] + content: `📖 **Bulk Collect Complete!** ${body.message}`, + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + embeds: [] + }); } } } diff --git a/src/components/modals/BulkDismantleModal.ts b/src/components/modals/BulkDismantleModal.ts index c2db119..4f9348c 100644 --- a/src/components/modals/BulkDismantleModal.ts +++ b/src/components/modals/BulkDismantleModal.ts @@ -1,32 +1,59 @@ -import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { + type ModalSubmitInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class BulkDismantleModal extends ModalSubmit { - constructor() { super({ customId: "bulk_dismantle_modal", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ + customId: 'bulk_dismantle_modal', + cooldown: 5, + isAuthorOnly: true + }); + } - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const selectedValues = interaction.fields.getStringSelectValues('bulk_dismantle_select'); + const selectedValues = interaction.fields.getStringSelectValues( + 'bulk_dismantle_select' + ); if (!selectedValues || selectedValues.length === 0) { await interaction.editReply({ content: '❌ No items were selected.' }); return; } - const items = selectedValues.map(val => { - const parts = val.split('-'); - if (parts.length >= 3) { - return { inventoryId: parts[0], itemId: parseInt(parts[1], 10), amount: parseInt(parts[2], 10) }; - } - return { itemId: parseInt(parts[0], 10), amount: parseInt(parts[1], 10) }; - }).filter(i => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); + const items = selectedValues + .map((val) => { + const parts = val.split('-'); + if (parts.length >= 3) { + return { + inventoryId: parts[0], + itemId: parseInt(parts[1], 10), + amount: parseInt(parts[2], 10) + }; + } + return { + itemId: parseInt(parts[0], 10), + amount: parseInt(parts[1], 10) + }; + }) + .filter((i) => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); if (items.length === 0) { - await interaction.editReply({ content: '❌ Could not parse selected items.' }); + await interaction.editReply({ + content: '❌ Could not parse selected items.' + }); return; } @@ -39,7 +66,9 @@ export default class BulkDismantleModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Bulk dismantle failed.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Bulk dismantle failed.') + }); return; } @@ -47,7 +76,9 @@ export default class BulkDismantleModal extends ModalSubmit { content: `🔥 **Bulk Dismantle Complete!** ${body.message}\n🔥 Total Embers: **${body.newEmbers?.toLocaleString() ?? '???'}**` }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/components/modals/BulkSellModal.ts b/src/components/modals/BulkSellModal.ts index 109aece..8dd03e9 100644 --- a/src/components/modals/BulkSellModal.ts +++ b/src/components/modals/BulkSellModal.ts @@ -1,16 +1,27 @@ -import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { + type ModalSubmitInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class BulkSellModal extends ModalSubmit { - constructor() { super({ customId: "bulk_sell_modal", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ customId: 'bulk_sell_modal', cooldown: 5, isAuthorOnly: true }); + } - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const selectedValues = interaction.fields.getStringSelectValues('bulk_sell_select'); + const selectedValues = + interaction.fields.getStringSelectValues('bulk_sell_select'); if (!selectedValues || selectedValues.length === 0) { await interaction.editReply({ content: '❌ No items were selected.' }); @@ -18,16 +29,27 @@ export default class BulkSellModal extends ModalSubmit { } // Parse values: "docId-itemId-quantity" or legacy "itemId-quantity" - const items = selectedValues.map(val => { - const parts = val.split('-'); - if (parts.length >= 3) { - return { inventoryId: parts[0], itemId: parseInt(parts[1], 10), amount: parseInt(parts[2], 10) }; - } - return { itemId: parseInt(parts[0], 10), amount: parseInt(parts[1], 10) }; - }).filter(i => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); + const items = selectedValues + .map((val) => { + const parts = val.split('-'); + if (parts.length >= 3) { + return { + inventoryId: parts[0], + itemId: parseInt(parts[1], 10), + amount: parseInt(parts[2], 10) + }; + } + return { + itemId: parseInt(parts[0], 10), + amount: parseInt(parts[1], 10) + }; + }) + .filter((i) => !isNaN(i.itemId) && !isNaN(i.amount) && i.amount > 0); if (items.length === 0) { - await interaction.editReply({ content: '❌ Could not parse selected items.' }); + await interaction.editReply({ + content: '❌ Could not parse selected items.' + }); return; } @@ -40,7 +62,9 @@ export default class BulkSellModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Bulk sell failed.') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Bulk sell failed.') + }); return; } @@ -48,7 +72,9 @@ export default class BulkSellModal extends ModalSubmit { content: `🪙 **Bulk Sell Complete!** ${body.message}\n💰 New Balance: **${body.newBalance?.toLocaleString() ?? '???'}** gold` }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/components/modals/CollectModal.ts b/src/components/modals/CollectModal.ts index 2850c9e..b9adb43 100644 --- a/src/components/modals/CollectModal.ts +++ b/src/components/modals/CollectModal.ts @@ -1,16 +1,20 @@ -import { type ModalSubmitInteraction, type Client } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class CollectModal extends ModalSubmit { constructor() { - super({ customId: "collect", cooldown: 2, isAuthorOnly: true }); + super({ customId: 'collect', cooldown: 2, isAuthorOnly: true }); } // customId format: collect: - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; @@ -18,26 +22,50 @@ export default class CollectModal extends ModalSubmit { const parsedAmount = parseInt(amount, 10); if (!docId || isNaN(parsedAmount)) { - await interaction.editReply({ content: 'Invalid input! Please try again.', files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: 'Invalid input! Please try again.', + files: [], + components: [], + embeds: [] + }); return; } try { const res = await apiFetch(Routes.collectionAdd(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) + body: JSON.stringify({ + discordId: interaction.user.id, + inventoryId: docId, + amount: parsedAmount + }) }); const { success, message, error } = await res.json(); if (!res.ok || !success) { - await interaction.editReply({ content: formatError(error ?? 'Collection failed'), components: [], files: [], embeds: [] }); + await interaction.editReply({ + content: formatError(error ?? 'Collection failed'), + components: [], + files: [], + embeds: [] + }); return; } - await interaction.editReply({ content: message ?? 'Items collected!', components: [], files: [], embeds: [] }); + await interaction.editReply({ + content: message ?? 'Items collected!', + components: [], + files: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), components: [], files: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + components: [], + files: [], + embeds: [] + }); } } } diff --git a/src/components/modals/ConsumeModal.ts b/src/components/modals/ConsumeModal.ts index b891134..9c30a91 100644 --- a/src/components/modals/ConsumeModal.ts +++ b/src/components/modals/ConsumeModal.ts @@ -1,16 +1,20 @@ -import { type ModalSubmitInteraction, type Client } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class ConsumeModal extends ModalSubmit { constructor() { - super({ customId: "consume", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'consume', cooldown: 3, isAuthorOnly: true }); } // customId format: consume: - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; @@ -18,26 +22,47 @@ export default class ConsumeModal extends ModalSubmit { const parsedAmount = parseInt(amount, 10); if (!docId || isNaN(parsedAmount)) { - await interaction.editReply({ content: 'Invalid input! Please try again.' }); + await interaction.editReply({ + content: 'Invalid input! Please try again.' + }); return; } try { const res = await apiFetch(Routes.consume(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) + body: JSON.stringify({ + discordId: interaction.user.id, + inventoryId: docId, + amount: parsedAmount + }) }); const { success, message, error } = await res.json(); if (!res.ok || !success) { - await interaction.editReply({ content: formatError(error ?? 'Consume failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(error ?? 'Consume failed'), + files: [], + components: [], + embeds: [] + }); return; } - await interaction.editReply({ content: message ?? 'Item consumed!', components: [], files: [], embeds: [] }); + await interaction.editReply({ + content: message ?? 'Item consumed!', + components: [], + files: [], + embeds: [] + }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/modals/MarketSellModal.ts b/src/components/modals/MarketSellModal.ts index d460e0d..c6ed897 100644 --- a/src/components/modals/MarketSellModal.ts +++ b/src/components/modals/MarketSellModal.ts @@ -1,17 +1,25 @@ -import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; -import ItemManager from "../../managers/ItemManager"; +import { + type ModalSubmitInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; +import ItemManager from '../../managers/ItemManager'; export default class MarketSellModal extends ModalSubmit { constructor() { - super({ customId: "mkt_sell_modal", cooldown: 5, isAuthorOnly: true }); + super({ customId: 'mkt_sell_modal', cooldown: 5, isAuthorOnly: true }); } // customId format: mkt_sell_modal:: - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const docId = args?.[0]; @@ -24,17 +32,23 @@ export default class MarketSellModal extends ModalSubmit { const pricePerUnit = parseInt(priceRaw, 10); if (!docId || isNaN(itemId)) { - await interaction.editReply({ content: '❌ Error parsing item data. Try again from `/market sell`.' }); + await interaction.editReply({ + content: '❌ Error parsing item data. Try again from `/market sell`.' + }); return; } if (isNaN(quantity) || quantity < 1) { - await interaction.editReply({ content: '❌ Invalid quantity. Enter a number 1 or higher.' }); + await interaction.editReply({ + content: '❌ Invalid quantity. Enter a number 1 or higher.' + }); return; } if (isNaN(pricePerUnit) || pricePerUnit < 1) { - await interaction.editReply({ content: '❌ Invalid price. Enter a number 1 or higher.' }); + await interaction.editReply({ + content: '❌ Invalid price. Enter a number 1 or higher.' + }); return; } @@ -53,13 +67,18 @@ export default class MarketSellModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to create listing') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to create listing') + }); return; } const def = ItemManager.get(itemId); const itemName = def?.name ?? `Item #${itemId}`; - const enhTag = body.listing?.enhanceLevel > 0 ? ` (+${body.listing.enhanceLevel})` : ''; + const enhTag = + body.listing?.enhanceLevel > 0 + ? ` (+${body.listing.enhanceLevel})` + : ''; const totalGold = quantity * pricePerUnit; await interaction.editReply({ @@ -74,7 +93,9 @@ export default class MarketSellModal extends ModalSubmit { ].join('\n') }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } } diff --git a/src/components/modals/SellModal.ts b/src/components/modals/SellModal.ts index 4abb7cd..077de08 100644 --- a/src/components/modals/SellModal.ts +++ b/src/components/modals/SellModal.ts @@ -1,16 +1,20 @@ -import { type ModalSubmitInteraction, type Client } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class SellModal extends ModalSubmit { constructor() { - super({ customId: "sell", cooldown: 3, isAuthorOnly: true }); + super({ customId: 'sell', cooldown: 3, isAuthorOnly: true }); } // customId format: sell: - public async execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise { await interaction.deferUpdate(); const docId = args?.[0]; @@ -18,29 +22,47 @@ export default class SellModal extends ModalSubmit { const parsedAmount = parseInt(amount, 10); if (!docId || isNaN(parsedAmount)) { - await interaction.editReply({ content: 'Invalid input! Please try again.' }); + await interaction.editReply({ + content: 'Invalid input! Please try again.' + }); return; } try { const res = await apiFetch(Routes.sell(), { method: 'POST', - body: JSON.stringify({ discordId: interaction.user.id, inventoryId: docId, amount: parsedAmount }) + body: JSON.stringify({ + discordId: interaction.user.id, + inventoryId: docId, + amount: parsedAmount + }) }); const { success, message, newBalance, error } = await res.json(); if (!res.ok || !success) { - await interaction.editReply({ content: formatError(error ?? 'Sell failed'), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(error ?? 'Sell failed'), + files: [], + components: [], + embeds: [] + }); return; } await interaction.editReply({ content: `${message}\n💰 New Balance: **${newBalance?.toLocaleString() ?? '???'}** gold`, - components: [], files: [], embeds: [] + components: [], + files: [], + embeds: [] }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code), files: [], components: [], embeds: [] }); + await interaction.editReply({ + content: formatError(err.message, err.code), + files: [], + components: [], + embeds: [] + }); } } } diff --git a/src/components/modals/SkillPointsModal.ts b/src/components/modals/SkillPointsModal.ts index 17a9f15..d362b49 100644 --- a/src/components/modals/SkillPointsModal.ts +++ b/src/components/modals/SkillPointsModal.ts @@ -1,13 +1,22 @@ -import { type ModalSubmitInteraction, type Client, MessageFlags } from "discord.js"; -import ModalSubmit from "../../structures/ModalSubmit"; -import { apiFetch } from "../../utilities/ApiClient"; -import { formatError } from "../../utilities/ErrorMessages"; -import Routes from "../../utilities/Routes"; +import { + type ModalSubmitInteraction, + type Client, + MessageFlags +} from 'discord.js'; +import ModalSubmit from '../../structures/ModalSubmit'; +import { apiFetch } from '../../utilities/ApiClient'; +import { formatError } from '../../utilities/ErrorMessages'; +import Routes from '../../utilities/Routes'; export default class SkillPointsModal extends ModalSubmit { - constructor() { super({ customId: "skillpoints_modal", cooldown: 5, isAuthorOnly: true }); } + constructor() { + super({ customId: 'skillpoints_modal', cooldown: 5, isAuthorOnly: true }); + } - public async execute(interaction: ModalSubmitInteraction, client: Client): Promise { + public async execute( + interaction: ModalSubmitInteraction, + client: Client + ): Promise { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const atkRaw = interaction.fields.getTextInputValue('sp_atk').trim(); @@ -17,12 +26,17 @@ export default class SkillPointsModal extends ModalSubmit { const defAmount = parseInt(defRaw, 10) || 0; if (atkAmount < 0 || defAmount < 0) { - await interaction.editReply({ content: '❌ Point values cannot be negative.' }); + await interaction.editReply({ + content: '❌ Point values cannot be negative.' + }); return; } if (atkAmount === 0 && defAmount === 0) { - await interaction.editReply({ content: '❌ You didn\'t allocate any points. Enter a number in at least one field.' }); + await interaction.editReply({ + content: + "❌ You didn't allocate any points. Enter a number in at least one field." + }); return; } @@ -39,7 +53,9 @@ export default class SkillPointsModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to allocate ATK points') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to allocate ATK points') + }); return; } results.push(`⚔️ **+${atkAmount} ATK** → Now: ${body.newStats.atk}`); @@ -54,7 +70,9 @@ export default class SkillPointsModal extends ModalSubmit { const body = await res.json(); if (!res.ok || !body.success) { - await interaction.editReply({ content: formatError(body.error ?? 'Failed to allocate DEF points') }); + await interaction.editReply({ + content: formatError(body.error ?? 'Failed to allocate DEF points') + }); return; } results.push(`🛡️ **+${defAmount} DEF** → Now: ${body.newStats.def}`); @@ -64,7 +82,9 @@ export default class SkillPointsModal extends ModalSubmit { content: `⭐ **Skill Points Allocated!**\n\n${results.join('\n')}\n\nRun \`/profile\` to see your updated stats.` }); } catch (err: any) { - await interaction.editReply({ content: formatError(err.message, err.code) }); + await interaction.editReply({ + content: formatError(err.message, err.code) + }); } } -} \ No newline at end of file +} diff --git a/src/events/ClientReadyEvent.ts b/src/events/ClientReadyEvent.ts index bb224c6..be743f9 100644 --- a/src/events/ClientReadyEvent.ts +++ b/src/events/ClientReadyEvent.ts @@ -1,9 +1,9 @@ -import { type Client } from "discord.js"; -import Event from "../structures/Event"; -import logger from "../utilities/Logger"; -import ItemManager from "../managers/ItemManager"; -import WorkerPool from "../utilities/WorkerPool"; -import PresenceManager from "../managers/PresenceManager"; +import { type Client } from 'discord.js'; +import Event from '../structures/Event'; +import logger from '../utilities/Logger'; +import ItemManager from '../managers/ItemManager'; +import WorkerPool from '../utilities/WorkerPool'; +import PresenceManager from '../managers/PresenceManager'; export default class ClientReadyEvent extends Event { constructor() { @@ -16,7 +16,9 @@ export default class ClientReadyEvent extends Event { public async execute(client: Client) { // Use cluster id from hybrid sharding, fallback to shard id const clusterId = (client as any).cluster?.id ?? client.shard?.ids[0] ?? 0; - logger.info(`[${this.constructor.name}] Successfully logged in as ${client.user?.tag}! (Cluster ${clusterId})`); + logger.info( + `[${this.constructor.name}] Successfully logged in as ${client.user?.tag}! (Cluster ${clusterId})` + ); // CRITICAL: Signal to the ClusterManager that this cluster is ready // Without this, the manager will timeout waiting for this cluster @@ -26,10 +28,12 @@ export default class ClientReadyEvent extends Event { WorkerPool.init(); // Stagger API requests by cluster ID to prevent slamming capi.gg - const delayMs = 1500 + clusterId * 2500; - + const delayMs = 1500 + clusterId * 2500; + setTimeout(async () => { - logger.info(`[Cluster ${clusterId}] Initiating staggered ItemManager sync...`); + logger.info( + `[Cluster ${clusterId}] Initiating staggered ItemManager sync...` + ); await ItemManager.refresh(); // Start rotating presence after items are loaded diff --git a/src/events/GuildCreateEvent.ts b/src/events/GuildCreateEvent.ts index 9097531..e397404 100644 --- a/src/events/GuildCreateEvent.ts +++ b/src/events/GuildCreateEvent.ts @@ -1,6 +1,18 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type Client, Colors, ContainerBuilder, EmbedBuilder, Events, type Guild, MessageFlags, type TextChannel } from "discord.js"; -import Event from "../structures/Event"; -import logger from "../utilities/Logger"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type Client, + Colors, + ContainerBuilder, + EmbedBuilder, + Events, + type Guild, + MessageFlags, + type TextChannel +} from 'discord.js'; +import Event from '../structures/Event'; +import logger from '../utilities/Logger'; export default class GuildCreateEvent extends Event { constructor() { @@ -13,19 +25,33 @@ export default class GuildCreateEvent extends Event { public async execute(guild: Guild, client: Client): Promise { // 1. Log to your private channel try { - const logChannel = await client.channels.fetch('1473407797289816074') as TextChannel; + const logChannel = (await client.channels.fetch( + '1473407797289816074' + )) as TextChannel; if (logChannel && guild) { - const container = new ContainerBuilder().setAccentColor(Colors.Green) - .addSectionComponents( - (section) => section.setThumbnailAccessory((t) => t.setURL(guild.iconURL() ?? client.user?.avatarURL()!)) + const container = new ContainerBuilder() + .setAccentColor(Colors.Green) + .addSectionComponents((section) => + section + .setThumbnailAccessory((t) => + t.setURL(guild.iconURL() ?? client.user?.avatarURL()!) + ) .addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('## I Joined A New Server!'), - (textDisplay) => textDisplay.setContent(`Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.`), - (textDisplay) => textDisplay.setContent(`-# ID: \`${guild.id}\``) + (textDisplay) => + textDisplay.setContent('## I Joined A New Server!'), + (textDisplay) => + textDisplay.setContent( + `Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.` + ), + (textDisplay) => + textDisplay.setContent(`-# ID: \`${guild.id}\``) ) ); - await logChannel.send({ components: [container], flags: MessageFlags.IsComponentsV2 }); + await logChannel.send({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); } } catch (e) { logger.error(e); @@ -34,24 +60,29 @@ export default class GuildCreateEvent extends Event { // 2. Send a welcome embed to the guild try { // Try system channel first, then the first text channel the bot can write in - const targetChannel = guild.systemChannel - ?? guild.channels.cache - .filter(c => c.isTextBased() && c.permissionsFor(guild.members.me!)?.has('SendMessages')) - .first() as TextChannel | undefined; + const targetChannel = + guild.systemChannel ?? + (guild.channels.cache + .filter( + (c) => + c.isTextBased() && + c.permissionsFor(guild.members.me!)?.has('SendMessages') + ) + .first() as TextChannel | undefined); if (!targetChannel) return; const embed = new EmbedBuilder() .setColor(0x10b981) - .setTitle('⚔️ Dragon\'s Fall Online') + .setTitle("⚔️ Dragon's Fall Online") .setDescription( 'Thanks for adding DFO! A lightweight text-based MMORPG where you can collect thousands of unique items, explore endless scenarios, and watch numbers go up.\n\n' + - '**Get started in 30 seconds:**\n' + - '> 1. `/register` — Create your character\n' + - '> 2. `/explore` — Venture into the world\n' + - '> 3. `/profile` — Check your stats\n' + - '> 4. `/help` — See all commands\n\n' + - 'You can also play on the web at **[capi.gg/dfo](https://capi.gg/dfo)**' + '**Get started in 30 seconds:**\n' + + '> 1. `/register` — Create your character\n' + + '> 2. `/explore` — Venture into the world\n' + + '> 3. `/profile` — Check your stats\n' + + '> 4. `/help` — See all commands\n\n' + + 'You can also play on the web at **[capi.gg/dfo](https://capi.gg/dfo)**' ) .setThumbnail(client.user?.displayAvatarURL() ?? '') .setFooter({ text: 'DFO Cross-Platform Integration' }); @@ -72,9 +103,11 @@ export default class GuildCreateEvent extends Event { await targetChannel.send({ embeds: [embed], components: [row] }); } catch (e) { // Not critical — some guilds block bot messages in all channels - logger.warn(`[GuildCreate] Could not send welcome message to ${guild.name}: ${e}`); + logger.warn( + `[GuildCreate] Could not send welcome message to ${guild.name}: ${e}` + ); } logger.info(`Joined a new guild! ${guild.name} (${guild.id})`); } -} \ No newline at end of file +} diff --git a/src/events/InteractionCreateEvent.ts b/src/events/InteractionCreateEvent.ts index 3182776..4a296cf 100644 --- a/src/events/InteractionCreateEvent.ts +++ b/src/events/InteractionCreateEvent.ts @@ -1,12 +1,17 @@ -import { type BaseInteraction, type InteractionReplyOptions, MessageFlags, type Client } from "discord.js"; -import Event from "../structures/Event"; -import SlashCommandHandler from "../handlers/SlashCommandHandler"; -import logger from "../utilities/Logger"; -import ButtonHandler from "../handlers/ButtonHandler"; -import SelectMenuHandler from "../handlers/SelectMenuHandler"; -import ModalSubmitHandler from "../handlers/ModalSubmitHandler"; -import { formatError } from "../utilities/ErrorMessages"; -import { ApiError } from "../utilities/ApiClient"; +import { + type BaseInteraction, + type InteractionReplyOptions, + MessageFlags, + type Client +} from 'discord.js'; +import Event from '../structures/Event'; +import SlashCommandHandler from '../handlers/SlashCommandHandler'; +import logger from '../utilities/Logger'; +import ButtonHandler from '../handlers/ButtonHandler'; +import SelectMenuHandler from '../handlers/SelectMenuHandler'; +import ModalSubmitHandler from '../handlers/ModalSubmitHandler'; +import { formatError } from '../utilities/ErrorMessages'; +import { ApiError } from '../utilities/ApiClient'; export default class InteractionCreateEvent extends Event { constructor() { @@ -23,9 +28,10 @@ export default class InteractionCreateEvent extends Event { if (!interaction.isRepliable()) return; // Use themed error messages for API errors, fallback for unknown errors - const message = err instanceof ApiError - ? formatError(err.message, err.code) - : formatError(err.message || String(err)); + const message = + err instanceof ApiError + ? formatError(err.message, err.code) + : formatError(err.message || String(err)); const payload: InteractionReplyOptions = { content: message, @@ -43,23 +49,42 @@ export default class InteractionCreateEvent extends Event { } } - public async execute(interaction: BaseInteraction, client: Client): Promise { + public async execute( + interaction: BaseInteraction, + client: Client + ): Promise { if (interaction.user.bot) return; try { if (interaction.isChatInputCommand()) { - await SlashCommandHandler.handle(interaction.commandName, interaction, client); + await SlashCommandHandler.handle( + interaction.commandName, + interaction, + client + ); } else if (interaction.isButton()) { await ButtonHandler.handle(interaction.customId, interaction, client); } else if (interaction.isAnySelectMenu()) { - await SelectMenuHandler.handle(interaction.customId, interaction, client); + await SelectMenuHandler.handle( + interaction.customId, + interaction, + client + ); } else if (interaction.isModalSubmit()) { - await ModalSubmitHandler.handle(interaction.customId, interaction, client); + await ModalSubmitHandler.handle( + interaction.customId, + interaction, + client + ); } else if (interaction.isAutocomplete()) { - await SlashCommandHandler.autocomplete(interaction.commandName, interaction, client); + await SlashCommandHandler.autocomplete( + interaction.commandName, + interaction, + client + ); } } catch (err) { await this.handleError(interaction, err); } } -} \ No newline at end of file +} diff --git a/src/handlers/ButtonHandler.ts b/src/handlers/ButtonHandler.ts index cd8d81a..ad83113 100644 --- a/src/handlers/ButtonHandler.ts +++ b/src/handlers/ButtonHandler.ts @@ -1,20 +1,30 @@ -import { type ButtonInteraction, Collection, MessageFlags, type Client } from "discord.js"; +import { + type ButtonInteraction, + Collection, + MessageFlags, + type Client +} from 'discord.js'; import { readdirSync } from 'fs'; -import { join } from "path"; -import Button from "../structures/Button"; -import logger from "../utilities/Logger"; -import CooldownManager from "../managers/CooldownManager"; -const filePath = join(__dirname, "../components/buttons"); +import { join } from 'path'; +import Button from '../structures/Button'; +import logger from '../utilities/Logger'; +import CooldownManager from '../managers/CooldownManager'; +const filePath = join(__dirname, '../components/buttons'); export default class ButtonHandler { private static _cache: Collection = new Collection(); public static load(): void { - const buttonFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const buttonFiles = readdirSync(filePath).filter( + (file) => + (file.endsWith('.ts') || file.endsWith('.js')) && + !file.endsWith('.d.ts') ); if (buttonFiles.length < 1) { - logger.info(`[ButtonHandler] No button executable data to cache. Skipping step`); + logger.info( + `[ButtonHandler] No button executable data to cache. Skipping step` + ); return; } @@ -25,40 +35,58 @@ export default class ButtonHandler { this._cache.set(button.customId, button); } - logger.info(`[ButtonHandler] Cached ${this._cache.size} button executables`); + logger.info( + `[ButtonHandler] Cached ${this._cache.size} button executables` + ); } - public static async handle(customId: string, interaction: ButtonInteraction, client: Client) { + public static async handle( + customId: string, + interaction: ButtonInteraction, + client: Client + ) { let id = customId; let target = null; if (customId.startsWith('page_')) return; if (customId.includes(':')) { - const [ name, ...args ] = customId.split(':'); + const [name, ...args] = customId.split(':'); id = name; target = args; } try { const button = this._cache.get(id); if (button == null) { - await interaction.reply({ content: 'This button is no longer supported or has deprecated code!', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'This button is no longer supported or has deprecated code!', + flags: MessageFlags.Ephemeral + }); return; } - if (button.isAuthorOnly && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; + if ( + button.isAuthorOnly && + interaction.user.id !== interaction.message.interactionMetadata?.user.id + ) + return; let key = `b-${id}-${interaction.user.id}`; if (customId === 'startNewDay') key = `adventure-${interaction.user.id}`; if (CooldownManager.onCooldown(key)) { const expireAt = CooldownManager.getExpiration(key); - await interaction.reply({ content: `⏳ You can use this button again .`, flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: `⏳ You can use this button again .`, + flags: MessageFlags.Ephemeral + }); return; } await button.execute(interaction, client, target); CooldownManager.addCooldown(key, button.cooldown); - logger.button(`${interaction.user.username} (${interaction.user.id}) used '${customId}'`); + logger.button( + `${interaction.user.username} (${interaction.user.id}) used '${customId}'` + ); } catch (err) { throw err; } } -} \ No newline at end of file +} diff --git a/src/handlers/EventHandler.ts b/src/handlers/EventHandler.ts index 17efc70..386a54f 100644 --- a/src/handlers/EventHandler.ts +++ b/src/handlers/EventHandler.ts @@ -14,7 +14,10 @@ export default class EventHandler { } private initialize(): void { - const eventFiles = readdirSync(filePath).filter(file => file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts')); + const eventFiles = readdirSync(filePath).filter( + (file) => + file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts') + ); for (const file of eventFiles) { let event = require(join(filePath, file)); @@ -22,10 +25,14 @@ export default class EventHandler { if (!(event instanceof Event)) continue; if (event.isOnce) { - this.client.once(event.name, (...args: any[]) => event.execute(...args, this.client)); + this.client.once(event.name, (...args: any[]) => + event.execute(...args, this.client) + ); } else { - this.client.on(event.name, (...args: any[]) => event.execute(...args, this.client)); + this.client.on(event.name, (...args: any[]) => + event.execute(...args, this.client) + ); } } } -} \ No newline at end of file +} diff --git a/src/handlers/ModalSubmitHandler.ts b/src/handlers/ModalSubmitHandler.ts index b21e4e0..a6a6361 100644 --- a/src/handlers/ModalSubmitHandler.ts +++ b/src/handlers/ModalSubmitHandler.ts @@ -1,20 +1,29 @@ -import ModalSubmit from "../structures/ModalSubmit"; -import { type ModalSubmitInteraction, Collection, type Client } from "discord.js"; -import { readdirSync } from "fs"; -import { join } from "path"; -import logger from "../utilities/Logger"; -import CooldownManager from "../managers/CooldownManager"; -const filePath = join(__dirname, "../components/modals"); +import ModalSubmit from '../structures/ModalSubmit'; +import { + type ModalSubmitInteraction, + Collection, + type Client +} from 'discord.js'; +import { readdirSync } from 'fs'; +import { join } from 'path'; +import logger from '../utilities/Logger'; +import CooldownManager from '../managers/CooldownManager'; +const filePath = join(__dirname, '../components/modals'); export default class ModalSubmitHandler { private static _cache: Collection = new Collection(); public static load(): void { - const modalFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const modalFiles = readdirSync(filePath).filter( + (file) => + (file.endsWith('.ts') || file.endsWith('.js')) && + !file.endsWith('.d.ts') ); if (modalFiles.length < 1) { - logger.info('[ModalSubmitHandler] No modal submit executable data to cache. Skipping step'); + logger.info( + '[ModalSubmitHandler] No modal submit executable data to cache. Skipping step' + ); return; } @@ -25,10 +34,16 @@ export default class ModalSubmitHandler { this._cache.set(modal.customId, modal); } - logger.info(`[ModalSubmitHandler] Cached a total of ${this._cache.size} modal executable data`); + logger.info( + `[ModalSubmitHandler] Cached a total of ${this._cache.size} modal executable data` + ); } - public static async handle(customId: string, interaction: ModalSubmitInteraction, client: Client) { + public static async handle( + customId: string, + interaction: ModalSubmitInteraction, + client: Client + ) { let id = customId; let target = null; if (customId.includes(':')) { @@ -39,7 +54,10 @@ export default class ModalSubmitHandler { try { const modal = this._cache.get(id); - if (modal == null) throw new Error(`No modal executable data could be found for the ID: ${customId}`); + if (modal == null) + throw new Error( + `No modal executable data could be found for the ID: ${customId}` + ); const key = `m-${customId}-${interaction.user.id}`; @@ -51,4 +69,4 @@ export default class ModalSubmitHandler { throw err; } } -} \ No newline at end of file +} diff --git a/src/handlers/SelectMenuHandler.ts b/src/handlers/SelectMenuHandler.ts index ab7c94c..d9b2c68 100644 --- a/src/handlers/SelectMenuHandler.ts +++ b/src/handlers/SelectMenuHandler.ts @@ -1,20 +1,29 @@ -import SelectMenu from "../structures/SelectMenu"; -import { type AnySelectMenuInteraction, Collection, type Client } from "discord.js"; +import SelectMenu from '../structures/SelectMenu'; +import { + type AnySelectMenuInteraction, + Collection, + type Client +} from 'discord.js'; import { readdirSync } from 'fs'; -import logger from "../utilities/Logger"; -import CooldownManager from "../managers/CooldownManager"; -import { join } from "path"; -const filePath = join(__dirname, "../components/menus"); +import logger from '../utilities/Logger'; +import CooldownManager from '../managers/CooldownManager'; +import { join } from 'path'; +const filePath = join(__dirname, '../components/menus'); export default class SelectMenuHandler { private static _cache: Collection = new Collection(); public static load(): void { - const menuFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const menuFiles = readdirSync(filePath).filter( + (file) => + (file.endsWith('.ts') || file.endsWith('.js')) && + !file.endsWith('.d.ts') ); if (menuFiles.length < 1) { - logger.info(`[SelectMenuHandler] No select menu executable data to cache. Skipping step`); + logger.info( + `[SelectMenuHandler] No select menu executable data to cache. Skipping step` + ); return; } @@ -25,10 +34,16 @@ export default class SelectMenuHandler { this._cache.set(menu.customId, menu); } - logger.info(`[SelectMenuHandler] Cached ${this._cache.size} menu executables`); + logger.info( + `[SelectMenuHandler] Cached ${this._cache.size} menu executables` + ); } - public static async handle(customId: string, interaction: AnySelectMenuInteraction, client: Client) { + public static async handle( + customId: string, + interaction: AnySelectMenuInteraction, + client: Client + ) { let id = customId; let target = null; if (customId.includes(':')) { @@ -39,9 +54,16 @@ export default class SelectMenuHandler { try { const menu = this._cache.get(id); - if (!menu) throw new Error(`No executable data could be found for menu with ID: ${customId}`); + if (!menu) + throw new Error( + `No executable data could be found for menu with ID: ${customId}` + ); - if (menu.isAuthorOnly && interaction.user.id !== interaction.message.interactionMetadata?.user.id) return; + if ( + menu.isAuthorOnly && + interaction.user.id !== interaction.message.interactionMetadata?.user.id + ) + return; const key = `s-${customId}-${interaction.user.id}`; @@ -53,4 +75,4 @@ export default class SelectMenuHandler { throw err; } } -} \ No newline at end of file +} diff --git a/src/handlers/SlashCommandHandler.ts b/src/handlers/SlashCommandHandler.ts index 5bf29d0..45bcd4b 100644 --- a/src/handlers/SlashCommandHandler.ts +++ b/src/handlers/SlashCommandHandler.ts @@ -1,9 +1,15 @@ -import { type AutocompleteInteraction, type ChatInputCommandInteraction, Collection, MessageFlags, type Client } from "discord.js"; +import { + type AutocompleteInteraction, + type ChatInputCommandInteraction, + Collection, + MessageFlags, + type Client +} from 'discord.js'; import { readdirSync } from 'fs'; -import { join } from "path"; -import SlashCommand from "../structures/SlashCommand"; -import logger from "../utilities/Logger"; -import CooldownManager from "../managers/CooldownManager"; +import { join } from 'path'; +import SlashCommand from '../structures/SlashCommand'; +import logger from '../utilities/Logger'; +import CooldownManager from '../managers/CooldownManager'; const filePath = join(__dirname, '../commands'); export default class SlashCommandHandler { @@ -14,7 +20,10 @@ export default class SlashCommandHandler { } public static load(): void { - const commandFiles = readdirSync(filePath).filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + const commandFiles = readdirSync(filePath).filter( + (file) => + (file.endsWith('.ts') || file.endsWith('.js')) && + !file.endsWith('.d.ts') ); for (const file of commandFiles) { @@ -24,16 +33,25 @@ export default class SlashCommandHandler { this._cache.set(command.data.name, command); } - logger.info(`[SlashCommandHandler] Cached a total of ${this._cache.size} commands`); + logger.info( + `[SlashCommandHandler] Cached a total of ${this._cache.size} commands` + ); } - public static async handle(name: string, interaction: ChatInputCommandInteraction, client: Client): Promise { + public static async handle( + name: string, + interaction: ChatInputCommandInteraction, + client: Client + ): Promise { const startTime = Date.now(); try { const command = this._cache.get(name); if (!command) { - await interaction.reply({ content: "This command is outdated or disabled", flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'This command is outdated or disabled', + flags: MessageFlags.Ephemeral + }); return; } @@ -41,20 +59,29 @@ export default class SlashCommandHandler { if (CooldownManager.onCooldown(key)) { const expiresAt = CooldownManager.getExpiration(key); - await interaction.reply({ content: `⏳ You can use this command again .`, flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: `⏳ You can use this command again .`, + flags: MessageFlags.Ephemeral + }); return; } await command.execute(interaction, client); CooldownManager.addCooldown(key, command.cooldown); - logger.command(`/${name} | ${interaction.user.username} (${interaction.user.id}) | ${interaction.guild?.name ?? 'DM'} | ${Date.now() - startTime}ms`); + logger.command( + `/${name} | ${interaction.user.username} (${interaction.user.id}) | ${interaction.guild?.name ?? 'DM'} | ${Date.now() - startTime}ms` + ); } catch (err) { throw err; } } - public static async autocomplete(name: string, interaction: AutocompleteInteraction, client: Client): Promise { + public static async autocomplete( + name: string, + interaction: AutocompleteInteraction, + client: Client + ): Promise { const command = this._cache.get(name); if (!command) return; @@ -66,4 +93,4 @@ export default class SlashCommandHandler { } } } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 8f9f5fe..7fc498e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,24 +23,26 @@ const botFile = path.join(__dirname, isCompiled ? 'bot.js' : 'bot.ts'); const manager = new ClusterManager(botFile, { token: process.env.BOT_TOKEN, - totalShards: 'auto', // Let Discord decide shard count - shardsPerClusters: 2, // 2 internal shards per cluster process - totalClusters: 'auto', // Auto-calculate cluster count - mode: 'process', // Each cluster is a separate process + totalShards: 'auto', // Let Discord decide shard count + shardsPerClusters: 2, // 2 internal shards per cluster process + totalClusters: 'auto', // Auto-calculate cluster count + mode: 'process', // Each cluster is a separate process respawn: true, restarts: { - max: 10, // Max restarts per cluster - interval: 60000 * 60 // Reset restart counter every hour + max: 10, // Max restarts per cluster + interval: 60000 * 60 // Reset restart counter every hour }, queue: { - auto: true, // Automatically manage spawn queue - timeout: 60000 // 60s timeout per cluster spawn + auto: true, // Automatically manage spawn queue + timeout: 60000 // 60s timeout per cluster spawn }, execArgv: isCompiled ? [] : ['-r', 'ts-node/register'] }); manager.on('clusterCreate', (cluster) => { - logger.info(`[System] Launched Cluster #${cluster.id} (Shards: ${cluster.shardList.join(', ')})`); + logger.info( + `[System] Launched Cluster #${cluster.id} (Shards: ${cluster.shardList.join(', ')})` + ); cluster.on('error', (err) => { logger.error(`[Cluster #${cluster.id}] Error: ${err}`); @@ -51,12 +53,17 @@ manager.on('clusterCreate', (cluster) => { }); cluster.on('message', (message: any) => { - if (message && typeof message === 'object' && 'type' in message && message.type === 'log') { + if ( + message && + typeof message === 'object' && + 'type' in message && + message.type === 'log' + ) { logger.info(`[Cluster #${cluster.id}] ${message.content}`); } }); }); -manager.spawn().catch(err => { +manager.spawn().catch((err) => { logger.error(`[System] Failed to spawn clusters: ${err}`); }); diff --git a/src/interfaces/ICollectionJSON.ts b/src/interfaces/ICollectionJSON.ts index 5911284..7be06a8 100644 --- a/src/interfaces/ICollectionJSON.ts +++ b/src/interfaces/ICollectionJSON.ts @@ -1,8 +1,8 @@ export interface ICollectionJSON { - userId: string; - items: Map; - totalItemsCollected: number; - uniqueItemsFound: number; - createdAt: Date; - updatedAt: Date; -} \ No newline at end of file + userId: string; + items: Map; + totalItemsCollected: number; + uniqueItemsFound: number; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/interfaces/ICombatJSON.ts b/src/interfaces/ICombatJSON.ts index 7906869..c62fb4e 100644 --- a/src/interfaces/ICombatJSON.ts +++ b/src/interfaces/ICombatJSON.ts @@ -1,5 +1,5 @@ -import { type IEnemyJSON } from "./IEnemyJSON"; -import { type IItemJSON } from "./IItemJSON"; +import { type IEnemyJSON } from './IEnemyJSON'; +import { type IItemJSON } from './IItemJSON'; export interface ICombatJSON { success: boolean; @@ -15,4 +15,4 @@ export interface ICombatJSON { levelsGained: number; }; error?: string; -} \ No newline at end of file +} diff --git a/src/interfaces/IEnemyJSON.ts b/src/interfaces/IEnemyJSON.ts index 5f7ece0..436ce4d 100644 --- a/src/interfaces/IEnemyJSON.ts +++ b/src/interfaces/IEnemyJSON.ts @@ -16,4 +16,4 @@ export interface IEnemyJSON { isMythic: boolean; isChampion: boolean; isElite: boolean; -} \ No newline at end of file +} diff --git a/src/interfaces/IExecutable.ts b/src/interfaces/IExecutable.ts index 3523885..0561846 100644 --- a/src/interfaces/IExecutable.ts +++ b/src/interfaces/IExecutable.ts @@ -1,3 +1,3 @@ export default interface IExecutable { execute: (...args: any[]) => Promise; -} \ No newline at end of file +} diff --git a/src/interfaces/IGameJSON.ts b/src/interfaces/IGameJSON.ts index 009fdca..120d2e0 100644 --- a/src/interfaces/IGameJSON.ts +++ b/src/interfaces/IGameJSON.ts @@ -1,9 +1,9 @@ // ========== TASKS ========== export interface ITaskJSON { - id: string; // API returns `id`, not `taskId` - action: string; // e.g. "EXPLORE_STEPS", "DEFEAT_ENEMIES" - label: string; // e.g. "Explore 50 times" (was `description`) + id: string; // API returns `id`, not `taskId` + action: string; // e.g. "EXPLORE_STEPS", "DEFEAT_ENEMIES" + label: string; // e.g. "Explore 50 times" (was `description`) icon: string; target: number; progress: number; @@ -15,8 +15,8 @@ export interface ITaskJSON { chestTier: string | null; }; completed: boolean; - claimed: boolean; // API returns `claimed`, not `isClaimed` - nextReset: string; // ISO date string + claimed: boolean; // API returns `claimed`, not `isClaimed` + nextReset: string; // ISO date string } export interface ITasksResponse { @@ -24,7 +24,7 @@ export interface ITasksResponse { tasks: ITaskJSON[]; embers: number; resets: { - daily: string; // ISO date strings, not numbers + daily: string; // ISO date strings, not numbers weekly: string; monthly: string; }; diff --git a/src/interfaces/IInventoryJSON.ts b/src/interfaces/IInventoryJSON.ts index 68937e4..2088399 100644 --- a/src/interfaces/IInventoryJSON.ts +++ b/src/interfaces/IInventoryJSON.ts @@ -1,13 +1,13 @@ export interface IInventoryItem { - _id: string; // MongoDB document ID — used for variant targeting + _id: string; // MongoDB document ID — used for variant targeting userId: string; itemId: number; quantity: number; isLocked: boolean; - enhanceLevel: number; // 0 = base, 1-10 = enhanced + enhanceLevel: number; // 0 = base, 1-10 = enhanced statOverrides: { atk: number; def: number; hp: number } | null; affixOverrides: { type: string; value: number }[] | null; petLevel: number; createdAt: Date; updatedAt: Date; -} \ No newline at end of file +} diff --git a/src/interfaces/IItemJSON.ts b/src/interfaces/IItemJSON.ts index f67fe55..e3c6b76 100644 --- a/src/interfaces/IItemJSON.ts +++ b/src/interfaces/IItemJSON.ts @@ -1,4 +1,10 @@ -export type ItemType = 'Weapon' | 'Armor' | 'Accessory' | 'Consumable' | 'Material' | 'Collectible'; +export type ItemType = + | 'Weapon' + | 'Armor' + | 'Accessory' + | 'Consumable' + | 'Material' + | 'Collectible'; export type EffectType = 'HEAL_HP' | 'GRANT_XP' | 'GRANT_GOLD' | 'NONE'; export type Rarity = | 'Common' @@ -11,9 +17,14 @@ export type Rarity = | 'Exotic'; export const RARITY_COLORS: Record = { - Common: 0xb0b0b0, Uncommon: 0x2ecc71, Rare: 0x3498db, - Elite: 0xe67e22, Epic: 0x9b59b6, Legendary: 0xf1c40f, - Divine: 0x00e5ff, Exotic: 0xff00cc + Common: 0xb0b0b0, + Uncommon: 0x2ecc71, + Rare: 0x3498db, + Elite: 0xe67e22, + Epic: 0x9b59b6, + Legendary: 0xf1c40f, + Divine: 0x00e5ff, + Exotic: 0xff00cc }; export type EquipmentSlot = @@ -63,4 +74,4 @@ export interface IItemJSON { createdBy: string; createdAt: Date; updatedAt: Date; -} \ No newline at end of file +} diff --git a/src/interfaces/IItemsJSON.ts b/src/interfaces/IItemsJSON.ts index 63bd79e..30d4d7a 100644 --- a/src/interfaces/IItemsJSON.ts +++ b/src/interfaces/IItemsJSON.ts @@ -1,7 +1,7 @@ -import { type IItemJSON } from "./IItemJSON"; +import { type IItemJSON } from './IItemJSON'; export interface IItemsJSON { success: boolean; count: number; data: IItemJSON[]; -} \ No newline at end of file +} diff --git a/src/interfaces/INPCJSON.ts b/src/interfaces/INPCJSON.ts index 06a67bb..a036376 100644 --- a/src/interfaces/INPCJSON.ts +++ b/src/interfaces/INPCJSON.ts @@ -2,4 +2,4 @@ export interface INPCJSON { id: number; name: string; description: string; -} \ No newline at end of file +} diff --git a/src/interfaces/IPlayerJSON.ts b/src/interfaces/IPlayerJSON.ts index b837006..745119a 100644 --- a/src/interfaces/IPlayerJSON.ts +++ b/src/interfaces/IPlayerJSON.ts @@ -1,11 +1,11 @@ -import { type IEnemyJSON } from "./IEnemyJSON"; +import { type IEnemyJSON } from './IEnemyJSON'; export type Privilege = - | 'Member' - | 'Donator' - | 'Moderator' - | 'Administrator' - | 'Developer'; + | 'Member' + | 'Donator' + | 'Moderator' + | 'Administrator' + | 'Developer'; export interface IPlayerJSON { id: string; @@ -68,4 +68,4 @@ class PlayerStatistics { playersDefeated: number = 0; timesDied: number = 0; questsDone: number = 0; -} \ No newline at end of file +} diff --git a/src/interfaces/IScenarioJSON.ts b/src/interfaces/IScenarioJSON.ts index 6df4848..c2faafe 100644 --- a/src/interfaces/IScenarioJSON.ts +++ b/src/interfaces/IScenarioJSON.ts @@ -4,4 +4,4 @@ export interface IScenarioJSON { createdBy: string; createdOn: Date; lastUpdated: Date; -} \ No newline at end of file +} diff --git a/src/interfaces/IStepJSON.ts b/src/interfaces/IStepJSON.ts index e73b3c7..2beac87 100644 --- a/src/interfaces/IStepJSON.ts +++ b/src/interfaces/IStepJSON.ts @@ -1,5 +1,5 @@ -import { type IEnemyJSON } from "./IEnemyJSON"; -import { type IItemJSON } from "./IItemJSON"; +import { type IEnemyJSON } from './IEnemyJSON'; +import { type IItemJSON } from './IItemJSON'; export interface IStepJSON { success: boolean; @@ -20,8 +20,8 @@ export interface IStepRewardsJSON { gold: number; item: IItemJSON | null; levelsGained: number; - chestDrop: string | null; // Chest tier found while exploring - toll: number; // Zone toll deducted this step + chestDrop: string | null; // Chest tier found while exploring + toll: number; // Zone toll deducted this step } export interface IActiveBonuses { @@ -43,4 +43,4 @@ export interface IStepPlayerStatsJSON { gold: number; expRequired: number; activeBonuses: IActiveBonuses; -} \ No newline at end of file +} diff --git a/src/managers/CooldownManager.ts b/src/managers/CooldownManager.ts index f0c94fb..a2faf78 100644 --- a/src/managers/CooldownManager.ts +++ b/src/managers/CooldownManager.ts @@ -1,4 +1,4 @@ -import { Collection } from "discord.js"; +import { Collection } from 'discord.js'; export default class CooldownManager { private static _cache: Collection = new Collection(); @@ -11,10 +11,9 @@ export default class CooldownManager { if (expiration > Date.now()) { return true; - } + } this._cache.delete(key); return false; - } public static getExpiration(key: string): number { @@ -35,4 +34,4 @@ export default class CooldownManager { const now = Date.now(); this._cache.sweep((expiration) => expiration <= now); } -} \ No newline at end of file +} diff --git a/src/managers/ItemManager.ts b/src/managers/ItemManager.ts index f6e7718..b79fc0e 100644 --- a/src/managers/ItemManager.ts +++ b/src/managers/ItemManager.ts @@ -1,11 +1,11 @@ -import { Collection } from "discord.js"; -import { type IItemJSON } from "../interfaces/IItemJSON"; -import logger from "../utilities/Logger"; -import Routes from "../utilities/Routes"; +import { Collection } from 'discord.js'; +import { type IItemJSON } from '../interfaces/IItemJSON'; +import logger from '../utilities/Logger'; +import Routes from '../utilities/Routes'; import 'dotenv/config'; const REFRESH_INTERVAL = 300_000; // 5 minutes -const FETCH_TIMEOUT = 15_000; // 15 second timeout for API calls +const FETCH_TIMEOUT = 15_000; // 15 second timeout for API calls export default class ItemManager { public static cache: Collection = new Collection(); @@ -34,7 +34,9 @@ export default class ItemManager { }); if (!res.ok) { - logger.error(`[ItemManager] Failed to fetch items: HTTP ${res.status} ${res.statusText}`); + logger.error( + `[ItemManager] Failed to fetch items: HTTP ${res.status} ${res.statusText}` + ); return; } @@ -61,7 +63,9 @@ export default class ItemManager { } catch (error: any) { // Distinguish timeout from other errors for clearer debugging if (error.name === 'TimeoutError' || error.name === 'AbortError') { - logger.error(`[ItemManager] Fetch timed out after ${FETCH_TIMEOUT / 1000}s`); + logger.error( + `[ItemManager] Fetch timed out after ${FETCH_TIMEOUT / 1000}s` + ); } else { logger.error(error, '[ItemManager] Critical fetch error:'); } @@ -103,7 +107,9 @@ export default class ItemManager { */ public static get(itemId: number): IItemJSON | undefined { if (!this.isLoaded) { - logger.warn(`[ItemManager] Attempted to get item ${itemId} before cache was loaded`); + logger.warn( + `[ItemManager] Attempted to get item ${itemId} before cache was loaded` + ); } return this.cache.get(itemId); } @@ -114,4 +120,4 @@ export default class ItemManager { public static get size(): number { return this.cache.size; } -} \ No newline at end of file +} diff --git a/src/managers/PresenceManager.ts b/src/managers/PresenceManager.ts index cf733a2..ef35a74 100644 --- a/src/managers/PresenceManager.ts +++ b/src/managers/PresenceManager.ts @@ -40,7 +40,9 @@ export default class PresenceManager { private static async fetchStats(): Promise { try { - const telemetryRes = await apiFetch('https://capi.gg/api/telemetry/db-stats'); + const telemetryRes = await apiFetch( + 'https://capi.gg/api/telemetry/db-stats' + ); if (telemetryRes.ok) { const data = await telemetryRes.json(); @@ -59,8 +61,13 @@ export default class PresenceManager { const cluster = (this.client as any).cluster; if (cluster) { try { - const results = await cluster.broadcastEval((c: any) => c.guilds.cache.size); - this.totalGuilds = results.reduce((acc: number, val: number) => acc + val, 0); + const results = await cluster.broadcastEval( + (c: any) => c.guilds.cache.size + ); + this.totalGuilds = results.reduce( + (acc: number, val: number) => acc + val, + 0 + ); } catch { this.totalGuilds = this.client.guilds.cache.size; } @@ -76,11 +83,23 @@ export default class PresenceManager { if (!this.client.user) return; const activities = [ - { type: ActivityType.Watching, name: `${this.stats.players.toLocaleString()} players` }, - { type: ActivityType.Watching, name: `${this.totalGuilds.toLocaleString()} servers` }, - { type: ActivityType.Watching, name: `${this.stats.items.toLocaleString()} items` }, - { type: ActivityType.Watching, name: `${this.stats.scenarios.toLocaleString()} scenarios` }, - { type: ActivityType.Playing, name: `capi.gg` } + { + type: ActivityType.Watching, + name: `${this.stats.players.toLocaleString()} players` + }, + { + type: ActivityType.Watching, + name: `${this.totalGuilds.toLocaleString()} servers` + }, + { + type: ActivityType.Watching, + name: `${this.stats.items.toLocaleString()} items` + }, + { + type: ActivityType.Watching, + name: `${this.stats.scenarios.toLocaleString()} scenarios` + }, + { type: ActivityType.Playing, name: `capi.gg` } ]; const current = activities[this.rotationIndex % activities.length]; diff --git a/src/structures/Button.ts b/src/structures/Button.ts index 1131150..d9a193b 100644 --- a/src/structures/Button.ts +++ b/src/structures/Button.ts @@ -1,5 +1,5 @@ -import type IExecutable from "../interfaces/IExecutable"; -import { type ButtonInteraction, type Client } from "discord.js"; +import type IExecutable from '../interfaces/IExecutable'; +import { type ButtonInteraction, type Client } from 'discord.js'; export interface ButtonOptions { customId: string; @@ -26,5 +26,9 @@ export default abstract class Button implements IExecutable { return this.options.isAuthorOnly; } - public abstract execute(interaction: ButtonInteraction, client: Client, args?: string[] | null): Promise; -} \ No newline at end of file + public abstract execute( + interaction: ButtonInteraction, + client: Client, + args?: string[] | null + ): Promise; +} diff --git a/src/structures/Event.ts b/src/structures/Event.ts index 806e27f..e3e2bd7 100644 --- a/src/structures/Event.ts +++ b/src/structures/Event.ts @@ -1,4 +1,4 @@ -import type IExecutable from "../interfaces/IExecutable"; +import type IExecutable from '../interfaces/IExecutable'; export interface EventOptions { name: string; @@ -21,4 +21,4 @@ export default abstract class Event implements IExecutable { } public abstract execute(...args: any[]): Promise; -} \ No newline at end of file +} diff --git a/src/structures/ModalSubmit.ts b/src/structures/ModalSubmit.ts index 505bab0..854da1e 100644 --- a/src/structures/ModalSubmit.ts +++ b/src/structures/ModalSubmit.ts @@ -1,5 +1,5 @@ -import { type ModalSubmitInteraction, type Client } from "discord.js"; -import type IExecutable from "../interfaces/IExecutable"; +import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import type IExecutable from '../interfaces/IExecutable'; export interface ModalSubmitOptions { customId: string; @@ -26,5 +26,9 @@ export default abstract class ModalSubmit implements IExecutable { return this.options.isAuthorOnly; } - public abstract execute(interaction: ModalSubmitInteraction, client: Client, args?: string[] | null): Promise; -} \ No newline at end of file + public abstract execute( + interaction: ModalSubmitInteraction, + client: Client, + args?: string[] | null + ): Promise; +} diff --git a/src/structures/SelectMenu.ts b/src/structures/SelectMenu.ts index 00426b1..cb8a117 100644 --- a/src/structures/SelectMenu.ts +++ b/src/structures/SelectMenu.ts @@ -1,5 +1,5 @@ -import { type AnySelectMenuInteraction, type Client } from "discord.js"; -import type IExecutable from "../interfaces/IExecutable"; +import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import type IExecutable from '../interfaces/IExecutable'; export interface SelectMenuOptions { customId: string; @@ -26,5 +26,9 @@ export default abstract class SelectMenu implements IExecutable { return this.options.isAuthorOnly; } - public abstract execute(interaction: AnySelectMenuInteraction, client: Client, args?: string[] | null): Promise; -} \ No newline at end of file + public abstract execute( + interaction: AnySelectMenuInteraction, + client: Client, + args?: string[] | null + ): Promise; +} diff --git a/src/structures/SlashCommand.ts b/src/structures/SlashCommand.ts index bcc8564..11009a4 100644 --- a/src/structures/SlashCommand.ts +++ b/src/structures/SlashCommand.ts @@ -1,6 +1,10 @@ -import { type AutocompleteInteraction, type ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; -import type IExecutable from "../interfaces/IExecutable"; -import { type Client } from "discord.js"; +import { + type AutocompleteInteraction, + type ChatInputCommandInteraction, + SlashCommandBuilder +} from 'discord.js'; +import type IExecutable from '../interfaces/IExecutable'; +import { type Client } from 'discord.js'; export interface SlashCommandOptions { name: string; @@ -45,7 +49,13 @@ export default abstract class SlashCommand implements IExecutable { return this.builder; } - public abstract execute(interaction: ChatInputCommandInteraction, client: Client): Promise; + public abstract execute( + interaction: ChatInputCommandInteraction, + client: Client + ): Promise; - public async autocomplete?(interaction: AutocompleteInteraction, client: Client): Promise; -} \ No newline at end of file + public async autocomplete?( + interaction: AutocompleteInteraction, + client: Client + ): Promise; +} diff --git a/src/structures/containers/AttackContainer.ts b/src/structures/containers/AttackContainer.ts index dbbf066..b511554 100644 --- a/src/structures/containers/AttackContainer.ts +++ b/src/structures/containers/AttackContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type ICombatJSON } from "../../interfaces/ICombatJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type ICombatJSON } from '../../interfaces/ICombatJSON'; export default class AttackContainer { private data: ICombatJSON; @@ -11,44 +11,59 @@ export default class AttackContainer { public build(): ContainerBuilder { const container = new ContainerBuilder(); - container.setAccentColor(this.data.victory ? 0x10b981 : this.data.combatEnded ? 0x6b7280 : 0xef4444); + container.setAccentColor( + this.data.victory ? 0x10b981 : this.data.combatEnded ? 0x6b7280 : 0xef4444 + ); - const cleanFlavorText = this.data.flavorText.replace(/\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, '**$1**'); + const cleanFlavorText = this.data.flavorText.replace( + /\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, + '**$1**' + ); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(cleanFlavorText) + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent(cleanFlavorText) ); if (!this.data.combatEnded && this.data.enemy) { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\``), - (textDisplay) => textDisplay.setContent(`-# Use /attack to strike again!`) + (textDisplay) => + textDisplay.setContent( + `**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\`` + ), + (textDisplay) => + textDisplay.setContent(`-# Use /attack to strike again!`) ); } if (this.data.victory && this.data.rewards) { container.addSeparatorComponents((s) => s); const rewardText = []; - if (this.data.rewards.xp) rewardText.push(`✨ +${this.data.rewards.xp.toLocaleString()} XP`); - if (this.data.rewards.gold) rewardText.push(`🪙 +${this.data.rewards.gold.toLocaleString()} Gold`); - if (this.data.rewards.item) rewardText.push(`🎒 Looted: **${this.data.rewards.item.name}**`); + if (this.data.rewards.xp) + rewardText.push(`✨ +${this.data.rewards.xp.toLocaleString()} XP`); + if (this.data.rewards.gold) + rewardText.push(`🪙 +${this.data.rewards.gold.toLocaleString()} Gold`); + if (this.data.rewards.item) + rewardText.push(`🎒 Looted: **${this.data.rewards.item.name}**`); for (const reward of rewardText) { - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(reward) + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent(reward) ); } } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; } -} \ No newline at end of file +} diff --git a/src/structures/containers/ExploreContainer.ts b/src/structures/containers/ExploreContainer.ts index 1c860ea..27c1ec5 100644 --- a/src/structures/containers/ExploreContainer.ts +++ b/src/structures/containers/ExploreContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type IStepJSON } from "../../interfaces/IStepJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type IStepJSON } from '../../interfaces/IStepJSON'; export default class ExploreContainer { private data: IStepJSON; @@ -11,30 +11,47 @@ export default class ExploreContainer { public build(): ContainerBuilder { const container = new ContainerBuilder(); - container.setAccentColor(this.data.combatTrigger || this.data.inCombat ? 0xef4444 : 0x3b82f6); + container.setAccentColor( + this.data.combatTrigger || this.data.inCombat ? 0xef4444 : 0x3b82f6 + ); - const cleanFlavorText = this.data.flavorText.replace(/\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, '**$1**'); + const cleanFlavorText = this.data.flavorText.replace( + /\[([^\]]+)\]\(color:#[0-9a-fA-F]+\)/g, + '**$1**' + ); container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent(cleanFlavorText), - (textDisplay) => textDisplay.setContent(`-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\``) + (textDisplay) => + textDisplay.setContent( + `-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\`` + ) ); if (this.data.enemy) { const enemy = this.data.enemy; container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\``), - (textDisplay) => textDisplay.setContent(`**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`-# Use the /attack command to fight`) + (textDisplay) => + textDisplay.setContent( + `**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), + (textDisplay) => + textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), + (textDisplay) => + textDisplay.setContent(`-# Use the /attack command to fight`) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -45,28 +62,36 @@ export default class ExploreContainer { const expRequired = stats.expRequired ?? 1; const rewardText = []; - if (this.data.rewards.xp) rewardText.push(`✨ +${this.data.rewards.xp} XP`); - if (this.data.rewards.gold) rewardText.push(`🪙 +${this.data.rewards.gold} Gold`); - if (this.data.rewards.item) rewardText.push(`🎒 Found: **${this.data.rewards.item.name}** (${this.data.rewards.item.rarity})`); - if (this.data.rewards.levelsGained > 0) rewardText.push('🆙 **LEVEL UP!**'); + if (this.data.rewards.xp) + rewardText.push(`✨ +${this.data.rewards.xp} XP`); + if (this.data.rewards.gold) + rewardText.push(`🪙 +${this.data.rewards.gold} Gold`); + if (this.data.rewards.item) + rewardText.push( + `🎒 Found: **${this.data.rewards.item.name}** (${this.data.rewards.item.rarity})` + ); + if (this.data.rewards.levelsGained > 0) + rewardText.push('🆙 **LEVEL UP!**'); if (rewardText.length >= 1) { container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\``) + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent( + `-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\`` + ) ); } for (const text of rewardText) { - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(text) + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent(text) ); } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -74,10 +99,10 @@ export default class ExploreContainer { container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; } -} \ No newline at end of file +} diff --git a/src/structures/containers/ItemLookupContainer.ts b/src/structures/containers/ItemLookupContainer.ts index 0e2ddfa..c98ce5d 100644 --- a/src/structures/containers/ItemLookupContainer.ts +++ b/src/structures/containers/ItemLookupContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type IItemJSON, RARITY_COLORS } from "../../interfaces/IItemJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type IItemJSON, RARITY_COLORS } from '../../interfaces/IItemJSON'; export default class ItemLookupContainer { private data: IItemJSON; @@ -9,29 +9,40 @@ export default class ItemLookupContainer { } public build(): ContainerBuilder { - const container = new ContainerBuilder().setAccentColor(RARITY_COLORS[this.data.rarity]); + const container = new ContainerBuilder().setAccentColor( + RARITY_COLORS[this.data.rarity] + ); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), - (textDisplay) => textDisplay.setContent(`-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*`), + (textDisplay) => + textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), + (textDisplay) => + textDisplay.setContent( + `-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*` + ), (textDisplay) => textDisplay.setContent(`*${this.data.description}*`), - (textDisplay) => textDisplay.setContent(`-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\``) + (textDisplay) => + textDisplay.setContent( + `-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\`` + ) ); if (this.data.affixes) { for (const affix of this.data.affixes) { - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\``) + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent( + `**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\`` + ) ); } } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; } -} \ No newline at end of file +} diff --git a/src/structures/containers/NPCLookupContainer.ts b/src/structures/containers/NPCLookupContainer.ts index f1f672b..4e78251 100644 --- a/src/structures/containers/NPCLookupContainer.ts +++ b/src/structures/containers/NPCLookupContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type INPCJSON } from "../../interfaces/INPCJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type INPCJSON } from '../../interfaces/INPCJSON'; export default class NPCLookupContainer { private data: INPCJSON; @@ -12,10 +12,11 @@ export default class NPCLookupContainer { const container = new ContainerBuilder(); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), + (textDisplay) => + textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), (textDisplay) => textDisplay.setContent(this.data.description) ); return container; } -} \ No newline at end of file +} diff --git a/src/structures/containers/ProfileContainer.ts b/src/structures/containers/ProfileContainer.ts index ac82119..7413210 100644 --- a/src/structures/containers/ProfileContainer.ts +++ b/src/structures/containers/ProfileContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type IPlayerJSON } from "../../interfaces/IPlayerJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type IPlayerJSON } from '../../interfaces/IPlayerJSON'; export default class ProfileContainer { private data: IPlayerJSON; @@ -11,40 +11,79 @@ export default class ProfileContainer { public build(): ContainerBuilder { const container = new ContainerBuilder(); - container.addSectionComponents( - (section) => section.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**Username:** \`${this.data.username}\``), - (textDisplay) => textDisplay.setContent(`**Level:** \`${this.data.level.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**Experience:** \`${this.data.experience.toLocaleString()}\``) - ).setThumbnailAccessory( - (tb) => tb.setURL(`https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png`) - ) + container.addSectionComponents((section) => + section + .addTextDisplayComponents( + (textDisplay) => + textDisplay.setContent(`**Username:** \`${this.data.username}\``), + (textDisplay) => + textDisplay.setContent( + `**Level:** \`${this.data.level.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**Experience:** \`${this.data.experience.toLocaleString()}\`` + ) + ) + .setThumbnailAccessory((tb) => + tb.setURL( + `https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png` + ) + ) ); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), - (textDisplay) => textDisplay.setContent(`**Coins:** \`${this.data.coins.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\``), - (textDisplay) => textDisplay.setContent(`**ATK:** \`${this.data.stats.atk.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**DEF:** \`${this.data.stats.def.toLocaleString()}\``) + (textDisplay) => + textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), + (textDisplay) => + textDisplay.setContent( + `**Coins:** \`${this.data.coins.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**ATK:** \`${this.data.stats.atk.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**DEF:** \`${this.data.stats.def.toLocaleString()}\`` + ) ); container.addSeparatorComponents((separator) => separator); container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent(`**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\``), - (textDisplay) => textDisplay.setContent(`**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\``) + (textDisplay) => + textDisplay.setContent( + `**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\`` + ), + (textDisplay) => + textDisplay.setContent( + `**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\`` + ) ); container.addSeparatorComponents((separator) => separator); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; } -} \ No newline at end of file +} diff --git a/src/structures/containers/ScenarioLookupContainer.ts b/src/structures/containers/ScenarioLookupContainer.ts index e434ccf..35476c8 100644 --- a/src/structures/containers/ScenarioLookupContainer.ts +++ b/src/structures/containers/ScenarioLookupContainer.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "discord.js"; -import { type IScenarioJSON } from "../../interfaces/IScenarioJSON"; +import { ContainerBuilder } from 'discord.js'; +import { type IScenarioJSON } from '../../interfaces/IScenarioJSON'; export default class ScenarioLookupContainer { private data: IScenarioJSON; @@ -14,16 +14,22 @@ export default class ScenarioLookupContainer { container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent('## Scenario Viewer'), (textDisplay) => textDisplay.setContent(this.data.description), - (textDisplay) => textDisplay.setContent(`-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\``), - (textDisplay) => textDisplay.setContent(`-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\``) + (textDisplay) => + textDisplay.setContent( + `-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\`` + ), + (textDisplay) => + textDisplay.setContent( + `-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\`` + ) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents( - (textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => + textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; } -} \ No newline at end of file +} diff --git a/src/utilities/AdventureImageBuilder.ts b/src/utilities/AdventureImageBuilder.ts index 0d914bc..80ec9de 100644 --- a/src/utilities/AdventureImageBuilder.ts +++ b/src/utilities/AdventureImageBuilder.ts @@ -4,12 +4,25 @@ import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { join } from 'path'; // Load OS-agnostic emoji font -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} export default class AdventureImageBuilder { - // Fully Upgraded Discord Markdown & Layout Engine - private static processText(ctx: any, text: string, startX: number, startY: number, maxWidth: number, baseLineHeight: number, defaultColor: string, draw: boolean): number { + private static processText( + ctx: any, + text: string, + startX: number, + startY: number, + maxWidth: number, + baseLineHeight: number, + defaultColor: string, + draw: boolean + ): number { const lines = text.split('\n'); let currentY = startY; @@ -23,13 +36,27 @@ export default class AdventureImageBuilder { let listPrefix = ''; let lineIndent = 0; - if (line.startsWith('>>> ')) { isQuote = true; line = line.substring(4); } - else if (line.startsWith('> ')) { isQuote = true; line = line.substring(2); } + if (line.startsWith('>>> ')) { + isQuote = true; + line = line.substring(4); + } else if (line.startsWith('> ')) { + isQuote = true; + line = line.substring(2); + } - if (line.startsWith('-# ')) { isSubtext = true; line = line.substring(3); } - else if (line.startsWith('### ')) { headerLevel = 3; line = line.substring(4); } - else if (line.startsWith('## ')) { headerLevel = 2; line = line.substring(3); } - else if (line.startsWith('# ')) { headerLevel = 1; line = line.substring(2); } + if (line.startsWith('-# ')) { + isSubtext = true; + line = line.substring(3); + } else if (line.startsWith('### ')) { + headerLevel = 3; + line = line.substring(4); + } else if (line.startsWith('## ')) { + headerLevel = 2; + line = line.substring(3); + } else if (line.startsWith('# ')) { + headerLevel = 1; + line = line.substring(2); + } const listMatch = line.match(/^(\s*[-*+]\s|\s*\d+\.\s)/); if (listMatch) { @@ -50,10 +77,17 @@ export default class AdventureImageBuilder { .replace(/__(.+?)__/g, '$1') // Underline .replace(/~~(.+?)~~/g, '$1') // Strikethrough .replace(/`([^`]+)`/g, '$1') // Inline Code - .replace(/\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, '$1'); // Hex Colors + .replace( + /\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, + '$1' + ); // Hex Colors // 3. Tokenize by our custom tags - const tokens = parsedLine.split(/(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g).filter(Boolean); + const tokens = parsedLine + .split( + /(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g + ) + .filter(Boolean); const currentState = { bold: headerLevel > 0, @@ -68,14 +102,21 @@ export default class AdventureImageBuilder { // Inject the bullet point/number if it's a list if (listPrefix) { - wordObjects.push({ word: `${listPrefix} `, state: { ...currentState, color: '#10b981', bold: true } }); + wordObjects.push({ + word: `${listPrefix} `, + state: { ...currentState, color: '#10b981', bold: true } + }); } // Apply state toggles and chunk text into words for (const token of tokens) { - if (token === '') { currentState.bold = true; currentState.italic = true; } - else if (token === '') { currentState.bold = false; currentState.italic = false; } - else if (token === '') currentState.bold = true; + if (token === '') { + currentState.bold = true; + currentState.italic = true; + } else if (token === '') { + currentState.bold = false; + currentState.italic = false; + } else if (token === '') currentState.bold = true; else if (token === '') currentState.bold = false; else if (token === '') currentState.italic = true; else if (token === '') currentState.italic = false; @@ -83,14 +124,29 @@ export default class AdventureImageBuilder { else if (token === '') currentState.underline = false; else if (token === '') currentState.strike = true; else if (token === '') currentState.strike = false; - else if (token === '') { currentState.code = true; currentState.color = '#6ee7b7'; } - else if (token === '') { currentState.code = false; currentState.color = isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor; } - else if (token.startsWith('') { currentState.color = isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor; } - else { + else if (token === '') { + currentState.code = true; + currentState.color = '#6ee7b7'; + } else if (token === '') { + currentState.code = false; + currentState.color = isSubtext + ? '#6b7280' + : isQuote + ? '#9ca3af' + : defaultColor; + } else if (token.startsWith('') { + currentState.color = isSubtext + ? '#6b7280' + : isQuote + ? '#9ca3af' + : defaultColor; + } else { const textWords = token.split(' '); for (let w = 0; w < textWords.length; w++) { - const wordStr = textWords[w] + (w < textWords.length - 1 ? ' ' : ''); + const wordStr = + textWords[w] + (w < textWords.length - 1 ? ' ' : ''); if (wordStr.length > 0) { wordObjects.push({ word: wordStr, state: { ...currentState } }); } @@ -102,7 +158,7 @@ export default class AdventureImageBuilder { const weight = state.bold ? 'bold ' : ''; const style = state.italic ? 'italic ' : ''; let size = 22; // Base size - + if (headerLevel === 1) size = 32; else if (headerLevel === 2) size = 28; else if (headerLevel === 3) size = 24; @@ -110,7 +166,7 @@ export default class AdventureImageBuilder { else if (state.code) size = 20; let family = state.code ? 'monospace' : 'sans-serif'; - if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; + if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; return `${style}${weight}${size}px ${family}`; }; @@ -133,18 +189,23 @@ export default class AdventureImageBuilder { // Draws a full wrapped line to the canvas const flushLine = () => { if (lineWords.length === 0) return; - + if (draw) { let drawX = startX + lineIndent; for (const lw of lineWords) { ctx.font = getFont(lw.state); ctx.fillStyle = lw.state.color; - + const m = ctx.measureText(lw.word); - + if (lw.state.code) { ctx.fillStyle = '#ffffff1a'; - ctx.fillRect(drawX, currentY - increment * 0.7, m.width, increment); + ctx.fillRect( + drawX, + currentY - increment * 0.7, + m.width, + increment + ); ctx.fillStyle = lw.state.color; } @@ -152,10 +213,20 @@ export default class AdventureImageBuilder { // Underlines and Strikethroughs if (lw.state.underline) { - ctx.fillRect(drawX, currentY + 4, m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); + ctx.fillRect( + drawX, + currentY + 4, + m.width - (lw.word.endsWith(' ') ? 8 : 0), + 2 + ); } if (lw.state.strike) { - ctx.fillRect(drawX, currentY - increment * 0.3, m.width - (lw.word.endsWith(' ') ? 8 : 0), 2); + ctx.fillRect( + drawX, + currentY - increment * 0.3, + m.width - (lw.word.endsWith(' ') ? 8 : 0), + 2 + ); } drawX += m.width; @@ -172,7 +243,10 @@ export default class AdventureImageBuilder { ctx.font = getFont(wObj.state); let metrics = ctx.measureText(wObj.word); - if (currentLineWidth + metrics.width > maxWidth - lineIndent && lineWords.length > 0) { + if ( + currentLineWidth + metrics.width > maxWidth - lineIndent && + lineWords.length > 0 + ) { flushLine(); // Strip leading space on wrap if (wObj.word.startsWith(' ')) { @@ -189,8 +263,13 @@ export default class AdventureImageBuilder { // Draw Block Quote bar across the entire paragraph block if (isQuote && draw) { - ctx.fillStyle = '#10b98180'; - ctx.fillRect(startX, startYOfParagraph - increment * 0.7, 4, currentY - startYOfParagraph); + ctx.fillStyle = '#10b98180'; + ctx.fillRect( + startX, + startYOfParagraph - increment * 0.7, + 4, + currentY - startYOfParagraph + ); } if (headerLevel > 0) currentY += 10; @@ -201,11 +280,10 @@ export default class AdventureImageBuilder { } public static async build(data: IStepJSON | ICombatJSON): Promise { - // --- NORMALIZE DATA PAYLOADS --- const flavorText = data.flavorText || 'Waiting for input...'; const enemyStats = data.enemy; - + const scenarioMeta = { id: (data as IStepJSON).scenarioId || '0', author: (data as IStepJSON).scenarioAuthor || 'SYSTEM' @@ -219,32 +297,43 @@ export default class AdventureImageBuilder { level, exp: Math.floor(pStats.experience ?? pStats.exp ?? 0), gold: pStats.coins ?? pStats.gold ?? 0, - expRequired: pStats.expRequired ?? Math.floor(50 * Math.max(1, level)**1.3), + expRequired: + pStats.expRequired ?? Math.floor(50 * Math.max(1, level) ** 1.3), activeBonuses: pStats.activeBonuses || {} }; const inCombat = !!enemyStats; const isDead = mappedStats.hp <= 0; - + const b = mappedStats.activeBonuses; - const hasBonuses = b && (b.critChance > 5 || b.lifeSteal > 0 || b.dodge > 0 || b.thorns > 0); - + const hasBonuses = + b && (b.critChance > 5 || b.lifeSteal > 0 || b.dodge > 0 || b.thorns > 0); + // --- PRE-CALCULATE TEXT HEIGHT --- const dummyCanvas = createCanvas(800, 10); const dummyCtx = dummyCanvas.getContext('2d'); const termW = 720; - + // Measure without drawing - const requiredTextHeight = this.processText(dummyCtx, flavorText, 0, 0, termW - 60, 32, '#ffffff', false); + const requiredTextHeight = this.processText( + dummyCtx, + flavorText, + 0, + 0, + termW - 60, + 32, + '#ffffff', + false + ); let extraHeight = 0; - const baseTextSpace = 160; + const baseTextSpace = 160; if (requiredTextHeight > baseTextSpace) { extraHeight = requiredTextHeight - baseTextSpace + 20; // Stretch canvas } // --- DYNAMIC CANVAS SIZING --- - let canvasHeight = 560 + extraHeight; + let canvasHeight = 560 + extraHeight; if (inCombat) canvasHeight += 75; if (hasBonuses) canvasHeight += 40; @@ -261,17 +350,24 @@ export default class AdventureImageBuilder { ctx.strokeStyle = '#ffffff05'; ctx.lineWidth = 1; for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); } // 2. Header ctx.fillStyle = themeColor; ctx.font = 'bold 36px sans-serif'; ctx.textAlign = 'center'; - const headerText = isDead ? 'SYSTEM FAILURE' : inCombat ? 'COMBAT ENGAGED' : 'ADVENTURE'; + const headerText = isDead + ? 'SYSTEM FAILURE' + : inCombat + ? 'COMBAT ENGAGED' + : 'ADVENTURE'; ctx.fillText(headerText, canvas.width / 2, 60); - // 3. Terminal Window + // 3. Terminal Window const termX = 40; const termY = 90; const termH = 280 + extraHeight; // Stretched dynamically @@ -291,49 +387,96 @@ export default class AdventureImageBuilder { const dotY = termY + 15; ctx.fillStyle = isDead ? '#dc2626' : '#ef4444'; - ctx.beginPath(); ctx.arc(termX + 20, dotY, 6, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); + ctx.arc(termX + 20, dotY, 6, 0, Math.PI * 2); + ctx.fill(); ctx.fillStyle = '#f59e0b'; - ctx.beginPath(); ctx.arc(termX + 40, dotY, 6, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); + ctx.arc(termX + 40, dotY, 6, 0, Math.PI * 2); + ctx.fill(); ctx.fillStyle = '#10b981'; - ctx.beginPath(); ctx.arc(termX + 60, dotY, 6, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); + ctx.arc(termX + 60, dotY, 6, 0, Math.PI * 2); + ctx.fill(); ctx.fillStyle = '#6b7280'; ctx.font = '12px monospace'; ctx.textAlign = 'left'; - ctx.fillText(inCombat ? 'combat_protocol.exe' : isDead ? 'system_dump.log' : 'adventure_logs.sh', termX + 80, termY + 20); + ctx.fillText( + inCombat + ? 'combat_protocol.exe' + : isDead + ? 'system_dump.log' + : 'adventure_logs.sh', + termX + 80, + termY + 20 + ); ctx.fillStyle = '#ffffff0a'; ctx.fillRect(termX, termY + termH - 25, termW, 25); ctx.fillStyle = '#4b5563'; ctx.font = '10px monospace'; - ctx.fillText(`ID: ${scenarioMeta.id.toString().padStart(6, '0')}`, termX + 15, termY + termH - 8); + ctx.fillText( + `ID: ${scenarioMeta.id.toString().padStart(6, '0')}`, + termX + 15, + termY + termH - 8 + ); ctx.textAlign = 'right'; - ctx.fillText(`Author: ${scenarioMeta.author}`, termX + termW - 15, termY + termH - 8); + ctx.fillText( + `Author: ${scenarioMeta.author}`, + termX + termW - 15, + termY + termH - 8 + ); const textColor = isDead ? '#fca5a5' : inCombat ? '#fca5a5' : themeColor; ctx.fillStyle = textColor; ctx.font = '22px monospace'; ctx.textAlign = 'left'; ctx.fillText('>', termX + 20, termY + 60); - - // Execute the final drawing with markdown support! - this.processText(ctx, flavorText, termX + 40, termY + 60, termW - 60, 32, textColor, true); - let yOffset = termY + termH + 25; + // Execute the final drawing with markdown support! + this.processText( + ctx, + flavorText, + termX + 40, + termY + 60, + termW - 60, + 32, + textColor, + true + ); + + let yOffset = termY + termH + 25; // 4. Enemy Stats if (inCombat && enemyStats && !isDead) { - ctx.fillStyle = '#450a0a'; - ctx.strokeStyle = '#ef44444d'; - ctx.beginPath(); ctx.roundRect(termX, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#ef4444b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('ATK', termX + 30, yOffset + 18); - ctx.fillStyle = '#f87171'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); + ctx.fillStyle = '#450a0a'; + ctx.strokeStyle = '#ef44444d'; + ctx.beginPath(); + ctx.roundRect(termX, yOffset, 60, 45, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = '#ef4444b3'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('ATK', termX + 30, yOffset + 18); + ctx.fillStyle = '#f87171'; + ctx.font = 'bold 16px monospace'; + ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); - ctx.fillStyle = '#172554'; + ctx.fillStyle = '#172554'; ctx.strokeStyle = '#3b82f64d'; - ctx.beginPath(); ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#3b82f6b3'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('DEF', termX + termW - 30, yOffset + 18); - ctx.fillStyle = '#60a5fa'; ctx.font = 'bold 16px monospace'; ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); + ctx.beginPath(); + ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = '#3b82f6b3'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('DEF', termX + termW - 30, yOffset + 18); + ctx.fillStyle = '#60a5fa'; + ctx.font = 'bold 16px monospace'; + ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); const eBarX = termX + 75; const eBarW = termW - 150; @@ -342,74 +485,144 @@ export default class AdventureImageBuilder { ctx.textAlign = 'left'; ctx.fillText(enemyStats.name, eBarX, yOffset + 15); ctx.textAlign = 'right'; - ctx.fillText(`${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, eBarX + eBarW, yOffset + 15); + ctx.fillText( + `${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, + eBarX + eBarW, + yOffset + 15 + ); ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); ctx.fill(); - const eHpPercent = Math.max(0, Math.min(enemyStats.currentHp / enemyStats.maxHp, 1)); + ctx.beginPath(); + ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); + ctx.fill(); + const eHpPercent = Math.max( + 0, + Math.min(enemyStats.currentHp / enemyStats.maxHp, 1) + ); ctx.fillStyle = '#dc2626'; - ctx.beginPath(); ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); ctx.fill(); + ctx.beginPath(); + ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); + ctx.fill(); yOffset += 75; } // 5. Player Stats - const hpPercent = Math.max(0, Math.min(mappedStats.hp / mappedStats.maxHp, 1)); - const expPercent = Math.max(0, Math.min(mappedStats.exp / mappedStats.expRequired, 1)); + const hpPercent = Math.max( + 0, + Math.min(mappedStats.hp / mappedStats.maxHp, 1) + ); + const expPercent = Math.max( + 0, + Math.min(mappedStats.exp / mappedStats.expRequired, 1) + ); ctx.fillStyle = isDead ? '#ef4444' : '#34d399'; - ctx.font = 'bold 14px sans-serif'; + ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('Player HP', termX, yOffset); ctx.textAlign = 'right'; - ctx.fillText(`${mappedStats.hp} / ${mappedStats.maxHp}`, termX + termW, yOffset); + ctx.fillText( + `${mappedStats.hp} / ${mappedStats.maxHp}`, + termX + termW, + yOffset + ); ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); ctx.roundRect(termX, yOffset + 12, termW, 12, 6); ctx.fill(); + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW, 12, 6); + ctx.fill(); ctx.fillStyle = isDead ? '#dc2626' : '#10b981'; - ctx.beginPath(); ctx.roundRect(termX, yOffset + 12, termW * hpPercent, 12, 6); ctx.fill(); + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW * hpPercent, 12, 6); + ctx.fill(); yOffset += 45; ctx.fillStyle = '#60a5fa'; - ctx.font = 'bold 14px sans-serif'; + ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`Level ${mappedStats.level}`, termX, yOffset); ctx.fillStyle = '#6b7280'; ctx.textAlign = 'right'; - ctx.fillText(`${mappedStats.exp} / ${mappedStats.expRequired} XP`, termX + termW, yOffset); + ctx.fillText( + `${mappedStats.exp} / ${mappedStats.expRequired} XP`, + termX + termW, + yOffset + ); ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); ctx.roundRect(termX, yOffset + 12, termW, 12, 6); ctx.fill(); + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW, 12, 6); + ctx.fill(); ctx.fillStyle = '#3b82f6'; - ctx.beginPath(); ctx.roundRect(termX, yOffset + 12, termW * expPercent, 12, 6); ctx.fill(); + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW * expPercent, 12, 6); + ctx.fill(); yOffset += 40; - // 6. Active Bonuses + // 6. Active Bonuses if (hasBonuses) { let pillX = termX; - - const drawBonusPill = (label: string, value: string, bgColor: string, borderColor: string, textColor: string) => { + + const drawBonusPill = ( + label: string, + value: string, + bgColor: string, + borderColor: string, + textColor: string + ) => { ctx.font = 'bold 10px sans-serif'; const text = `${label}: ${value}`; const textWidth = ctx.measureText(text).width; - + ctx.fillStyle = bgColor; ctx.strokeStyle = borderColor; - ctx.beginPath(); ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); ctx.fill(); ctx.stroke(); - + ctx.beginPath(); + ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = textColor; ctx.textAlign = 'center'; ctx.fillText(text, pillX + (textWidth + 16) / 2, yOffset + 14); - + pillX += textWidth + 24; }; - if (b.critChance > 5) drawBonusPill('Crit', `${b.critChance}%`, '#713f1233', '#eab30833', '#facc15'); - if (b.lifeSteal > 0) drawBonusPill('Vamp', `${b.lifeSteal}%`, '#450a0a33', '#ef444433', '#f87171'); - if (b.dodge > 0) drawBonusPill('Dodge', `${b.dodge}%`, '#17255433', '#3b82f633', '#93c5fd'); - if (b.thorns > 0) drawBonusPill('Thorns', `${b.thorns}`, '#7c2d1233', '#f9731633', '#fb923c'); + if (b.critChance > 5) + drawBonusPill( + 'Crit', + `${b.critChance}%`, + '#713f1233', + '#eab30833', + '#facc15' + ); + if (b.lifeSteal > 0) + drawBonusPill( + 'Vamp', + `${b.lifeSteal}%`, + '#450a0a33', + '#ef444433', + '#f87171' + ); + if (b.dodge > 0) + drawBonusPill( + 'Dodge', + `${b.dodge}%`, + '#17255433', + '#3b82f633', + '#93c5fd' + ); + if (b.thorns > 0) + drawBonusPill( + 'Thorns', + `${b.thorns}`, + '#7c2d1233', + '#f9731633', + '#fb923c' + ); yOffset += 35; } @@ -419,20 +632,40 @@ export default class AdventureImageBuilder { ctx.textAlign = 'left'; ctx.font = '16px "NotoEmoji", sans-serif'; ctx.fillText('🪙', termX, yOffset + 15); - + ctx.font = 'bold 16px sans-serif'; - ctx.fillText(` GOLD ${mappedStats.gold.toLocaleString()}`, termX + 22, yOffset + 15); + ctx.fillText( + ` GOLD ${mappedStats.gold.toLocaleString()}`, + termX + 22, + yOffset + 15 + ); // 8. TOAST NOTIFICATIONS (Rewards) const rewards = (data as any).rewards; if (rewards) { const toasts: { msg: string; color: string; icon: string }[] = []; - - if (rewards.xp) toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); - if (rewards.gold) toasts.push({ msg: `+${rewards.gold} Gold`, color: '#eab308', icon: '🪙' }); - if (rewards.levelsGained > 0) toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); + + if (rewards.xp) + toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); + if (rewards.gold) + toasts.push({ + msg: `+${rewards.gold} Gold`, + color: '#eab308', + icon: '🪙' + }); + if (rewards.levelsGained > 0) + toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); if (rewards.item) { - const RARITY_COLORS: Record = { Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff', Exotic: '#ff00cc' }; + const RARITY_COLORS: Record = { + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' + }; const itemColor = RARITY_COLORS[rewards.item.rarity] || '#ffffff'; toasts.push({ msg: rewards.item.name, color: itemColor, icon: '🎒' }); } @@ -444,9 +677,11 @@ export default class AdventureImageBuilder { const toastW = msgWidth + 60; const toastH = 40; - ctx.fillStyle = '#0a0a0ae6'; - ctx.beginPath(); ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); ctx.fill(); - + ctx.fillStyle = '#0a0a0ae6'; + ctx.beginPath(); + ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); + ctx.fill(); + ctx.lineWidth = 1; ctx.strokeStyle = `${toast.color}40`; ctx.stroke(); @@ -469,4 +704,4 @@ export default class AdventureImageBuilder { return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/ApiClient.ts b/src/utilities/ApiClient.ts index 153dc22..9ab0e86 100644 --- a/src/utilities/ApiClient.ts +++ b/src/utilities/ApiClient.ts @@ -27,7 +27,9 @@ class CircuitBreaker { this.failures++; if (this.failures >= CIRCUIT_BREAKER_THRESHOLD) { this.openUntil = Date.now() + CIRCUIT_BREAKER_COOLDOWN; - logger.warn(`[ApiClient] Circuit breaker OPEN — API unreachable after ${this.failures} failures. Retrying in ${CIRCUIT_BREAKER_COOLDOWN / 1000}s`); + logger.warn( + `[ApiClient] Circuit breaker OPEN — API unreachable after ${this.failures} failures. Retrying in ${CIRCUIT_BREAKER_COOLDOWN / 1000}s` + ); } } } @@ -38,9 +40,16 @@ const breaker = new CircuitBreaker(); * Centralized API fetch wrapper. * Provides: default headers, request timeout, circuit breaker, structured error context. */ -export async function apiFetch(url: string, options?: RequestInit): Promise { +export async function apiFetch( + url: string, + options?: RequestInit +): Promise { if (breaker.isOpen()) { - throw new ApiError('API_UNAVAILABLE', 'The game server is temporarily unreachable. Please try again in a moment.', 503); + throw new ApiError( + 'API_UNAVAILABLE', + 'The game server is temporarily unreachable. Please try again in a moment.', + 503 + ); } try { @@ -53,7 +62,12 @@ export async function apiFetch(url: string, options?: RequestInit): Promise= 500) { @@ -65,10 +79,18 @@ export async function apiFetch(url: string, options?: RequestInit): Promise = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', Divine: '#00e5ff' }; const TIER_EMOJIS: Record = { - Common: '📦', Uncommon: '🟢', Rare: '🔵', Elite: '🟠', - Epic: '🟣', Legendary: '⭐', Divine: '💎' + Common: '📦', + Uncommon: '🟢', + Rare: '🔵', + Elite: '🟠', + Epic: '🟣', + Legendary: '⭐', + Divine: '💎' }; export interface ChestsPageConfig { @@ -23,7 +37,10 @@ export interface ChestsPageConfig { } export default class ChestsImageBuilder { - public static async build(chests: IChestSlot[], config: ChestsPageConfig): Promise { + public static async build( + chests: IChestSlot[], + config: ChestsPageConfig + ): Promise { const slotW = 160; const slotH = 200; const cols = 4; @@ -58,13 +75,21 @@ export default class ChestsImageBuilder { // Stats line ctx.fillStyle = '#6b7280'; ctx.font = '11px sans-serif'; - ctx.fillText(`${chests.length} / ${config.maxSlots} slots • ${config.totalOpened} opened`, padding, 70); + ctx.fillText( + `${chests.length} / ${config.maxSlots} slots • ${config.totalOpened} opened`, + padding, + 70 + ); // Pity progress ctx.textAlign = 'right'; ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 11px sans-serif'; - ctx.fillText(`Divine Pity: ${config.divinePity}/${config.pityThreshold}`, canvas.width - padding, 40); + ctx.fillText( + `Divine Pity: ${config.divinePity}/${config.pityThreshold}`, + canvas.width - padding, + 40 + ); // Pity bar const pityBarX = canvas.width - padding - 200; @@ -156,9 +181,10 @@ export default class ChestsImageBuilder { } else if (chest.status === 'unlocking') { const remainSec = Math.max(0, Math.floor(chest.remainingMs / 1000)); const h = Math.floor(remainSec / 3600); - const m = Math.floor(remainSec % 3600 / 60); + const m = Math.floor((remainSec % 3600) / 60); const s = remainSec % 60; - const timeStr = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; + const timeStr = + h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; ctx.fillStyle = '#eab308'; ctx.fillText(`⏳ ${timeStr}`, x + slotW / 2, y + 135); @@ -193,4 +219,4 @@ export default class ChestsImageBuilder { return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/CombatResponseBuilder.ts b/src/utilities/CombatResponseBuilder.ts index fb71b01..61da416 100644 --- a/src/utilities/CombatResponseBuilder.ts +++ b/src/utilities/CombatResponseBuilder.ts @@ -1,4 +1,10 @@ -import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder +} from 'discord.js'; import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { type IStepJSON } from '../interfaces/IStepJSON'; import ImageService from './ImageService'; @@ -15,9 +21,13 @@ export interface CombatResponse { * * Single source of truth for the combat UI — change it here, changes everywhere. */ -export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promise { +export async function buildCombatResponse( + data: ICombatJSON | IStepJSON +): Promise { const imageBuffer = await ImageService.adventure(data); - const attachment = new AttachmentBuilder(imageBuffer, { name: 'adventure.png' }); + const attachment = new AttachmentBuilder(imageBuffer, { + name: 'adventure.png' + }); const hasEnemy = !!(data as any).enemy; const embed = new EmbedBuilder() @@ -32,8 +42,7 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis const playerStats = isStepData.playerStats; const showCombatButtons = - isCombatData.combatEnded === false || - isStepData.combatTrigger === true; + isCombatData.combatEnded === false || isStepData.combatTrigger === true; if (showCombatButtons) { const row = new ActionRowBuilder().setComponents( @@ -65,7 +74,11 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis } // Rest button if HP < max and not dead - if (playerStats && playerStats.hp > 0 && playerStats.hp < playerStats.maxHp) { + if ( + playerStats && + playerStats.hp > 0 && + playerStats.hp < playerStats.maxHp + ) { actionRow.addComponents( new ButtonBuilder() .setCustomId('rest') @@ -97,7 +110,9 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis } if (rewards?.chestDrop) { - descParts.push(`📦 Found a **${rewards.chestDrop} Chest** while exploring!`); + descParts.push( + `📦 Found a **${rewards.chestDrop} Chest** while exploring!` + ); } if (descParts.length > 0) { @@ -105,4 +120,4 @@ export async function buildCombatResponse(data: ICombatJSON | IStepJSON): Promis } return { embeds: [embed], files: [attachment], components }; -} \ No newline at end of file +} diff --git a/src/utilities/ErrorMessages.ts b/src/utilities/ErrorMessages.ts index e6ba440..03d1273 100644 --- a/src/utilities/ErrorMessages.ts +++ b/src/utilities/ErrorMessages.ts @@ -5,27 +5,46 @@ const ERROR_MAP: Record = { // API error codes (structured) - 'PLAYER_NOT_FOUND': '📜 **Adventurer not found!** Begin your journey with .', - 'IN_COMBAT': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', - 'INCAPACITATED': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', - 'NO_ACTIVE_COMBAT': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', - 'API_UNAVAILABLE': '🔧 **The realm is under maintenance.** The game server is temporarily unreachable. Please try again in a moment.', - 'API_TIMEOUT': '⏳ **The winds of fate are slow today.** The game server took too long to respond. Please try again.', - 'API_NETWORK_ERROR': '🌐 **Lost connection to the realm.** Could not reach the game server. Please try again later.', + PLAYER_NOT_FOUND: + '📜 **Adventurer not found!** Begin your journey with .', + IN_COMBAT: + "⚔️ **You're already in battle!** Use `/attack` to fight or `/flee` to escape.", + INCAPACITATED: + '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', + NO_ACTIVE_COMBAT: + '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', + API_UNAVAILABLE: + '🔧 **The realm is under maintenance.** The game server is temporarily unreachable. Please try again in a moment.', + API_TIMEOUT: + '⏳ **The winds of fate are slow today.** The game server took too long to respond. Please try again.', + API_NETWORK_ERROR: + '🌐 **Lost connection to the realm.** Could not reach the game server. Please try again later.', // Raw API error strings (legacy matching) - 'You are incapacitated.': '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', - 'You are incapacitated. Wait for regeneration.': '💀 **You have fallen!** Wait for your health to regenerate before venturing out.', - 'No active combat found.': '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', - 'You are currently in combat!': '⚔️ **You\'re already in battle!** Use `/attack` to fight or `/flee` to escape.', - 'Player not found': '📜 **Adventurer not found!** Begin your journey with `/register`.', - 'Player load failed': '📜 **Adventurer not found!** Begin your journey with `/register`.', - 'You need to create player data in order to explore!': '📜 **Adventurer not found!** Begin your journey with `/register`.', - 'Item not found': '🔍 **That item doesn\'t exist.** Check the ID and try again.', - 'Not enough items': '🎒 **Not enough items!** You don\'t have that many in your inventory.', - 'Cannot sell a locked item.': '🔒 **This item is locked!** Unlock it first before selling.', - 'This item cannot be consumed': '❌ **This item can\'t be consumed.** Only consumable items have effects.', - 'You already have player data!': '✅ **You\'re already registered!** Use `/profile` to see your character.' + 'You are incapacitated.': + '💀 **You have fallen!** Your wounds are too severe. Rest and recover before venturing out again.', + 'You are incapacitated. Wait for regeneration.': + '💀 **You have fallen!** Wait for your health to regenerate before venturing out.', + 'No active combat found.': + '🌿 **No enemy in sight.** Use `/explore` to find your next encounter.', + 'You are currently in combat!': + "⚔️ **You're already in battle!** Use `/attack` to fight or `/flee` to escape.", + 'Player not found': + '📜 **Adventurer not found!** Begin your journey with `/register`.', + 'Player load failed': + '📜 **Adventurer not found!** Begin your journey with `/register`.', + 'You need to create player data in order to explore!': + '📜 **Adventurer not found!** Begin your journey with `/register`.', + 'Item not found': + "🔍 **That item doesn't exist.** Check the ID and try again.", + 'Not enough items': + "🎒 **Not enough items!** You don't have that many in your inventory.", + 'Cannot sell a locked item.': + '🔒 **This item is locked!** Unlock it first before selling.', + 'This item cannot be consumed': + "❌ **This item can't be consumed.** Only consumable items have effects.", + 'You already have player data!': + "✅ **You're already registered!** Use `/profile` to see your character." }; /** @@ -52,15 +71,20 @@ export function formatError(error: string, code?: string): string { * Formats a 429 cooldown response with a proper Discord timestamp. * @param cooldownRemainingMs - milliseconds remaining (from API), or null for a generic message */ -export function formatCooldown(action: 'step' | 'combat', cooldownRemainingMs?: number): string { +export function formatCooldown( + action: 'step' | 'combat', + cooldownRemainingMs?: number +): string { if (cooldownRemainingMs) { - const futureTimestamp = Math.floor(Date.now() / 1000) + Math.ceil(cooldownRemainingMs / 1000); + const futureTimestamp = + Math.floor(Date.now() / 1000) + Math.ceil(cooldownRemainingMs / 1000); if (action === 'step') { return `⏳ **Recovering...** You can explore again .`; } return `⏳ **Weapon cooling down!** You can attack again .`; } - if (action === 'step') return '⏳ **Recovering...** Please wait before exploring again.'; + if (action === 'step') + return '⏳ **Recovering...** Please wait before exploring again.'; return '⏳ **Weapon cooling down!** Please wait before attacking again.'; -} \ No newline at end of file +} diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index ca6186b..64bacb1 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -7,7 +7,10 @@ import { type IInventoryItem } from '../interfaces/IInventoryJSON'; import { type ITaskJSON, type IChestSlot } from '../interfaces/IGameJSON'; import ItemManager from '../managers/ItemManager'; import WorkerPool from './WorkerPool'; -import type { LeaderboardEntry, LeaderboardConfig } from './LeaderboardImageBuilder'; +import type { + LeaderboardEntry, + LeaderboardConfig +} from './LeaderboardImageBuilder'; import type { MarketListing, MarketPageConfig } from './MarketImageBuilder'; import type { TasksPageConfig } from './TasksImageBuilder'; import type { ChestsPageConfig } from './ChestsImageBuilder'; @@ -17,7 +20,6 @@ import type { ChestsPageConfig } from './ChestsImageBuilder'; * Routes all canvas work through the WorkerPool. */ export default class ImageService { - private static serializeItemCache(): Record { const cache: Record = {}; for (const [id, item] of ItemManager.cache) { @@ -30,7 +32,10 @@ export default class ImageService { return WorkerPool.run('adventure', { data }); } - public static profile(player: IPlayerJSON, discordUser: User): Promise { + public static profile( + player: IPlayerJSON, + discordUser: User + ): Promise { return WorkerPool.run('profile', { player, avatarUrl: discordUser.displayAvatarURL({ extension: 'png', size: 256 }), @@ -38,7 +43,10 @@ export default class ImageService { }); } - public static inventory(chunk: IInventoryItem[], player: IPlayerJSON): Promise { + public static inventory( + chunk: IInventoryItem[], + player: IPlayerJSON + ): Promise { return WorkerPool.run('inventory', { chunk, player, @@ -50,23 +58,38 @@ export default class ImageService { return WorkerPool.run('item', { item: itemData }); } - public static leaderboard(entries: LeaderboardEntry[], config: LeaderboardConfig): Promise { + public static leaderboard( + entries: LeaderboardEntry[], + config: LeaderboardConfig + ): Promise { return WorkerPool.run('leaderboard', { entries, config }); } - public static market(listings: MarketListing[], config: MarketPageConfig): Promise { + public static market( + listings: MarketListing[], + config: MarketPageConfig + ): Promise { return WorkerPool.run('market', { listings, config }); } - public static travel(playerLevel: number, currentZoneId: number): Promise { + public static travel( + playerLevel: number, + currentZoneId: number + ): Promise { return WorkerPool.run('travel', { playerLevel, currentZoneId }); } - public static tasks(tasks: ITaskJSON[], config: TasksPageConfig): Promise { + public static tasks( + tasks: ITaskJSON[], + config: TasksPageConfig + ): Promise { return WorkerPool.run('tasks', { tasks, config }); } - public static chests(chests: IChestSlot[], config: ChestsPageConfig): Promise { + public static chests( + chests: IChestSlot[], + config: ChestsPageConfig + ): Promise { return WorkerPool.run('chests', { chests, config }); } } diff --git a/src/utilities/ImageWorker.ts b/src/utilities/ImageWorker.ts index bf2d2f5..7c6d6d2 100644 --- a/src/utilities/ImageWorker.ts +++ b/src/utilities/ImageWorker.ts @@ -18,61 +18,75 @@ parentPort.on('message', async (msg: { builderName: string; payload: any }) => { let buffer: Buffer; switch (msg.builderName) { - case 'adventure': - buffer = await AdventureImageBuilder.build(msg.payload.data); - break; + case 'adventure': + buffer = await AdventureImageBuilder.build(msg.payload.data); + break; - case 'profile': - buffer = await ProfileImageBuilder.build( - msg.payload.player, - msg.payload.avatarUrl, - msg.payload.itemCache - ); - break; + case 'profile': + buffer = await ProfileImageBuilder.build( + msg.payload.player, + msg.payload.avatarUrl, + msg.payload.itemCache + ); + break; - case 'inventory': - buffer = await InventoryImageBuilder.build( - msg.payload.chunk, - msg.payload.player, - msg.payload.itemCache - ); - break; + case 'inventory': + buffer = await InventoryImageBuilder.build( + msg.payload.chunk, + msg.payload.player, + msg.payload.itemCache + ); + break; - case 'item': - buffer = await ItemImageBuilder.build(msg.payload.item); - break; + case 'item': + buffer = await ItemImageBuilder.build(msg.payload.item); + break; - case 'leaderboard': - buffer = await LeaderboardImageBuilder.build(msg.payload.entries, msg.payload.config); - break; + case 'leaderboard': + buffer = await LeaderboardImageBuilder.build( + msg.payload.entries, + msg.payload.config + ); + break; - case 'market': - buffer = await MarketImageBuilder.build(msg.payload.listings, msg.payload.config); - break; + case 'market': + buffer = await MarketImageBuilder.build( + msg.payload.listings, + msg.payload.config + ); + break; - case 'travel': - buffer = await TravelImageBuilder.build(msg.payload.playerLevel, msg.payload.currentZoneId); - break; + case 'travel': + buffer = await TravelImageBuilder.build( + msg.payload.playerLevel, + msg.payload.currentZoneId + ); + break; - case 'tasks': - buffer = await TasksImageBuilder.build(msg.payload.tasks, msg.payload.config); - break; + case 'tasks': + buffer = await TasksImageBuilder.build( + msg.payload.tasks, + msg.payload.config + ); + break; - case 'chests': - buffer = await ChestsImageBuilder.build(msg.payload.chests, msg.payload.config); - break; + case 'chests': + buffer = await ChestsImageBuilder.build( + msg.payload.chests, + msg.payload.config + ); + break; - default: - throw new Error(`Unknown builder: ${msg.builderName}`); + default: + throw new Error(`Unknown builder: ${msg.builderName}`); } const arrayBuffer = new ArrayBuffer(buffer.byteLength); new Uint8Array(arrayBuffer).set(buffer); - parentPort!.postMessage( - { success: true, buffer: arrayBuffer }, - [arrayBuffer] - ); + parentPort!.postMessage({ success: true, buffer: arrayBuffer }, [ + arrayBuffer + ]); } catch (err: any) { parentPort!.postMessage({ success: false, diff --git a/src/utilities/InventoryImageBuilder.ts b/src/utilities/InventoryImageBuilder.ts index 8e08af2..97f4f90 100644 --- a/src/utilities/InventoryImageBuilder.ts +++ b/src/utilities/InventoryImageBuilder.ts @@ -5,23 +5,46 @@ import { type IItemJSON } from '../interfaces/IItemJSON'; import ItemManager from '../managers/ItemManager'; import { join } from 'path'; -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' }; const SLOT_ICONS: Record = { - 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', - 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', - 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' + Head: '⛑️', + Necklace: '📿', + Chest: '👕', + MainHand: '⚔️', + Legs: '👖', + OffHand: '🛡️', + Hands: '🧤', + RingA: '💍', + RingB: '💍', + Feet: '👢', + Pet: '🐾', + Special: '✨' }; const CATEGORY_ICONS: Record = { - 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', - 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' + Weapon: '⚔️', + Armor: '🛡️', + Accessory: '💍', + Consumable: '🧪', + Material: '🪵', + Collectible: '🗿' }; function getItemIcon(item: any) { @@ -64,7 +87,9 @@ export default class InventoryImageBuilder { ctx.fillStyle = '#10b981'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'right'; - const goldFormatted = new Intl.NumberFormat('en-US').format(player.coins || 0); + const goldFormatted = new Intl.NumberFormat('en-US').format( + player.coins || 0 + ); ctx.fillText(`LVL ${player.level} • ${goldFormatted} GOLD`, 860, 55); // Divider @@ -86,7 +111,7 @@ export default class InventoryImageBuilder { for (let i = 0; i < chunk.length; i++) { const invEntry = chunk[i]; const itemData = getItem(invEntry.itemId); - + const col = i % 5; const row = Math.floor(i / 5); const boxX = startX + col * (boxW + gapX); @@ -119,7 +144,7 @@ export default class InventoryImageBuilder { const badgeText = `+${enhanceLevel}`; ctx.font = 'bold 10px sans-serif'; const badgeW = ctx.measureText(badgeText).width + 8; - + ctx.fillStyle = '#92400e88'; // amber-900/50 ctx.beginPath(); ctx.roundRect(badgeX, boxY + 10, badgeW, 16, 3); @@ -127,7 +152,7 @@ export default class InventoryImageBuilder { ctx.strokeStyle = '#f59e0b66'; ctx.lineWidth = 1; ctx.stroke(); - + ctx.fillStyle = '#fbbf24'; ctx.textAlign = 'center'; ctx.fillText(badgeText, badgeX + badgeW / 2, boxY + 22); @@ -152,25 +177,38 @@ export default class InventoryImageBuilder { // Bottom Panel BG ctx.fillStyle = '#00000099'; ctx.beginPath(); - ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); + ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); ctx.fill(); // Item Name (with +level suffix if enhanced) ctx.fillStyle = color; ctx.font = 'bold 12px sans-serif'; - const displayName = enhanceLevel > 0 ? `${itemData.name} +${enhanceLevel}` : itemData.name; + const displayName = + enhanceLevel > 0 + ? `${itemData.name} +${enhanceLevel}` + : itemData.name; ctx.fillText(displayName, boxX + boxW / 2, boxY + 132, boxW - 10); // Type & Level ctx.fillStyle = '#6b7280'; ctx.font = '10px sans-serif'; - ctx.fillText(`${itemData.type.toUpperCase()} | LVL ${itemData.level}`, boxX + boxW / 2, boxY + 148); + ctx.fillText( + `${itemData.type.toUpperCase()} | LVL ${itemData.level}`, + boxX + boxW / 2, + boxY + 148 + ); // Value ctx.fillStyle = '#eab308'; ctx.font = '10px sans-serif'; - const totalValue = Math.floor((itemData.value || 0) * invEntry.quantity); - ctx.fillText(`${totalValue.toLocaleString()}g`, boxX + boxW / 2, boxY + 164); + const totalValue = Math.floor( + (itemData.value || 0) * invEntry.quantity + ); + ctx.fillText( + `${totalValue.toLocaleString()}g`, + boxX + boxW / 2, + boxY + 164 + ); // Bottom Rarity Border ctx.beginPath(); @@ -189,4 +227,4 @@ export default class InventoryImageBuilder { return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/ItemImageBuilder.ts b/src/utilities/ItemImageBuilder.ts index c36d825..2bddc9c 100644 --- a/src/utilities/ItemImageBuilder.ts +++ b/src/utilities/ItemImageBuilder.ts @@ -2,27 +2,51 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { type IItemJSON } from '../interfaces/IItemJSON'; import { join } from 'path'; -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' }; const SLOT_ICONS: Record = { - 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', - 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', - 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' + Head: '⛑️', + Necklace: '📿', + Chest: '👕', + MainHand: '⚔️', + Legs: '👖', + OffHand: '🛡️', + Hands: '🧤', + RingA: '💍', + RingB: '💍', + Feet: '👢', + Pet: '🐾', + Special: '✨' }; const CATEGORY_ICONS: Record = { - 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', - 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' + Weapon: '⚔️', + Armor: '🛡️', + Accessory: '💍', + Consumable: '🧪', + Material: '🪵', + Collectible: '🗿' }; function getItemIcon(item: IItemJSON) { - if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; + if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) + return SLOT_ICONS[item.slot]; return CATEGORY_ICONS[item.type] || '📦'; } @@ -31,7 +55,7 @@ export default class ItemImageBuilder { const affixesCount = item.affixes?.length || 0; const enhanceLevel = (item as any).enhanceLevel || 0; const canvasHeight = affixesCount > 0 ? 430 + affixesCount * 45 : 400; - + const canvas = createCanvas(600, canvasHeight); const ctx = canvas.getContext('2d'); const color = RARITY_COLORS[item.rarity] || '#ffffff'; @@ -57,7 +81,8 @@ export default class ItemImageBuilder { ctx.fillText(getItemIcon(item), canvas.width / 2, 85); // Item name with enhance level - const displayName = enhanceLevel > 0 ? `${item.name} +${enhanceLevel}` : item.name; + const displayName = + enhanceLevel > 0 ? `${item.name} +${enhanceLevel}` : item.name; ctx.fillStyle = color; ctx.font = 'bold 32px sans-serif'; ctx.fillText(displayName, canvas.width / 2, 135, 560); @@ -66,7 +91,8 @@ export default class ItemImageBuilder { ctx.font = 'bold 10px sans-serif'; const rarityText = item.rarity.toUpperCase(); let typeText = item.type.toUpperCase(); - if (item.slot && item.slot !== 'None') typeText += ` • ${item.slot.toUpperCase()}`; + if (item.slot && item.slot !== 'None') + typeText += ` • ${item.slot.toUpperCase()}`; const rWidth = ctx.measureText(rarityText).width + 20; const tWidth = ctx.measureText(typeText).width + 20; @@ -79,7 +105,8 @@ export default class ItemImageBuilder { enhWidth = ctx.measureText(enhText).width + 20; } - const totalBadgeWidth = rWidth + tWidth + (enhWidth > 0 ? enhWidth + 10 : 0) + 10; + const totalBadgeWidth = + rWidth + tWidth + (enhWidth > 0 ? enhWidth + 10 : 0) + 10; let currentX = (canvas.width - totalBadgeWidth) / 2; // Rarity Badge @@ -107,7 +134,7 @@ export default class ItemImageBuilder { // Enhancement Badge (amber) if (enhanceLevel > 0) { currentX += tWidth + 10; - ctx.fillStyle = '#92400e44'; // amber-900/25 + ctx.fillStyle = '#92400e44'; // amber-900/25 ctx.strokeStyle = '#f59e0b66'; // amber-500/40 ctx.beginPath(); ctx.roundRect(currentX, 155, enhWidth, 24, 4); @@ -139,20 +166,31 @@ export default class ItemImageBuilder { let effectText = 'Unknown Effect'; let effectColor = '#ffffff'; - if (item.action?.effect === 'HEAL_HP') { effectText = `Restores ${item.action.amount} HP`; effectColor = '#4ade80'; } - else if (item.action?.effect === 'GRANT_XP') { effectText = `Grants ${item.action.amount} XP`; effectColor = '#c084fc'; } - else if (item.action?.effect === 'GRANT_GOLD') { effectText = `Grants ${item.action.amount} Gold`; effectColor = '#fbbf24'; } + if (item.action?.effect === 'HEAL_HP') { + effectText = `Restores ${item.action.amount} HP`; + effectColor = '#4ade80'; + } else if (item.action?.effect === 'GRANT_XP') { + effectText = `Grants ${item.action.amount} XP`; + effectColor = '#c084fc'; + } else if (item.action?.effect === 'GRANT_GOLD') { + effectText = `Grants ${item.action.amount} Gold`; + effectColor = '#fbbf24'; + } ctx.fillStyle = effectColor; ctx.font = 'bold 20px sans-serif'; ctx.fillText(effectText, canvas.width / 2, yOffset + 50); - } else if (item.stats) { const boxW = 150; const gap = 20; const statX = (canvas.width - (boxW * 3 + gap * 2)) / 2; - const drawStatBox = (x: number, label: string, val: number, valColor: string) => { + const drawStatBox = ( + x: number, + label: string, + val: number, + valColor: string + ) => { ctx.fillStyle = '#ffffff0a'; ctx.strokeStyle = '#ffffff1a'; ctx.beginPath(); @@ -172,7 +210,12 @@ export default class ItemImageBuilder { drawStatBox(statX, 'ATK', item.stats.atk || 0, '#f87171'); drawStatBox(statX + boxW + gap, 'DEF', item.stats.def || 0, '#60a5fa'); - drawStatBox(statX + (boxW + gap) * 2, 'HP', item.stats.hp || 0, '#4ade80'); + drawStatBox( + statX + (boxW + gap) * 2, + 'HP', + item.stats.hp || 0, + '#4ade80' + ); } // 6. Affixes @@ -185,37 +228,37 @@ export default class ItemImageBuilder { ctx.fillText('SPECIAL EFFECTS', canvas.width / 2, yOffset); yOffset += 15; - item.affixes!.forEach(affix => { - ctx.fillStyle = '#581c8733'; - ctx.strokeStyle = '#a855f733'; - ctx.beginPath(); - ctx.roundRect(150, yOffset, 300, 32, 4); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#e9d5ff'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); - - const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px monospace'; - ctx.textAlign = 'right'; - ctx.fillText(valText, 435, yOffset + 22); - - yOffset += 40; - }); + item.affixes!.forEach((affix) => { + ctx.fillStyle = '#581c8733'; + ctx.strokeStyle = '#a855f733'; + ctx.beginPath(); + ctx.roundRect(150, yOffset, 300, 32, 4); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#e9d5ff'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); + + const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px monospace'; + ctx.textAlign = 'right'; + ctx.fillText(valText, 435, yOffset + 22); + + yOffset += 40; + }); } // 7. Footer ctx.fillStyle = '#4b5563'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; - + let footerText = `ID: ${item.itemId}`; if (item.level > 1) footerText += ` | REQ LVL: ${item.level}`; - + ctx.fillText(footerText, canvas.width / 2, canvas.height - 20); return canvas.toBuffer('image/png'); diff --git a/src/utilities/ItemViewBuilder.ts b/src/utilities/ItemViewBuilder.ts index dfd1897..869676b 100644 --- a/src/utilities/ItemViewBuilder.ts +++ b/src/utilities/ItemViewBuilder.ts @@ -1,9 +1,15 @@ -import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, type EmbedBuilder } from "discord.js"; -import { type IInventoryItem } from "../interfaces/IInventoryJSON"; -import ItemManager from "../managers/ItemManager"; -import ImageService from "./ImageService"; -import { type IPlayerJSON } from "../interfaces/IPlayerJSON"; -import { type IItemJSON } from "../interfaces/IItemJSON"; +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + type EmbedBuilder +} from 'discord.js'; +import { type IInventoryItem } from '../interfaces/IInventoryJSON'; +import ItemManager from '../managers/ItemManager'; +import ImageService from './ImageService'; +import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; +import { type IItemJSON } from '../interfaces/IItemJSON'; export interface ItemViewResponse { embeds: EmbedBuilder[]; @@ -15,7 +21,10 @@ export interface ItemViewResponse { * Builds the single-item detail view with action buttons. * All buttons encode the inventory document _id for variant-safe operations. */ -export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): Promise { +export async function buildItemView( + player: IPlayerJSON, + item: IInventoryItem +): Promise { const hydratedItem = ItemManager.get(item.itemId); if (!hydratedItem || !item || !player) { @@ -34,36 +43,51 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): (displayItem as any).enhanceLevel = item.enhanceLevel || 0; const buffer = await ImageService.item(displayItem); - const attachment = new AttachmentBuilder(buffer, { name: `${hydratedItem.itemId}.png` }); + const attachment = new AttachmentBuilder(buffer, { + name: `${hydratedItem.itemId}.png` + }); const isWithinLevel = player.level >= hydratedItem.level; const hasSlot = hydratedItem.slot !== 'None'; const isConsumable = hydratedItem.type === 'Consumable'; const isLocked = item.isLocked; - const isModified = item.enhanceLevel > 0 || !!item.statOverrides || !!item.affixOverrides; + const isModified = + item.enhanceLevel > 0 || !!item.statOverrides || !!item.affixOverrides; const docId = item._id; // MongoDB document _id for variant targeting // === ROW 1: Equip / Consume + Lock === let equipText = 'Equip'; - if (!isWithinLevel) equipText = `Required Level: ${hydratedItem.level.toLocaleString()}`; + if (!isWithinLevel) + equipText = `Required Level: ${hydratedItem.level.toLocaleString()}`; if (!hasSlot) equipText = 'Cannot Equip'; if (isLocked) equipText = '🔒 Locked Item'; let equipDisabled = !isWithinLevel || !hasSlot || isLocked; let equipStyle = equipDisabled ? ButtonStyle.Secondary : ButtonStyle.Primary; - if (isConsumable) { equipDisabled = false; equipStyle = ButtonStyle.Primary; } + if (isConsumable) { + equipDisabled = false; + equipStyle = ButtonStyle.Primary; + } const equipButton = new ButtonBuilder() - .setCustomId(isConsumable ? `consume:${docId}:${item.quantity}` : `equip:${docId}:${item.itemId}`) + .setCustomId( + isConsumable + ? `consume:${docId}:${item.quantity}` + : `equip:${docId}:${item.itemId}` + ) .setLabel(isConsumable ? 'Consume' : equipText) - .setDisabled(equipDisabled).setStyle(equipStyle); + .setDisabled(equipDisabled) + .setStyle(equipStyle); const lockButton = new ButtonBuilder() .setCustomId(`lock:${docId}:${item.isLocked ? '1' : '0'}`) .setLabel(item.isLocked ? '🔓 Unlock' : '🔒 Lock') .setStyle(item.isLocked ? ButtonStyle.Success : ButtonStyle.Danger); - const row1 = new ActionRowBuilder().setComponents(equipButton, lockButton); + const row1 = new ActionRowBuilder().setComponents( + equipButton, + lockButton + ); // === ROW 2: Sell + Collect === // Modified items can't be vendor-sold or collected @@ -83,18 +107,33 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): } const sellButton = new ButtonBuilder() - .setCustomId(isModified && !isLocked ? `market_redirect:${item.itemId}` : `sell:${docId}:${item.quantity}`) + .setCustomId( + isModified && !isLocked + ? `market_redirect:${item.itemId}` + : `sell:${docId}:${item.quantity}` + ) .setLabel(sellText) - .setStyle(isConsumable || isLocked || isModified ? ButtonStyle.Secondary : ButtonStyle.Success) + .setStyle( + isConsumable || isLocked || isModified + ? ButtonStyle.Secondary + : ButtonStyle.Success + ) .setDisabled(sellDisabled && !isModified); const collectButton = new ButtonBuilder() .setCustomId(`collect:${docId}:${item.quantity}`) .setLabel(collectText) - .setStyle(isConsumable || isLocked || isModified ? ButtonStyle.Secondary : ButtonStyle.Primary) + .setStyle( + isConsumable || isLocked || isModified + ? ButtonStyle.Secondary + : ButtonStyle.Primary + ) .setDisabled(collectDisabled); - const row2 = new ActionRowBuilder().setComponents(sellButton, collectButton); + const row2 = new ActionRowBuilder().setComponents( + sellButton, + collectButton + ); // === ROW 3: Workshop (Enhance / Reforge / Dismantle) — non-consumable only === const rows: ActionRowBuilder[] = [row1, row2]; @@ -102,7 +141,9 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): if (!isConsumable && hasSlot) { const enhanceButton = new ButtonBuilder() .setCustomId(`enhance:${docId}:${item.itemId}`) - .setLabel(`⬆️ Enhance${item.enhanceLevel > 0 ? ` (+${item.enhanceLevel})` : ''}`) + .setLabel( + `⬆️ Enhance${item.enhanceLevel > 0 ? ` (+${item.enhanceLevel})` : ''}` + ) .setStyle(ButtonStyle.Primary) .setDisabled(isLocked); @@ -118,7 +159,11 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): .setStyle(ButtonStyle.Danger) .setDisabled(isLocked); - const row3 = new ActionRowBuilder().setComponents(enhanceButton, reforgeButton, dismantleButton); + const row3 = new ActionRowBuilder().setComponents( + enhanceButton, + reforgeButton, + dismantleButton + ); rows.push(row3); } else if (!isConsumable && !hasSlot) { // Items without a slot (like materials) can still be dismantled @@ -128,9 +173,11 @@ export async function buildItemView(player: IPlayerJSON, item: IInventoryItem): .setStyle(ButtonStyle.Danger) .setDisabled(isLocked); - const row3 = new ActionRowBuilder().setComponents(dismantleButton); + const row3 = new ActionRowBuilder().setComponents( + dismantleButton + ); rows.push(row3); } return { embeds: [], files: [attachment], components: rows }; -} \ No newline at end of file +} diff --git a/src/utilities/LeaderboardImageBuilder.ts b/src/utilities/LeaderboardImageBuilder.ts index 3c6fdf4..4cd9923 100644 --- a/src/utilities/LeaderboardImageBuilder.ts +++ b/src/utilities/LeaderboardImageBuilder.ts @@ -2,7 +2,12 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { join } from 'path'; // Load emoji font -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} export interface LeaderboardEntry { username: string; @@ -14,8 +19,8 @@ export interface LeaderboardConfig { title: string; stat: string; emoji: string; - accentColor: string; // hex, e.g. '#eab308' - accentColorDim: string; // hex with opacity, e.g. '#eab30833' + accentColor: string; // hex, e.g. '#eab308' + accentColorDim: string; // hex with opacity, e.g. '#eab30833' } const MEDAL_COLORS = ['#fbbf24', '#c0c0c0', '#cd7f32']; // Gold, Silver, Bronze @@ -26,9 +31,13 @@ const PADDING = 40; const CANVAS_WIDTH = 800; export default class LeaderboardImageBuilder { - public static async build(entries: LeaderboardEntry[], config: LeaderboardConfig): Promise { + public static async build( + entries: LeaderboardEntry[], + config: LeaderboardConfig + ): Promise { const rowCount = Math.min(entries.length, 10); - const canvasHeight = HEADER_HEIGHT + rowCount * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; + const canvasHeight = + HEADER_HEIGHT + rowCount * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); @@ -42,7 +51,10 @@ export default class LeaderboardImageBuilder { ctx.strokeStyle = '#ffffff05'; ctx.lineWidth = 1; for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); } // Top accent gradient @@ -68,7 +80,11 @@ export default class LeaderboardImageBuilder { // Subtitle ctx.fillStyle = '#6b7280'; ctx.font = '14px sans-serif'; - ctx.fillText(`Top ${rowCount} Players — Ranked by ${config.stat}`, canvas.width / 2, 105); + ctx.fillText( + `Top ${rowCount} Players — Ranked by ${config.stat}`, + canvas.width / 2, + 105 + ); // --- 3. Rows --- const startY = HEADER_HEIGHT + 10; @@ -122,7 +138,11 @@ export default class LeaderboardImageBuilder { ctx.textAlign = 'right'; ctx.fillStyle = isTop3 ? config.accentColor : '#9ca3af'; ctx.font = `bold ${isTop3 ? '22' : '18'}px monospace`; - ctx.fillText(entry.value.toLocaleString(), PADDING + contentWidth - 15, rowY + 40); + ctx.fillText( + entry.value.toLocaleString(), + PADDING + contentWidth - 15, + rowY + 40 + ); } // --- 4. Footer --- @@ -131,8 +151,12 @@ export default class LeaderboardImageBuilder { ctx.textAlign = 'center'; ctx.fillStyle = '#374151'; ctx.font = '11px sans-serif'; - ctx.fillText('⚔️ DFO Cross-Platform Integration — capi.gg', canvas.width / 2, footerY); + ctx.fillText( + '⚔️ DFO Cross-Platform Integration — capi.gg', + canvas.width / 2, + footerY + ); return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/Logger.ts b/src/utilities/Logger.ts index 28d946f..beefe2d 100644 --- a/src/utilities/Logger.ts +++ b/src/utilities/Logger.ts @@ -23,7 +23,11 @@ const additionalLevels = { // --- File destination (reopenable for rotation) --- // minLength: 0 ensures writes flush quickly — prevents sonic-boom "not ready" on exit -const fileDestination = pino.destination({ dest: LOG_FILE, sync: false, minLength: 0 }); +const fileDestination = pino.destination({ + dest: LOG_FILE, + sync: false, + minLength: 0 +}); /** * Rotates log files when bot.log exceeds MAX_SIZE_BYTES. @@ -50,7 +54,9 @@ function rotateIfNeeded(): void { renameSync(LOG_FILE, join(LOG_DIR, 'bot.1.log')); fileDestination.reopen(); - logger.info(`[Logger] Log rotated. Previous file exceeded ${MAX_SIZE_BYTES / 1024 / 1024}MB`); + logger.info( + `[Logger] Log rotated. Previous file exceeded ${MAX_SIZE_BYTES / 1024 / 1024}MB` + ); } catch (err) { console.error('[Logger] Rotation failed:', err); } @@ -73,7 +79,8 @@ const logger = pino( ignore: 'pid,hostname', levelFirst: true, customLevels: 'dev:35,command:34,player:33,button:32', - customColors: 'dev:magenta,command:magenta,player:magenta,button:magenta', + customColors: + 'dev:magenta,command:magenta,player:magenta,button:magenta', useOnlyCustomProps: false } }) @@ -103,7 +110,15 @@ export function flushAndClose(): void { } // Safely handle exit — wrap in try-catch to silence sonic-boom errors -process.on('beforeExit', () => { try { fileDestination.flushSync(); } catch {} }); -process.on('exit', () => { try { fileDestination.flushSync(); } catch {} }); +process.on('beforeExit', () => { + try { + fileDestination.flushSync(); + } catch {} +}); +process.on('exit', () => { + try { + fileDestination.flushSync(); + } catch {} +}); export default logger; diff --git a/src/utilities/MarketImageBuilder.ts b/src/utilities/MarketImageBuilder.ts index 30ae43d..2c4e698 100644 --- a/src/utilities/MarketImageBuilder.ts +++ b/src/utilities/MarketImageBuilder.ts @@ -1,27 +1,51 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { join } from 'path'; -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' }; const SLOT_ICONS: Record = { - 'Head': '⛑️', 'Necklace': '📿', 'Chest': '👕', 'MainHand': '⚔️', - 'Legs': '👖', 'OffHand': '🛡️', 'Hands': '🧤', 'RingA': '💍', - 'RingB': '💍', 'Feet': '👢', 'Pet': '🐾', 'Special': '✨' + Head: '⛑️', + Necklace: '📿', + Chest: '👕', + MainHand: '⚔️', + Legs: '👖', + OffHand: '🛡️', + Hands: '🧤', + RingA: '💍', + RingB: '💍', + Feet: '👢', + Pet: '🐾', + Special: '✨' }; const CATEGORY_ICONS: Record = { - 'Weapon': '⚔️', 'Armor': '🛡️', 'Accessory': '💍', - 'Consumable': '🧪', 'Material': '🪵', 'Collectible': '🗿' + Weapon: '⚔️', + Armor: '🛡️', + Accessory: '💍', + Consumable: '🧪', + Material: '🪵', + Collectible: '🗿' }; function getItemIcon(item: any): string { - if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; + if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) + return SLOT_ICONS[item.slot]; return CATEGORY_ICONS[item.type] || '📦'; } @@ -55,9 +79,16 @@ const PADDING = 30; const CANVAS_WIDTH = 850; export default class MarketImageBuilder { - public static async build(listings: MarketListing[], config: MarketPageConfig): Promise { + public static async build( + listings: MarketListing[], + config: MarketPageConfig + ): Promise { const rowCount = listings.length; - const canvasHeight = HEADER_HEIGHT + Math.max(rowCount, 1) * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; + const canvasHeight = + HEADER_HEIGHT + + Math.max(rowCount, 1) * ROW_HEIGHT + + FOOTER_HEIGHT + + PADDING; const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); @@ -70,7 +101,10 @@ export default class MarketImageBuilder { ctx.strokeStyle = '#ffffff05'; ctx.lineWidth = 1; for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); } const accentColor = config.mode === 'my_listings' ? '#3b82f6' : '#10b981'; @@ -84,13 +118,18 @@ export default class MarketImageBuilder { ctx.textAlign = 'center'; ctx.fillStyle = accentColor; ctx.font = 'bold 28px sans-serif'; - ctx.fillText(config.mode === 'my_listings' ? 'My Listings' : 'Global Market', canvas.width / 2, 42); + ctx.fillText( + config.mode === 'my_listings' ? 'My Listings' : 'Global Market', + canvas.width / 2, + 42 + ); ctx.fillStyle = '#6b7280'; ctx.font = '13px sans-serif'; ctx.fillText( `${config.totalItems.toLocaleString()} listing${config.totalItems !== 1 ? 's' : ''} — Page ${config.page} of ${Math.max(1, config.totalPages)}`, - canvas.width / 2, 68 + canvas.width / 2, + 68 ); // --- Empty state --- @@ -101,8 +140,11 @@ export default class MarketImageBuilder { ctx.font = 'italic 18px sans-serif'; ctx.textAlign = 'center'; ctx.fillText( - config.mode === 'my_listings' ? 'You have no active listings.' : 'No listings found.', - canvas.width / 2, startY + 36 + config.mode === 'my_listings' + ? 'You have no active listings.' + : 'No listings found.', + canvas.width / 2, + startY + 36 ); } @@ -174,12 +216,20 @@ export default class MarketImageBuilder { ctx.textAlign = 'right'; ctx.font = 'bold 18px monospace'; ctx.fillStyle = '#fbbf24'; - ctx.fillText(`${listing.pricePerUnit.toLocaleString()}g`, PADDING + contentWidth - 12, rowY + 34); + ctx.fillText( + `${listing.pricePerUnit.toLocaleString()}g`, + PADDING + contentWidth - 12, + rowY + 34 + ); if (listing.quantity > 1) { ctx.fillStyle = '#6b7280'; ctx.font = '11px sans-serif'; - ctx.fillText(`Total: ${(listing.pricePerUnit * listing.quantity).toLocaleString()}g`, PADDING + contentWidth - 12, rowY + 52); + ctx.fillText( + `Total: ${(listing.pricePerUnit * listing.quantity).toLocaleString()}g`, + PADDING + contentWidth - 12, + rowY + 52 + ); } } @@ -188,8 +238,12 @@ export default class MarketImageBuilder { ctx.textAlign = 'center'; ctx.fillStyle = '#374151'; ctx.font = '11px sans-serif'; - ctx.fillText('⚔️ DFO Cross-Platform Market — capi.gg', canvas.width / 2, footerY); + ctx.fillText( + '⚔️ DFO Cross-Platform Market — capi.gg', + canvas.width / 2, + footerY + ); return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/PaginatorBuilder.ts b/src/utilities/PaginatorBuilder.ts index b09edd5..a5d4043 100644 --- a/src/utilities/PaginatorBuilder.ts +++ b/src/utilities/PaginatorBuilder.ts @@ -13,7 +13,8 @@ import { export default class PaginatorBuilder { private pages: EmbedBuilder[] = []; private files: AttachmentBuilder[] = []; - private extraRows: ActionRowBuilder[][] = []; + private extraRows: ActionRowBuilder[][] = + []; private idleTimeout: number = 60000; private targetUserId: string | null = null; private isEphemeral: boolean = false; @@ -38,7 +39,9 @@ export default class PaginatorBuilder { * Indexed per page — extraRows[0] = rows for page 0, etc. * Discord max 5 rows total; pagination takes 1, so up to 4 extra per page. */ - public setExtraRows(rows: ActionRowBuilder[][]): this { + public setExtraRows( + rows: ActionRowBuilder[][] + ): this { this.extraRows = rows; return this; } @@ -58,17 +61,33 @@ export default class PaginatorBuilder { return this; } - public async start(interaction: CommandInteraction | MessageComponentInteraction): Promise { + public async start( + interaction: CommandInteraction | MessageComponentInteraction + ): Promise { if (this.pages.length === 0) { - throw new Error('[PaginatorBuilder] Cannot start a paginator with 0 pages.'); + throw new Error( + '[PaginatorBuilder] Cannot start a paginator with 0 pages.' + ); } let currentPage = 0; - const firstBtn = new ButtonBuilder().setCustomId('page_first').setLabel('⏪').setStyle(ButtonStyle.Secondary); - const prevBtn = new ButtonBuilder().setCustomId('page_prev').setLabel('◀').setStyle(ButtonStyle.Primary); - const nextBtn = new ButtonBuilder().setCustomId('page_next').setLabel('▶').setStyle(ButtonStyle.Primary); - const lastBtn = new ButtonBuilder().setCustomId('page_last').setLabel('⏩').setStyle(ButtonStyle.Secondary); + const firstBtn = new ButtonBuilder() + .setCustomId('page_first') + .setLabel('⏪') + .setStyle(ButtonStyle.Secondary); + const prevBtn = new ButtonBuilder() + .setCustomId('page_prev') + .setLabel('◀') + .setStyle(ButtonStyle.Primary); + const nextBtn = new ButtonBuilder() + .setCustomId('page_next') + .setLabel('▶') + .setStyle(ButtonStyle.Primary); + const lastBtn = new ButtonBuilder() + .setCustomId('page_last') + .setLabel('⏩') + .setStyle(ButtonStyle.Secondary); const getNavRow = (index: number) => { return new ActionRowBuilder().addComponents( @@ -79,7 +98,9 @@ export default class PaginatorBuilder { ); }; - const getComponents = (index: number): ActionRowBuilder[] => { + const getComponents = ( + index: number + ): ActionRowBuilder[] => { const rows: ActionRowBuilder[] = []; const pageExtras = this.extraRows[index] ?? []; rows.push(...pageExtras); @@ -91,7 +112,7 @@ export default class PaginatorBuilder { const originalEmbed = this.pages[index]; const embed = EmbedBuilder.from(originalEmbed); const currentFooter = originalEmbed.data.footer?.text || ''; - return embed.setFooter({ + return embed.setFooter({ text: `${currentFooter ? `${currentFooter} | Page ${index + 1} of ${this.pages.length}` : `Page ${index + 1} of ${this.pages.length}`} | ⚔️ DFO Cross-Platform`, iconURL: originalEmbed.data.footer?.icon_url }); @@ -120,7 +141,10 @@ export default class PaginatorBuilder { filter: (i) => { if (!i.customId.startsWith('page_')) return false; if (this.targetUserId && i.user.id !== this.targetUserId) { - i.reply({ content: 'You cannot use these buttons.', ephemeral: true }); + i.reply({ + content: 'You cannot use these buttons.', + ephemeral: true + }); return false; } return true; @@ -130,22 +154,34 @@ export default class PaginatorBuilder { collector.on('collect', async (i) => { collector.resetTimer(); switch (i.customId) { - case 'page_first': currentPage = 0; break; - case 'page_prev': currentPage = Math.max(0, currentPage - 1); break; - case 'page_next': currentPage = Math.min(this.pages.length - 1, currentPage + 1); break; - case 'page_last': currentPage = this.pages.length - 1; break; + case 'page_first': + currentPage = 0; + break; + case 'page_prev': + currentPage = Math.max(0, currentPage - 1); + break; + case 'page_next': + currentPage = Math.min(this.pages.length - 1, currentPage + 1); + break; + case 'page_last': + currentPage = this.pages.length - 1; + break; } await i.update({ embeds: [getEmbed(currentPage)], components: getComponents(currentPage), - files: this.files.length > 0 ? [this.files[currentPage]] : [] + files: this.files.length > 0 ? [this.files[currentPage]] : [] }); }); collector.on('end', async () => { const finalComponents = getComponents(currentPage); - finalComponents.forEach(row => row.components.forEach(c => c.setDisabled(true))); - try { await interaction.editReply({ components: finalComponents }); } catch (e) {} + finalComponents.forEach((row) => + row.components.forEach((c) => c.setDisabled(true)) + ); + try { + await interaction.editReply({ components: finalComponents }); + } catch (e) {} }); } -} \ No newline at end of file +} diff --git a/src/utilities/PlayerGuard.ts b/src/utilities/PlayerGuard.ts index cfe18f9..2a9334a 100644 --- a/src/utilities/PlayerGuard.ts +++ b/src/utilities/PlayerGuard.ts @@ -1,11 +1,15 @@ -import { type ChatInputCommandInteraction, type ButtonInteraction, MessageFlags } from 'discord.js'; +import { + type ChatInputCommandInteraction, + type ButtonInteraction, + MessageFlags +} from 'discord.js'; import Routes from './Routes'; import { apiFetch } from './ApiClient'; /** * Checks if a player is registered before allowing a gameplay command to proceed. * Returns the API response data if registered, or null if not (after sending an error reply). - * + * * Usage: * const playerData = await PlayerGuard.check(interaction); * if (!playerData) return; // Guard already replied with a helpful message @@ -25,7 +29,8 @@ export default class PlayerGuard { const res = await apiFetch(Routes.player(id)); if (res.status === 404) { - const content = '📜 **Adventurer not found!**\nYou need to register before you can play. Use the `/register` command to begin your journey!'; + const content = + '📜 **Adventurer not found!**\nYou need to register before you can play. Use the `/register` command to begin your journey!'; if (interaction.deferred || interaction.replied) { await interaction.editReply({ content }); @@ -43,4 +48,4 @@ export default class PlayerGuard { return null; } } -} \ No newline at end of file +} diff --git a/src/utilities/ProfileImageBuilder.ts b/src/utilities/ProfileImageBuilder.ts index 86f47fe..100a4d6 100644 --- a/src/utilities/ProfileImageBuilder.ts +++ b/src/utilities/ProfileImageBuilder.ts @@ -7,7 +7,10 @@ import { join } from 'path'; // --- CRITICAL FIX: Load the font directly from the project files --- // process.cwd() ensures it always looks in the root folder, regardless of compiled dist/ paths -GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); +GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' +); export default class ProfileImageBuilder { /** @@ -32,7 +35,7 @@ export default class ProfileImageBuilder { const ctx = canvas.getContext('2d'); // --- Svelte Logic Conversions --- - const xpToNext = Math.floor(50 * (player.level || 1)**1.3); + const xpToNext = Math.floor(50 * (player.level || 1) ** 1.3); const xpProgress = Math.min(player.experience / xpToNext, 1); const totalAtk = player.stats?.atk || 0; const totalDef = player.stats?.def || 0; @@ -56,20 +59,35 @@ export default class ProfileImageBuilder { ctx.save(); ctx.beginPath(); - ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true); + ctx.arc( + avatarX + avatarSize / 2, + avatarY + avatarSize / 2, + avatarSize / 2, + 0, + Math.PI * 2, + true + ); ctx.closePath(); ctx.clip(); // Support both a Discord User object (main thread) and a plain URL string (worker thread) - const avatarUrl = typeof discordUser === 'string' - ? discordUser - : discordUser.displayAvatarURL({ extension: 'png', size: 256 }); + const avatarUrl = + typeof discordUser === 'string' + ? discordUser + : discordUser.displayAvatarURL({ extension: 'png', size: 256 }); const avatarImage = await loadImage(avatarUrl); ctx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize); ctx.restore(); ctx.beginPath(); - ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true); + ctx.arc( + avatarX + avatarSize / 2, + avatarY + avatarSize / 2, + avatarSize / 2, + 0, + Math.PI * 2, + true + ); ctx.lineWidth = 4; ctx.strokeStyle = '#10b981'; ctx.stroke(); @@ -84,31 +102,63 @@ export default class ProfileImageBuilder { ctx.fillStyle = '#10b981'; ctx.font = 'bold 16px sans-serif'; - ctx.fillText(`[${player.privilege.toUpperCase()}]`, 180 + nameWidth + 15, 72); + ctx.fillText( + `[${player.privilege.toUpperCase()}]`, + 180 + nameWidth + 15, + 72 + ); ctx.fillStyle = '#9ca3af'; ctx.font = '14px monospace'; ctx.fillText(`ID: ${player.id}`, 180, 100); // --- 4. Stats Grid --- - const drawGridBox = (x: number, y: number, label: string, value: string, borderColor: string, valueColor: string) => { + const drawGridBox = ( + x: number, + y: number, + label: string, + value: string, + borderColor: string, + valueColor: string + ) => { ctx.fillStyle = '#1a1a1a'; ctx.fillRect(x, y, 180, 70); ctx.fillStyle = borderColor; ctx.fillRect(x, y, 4, 70); - + ctx.fillStyle = '#6b7280'; ctx.font = '12px sans-serif'; ctx.fillText(label.toUpperCase(), x + 15, y + 25); - + ctx.fillStyle = valueColor; ctx.font = 'bold 24px monospace'; ctx.fillText(value, x + 15, y + 55); }; - drawGridBox(180, 130, 'Level', player.level.toString(), '#eab308', '#ffffff'); - drawGridBox(375, 130, 'Skill Points', player.skillPoints.toString(), '#3b82f6', '#ffffff'); - drawGridBox(570, 130, 'Coins', player.coins.toLocaleString(), '#f59e0b', '#fbbf24'); + drawGridBox( + 180, + 130, + 'Level', + player.level.toString(), + '#eab308', + '#ffffff' + ); + drawGridBox( + 375, + 130, + 'Skill Points', + player.skillPoints.toString(), + '#3b82f6', + '#ffffff' + ); + drawGridBox( + 570, + 130, + 'Coins', + player.coins.toLocaleString(), + '#f59e0b', + '#fbbf24' + ); // --- 5. XP Bar --- const barX = 180; @@ -129,7 +179,11 @@ export default class ProfileImageBuilder { ctx.fillStyle = '#ffffff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(`${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, barX + barWidth / 2, barY + 16); + ctx.fillText( + `${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, + barX + barWidth / 2, + barY + 16 + ); ctx.textAlign = 'left'; // --- 6. Combat Stats Panel --- @@ -156,7 +210,13 @@ export default class ProfileImageBuilder { ctx.roundRect(60, panelY + 45, 670 * hpProgress, 12, 6); ctx.fill(); - const drawStatBox = (x: number, y: number, label: string, value: string, color: string) => { + const drawStatBox = ( + x: number, + y: number, + label: string, + value: string, + color: string + ) => { ctx.fillStyle = '#00000066'; ctx.beginPath(); ctx.roundRect(x, y, 325, 50, 6); @@ -193,15 +253,29 @@ export default class ProfileImageBuilder { // Grid Settings (4 columns, 3 rows) const equipSlots = [ - { key: 'Head', icon: '⛑️' }, { key: 'Necklace', icon: '📿' }, { key: 'Chest', icon: '👕' }, { key: 'MainHand', icon: '⚔️' }, - { key: 'Legs', icon: '👖' }, { key: 'OffHand', icon: '🛡️' }, { key: 'Hands', icon: '🧤' }, { key: 'RingA', icon: '💍' }, - { key: 'Feet', icon: '👢' }, { key: 'RingB', icon: '💍' }, { key: 'Pet', icon: '🐾' }, { key: 'Special', icon: '✨' } + { key: 'Head', icon: '⛑️' }, + { key: 'Necklace', icon: '📿' }, + { key: 'Chest', icon: '👕' }, + { key: 'MainHand', icon: '⚔️' }, + { key: 'Legs', icon: '👖' }, + { key: 'OffHand', icon: '🛡️' }, + { key: 'Hands', icon: '🧤' }, + { key: 'RingA', icon: '💍' }, + { key: 'Feet', icon: '👢' }, + { key: 'RingB', icon: '💍' }, + { key: 'Pet', icon: '🐾' }, + { key: 'Special', icon: '✨' } ]; const RARITY_COLORS: Record = { - Common: '#b0b0b0', Uncommon: '#2ecc71', Rare: '#3498db', - Elite: '#e67e22', Epic: '#9b59b6', Legendary: '#f1c40f', - Divine: '#00e5ff', Exotic: '#ff00cc' + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' }; const gridStartX = 60; @@ -220,7 +294,9 @@ export default class ProfileImageBuilder { const boxY = gridStartY + row * (boxHeight + gapY); // Fetch item data if equipped - const equippedRef = player.equipment ? (player.equipment as any)[slot.key] : null; + const equippedRef = player.equipment + ? (player.equipment as any)[slot.key] + : null; let itemData = null; if (equippedRef?.itemId) { itemData = getItem(equippedRef.itemId) ?? null; @@ -251,11 +327,16 @@ export default class ProfileImageBuilder { if (itemData) { const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - + // Truncate name if it's too long (using canvas maxWidth param) ctx.fillStyle = color; ctx.font = 'bold 13px sans-serif'; - ctx.fillText(itemData.name, boxX + boxWidth / 2, boxY + 65, boxWidth - 10); + ctx.fillText( + itemData.name, + boxX + boxWidth / 2, + boxY + 65, + boxWidth - 10 + ); // Item Level ctx.fillStyle = '#6b7280'; @@ -281,4 +362,4 @@ export default class ProfileImageBuilder { return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/Routes.ts b/src/utilities/Routes.ts index 8229908..645cbf0 100644 --- a/src/utilities/Routes.ts +++ b/src/utilities/Routes.ts @@ -1,6 +1,6 @@ export default class Routes { public static HEADERS = () => ({ - 'Authorization': `Bearer ${process.env.BOT_TOKEN}`, + Authorization: `Bearer ${process.env.BOT_TOKEN}`, 'Content-Type': 'application/json' }); @@ -136,12 +136,22 @@ export default class Routes { // ========== MARKET ========== - public static marketBrowse(discordId: string, params?: { page?: number; search?: string; rarity?: string; type?: string; sort?: string }): string { + public static marketBrowse( + discordId: string, + params?: { + page?: number; + search?: string; + rarity?: string; + type?: string; + sort?: string; + } + ): string { const base = `https://capi.gg/api/market?discordId=${discordId}&limit=8`; const qs = new URLSearchParams(); if (params?.page) qs.set('page', String(params.page)); if (params?.search) qs.set('search', params.search); - if (params?.rarity && params.rarity !== 'All') qs.set('rarity', params.rarity); + if (params?.rarity && params.rarity !== 'All') + qs.set('rarity', params.rarity); if (params?.type && params.type !== 'All') qs.set('type', params.type); if (params?.sort) qs.set('sort', params.sort); const extra = qs.toString(); @@ -187,4 +197,4 @@ export default class Routes { public static bulkDismantle(): string { return 'https://capi.gg/api/inventory/bulk-dismantle'; } -} \ No newline at end of file +} diff --git a/src/utilities/TasksImageBuilder.ts b/src/utilities/TasksImageBuilder.ts index 49535e6..5b816ac 100644 --- a/src/utilities/TasksImageBuilder.ts +++ b/src/utilities/TasksImageBuilder.ts @@ -2,9 +2,17 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { join } from 'path'; import type { ITaskJSON } from '../interfaces/IGameJSON'; -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} - -const PERIOD_COLORS: Record = { +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} + +const PERIOD_COLORS: Record< + string, + { bg: string; border: string; text: string } +> = { daily: { bg: '#064e3b33', border: '#10b98144', text: '#34d399' }, weekly: { bg: '#1e1b4b33', border: '#6366f144', text: '#818cf8' }, monthly: { bg: '#4a044e33', border: '#c026d344', text: '#e879f9' } @@ -17,7 +25,10 @@ export interface TasksPageConfig { } export default class TasksImageBuilder { - public static async build(tasks: ITaskJSON[], config: TasksPageConfig): Promise { + public static async build( + tasks: ITaskJSON[], + config: TasksPageConfig + ): Promise { const rowH = 90; const headerH = 100; const footerH = 40; @@ -63,7 +74,8 @@ export default class TasksImageBuilder { // Reset timer const resetMin = Math.floor(config.resetIn / 60000); const resetH = Math.floor(resetMin / 60); - const resetStr = resetH > 0 ? `${resetH}h ${resetMin % 60}m` : `${resetMin}m`; + const resetStr = + resetH > 0 ? `${resetH}h ${resetMin % 60}m` : `${resetMin}m`; ctx.fillStyle = '#6b7280'; ctx.font = '10px sans-serif'; ctx.textAlign = 'right'; @@ -87,16 +99,27 @@ export default class TasksImageBuilder { let y = headerH; for (const task of tasks) { - const pct = Math.min(100, Math.floor(task.progress / task.target * 100)); + const pct = Math.min( + 100, + Math.floor((task.progress / task.target) * 100) + ); const isComplete = task.progress >= task.target; const isClaimed = task.claimed; // Row background - ctx.fillStyle = isClaimed ? '#00000020' : isComplete ? '#064e3b22' : '#ffffff06'; + ctx.fillStyle = isClaimed + ? '#00000020' + : isComplete + ? '#064e3b22' + : '#ffffff06'; ctx.beginPath(); ctx.roundRect(30, y, canvas.width - 60, rowH - 10, 10); ctx.fill(); - ctx.strokeStyle = isClaimed ? '#ffffff0a' : isComplete ? '#10b98133' : '#ffffff10'; + ctx.strokeStyle = isClaimed + ? '#ffffff0a' + : isComplete + ? '#10b98133' + : '#ffffff10'; ctx.lineWidth = 1; ctx.stroke(); @@ -114,7 +137,11 @@ export default class TasksImageBuilder { // Progress text ctx.font = '11px monospace'; ctx.fillStyle = isComplete ? '#34d399' : '#9ca3af'; - ctx.fillText(`${task.progress.toLocaleString()} / ${task.target.toLocaleString()}`, 90, y + 50); + ctx.fillText( + `${task.progress.toLocaleString()} / ${task.target.toLocaleString()}`, + 90, + y + 50 + ); // Progress bar const barX = 90; @@ -128,7 +155,11 @@ export default class TasksImageBuilder { ctx.fill(); if (pct > 0) { - ctx.fillStyle = isClaimed ? '#4b5563' : isComplete ? '#10b981' : '#3b82f6'; + ctx.fillStyle = isClaimed + ? '#4b5563' + : isComplete + ? '#10b981' + : '#3b82f6'; ctx.beginPath(); ctx.roundRect(barX, barY, barW * (pct / 100), barH, 4); ctx.fill(); @@ -171,9 +202,13 @@ export default class TasksImageBuilder { ctx.fillStyle = '#6b7280'; ctx.font = 'italic 16px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText('No tasks available for this period.', canvas.width / 2, headerH + 50); + ctx.fillText( + 'No tasks available for this period.', + canvas.width / 2, + headerH + 50 + ); } return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/TravelImageBuilder.ts b/src/utilities/TravelImageBuilder.ts index cc24768..7b0cf6a 100644 --- a/src/utilities/TravelImageBuilder.ts +++ b/src/utilities/TravelImageBuilder.ts @@ -2,11 +2,20 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { join } from 'path'; import { ZONES, type ZoneInfo } from './ZoneData'; -try { GlobalFonts.registerFromPath(join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), 'NotoEmoji'); } catch(e) {} +try { + GlobalFonts.registerFromPath( + join(process.cwd(), 'assets', 'NotoColorEmoji-Regular.ttf'), + 'NotoEmoji' + ); +} catch (e) {} const RARITY_COLORS: Record = { - Uncommon: '#2ecc71', Rare: '#3498db', Elite: '#e67e22', - Epic: '#9b59b6', Legendary: '#f1c40f', Divine: '#00e5ff' + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff' }; const TIER_COLORS: Record = { @@ -15,7 +24,7 @@ const TIER_COLORS: Record = { 'The Hero': '#e67e22', 'The Ascendant': '#9b59b6', 'The Cosmic': '#00e5ff', - 'Beyond': '#ff00cc' + Beyond: '#ff00cc' }; const ROW_HEIGHT = 52; @@ -26,7 +35,10 @@ const PADDING = 30; const CANVAS_WIDTH = 800; export default class TravelImageBuilder { - public static async build(playerLevel: number, currentZoneId: number): Promise { + public static async build( + playerLevel: number, + currentZoneId: number + ): Promise { // Group zones by tier const tiers = new Map(); for (const zone of ZONES) { @@ -43,7 +55,12 @@ export default class TravelImageBuilder { totalRows += zones.length; } - const canvasHeight = HEADER_HEIGHT + tierCount * TIER_HEADER_HEIGHT + totalRows * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; + const canvasHeight = + HEADER_HEIGHT + + tierCount * TIER_HEADER_HEIGHT + + totalRows * ROW_HEIGHT + + FOOTER_HEIGHT + + PADDING; const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); const ctx = canvas.getContext('2d'); @@ -56,7 +73,10 @@ export default class TravelImageBuilder { ctx.strokeStyle = '#ffffff05'; ctx.lineWidth = 1; for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); } // Header gradient @@ -72,12 +92,13 @@ export default class TravelImageBuilder { ctx.font = 'bold 26px sans-serif'; ctx.fillText('Zone Map', canvas.width / 2, 38); - const currentZone = ZONES.find(z => z.id === currentZoneId); + const currentZone = ZONES.find((z) => z.id === currentZoneId); ctx.fillStyle = '#6b7280'; ctx.font = '13px sans-serif'; ctx.fillText( `Current: ${currentZone?.name ?? 'Unknown'} • Level ${playerLevel}`, - canvas.width / 2, 62 + canvas.width / 2, + 62 ); // --- Zone rows grouped by tier --- @@ -133,7 +154,9 @@ export default class TravelImageBuilder { ctx.font = `${isCurrentZone ? 'bold ' : ''}15px sans-serif`; ctx.fillText( `${isAccessible ? '' : '🔒 '}${zone.name}`, - textX, y + 22, 340 + textX, + y + 22, + 340 ); // Description @@ -145,7 +168,11 @@ export default class TravelImageBuilder { ctx.textAlign = 'right'; ctx.fillStyle = isAccessible ? '#4b5563' : '#ef4444'; ctx.font = '11px sans-serif'; - ctx.fillText(`Lvl ${zone.levelReq}+`, PADDING + contentWidth - 100, y + 22); + ctx.fillText( + `Lvl ${zone.levelReq}+`, + PADDING + contentWidth - 100, + y + 22 + ); // Rarity cap pill ctx.fillStyle = `${rarityColor}20`; @@ -175,4 +202,4 @@ export default class TravelImageBuilder { return canvas.toBuffer('image/png'); } -} \ No newline at end of file +} diff --git a/src/utilities/WorkerPool.ts b/src/utilities/WorkerPool.ts index 6e2d2d8..950894d 100644 --- a/src/utilities/WorkerPool.ts +++ b/src/utilities/WorkerPool.ts @@ -28,7 +28,10 @@ export default class WorkerPool { // Resolve to the compiled .js worker in production, or .ts in development const isCompiled = __filename.endsWith('.js'); - const workerFile = join(__dirname, isCompiled ? 'ImageWorker.js' : 'ImageWorker.ts'); + const workerFile = join( + __dirname, + isCompiled ? 'ImageWorker.js' : 'ImageWorker.ts' + ); for (let i = 0; i < poolSize; i++) { const worker = new Worker(workerFile, { @@ -51,7 +54,9 @@ export default class WorkerPool { } this.isInitialized = true; - logger.info(`[WorkerPool] Initialized ${poolSize} workers (${coreCount} CPU cores detected)`); + logger.info( + `[WorkerPool] Initialized ${poolSize} workers (${coreCount} CPU cores detected)` + ); } /** @@ -60,7 +65,9 @@ export default class WorkerPool { */ public static run(builderName: string, payload: any): Promise { if (!this.isInitialized) { - throw new Error('[WorkerPool] Pool not initialized — call WorkerPool.init() first'); + throw new Error( + '[WorkerPool] Pool not initialized — call WorkerPool.init() first' + ); } return new Promise((resolve, reject) => { @@ -79,7 +86,11 @@ export default class WorkerPool { * Send a task to a specific worker and listen for the result. */ private static execute(worker: Worker, task: QueuedTask): void { - const onMessage = (result: { success: boolean; buffer?: ArrayBuffer; error?: string }) => { + const onMessage = (result: { + success: boolean; + buffer?: ArrayBuffer; + error?: string; + }) => { // Clean up this specific listener worker.off('message', onMessage); worker.off('error', onError); @@ -146,4 +157,4 @@ export default class WorkerPool { queued: this.queue.length }; } -} \ No newline at end of file +} diff --git a/src/utilities/ZoneData.ts b/src/utilities/ZoneData.ts index 7cc1d96..7b74be2 100644 --- a/src/utilities/ZoneData.ts +++ b/src/utilities/ZoneData.ts @@ -33,36 +33,217 @@ function getTier(zoneId: number): string { } export const ZONES: ZoneInfo[] = [ - { id: 1, name: 'Greenleaf Meadow', description: 'A safe haven for beginners. Slimes and lost trinkets abound.', levelReq: 1, tier: getTier(1), rarityCap: 'Uncommon', combatChance: 10, tollCost: 0 }, - { id: 2, name: 'Misty Creek', description: 'The fog hides goblins, but the river washes up gold.', levelReq: 5, tier: getTier(2), rarityCap: 'Uncommon', combatChance: 12, tollCost: 0 }, - { id: 3, name: "Bandit's Highway", description: 'A dangerous trade route. High risk, but the thieves are wealthy.', levelReq: 10, tier: getTier(3), rarityCap: 'Rare', combatChance: 18, tollCost: 0 }, - { id: 4, name: 'Whispering Woods', description: 'Ancient trees that hum with magic. Good for training.', levelReq: 15, tier: getTier(4), rarityCap: 'Rare', combatChance: 15, tollCost: 0 }, - { id: 5, name: 'Crumbling Ruins', description: 'The remains of an old kingdom. Undead guard the treasures.', levelReq: 25, tier: getTier(5), rarityCap: 'Elite', combatChance: 20, tollCost: 0 }, - { id: 6, name: 'Sunken Grotto', description: 'Damp, dark, and filled with glowing crystals.', levelReq: 35, tier: getTier(6), rarityCap: 'Elite', combatChance: 18, tollCost: 0 }, - { id: 7, name: 'Ironclad Fortress', description: 'A stronghold of elite soldiers. Brutal combat training.', levelReq: 50, tier: getTier(7), rarityCap: 'Epic', combatChance: 30, tollCost: 5 }, - { id: 8, name: 'Crystal Spire', description: 'A tower reaching for the heavens. The air hums with power.', levelReq: 75, tier: getTier(8), rarityCap: 'Epic', combatChance: 22, tollCost: 10 }, - { id: 9, name: 'Molten Core', description: 'The heat is unbearable. Only the strongest survive.', levelReq: 100, tier: getTier(9), rarityCap: 'Legendary', combatChance: 28, tollCost: 25 }, - { id: 10, name: "The Void's Edge", description: 'Reality flickers here. The loot is otherworldly.', levelReq: 150, tier: getTier(10), rarityCap: 'Legendary', combatChance: 35, tollCost: 50 }, - { id: 11, name: "Dragon's Fall", description: 'The impact site of the Great Fall. The source of all magic.', levelReq: 200, tier: getTier(11), rarityCap: 'Divine', combatChance: 45, tollCost: 100 }, - { id: 12, name: 'Plane of Eternal Fire', description: 'The ground itself is alive. Elementals roam freely.', levelReq: 250, tier: getTier(12), rarityCap: 'Divine', combatChance: 40, tollCost: 150 }, - { id: 13, name: 'Glacial Expanse', description: 'Time moves slower here. The cold stops the heart.', levelReq: 300, tier: getTier(13), rarityCap: 'Divine', combatChance: 42, tollCost: 200 }, - { id: 14, name: 'Thunderpeak', description: 'A mountain peak above the clouds. Storms never cease.', levelReq: 350, tier: getTier(14), rarityCap: 'Divine', combatChance: 45, tollCost: 300 }, - { id: 15, name: "Titan's Grave", description: 'Where the giants fell. Their bones form the landscape.', levelReq: 400, tier: getTier(15), rarityCap: 'Divine', combatChance: 48, tollCost: 400 }, - { id: 16, name: 'Stardust Sanctuary', description: 'Gravity is a suggestion here. Stars drift like sand.', levelReq: 500, tier: getTier(16), rarityCap: 'Divine', combatChance: 35, tollCost: 500 }, - { id: 17, name: 'Nebula of Souls', description: 'The spirits of the ancients watch your every step.', levelReq: 600, tier: getTier(17), rarityCap: 'Divine', combatChance: 40, tollCost: 650 }, - { id: 18, name: 'Black Hole Horizon', description: 'Light cannot escape. Hope struggles to survive.', levelReq: 700, tier: getTier(18), rarityCap: 'Divine', combatChance: 50, tollCost: 800 }, - { id: 19, name: "Creation's Forge", description: 'Where worlds are made and destroyed.', levelReq: 800, tier: getTier(19), rarityCap: 'Divine', combatChance: 55, tollCost: 1000 }, - { id: 20, name: 'The Absolute', description: 'The end of all things. The beginning of eternity.', levelReq: 900, tier: getTier(20), rarityCap: 'Divine', combatChance: 60, tollCost: 1200 } + { + id: 1, + name: 'Greenleaf Meadow', + description: 'A safe haven for beginners. Slimes and lost trinkets abound.', + levelReq: 1, + tier: getTier(1), + rarityCap: 'Uncommon', + combatChance: 10, + tollCost: 0 + }, + { + id: 2, + name: 'Misty Creek', + description: 'The fog hides goblins, but the river washes up gold.', + levelReq: 5, + tier: getTier(2), + rarityCap: 'Uncommon', + combatChance: 12, + tollCost: 0 + }, + { + id: 3, + name: "Bandit's Highway", + description: + 'A dangerous trade route. High risk, but the thieves are wealthy.', + levelReq: 10, + tier: getTier(3), + rarityCap: 'Rare', + combatChance: 18, + tollCost: 0 + }, + { + id: 4, + name: 'Whispering Woods', + description: 'Ancient trees that hum with magic. Good for training.', + levelReq: 15, + tier: getTier(4), + rarityCap: 'Rare', + combatChance: 15, + tollCost: 0 + }, + { + id: 5, + name: 'Crumbling Ruins', + description: 'The remains of an old kingdom. Undead guard the treasures.', + levelReq: 25, + tier: getTier(5), + rarityCap: 'Elite', + combatChance: 20, + tollCost: 0 + }, + { + id: 6, + name: 'Sunken Grotto', + description: 'Damp, dark, and filled with glowing crystals.', + levelReq: 35, + tier: getTier(6), + rarityCap: 'Elite', + combatChance: 18, + tollCost: 0 + }, + { + id: 7, + name: 'Ironclad Fortress', + description: 'A stronghold of elite soldiers. Brutal combat training.', + levelReq: 50, + tier: getTier(7), + rarityCap: 'Epic', + combatChance: 30, + tollCost: 5 + }, + { + id: 8, + name: 'Crystal Spire', + description: 'A tower reaching for the heavens. The air hums with power.', + levelReq: 75, + tier: getTier(8), + rarityCap: 'Epic', + combatChance: 22, + tollCost: 10 + }, + { + id: 9, + name: 'Molten Core', + description: 'The heat is unbearable. Only the strongest survive.', + levelReq: 100, + tier: getTier(9), + rarityCap: 'Legendary', + combatChance: 28, + tollCost: 25 + }, + { + id: 10, + name: "The Void's Edge", + description: 'Reality flickers here. The loot is otherworldly.', + levelReq: 150, + tier: getTier(10), + rarityCap: 'Legendary', + combatChance: 35, + tollCost: 50 + }, + { + id: 11, + name: "Dragon's Fall", + description: 'The impact site of the Great Fall. The source of all magic.', + levelReq: 200, + tier: getTier(11), + rarityCap: 'Divine', + combatChance: 45, + tollCost: 100 + }, + { + id: 12, + name: 'Plane of Eternal Fire', + description: 'The ground itself is alive. Elementals roam freely.', + levelReq: 250, + tier: getTier(12), + rarityCap: 'Divine', + combatChance: 40, + tollCost: 150 + }, + { + id: 13, + name: 'Glacial Expanse', + description: 'Time moves slower here. The cold stops the heart.', + levelReq: 300, + tier: getTier(13), + rarityCap: 'Divine', + combatChance: 42, + tollCost: 200 + }, + { + id: 14, + name: 'Thunderpeak', + description: 'A mountain peak above the clouds. Storms never cease.', + levelReq: 350, + tier: getTier(14), + rarityCap: 'Divine', + combatChance: 45, + tollCost: 300 + }, + { + id: 15, + name: "Titan's Grave", + description: 'Where the giants fell. Their bones form the landscape.', + levelReq: 400, + tier: getTier(15), + rarityCap: 'Divine', + combatChance: 48, + tollCost: 400 + }, + { + id: 16, + name: 'Stardust Sanctuary', + description: 'Gravity is a suggestion here. Stars drift like sand.', + levelReq: 500, + tier: getTier(16), + rarityCap: 'Divine', + combatChance: 35, + tollCost: 500 + }, + { + id: 17, + name: 'Nebula of Souls', + description: 'The spirits of the ancients watch your every step.', + levelReq: 600, + tier: getTier(17), + rarityCap: 'Divine', + combatChance: 40, + tollCost: 650 + }, + { + id: 18, + name: 'Black Hole Horizon', + description: 'Light cannot escape. Hope struggles to survive.', + levelReq: 700, + tier: getTier(18), + rarityCap: 'Divine', + combatChance: 50, + tollCost: 800 + }, + { + id: 19, + name: "Creation's Forge", + description: 'Where worlds are made and destroyed.', + levelReq: 800, + tier: getTier(19), + rarityCap: 'Divine', + combatChance: 55, + tollCost: 1000 + }, + { + id: 20, + name: 'The Absolute', + description: 'The end of all things. The beginning of eternity.', + levelReq: 900, + tier: getTier(20), + rarityCap: 'Divine', + combatChance: 60, + tollCost: 1200 + } ]; export function getZone(id: number): ZoneInfo | undefined { - return ZONES.find(z => z.id === id); + return ZONES.find((z) => z.id === id); } export function getAccessibleZones(playerLevel: number): ZoneInfo[] { - return ZONES.filter(z => playerLevel >= z.levelReq); + return ZONES.filter((z) => playerLevel >= z.levelReq); } export function getAllZones(): ZoneInfo[] { return ZONES; -} \ No newline at end of file +} From 3cbe56e8e0b99c73fd03855c8a814580e8c8f32d Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:22:45 +0700 Subject: [PATCH 4/7] refactor: apply all eslint suggestions --- eslint.config.mjs | 5 +- package.json | 2 +- src/bot.ts | 20 +- src/commands/AttackCommand.ts | 2 +- src/commands/ChestsCommand.ts | 4 +- src/commands/CollectionCommand.ts | 7 +- src/commands/ExploreCommand.ts | 2 +- src/commands/FleeCommand.ts | 2 +- src/commands/GuideCommand.ts | 21 +- src/commands/HelpCommand.ts | 6 +- src/commands/InventoryCommand.ts | 6 +- src/commands/LeaderboardCommand.ts | 15 +- src/commands/LookupCommand.ts | 279 ++-- src/commands/MarketCommand.ts | 98 +- src/commands/NetworkCommand.ts | 283 ++-- src/commands/ProfileCommand.ts | 15 +- src/commands/RestCommand.ts | 2 +- src/commands/TasksCommand.ts | 23 +- src/commands/TravelCommand.ts | 17 +- src/components/buttons/AttackButton.ts | 2 +- src/components/buttons/BulkCollectButton.ts | 4 +- src/components/buttons/BulkDismantleButton.ts | 4 +- src/components/buttons/BulkSellButton.ts | 4 +- src/components/buttons/ChestBuyButton.ts | 2 +- src/components/buttons/ChestOpenButton.ts | 2 +- src/components/buttons/ChestStartButton.ts | 2 +- src/components/buttons/CollectButton.ts | 24 +- src/components/buttons/ConsumeButton.ts | 18 +- src/components/buttons/DismantleButton.ts | 2 +- src/components/buttons/EmbedAttackButton.ts | 2 +- src/components/buttons/EmbedFleeButton.ts | 2 +- src/components/buttons/EnhanceButton.ts | 2 +- src/components/buttons/EquipButton.ts | 2 +- src/components/buttons/ExploreAgainButton.ts | 2 +- src/components/buttons/ExploreButton.ts | 2 +- src/components/buttons/FleeButton.ts | 2 +- src/components/buttons/LockButton.ts | 2 +- src/components/buttons/MarketBuyButton.ts | 2 +- src/components/buttons/MarketCancelButton.ts | 2 +- src/components/buttons/MarketPrevButton.ts | 8 +- .../buttons/RegisterAcceptButton.ts | 23 +- src/components/buttons/RestButton.ts | 2 +- src/components/buttons/SellButton.ts | 18 +- src/components/buttons/TaskClaimButton.ts | 2 +- src/components/buttons/TasksTabButton.ts | 4 +- src/components/menus/InvSelectMenu.ts | 2 +- src/components/menus/MarketSellMenu.ts | 46 +- src/components/menus/ReforgeSelectMenu.ts | 4 +- src/components/menus/TravelSelectMenu.ts | 2 +- src/components/menus/UnequipMenu.ts | 12 +- src/components/modals/BulkCollectModal.ts | 2 +- src/components/modals/BulkDismantleModal.ts | 2 +- src/components/modals/BulkSellModal.ts | 2 +- src/components/modals/CollectModal.ts | 2 +- src/components/modals/ConsumeModal.ts | 2 +- src/components/modals/MarketSellModal.ts | 4 +- src/components/modals/SellModal.ts | 2 +- src/components/modals/SkillPointsModal.ts | 2 +- src/events/ClientReadyEvent.ts | 8 +- src/events/GuildCreateEvent.ts | 28 +- src/events/InteractionCreateEvent.ts | 13 +- src/handlers/ButtonHandler.ts | 129 +- src/handlers/EventHandler.ts | 40 +- src/handlers/ModalSubmitHandler.ts | 92 +- src/handlers/SelectMenuHandler.ts | 100 +- src/handlers/SlashCommandHandler.ts | 120 +- src/managers/CooldownManager.ts | 49 +- src/managers/ItemManager.ts | 196 ++- src/managers/PresenceManager.ts | 181 ++- src/structures/containers/AttackContainer.ts | 26 +- src/structures/containers/ExploreContainer.ts | 49 +- .../containers/ItemLookupContainer.ts | 27 +- .../containers/NPCLookupContainer.ts | 3 +- src/structures/containers/ProfileContainer.ts | 100 +- .../containers/ScenarioLookupContainer.ts | 17 +- src/utilities/AdventureImageBuilder.ts | 1213 ++++++++--------- src/utilities/ApiClient.ts | 2 +- src/utilities/ChestsImageBuilder.ts | 328 +++-- src/utilities/CombatResponseBuilder.ts | 2 +- src/utilities/ImageService.ts | 132 +- src/utilities/ImageWorker.ts | 122 +- src/utilities/InventoryImageBuilder.ts | 316 +++-- src/utilities/ItemImageBuilder.ts | 373 +++-- src/utilities/ItemViewBuilder.ts | 4 +- src/utilities/LeaderboardImageBuilder.ts | 226 ++- src/utilities/MarketImageBuilder.ts | 296 ++-- src/utilities/PaginatorBuilder.ts | 31 +- src/utilities/PlayerGuard.ts | 55 +- src/utilities/ProfileImageBuilder.ts | 651 +++++---- src/utilities/Routes.ts | 294 ++-- src/utilities/TasksImageBuilder.ts | 332 +++-- src/utilities/TravelImageBuilder.ts | 302 ++-- src/utilities/WorkerPool.ts | 262 ++-- 93 files changed, 3473 insertions(+), 3680 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c82d623..539d6f8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import globals from 'globals' export default [ { - files: ['src/**/*.ts', 'bin/*.ts'], + files: ['src/**/*.ts'], plugins: { '@typescript-eslint': typescriptEslint, @@ -63,7 +63,6 @@ export default [ '@stylistic/no-extra-parens': 'error', '@stylistic/no-extra-semi': 'error', '@typescript-eslint/no-extraneous-class': 'error', - '@typescript-eslint/no-loop-func': 'error', '@typescript-eslint/no-loss-of-precision': 'error', '@typescript-eslint/no-misused-promises': [ @@ -109,7 +108,6 @@ export default [ 'block-scoped-var': 'error', 'block-spacing': 'error', camelcase: 'error', - 'class-methods-use-this': 'error', 'comma-style': 'error', 'default-case-last': 'error', 'dot-notation': 'error', @@ -142,7 +140,6 @@ export default [ 'no-invalid-regexp': 'error', 'no-label-var': 'error', 'no-lonely-if': 'error', - 'no-mixed-operators': 'error', 'no-mixed-spaces-and-tabs': 'error', 'no-new': 'error', 'no-new-func': 'error', diff --git a/package.json b/package.json index 794e1a8..170a732 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "dist/index.js", "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", - "lint": "eslint", + "lint": "eslint --fix", "prettier": "prettier **/*.ts --write", "build": "tsc && node -e \"const fs=require('fs'); ['commands', 'events', 'components/buttons', 'components/menus', 'components/modals'].forEach(d => fs.mkdirSync('./dist/' + d, { recursive: true }));\"", "start": "node dist/index.js" diff --git a/src/bot.ts b/src/bot.ts index 98632b4..8d23e70 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -16,21 +16,21 @@ import logger, { flushAndClose } from './utilities/Logger'; import { Client, GatewayIntentBits } from 'discord.js'; import 'dotenv/config'; -import EventHandler from './handlers/EventHandler'; -import SlashCommandHandler from './handlers/SlashCommandHandler'; -import ButtonHandler from './handlers/ButtonHandler'; -import SelectMenuHandler from './handlers/SelectMenuHandler'; -import ModalSubmitHandler from './handlers/ModalSubmitHandler'; -import WorkerPool from './utilities/WorkerPool'; -import PresenceManager from './managers/PresenceManager'; +import * as SlashCommandHandler from './handlers/SlashCommandHandler'; +import * as ButtonHandler from './handlers/ButtonHandler'; +import * as SelectMenuHandler from './handlers/SelectMenuHandler'; +import * as ModalSubmitHandler from './handlers/ModalSubmitHandler'; +import * as WorkerPool from './utilities/WorkerPool'; +import * as PresenceManager from './managers/PresenceManager'; +import initializeEventHandler from './handlers/EventHandler'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); -(async () => { +(async (): Promise => { try { - new EventHandler(client); + initializeEventHandler(client); SlashCommandHandler.load(); ButtonHandler.load(); SelectMenuHandler.load(); @@ -43,7 +43,7 @@ const client = new Client({ } })(); -async function shutdown(signal: string) { +async function shutdown(signal: string): Promise { logger.info(`[System] Received ${signal}. Starting shutdown...`); try { diff --git a/src/commands/AttackCommand.ts b/src/commands/AttackCommand.ts index 172a6b8..821118e 100644 --- a/src/commands/AttackCommand.ts +++ b/src/commands/AttackCommand.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { apiFetch } from '../utilities/ApiClient'; import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; export default class AttackCommand extends SlashCommand { constructor() { diff --git a/src/commands/ChestsCommand.ts b/src/commands/ChestsCommand.ts index 719d803..2c5bbb9 100644 --- a/src/commands/ChestsCommand.ts +++ b/src/commands/ChestsCommand.ts @@ -10,8 +10,8 @@ import { import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; -import ImageService from '../utilities/ImageService'; +import * as Routes from '../utilities/Routes'; +import * as ImageService from '../utilities/ImageService'; import type { IChestSlot } from '../interfaces/IGameJSON'; export default class ChestsCommand extends SlashCommand { diff --git a/src/commands/CollectionCommand.ts b/src/commands/CollectionCommand.ts index 501175b..910882a 100644 --- a/src/commands/CollectionCommand.ts +++ b/src/commands/CollectionCommand.ts @@ -5,9 +5,9 @@ import { } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { type ICollectionJSON } from '../interfaces/ICollectionJSON'; -import ItemManager from '../managers/ItemManager'; +import * as ItemManager from '../managers/ItemManager'; import PaginatorBuilder from '../utilities/PaginatorBuilder'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; @@ -21,8 +21,7 @@ export default class CollectionCommand extends SlashCommand { isGlobalCommand: true }); - this.builder.addUserOption((o) => - o.setName('user').setDescription('Select a user').setRequired(false) + this.builder.addUserOption((o) => o.setName('user').setDescription('Select a user').setRequired(false) ); } diff --git a/src/commands/ExploreCommand.ts b/src/commands/ExploreCommand.ts index 77e6a28..f996368 100644 --- a/src/commands/ExploreCommand.ts +++ b/src/commands/ExploreCommand.ts @@ -4,7 +4,7 @@ import { type IStepJSON } from '../interfaces/IStepJSON'; import { apiFetch } from '../utilities/ApiClient'; import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; export default class ExploreCommand extends SlashCommand { constructor() { diff --git a/src/commands/FleeCommand.ts b/src/commands/FleeCommand.ts index 9b2e1b5..9126606 100644 --- a/src/commands/FleeCommand.ts +++ b/src/commands/FleeCommand.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { apiFetch } from '../utilities/ApiClient'; import { buildCombatResponse } from '../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; export default class FleeCommand extends SlashCommand { constructor() { diff --git a/src/commands/GuideCommand.ts b/src/commands/GuideCommand.ts index 8314209..68a6a79 100644 --- a/src/commands/GuideCommand.ts +++ b/src/commands/GuideCommand.ts @@ -145,17 +145,16 @@ export default class GuideCommand extends SlashCommand { cooldown: 3, isGlobalCommand: true }); - this.builder.addStringOption((o) => - o - .setName('section') - .setDescription('Jump to a specific section') - .setRequired(false) - .addChoices( - ...SECTION_ORDER.map((key) => ({ - name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, - value: key - })) - ) + this.builder.addStringOption((o) => o + .setName('section') + .setDescription('Jump to a specific section') + .setRequired(false) + .addChoices( + ...SECTION_ORDER.map((key) => ({ + name: `${SECTIONS[key].emoji} ${SECTIONS[key].title}`, + value: key + })) + ) ); } diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index 9d82a68..7fa7bf3 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -5,7 +5,7 @@ import { Colors } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; -import SlashCommandHandler from '../handlers/SlashCommandHandler'; +import * as SlashCommandHandler from '../handlers/SlashCommandHandler'; import PaginatorBuilder from '../utilities/PaginatorBuilder'; const CATEGORY_ICONS: Record = { @@ -39,12 +39,10 @@ export default class HelpCommand extends SlashCommand { ): Promise { await interaction.deferReply(); - const commands = SlashCommandHandler.getCache(); - // Group commands by category const categories = new Map(); - for (const command of commands.values()) { + for (const command of SlashCommandHandler.cache.values()) { // Hide developer commands from regular users if (command.category === 'Developer') continue; diff --git a/src/commands/InventoryCommand.ts b/src/commands/InventoryCommand.ts index 0e3b79f..4dbfbc2 100644 --- a/src/commands/InventoryCommand.ts +++ b/src/commands/InventoryCommand.ts @@ -13,11 +13,11 @@ import SlashCommand from '../structures/SlashCommand'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import PaginatorBuilder from '../utilities/PaginatorBuilder'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import ItemManager from '../managers/ItemManager'; -import ImageService from '../utilities/ImageService'; +import * as ItemManager from '../managers/ItemManager'; +import * as ImageService from '../utilities/ImageService'; export default class InventoryCommand extends SlashCommand { constructor() { diff --git a/src/commands/LeaderboardCommand.ts b/src/commands/LeaderboardCommand.ts index 06c876b..2af7cf7 100644 --- a/src/commands/LeaderboardCommand.ts +++ b/src/commands/LeaderboardCommand.ts @@ -6,13 +6,13 @@ import { } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { formatError } from '../utilities/ErrorMessages'; import { type LeaderboardEntry, type LeaderboardConfig } from '../utilities/LeaderboardImageBuilder'; -import ImageService from '../utilities/ImageService'; +import * as ImageService from '../utilities/ImageService'; const STAT_OPTIONS = [ { name: 'Level', value: 'level' }, @@ -62,12 +62,11 @@ export default class LeaderboardCommand extends SlashCommand { isGlobalCommand: true }); - this.builder.addStringOption((o) => - o - .setName('stat') - .setDescription('Which stat to rank by') - .setChoices(STAT_OPTIONS) - .setRequired(false) + this.builder.addStringOption((o) => o + .setName('stat') + .setDescription('Which stat to rank by') + .setChoices(STAT_OPTIONS) + .setRequired(false) ); } diff --git a/src/commands/LookupCommand.ts b/src/commands/LookupCommand.ts index 046b357..9599f80 100644 --- a/src/commands/LookupCommand.ts +++ b/src/commands/LookupCommand.ts @@ -7,11 +7,11 @@ import { MessageFlags } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; -import ItemManager from '../managers/ItemManager'; +import * as ItemManager from '../managers/ItemManager'; import PaginatorBuilder from '../utilities/PaginatorBuilder'; import ItemLookupContainer from '../structures/containers/ItemLookupContainer'; import { apiFetch } from '../utilities/ApiClient'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { type IScenarioJSON } from '../interfaces/IScenarioJSON'; import ScenarioLookupContainer from '../structures/containers/ScenarioLookupContainer'; import { type INPCJSON } from '../interfaces/INPCJSON'; @@ -33,20 +33,18 @@ export default class LookupCommand extends SlashCommand { isGlobalCommand: false }); - this.builder.addStringOption((o) => - o - .setName('type') - .setDescription('Select a type') - .setChoices(typeOptions) - .setRequired(true) + this.builder.addStringOption((o) => o + .setName('type') + .setDescription('Select a type') + .setChoices(typeOptions) + .setRequired(true) ); - this.builder.addIntegerOption((o) => - o - .setName('id') - .setDescription('Enter an id to lookup. Use -1 for all') - .setMinValue(-1) - .setRequired(true) - .setAutocomplete(true) + this.builder.addIntegerOption((o) => o + .setName('id') + .setDescription('Enter an id to lookup. Use -1 for all') + .setMinValue(-1) + .setRequired(true) + .setAutocomplete(true) ); } @@ -71,8 +69,7 @@ export default class LookupCommand extends SlashCommand { // Filter by name match, return up to 25 suggestions (Discord limit) const matches = items .filter( - (item) => - item.name.toLowerCase().includes(query) || + (item) => item.name.toLowerCase().includes(query) || String(item.itemId).startsWith(query) ) .slice(0, 25) @@ -94,151 +91,151 @@ export default class LookupCommand extends SlashCommand { const id = interaction.options.getInteger('id', true); switch (choice) { - case 'item': - if (id === -1) { - const items = Array.from(ItemManager.cache.values()); - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { - const chunk = items.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - - for (const item of chunk) { - descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; - descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; - if (item.affixes) { - let textToAdd = ''; - for (const affix of item.affixes) { - textToAdd += + case 'item': + if (id === -1) { + const items = Array.from(ItemManager.cache.values()); + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < items.length; i += ITEMS_PER_PAGE) { + const chunk = items.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + + for (const item of chunk) { + descriptionText += `**LVL ${item.level} ${item.name} ID:** \`${item.itemId}\`\n`; + descriptionText += `└ ${item.rarity} ${item.type} | **HP:** \`${item.stats.hp.toLocaleString()}\`; **ATK:** \`${item.stats.atk.toLocaleString()}\`; **DEF:** \`${item.stats.def.toLocaleString()}\`\n`; + if (item.affixes) { + let textToAdd = ''; + for (const affix of item.affixes) { + textToAdd += affix.type === 'THORNS' ? ` **${affix.type}:** \`${affix.value.toLocaleString()}\` |` : ` **${affix.type}:** \`${affix.value.toLocaleString()}%\` |`; - } - if (textToAdd !== '') - descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; - else descriptionText += '\n'; } + if (textToAdd !== '') + descriptionText += `‎ ‎ ‎ ‎ └ ${textToAdd}\n\n`; + else descriptionText += '\n'; } - - pages.push( - new EmbedBuilder() - .setColor(Colors.Green) - .setTitle('Item Manager') - .setDescription(descriptionText) - ); } - await new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000) - .start(interaction); - } else { - const item = ItemManager.get(id); - if (!item) { - await interaction.editReply({ - content: 'No item with that id exists!' - }); - return; - } + pages.push( + new EmbedBuilder() + .setColor(Colors.Green) + .setTitle('Item Manager') + .setDescription(descriptionText) + ); + } + + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const item = ItemManager.get(id); + if (!item) { await interaction.editReply({ - components: [new ItemLookupContainer(item).build()], - flags: MessageFlags.IsComponentsV2 + content: 'No item with that id exists!' }); + return; } - break; - - case 'scenario': - if (id === -1) { - const res = await apiFetch(Routes.scenarios()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const scenario of chunk) { - descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? `${scenario.description.substring(0, 125)}...` : scenario.description}\`\`\`\n`; - descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; - } - pages.push( - new EmbedBuilder() - .setTitle('Scenario Manager') - .setDescription(descriptionText) - ); + await interaction.editReply({ + components: [new ItemLookupContainer(item).build()], + flags: MessageFlags.IsComponentsV2 + }); + } + break; + + case 'scenario': + if (id === -1) { + const res = await apiFetch(Routes.scenarios()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const scenario of chunk) { + descriptionText += `📜 \`\`\`${scenario.description.length > 128 ? `${scenario.description.substring(0, 125)}...` : scenario.description}\`\`\`\n`; + descriptionText += `└ **ID:** \`${scenario.id}\` | **Author:** \`${scenario.createdBy}\`\n\n`; } + pages.push( + new EmbedBuilder() + .setTitle('Scenario Manager') + .setDescription(descriptionText) + ); + } - await new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000) - .start(interaction); - } else { - const res = await apiFetch(Routes.scenario(id)); - if (res.status === 404) { - await interaction.editReply({ - content: 'No scenario was found for this id!' - }); - return; - } - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: IScenarioJSON } = await res.json(); + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const res = await apiFetch(Routes.scenario(id)); + if (res.status === 404) { await interaction.editReply({ - components: [new ScenarioLookupContainer(data).build()], - flags: MessageFlags.IsComponentsV2 + content: 'No scenario was found for this id!' }); + return; } - break; - - case 'npc': - if (id === -1) { - const res = await apiFetch(Routes.npcs()); - if (!res.ok) throw new Error('API Error!'); - const { data }: { data: INPCJSON[] } = await res.json(); - - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; - - for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { - const chunk = data.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; - for (const npc of chunk) { - descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; - descriptionText += `└ ${npc.description.length > 128 ? `${npc.description.substring(0, 125)}...` : npc.description}\n\n`; - } - pages.push( - new EmbedBuilder() - .setTitle('NPC Manager') - .setDescription(descriptionText) - ); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: IScenarioJSON } = await res.json(); + await interaction.editReply({ + components: [new ScenarioLookupContainer(data).build()], + flags: MessageFlags.IsComponentsV2 + }); + } + break; + + case 'npc': + if (id === -1) { + const res = await apiFetch(Routes.npcs()); + if (!res.ok) throw new Error('API Error!'); + const { data }: { data: INPCJSON[] } = await res.json(); + + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; + + for (let i = 0; i < data.length; i += ITEMS_PER_PAGE) { + const chunk = data.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + for (const npc of chunk) { + descriptionText += `💀 (ID: \`${npc.id}\`) **${npc.name}**\n`; + descriptionText += `└ ${npc.description.length > 128 ? `${npc.description.substring(0, 125)}...` : npc.description}\n\n`; } + pages.push( + new EmbedBuilder() + .setTitle('NPC Manager') + .setDescription(descriptionText) + ); + } - await new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000) - .start(interaction); - } else { - const res = await apiFetch(Routes.npc(id)); - if (!res.ok) throw new Error('API Error!'); - const { success, data }: { success: boolean; data: INPCJSON } = + await new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000) + .start(interaction); + } else { + const res = await apiFetch(Routes.npc(id)); + if (!res.ok) throw new Error('API Error!'); + const { success, data }: { success: boolean; data: INPCJSON } = await res.json(); - if (!success) { - await interaction.editReply({ - content: 'No NPC was found for the provided ID!' - }); - return; - } + if (!success) { await interaction.editReply({ - components: [new NPCLookupContainer(data).build()], - flags: MessageFlags.IsComponentsV2 + content: 'No NPC was found for the provided ID!' }); + return; } - break; + await interaction.editReply({ + components: [new NPCLookupContainer(data).build()], + flags: MessageFlags.IsComponentsV2 + }); + } + break; } } } diff --git a/src/commands/MarketCommand.ts b/src/commands/MarketCommand.ts index 2466731..38712e8 100644 --- a/src/commands/MarketCommand.ts +++ b/src/commands/MarketCommand.ts @@ -13,13 +13,13 @@ import { import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { type MarketListing, type MarketPageConfig } from '../utilities/MarketImageBuilder'; -import ImageService from '../utilities/ImageService'; -import ItemManager from '../managers/ItemManager'; +import * as ImageService from '../utilities/ImageService'; +import * as ItemManager from '../managers/ItemManager'; import type { IInventoryItem } from '../interfaces/IInventoryJSON'; const RARITY_CHOICES = [ @@ -65,49 +65,42 @@ export default class MarketCommand extends SlashCommand { }); this.data - .addSubcommand((sub) => - sub - .setName('browse') - .setDescription('Browse items for sale on the Global Market') - .addStringOption((o) => - o - .setName('search') - .setDescription('Search by item name') - .setRequired(false) - ) - .addStringOption((o) => - o - .setName('rarity') - .setDescription('Filter by rarity') - .setChoices(RARITY_CHOICES) - .setRequired(false) - ) - .addStringOption((o) => - o - .setName('type') - .setDescription('Filter by item type') - .setChoices(TYPE_CHOICES) - .setRequired(false) - ) - .addStringOption((o) => - o - .setName('sort') - .setDescription('Sort order') - .setChoices(SORT_CHOICES) - .setRequired(false) - ) + .addSubcommand((sub) => sub + .setName('browse') + .setDescription('Browse items for sale on the Global Market') + .addStringOption((o) => o + .setName('search') + .setDescription('Search by item name') + .setRequired(false) + ) + .addStringOption((o) => o + .setName('rarity') + .setDescription('Filter by rarity') + .setChoices(RARITY_CHOICES) + .setRequired(false) + ) + .addStringOption((o) => o + .setName('type') + .setDescription('Filter by item type') + .setChoices(TYPE_CHOICES) + .setRequired(false) + ) + .addStringOption((o) => o + .setName('sort') + .setDescription('Sort order') + .setChoices(SORT_CHOICES) + .setRequired(false) + ) ) - .addSubcommand((sub) => - sub - .setName('listings') - .setDescription('View your active market listings') + .addSubcommand((sub) => sub + .setName('listings') + .setDescription('View your active market listings') ) - .addSubcommand((sub) => - sub - .setName('sell') - .setDescription( - 'Select an item from your inventory to list on the market' - ) + .addSubcommand((sub) => sub + .setName('sell') + .setDescription( + 'Select an item from your inventory to list on the market' + ) ); } @@ -119,12 +112,12 @@ export default class MarketCommand extends SlashCommand { const discordId = interaction.user.id; switch (sub) { - case 'browse': - return this.handleBrowse(interaction, discordId); - case 'listings': - return this.handleListings(interaction, discordId); - case 'sell': - return this.handleSell(interaction, discordId); + case 'browse': + return this.handleBrowse(interaction, discordId); + case 'listings': + return this.handleListings(interaction, discordId); + case 'sell': + return this.handleSell(interaction, discordId); } } @@ -394,9 +387,8 @@ function buildMarketButtons( for (const chunk of chunks.slice(0, 2)) { const row = new ActionRowBuilder(); - for (let i = 0; i < chunk.length; i++) { - const globalIndex = listings.indexOf(chunk[i]); - const listing = chunk[i]; + for (const listing of chunk) { + const globalIndex = listings.indexOf(listing); if (isBrowse) { row.addComponents( diff --git a/src/commands/NetworkCommand.ts b/src/commands/NetworkCommand.ts index 52281ae..040850b 100644 --- a/src/commands/NetworkCommand.ts +++ b/src/commands/NetworkCommand.ts @@ -44,20 +44,18 @@ export default class NetworkCommand extends SlashCommand { isGlobalCommand: false }); - this.builder.addStringOption((o) => - o - .setName('type') - .setDescription('Select a view type') - .setChoices(options) - .setRequired(true) + this.builder.addStringOption((o) => o + .setName('type') + .setDescription('Select a view type') + .setChoices(options) + .setRequired(true) ); - this.builder.addStringOption((o) => - o - .setName('id') - .setDescription( - "Enter a Cluster ID or Guild ID. Use 'all' to view everything." - ) - .setRequired(false) + this.builder.addStringOption((o) => o + .setName('id') + .setDescription( + "Enter a Cluster ID or Guild ID. Use 'all' to view everything." + ) + .setRequired(false) ); } @@ -71,157 +69,151 @@ export default class NetworkCommand extends SlashCommand { const id = interaction.options.getString('id') || 'all'; switch (choice) { - case 'overview': { - const clusters = await this.getClusters(client); - const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); - const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); - const avgPing = + case 'overview': { + const clusters = await this.getClusters(client); + const totalGuilds = clusters.reduce((acc, s) => acc + s.guilds, 0); + const totalUsers = clusters.reduce((acc, s) => acc + s.users, 0); + const avgPing = clusters.reduce((acc, s) => acc + s.ping, 0) / clusters.length; - const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); - - const container = new ContainerBuilder() - .setAccentColor(Colors.Blurple) - .addTextDisplayComponents((text) => - text.setContent(`# 🌐 Global Network Overview`) - ) - .addSeparatorComponents((sep) => sep.setDivider(true)) - .addTextDisplayComponents((text) => - text.setContent( - `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` - ) - ); + const totalShards = clusters.reduce((acc, s) => acc + s.shardCount, 0); - await interaction.editReply({ - components: [container], - flags: MessageFlags.IsComponentsV2 - }); - break; - } + const container = new ContainerBuilder() + .setAccentColor(Colors.Blurple) + .addTextDisplayComponents((text) => text.setContent(`# 🌐 Global Network Overview`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => text.setContent( + `**Clusters:** \`${clusters.length}\`\n**Total Shards:** \`${totalShards}\`\n**Total Guilds:** \`${totalGuilds.toLocaleString()}\`\n**Total Users:** \`${totalUsers.toLocaleString()}\`\n**Average Latency:** \`${Math.round(avgPing)}ms\`` + ) + ); - case 'shard': { - const clusters = await this.getClusters(client); + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); + break; + } - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + case 'shard': { + const clusters = await this.getClusters(client); - for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { - const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - for (const cluster of chunk) { - descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; - descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; - } + for (let i = 0; i < clusters.length; i += ITEMS_PER_PAGE) { + const chunk = clusters.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; - pages.push( - new EmbedBuilder() - .setColor(Colors.Blue) - .setTitle('Network Manager: Clusters') - .setDescription(descriptionText) - ); + for (const cluster of chunk) { + descriptionText += `💎 **Cluster #${cluster.id}** | **Ping:** \`${cluster.ping}ms\` | **Shards:** \`${cluster.shards.join(', ')}\`\n`; + descriptionText += `└ **Guilds:** \`${cluster.guilds.toLocaleString()}\` | **Users:** \`${cluster.users.toLocaleString()}\`\n\n`; } - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); - - await paginator.start(interaction); - } else { - const targetCluster = clusters.find((s) => s.id.toString() === id); - if (!targetCluster) { - await interaction.editReply({ - content: `❌ No cluster could be found with the ID: \`${id}\`` - }); - return; - } + pages.push( + new EmbedBuilder() + .setColor(Colors.Blue) + .setTitle('Network Manager: Clusters') + .setDescription(descriptionText) + ); + } - const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); - const uptimeHours = Math.floor(uptimeMins / 60); - - const container = new ContainerBuilder() - .setAccentColor(Colors.Blue) - .addTextDisplayComponents((text) => - text.setContent(`# 💎 Cluster #${targetCluster.id}`) - ) - .addSeparatorComponents((sep) => sep.setDivider(true)) - .addTextDisplayComponents((text) => - text.setContent( - `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` - ) - ); + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + await paginator.start(interaction); + } else { + const targetCluster = clusters.find((s) => s.id.toString() === id); + if (!targetCluster) { await interaction.editReply({ - components: [container], - flags: MessageFlags.IsComponentsV2 + content: `❌ No cluster could be found with the ID: \`${id}\`` }); + return; } - break; - } - case 'guild': { - const guilds = await this.getGuilds(client); + const uptimeMins = Math.floor((targetCluster.uptime || 0) / 60000); + const uptimeHours = Math.floor(uptimeMins / 60); - if (id.toLowerCase() === 'all') { - const ITEMS_PER_PAGE = 10; - const pages: EmbedBuilder[] = []; + const container = new ContainerBuilder() + .setAccentColor(Colors.Blue) + .addTextDisplayComponents((text) => text.setContent(`# 💎 Cluster #${targetCluster.id}`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => text.setContent( + `**Status:** \`Online\`\n**Ping:** \`${targetCluster.ping}ms\`\n**Internal Shards:** \`${targetCluster.shards.join(', ')}\`\n**Guilds Hosted:** \`${targetCluster.guilds.toLocaleString()}\`\n**Users Tracked:** \`${targetCluster.users.toLocaleString()}\`\n**Uptime:** \`${uptimeHours}h ${uptimeMins % 60}m\`` + ) + ); - for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { - const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); - let descriptionText = ''; + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); + } + break; + } - for (const guild of chunk) { - descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; - descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; - } + case 'guild': { + const guilds = await this.getGuilds(client); - pages.push( - new EmbedBuilder() - .setColor(Colors.Purple) - .setTitle('Network Manager: Guilds') - .setDescription(descriptionText) - ); - } + if (id.toLowerCase() === 'all') { + const ITEMS_PER_PAGE = 10; + const pages: EmbedBuilder[] = []; - const paginator = new PaginatorBuilder() - .setPages(pages) - .setTargetUser(interaction.user.id) - .setIdleTimeout(60_000); - - await paginator.start(interaction); - } else { - const targetGuild = guilds.find((g) => g.id === id); - if (!targetGuild) { - await interaction.editReply({ - content: `❌ No guild could be found with the ID: \`${id}\`` - }); - return; + for (let i = 0; i < guilds.length; i += ITEMS_PER_PAGE) { + const chunk = guilds.slice(i, i + ITEMS_PER_PAGE); + let descriptionText = ''; + + for (const guild of chunk) { + descriptionText += `🛡️ **${guild.name}** (ID: \`${guild.id}\`)\n`; + descriptionText += `└ **Cluster:** \`${guild.clusterId}\` | **Members:** \`${guild.memberCount.toLocaleString()}\`\n\n`; } - const joinedTimestamp = targetGuild.joinedAt - ? `` - : 'Unknown'; - - const container = new ContainerBuilder() - .setAccentColor(Colors.Purple) - .addTextDisplayComponents((text) => - text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`) - ) - .addSeparatorComponents((sep) => sep.setDivider(true)) - .addTextDisplayComponents((text) => - text.setContent( - `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` - ) - ); + pages.push( + new EmbedBuilder() + .setColor(Colors.Purple) + .setTitle('Network Manager: Guilds') + .setDescription(descriptionText) + ); + } + + const paginator = new PaginatorBuilder() + .setPages(pages) + .setTargetUser(interaction.user.id) + .setIdleTimeout(60_000); + await paginator.start(interaction); + } else { + const targetGuild = guilds.find((g) => g.id === id); + if (!targetGuild) { await interaction.editReply({ - components: [container], - flags: MessageFlags.IsComponentsV2 + content: `❌ No guild could be found with the ID: \`${id}\`` }); + return; } - break; + + const joinedTimestamp = targetGuild.joinedAt + ? `` + : 'Unknown'; + + const container = new ContainerBuilder() + .setAccentColor(Colors.Purple) + .addTextDisplayComponents((text) => text.setContent(`# 🛡️ Guild Details\n**${targetGuild.name}**`) + ) + .addSeparatorComponents((sep) => sep.setDivider(true)) + .addTextDisplayComponents((text) => text.setContent( + `**Guild ID:** \`${targetGuild.id}\`\n**Hosted on Cluster:** \`${targetGuild.clusterId}\`\n**Total Members:** \`${targetGuild.memberCount.toLocaleString()}\`\n**Owner ID:** \`${targetGuild.ownerId}\`\n**Joined Bot:** ${joinedTimestamp}` + ) + ); + + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2 + }); } + break; + } } } @@ -269,15 +261,14 @@ export default class NetworkCommand extends SlashCommand { private async getGuilds(client: Client): Promise { const cluster = (client as any).cluster; if (cluster) { - const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => - c.guilds.cache.map((g: any) => ({ - id: g.id, - name: g.name, - memberCount: g.memberCount, - clusterId: c.cluster?.id ?? 0, - ownerId: g.ownerId, - joinedAt: g.joinedTimestamp - })) + const results: GuildInfo[][] = await cluster.broadcastEval((c: any) => c.guilds.cache.map((g: any) => ({ + id: g.id, + name: g.name, + memberCount: g.memberCount, + clusterId: c.cluster?.id ?? 0, + ownerId: g.ownerId, + joinedAt: g.joinedTimestamp + })) ); return results.flat(); } diff --git a/src/commands/ProfileCommand.ts b/src/commands/ProfileCommand.ts index cbf1d7c..dfd17ca 100644 --- a/src/commands/ProfileCommand.ts +++ b/src/commands/ProfileCommand.ts @@ -12,11 +12,11 @@ import SlashCommand from '../structures/SlashCommand'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; import { type ICollectionJSON } from '../interfaces/ICollectionJSON'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; import { type EquipmentSlot } from '../interfaces/IItemJSON'; -import ImageService from '../utilities/ImageService'; +import * as ImageService from '../utilities/ImageService'; export default class ProfileCommand extends SlashCommand { constructor() { @@ -27,8 +27,7 @@ export default class ProfileCommand extends SlashCommand { cooldown: 5, isGlobalCommand: true }); - this.builder.addUserOption((o) => - o.setName('user').setDescription('Select a user').setRequired(false) + this.builder.addUserOption((o) => o.setName('user').setDescription('Select a user').setRequired(false) ); } @@ -90,10 +89,10 @@ export default class ProfileCommand extends SlashCommand { options.length >= 1 ? options : [ - new StringSelectMenuOptionBuilder() - .setLabel('None') - .setValue('None') - ] + new StringSelectMenuOptionBuilder() + .setLabel('None') + .setValue('None') + ] ) .setMaxValues(1) .setPlaceholder('Unequip Slot'); diff --git a/src/commands/RestCommand.ts b/src/commands/RestCommand.ts index 6f628fa..b3b9543 100644 --- a/src/commands/RestCommand.ts +++ b/src/commands/RestCommand.ts @@ -2,7 +2,7 @@ import { type ChatInputCommandInteraction, type Client } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; export default class RestCommand extends SlashCommand { constructor() { diff --git a/src/commands/TasksCommand.ts b/src/commands/TasksCommand.ts index 8ac67c2..5952e76 100644 --- a/src/commands/TasksCommand.ts +++ b/src/commands/TasksCommand.ts @@ -12,8 +12,8 @@ import { import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; -import ImageService from '../utilities/ImageService'; +import * as Routes from '../utilities/Routes'; +import * as ImageService from '../utilities/ImageService'; import type { ITaskJSON } from '../interfaces/IGameJSON'; export default class TasksCommand extends SlashCommand { @@ -25,16 +25,15 @@ export default class TasksCommand extends SlashCommand { cooldown: 5, isGlobalCommand: true }); - this.builder.addStringOption((o) => - o - .setName('period') - .setDescription('Task period to view') - .setRequired(false) - .addChoices( - { name: 'Daily', value: 'daily' }, - { name: 'Weekly', value: 'weekly' }, - { name: 'Monthly', value: 'monthly' } - ) + this.builder.addStringOption((o) => o + .setName('period') + .setDescription('Task period to view') + .setRequired(false) + .addChoices( + { name: 'Daily', value: 'daily' }, + { name: 'Weekly', value: 'weekly' }, + { name: 'Monthly', value: 'monthly' } + ) ); } diff --git a/src/commands/TravelCommand.ts b/src/commands/TravelCommand.ts index 253cc89..df854f5 100644 --- a/src/commands/TravelCommand.ts +++ b/src/commands/TravelCommand.ts @@ -11,13 +11,13 @@ import { import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import { getAccessibleZones, getZone, type ZoneInfo } from '../utilities/ZoneData'; -import ImageService from '../utilities/ImageService'; +import * as ImageService from '../utilities/ImageService'; export default class TravelCommand extends SlashCommand { constructor() { @@ -77,13 +77,12 @@ export default class TravelCommand extends SlashCommand { const components: ActionRowBuilder[] = []; if (accessible.length > 0) { - const options = accessible.map((zone) => - new StringSelectMenuOptionBuilder() - .setLabel(zone.name) - .setDescription( - `Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat` - ) - .setValue(String(zone.id)) + const options = accessible.map((zone) => new StringSelectMenuOptionBuilder() + .setLabel(zone.name) + .setDescription( + `Lvl ${zone.levelReq}+ • ${zone.rarityCap} cap • ${zone.combatChance}% combat` + ) + .setValue(String(zone.id)) ); const selectMenu = new StringSelectMenuBuilder() diff --git a/src/components/buttons/AttackButton.ts b/src/components/buttons/AttackButton.ts index b24ba36..fabfb10 100644 --- a/src/components/buttons/AttackButton.ts +++ b/src/components/buttons/AttackButton.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class AttackButton extends Button { constructor() { diff --git a/src/components/buttons/BulkCollectButton.ts b/src/components/buttons/BulkCollectButton.ts index 84a8a41..93560c3 100644 --- a/src/components/buttons/BulkCollectButton.ts +++ b/src/components/buttons/BulkCollectButton.ts @@ -9,9 +9,9 @@ import { TextDisplayBuilder } from 'discord.js'; import Button from '../../structures/Button'; -import ItemManager from '../../managers/ItemManager'; +import * as ItemManager from '../../managers/ItemManager'; import { apiFetch } from '../../utilities/ApiClient'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; diff --git a/src/components/buttons/BulkDismantleButton.ts b/src/components/buttons/BulkDismantleButton.ts index a41d26d..44d2b93 100644 --- a/src/components/buttons/BulkDismantleButton.ts +++ b/src/components/buttons/BulkDismantleButton.ts @@ -9,9 +9,9 @@ import { TextDisplayBuilder } from 'discord.js'; import Button from '../../structures/Button'; -import ItemManager from '../../managers/ItemManager'; +import * as ItemManager from '../../managers/ItemManager'; import { apiFetch } from '../../utilities/ApiClient'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; diff --git a/src/components/buttons/BulkSellButton.ts b/src/components/buttons/BulkSellButton.ts index 82b341d..42b1ef7 100644 --- a/src/components/buttons/BulkSellButton.ts +++ b/src/components/buttons/BulkSellButton.ts @@ -9,9 +9,9 @@ import { TextDisplayBuilder } from 'discord.js'; import Button from '../../structures/Button'; -import ItemManager from '../../managers/ItemManager'; +import * as ItemManager from '../../managers/ItemManager'; import { apiFetch } from '../../utilities/ApiClient'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; const ITEMS_PER_PAGE = 15; diff --git a/src/components/buttons/ChestBuyButton.ts b/src/components/buttons/ChestBuyButton.ts index 586e1e4..bae1e0e 100644 --- a/src/components/buttons/ChestBuyButton.ts +++ b/src/components/buttons/ChestBuyButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class ChestBuyButton extends Button { constructor() { diff --git a/src/components/buttons/ChestOpenButton.ts b/src/components/buttons/ChestOpenButton.ts index 040b1e3..112996b 100644 --- a/src/components/buttons/ChestOpenButton.ts +++ b/src/components/buttons/ChestOpenButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; const RARITY_EMOJIS: Record = { Common: '⬜', diff --git a/src/components/buttons/ChestStartButton.ts b/src/components/buttons/ChestStartButton.ts index 45b61ae..8fec692 100644 --- a/src/components/buttons/ChestStartButton.ts +++ b/src/components/buttons/ChestStartButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class ChestStartButton extends Button { constructor() { diff --git a/src/components/buttons/CollectButton.ts b/src/components/buttons/CollectButton.ts index e6cd951..830027b 100644 --- a/src/components/buttons/CollectButton.ts +++ b/src/components/buttons/CollectButton.ts @@ -23,19 +23,17 @@ export default class CollectButton extends Button { const modal = new ModalBuilder() .setCustomId(`collect:${docId}`) .setTitle('⚠️ Collect Item (Permanent)') - .addLabelComponents((label) => - label - .setLabel('Amount') - .setDescription( - `⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})` - ) - .setTextInputComponent((ti) => - ti - .setCustomId('ti1') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(maxQty) - ) + .addLabelComponents((label) => label + .setLabel('Amount') + .setDescription( + `⚠️ This is PERMANENT and cannot be undone. Items are removed from inventory and added to your Collection Book. (Max: ${maxQty})` + ) + .setTextInputComponent((ti) => ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(maxQty) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/ConsumeButton.ts b/src/components/buttons/ConsumeButton.ts index ddc06e0..c5b2a2b 100644 --- a/src/components/buttons/ConsumeButton.ts +++ b/src/components/buttons/ConsumeButton.ts @@ -23,16 +23,14 @@ export default class ConsumeButton extends Button { const modal = new ModalBuilder() .setTitle('Consume Item') .setCustomId(`consume:${docId}`) - .addLabelComponents((label) => - label - .setLabel('Amount') - .setDescription(`Enter amount to consume (Max: ${maxQty})`) - .setTextInputComponent((ti) => - ti - .setCustomId('ti1') - .setRequired(true) - .setStyle(TextInputStyle.Short) - ) + .addLabelComponents((label) => label + .setLabel('Amount') + .setDescription(`Enter amount to consume (Max: ${maxQty})`) + .setTextInputComponent((ti) => ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/DismantleButton.ts b/src/components/buttons/DismantleButton.ts index b5b0eec..e02b7d0 100644 --- a/src/components/buttons/DismantleButton.ts +++ b/src/components/buttons/DismantleButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class DismantleButton extends Button { constructor() { diff --git a/src/components/buttons/EmbedAttackButton.ts b/src/components/buttons/EmbedAttackButton.ts index f7e65b4..1dbfad9 100644 --- a/src/components/buttons/EmbedAttackButton.ts +++ b/src/components/buttons/EmbedAttackButton.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class EmbedAttackButton extends Button { constructor() { diff --git a/src/components/buttons/EmbedFleeButton.ts b/src/components/buttons/EmbedFleeButton.ts index 3ec3efc..6b00e61 100644 --- a/src/components/buttons/EmbedFleeButton.ts +++ b/src/components/buttons/EmbedFleeButton.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class EmbedFleeButton extends Button { constructor() { diff --git a/src/components/buttons/EnhanceButton.ts b/src/components/buttons/EnhanceButton.ts index 21886fb..5cd543e 100644 --- a/src/components/buttons/EnhanceButton.ts +++ b/src/components/buttons/EnhanceButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class EnhanceButton extends Button { constructor() { diff --git a/src/components/buttons/EquipButton.ts b/src/components/buttons/EquipButton.ts index 43fb9e5..31f4bbd 100644 --- a/src/components/buttons/EquipButton.ts +++ b/src/components/buttons/EquipButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class EquipButton extends Button { constructor() { diff --git a/src/components/buttons/ExploreAgainButton.ts b/src/components/buttons/ExploreAgainButton.ts index 40cfcde..f579b0a 100644 --- a/src/components/buttons/ExploreAgainButton.ts +++ b/src/components/buttons/ExploreAgainButton.ts @@ -3,7 +3,7 @@ import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import { type IStepJSON } from '../../interfaces/IStepJSON'; export default class ExploreAgainButton extends Button { diff --git a/src/components/buttons/ExploreButton.ts b/src/components/buttons/ExploreButton.ts index 020461f..388723b 100644 --- a/src/components/buttons/ExploreButton.ts +++ b/src/components/buttons/ExploreButton.ts @@ -4,7 +4,7 @@ import { type IStepJSON } from '../../interfaces/IStepJSON'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class ExploreButton extends Button { constructor() { diff --git a/src/components/buttons/FleeButton.ts b/src/components/buttons/FleeButton.ts index 4d759aa..2e27a1c 100644 --- a/src/components/buttons/FleeButton.ts +++ b/src/components/buttons/FleeButton.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; import { formatError, formatCooldown } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class FleeButton extends Button { constructor() { diff --git a/src/components/buttons/LockButton.ts b/src/components/buttons/LockButton.ts index c8c5322..86ce7fd 100644 --- a/src/components/buttons/LockButton.ts +++ b/src/components/buttons/LockButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class LockButton extends Button { constructor() { diff --git a/src/components/buttons/MarketBuyButton.ts b/src/components/buttons/MarketBuyButton.ts index 822aec4..f8d6fe5 100644 --- a/src/components/buttons/MarketBuyButton.ts +++ b/src/components/buttons/MarketBuyButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class MarketBuyButton extends Button { constructor() { diff --git a/src/components/buttons/MarketCancelButton.ts b/src/components/buttons/MarketCancelButton.ts index d3d1dbc..419fcad 100644 --- a/src/components/buttons/MarketCancelButton.ts +++ b/src/components/buttons/MarketCancelButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client, MessageFlags } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class MarketCancelButton extends Button { constructor() { diff --git a/src/components/buttons/MarketPrevButton.ts b/src/components/buttons/MarketPrevButton.ts index 0a79f65..0b39a0e 100644 --- a/src/components/buttons/MarketPrevButton.ts +++ b/src/components/buttons/MarketPrevButton.ts @@ -5,14 +5,14 @@ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, - ButtonStyle, - MessageFlags + ButtonStyle } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; -import MarketImageBuilder, { +import * as Routes from '../../utilities/Routes'; +import * as MarketImageBuilder from '../../utilities/MarketImageBuilder'; +import { type MarketListing, type MarketPageConfig } from '../../utilities/MarketImageBuilder'; diff --git a/src/components/buttons/RegisterAcceptButton.ts b/src/components/buttons/RegisterAcceptButton.ts index 773ab28..2fb07df 100644 --- a/src/components/buttons/RegisterAcceptButton.ts +++ b/src/components/buttons/RegisterAcceptButton.ts @@ -8,7 +8,7 @@ import { import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class RegisterAcceptButton extends Button { constructor() { @@ -56,25 +56,22 @@ export default class RegisterAcceptButton extends Button { container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent('## ⚔️ Character Created!'), - (textDisplay) => - textDisplay.setContent( - `Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.` - ), - (textDisplay) => - textDisplay.setContent( - '**Get started:**\n' + + (textDisplay) => textDisplay.setContent( + `Welcome to Dragon's Fall Online, **${username}**! Your adventure begins now.` + ), + (textDisplay) => textDisplay.setContent( + '**Get started:**\n' + '> `/explore` — Venture into the world\n' + '> `/profile` — View your character\n' + '> `/help` — See all commands' - ) + ) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent( - '-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer' - ) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent( + '-# ⚔️ DFO Cross-Platform Integration • To request data deletion, contact the developer' + ) ); await interaction.editReply({ diff --git a/src/components/buttons/RestButton.ts b/src/components/buttons/RestButton.ts index 86d42ef..fcc4677 100644 --- a/src/components/buttons/RestButton.ts +++ b/src/components/buttons/RestButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class RestButton extends Button { constructor() { diff --git a/src/components/buttons/SellButton.ts b/src/components/buttons/SellButton.ts index a9f37cf..18a7f94 100644 --- a/src/components/buttons/SellButton.ts +++ b/src/components/buttons/SellButton.ts @@ -23,16 +23,14 @@ export default class SellButton extends Button { const modal = new ModalBuilder() .setCustomId(`sell:${docId}`) .setTitle('Sell Item') - .addLabelComponents((label) => - label - .setLabel('Amount') - .setDescription(`Enter amount to sell. (Max: ${maxQty})`) - .setTextInputComponent((ti) => - ti - .setCustomId('ti1') - .setRequired(true) - .setStyle(TextInputStyle.Short) - ) + .addLabelComponents((label) => label + .setLabel('Amount') + .setDescription(`Enter amount to sell. (Max: ${maxQty})`) + .setTextInputComponent((ti) => ti + .setCustomId('ti1') + .setRequired(true) + .setStyle(TextInputStyle.Short) + ) ); await interaction.showModal(modal); diff --git a/src/components/buttons/TaskClaimButton.ts b/src/components/buttons/TaskClaimButton.ts index f9a9c8a..7157402 100644 --- a/src/components/buttons/TaskClaimButton.ts +++ b/src/components/buttons/TaskClaimButton.ts @@ -2,7 +2,7 @@ import { type ButtonInteraction, type Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class TaskClaimButton extends Button { constructor() { diff --git a/src/components/buttons/TasksTabButton.ts b/src/components/buttons/TasksTabButton.ts index 42892e1..81e7804 100644 --- a/src/components/buttons/TasksTabButton.ts +++ b/src/components/buttons/TasksTabButton.ts @@ -10,8 +10,8 @@ import { import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; -import ImageService from '../../utilities/ImageService'; +import * as Routes from '../../utilities/Routes'; +import * as ImageService from '../../utilities/ImageService'; import type { ITaskJSON } from '../../interfaces/IGameJSON'; export default class TasksTabButton extends Button { diff --git a/src/components/menus/InvSelectMenu.ts b/src/components/menus/InvSelectMenu.ts index a356b6b..d43ff64 100644 --- a/src/components/menus/InvSelectMenu.ts +++ b/src/components/menus/InvSelectMenu.ts @@ -2,7 +2,7 @@ import { type AnySelectMenuInteraction, type Client } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import { buildItemView } from '../../utilities/ItemViewBuilder'; import type { IInventoryItem } from '../../interfaces/IInventoryJSON'; import type { IPlayerJSON } from '../../interfaces/IPlayerJSON'; diff --git a/src/components/menus/MarketSellMenu.ts b/src/components/menus/MarketSellMenu.ts index bf24286..c0e0bd2 100644 --- a/src/components/menus/MarketSellMenu.ts +++ b/src/components/menus/MarketSellMenu.ts @@ -6,7 +6,7 @@ import { TextInputStyle } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; -import ItemManager from '../../managers/ItemManager'; +import * as ItemManager from '../../managers/ItemManager'; export default class MarketSellMenu extends SelectMenu { constructor() { @@ -42,31 +42,27 @@ export default class MarketSellMenu extends SelectMenu { const modal = new ModalBuilder() .setCustomId(`mkt_sell_modal:${docId}:${itemId}`) .setTitle(`🏪 List: ${itemName.slice(0, 30)}`) - .addLabelComponents((label) => - label - .setLabel('Quantity') - .setDescription(`How many to list (Max: ${maxQty})`) - .setTextInputComponent((ti) => - ti - .setCustomId('quantity') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`1 - ${maxQty}`) - ) + .addLabelComponents((label) => label + .setLabel('Quantity') + .setDescription(`How many to list (Max: ${maxQty})`) + .setTextInputComponent((ti) => ti + .setCustomId('quantity') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`1 - ${maxQty}`) + ) ) - .addLabelComponents((label) => - label - .setLabel('Price per unit (gold)') - .setDescription( - `Suggested: ${baseValue.toLocaleString()}g (base value)` - ) - .setTextInputComponent((ti) => - ti - .setCustomId('price') - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(`e.g. ${baseValue || 100}`) - ) + .addLabelComponents((label) => label + .setLabel('Price per unit (gold)') + .setDescription( + `Suggested: ${baseValue.toLocaleString()}g (base value)` + ) + .setTextInputComponent((ti) => ti + .setCustomId('price') + .setRequired(true) + .setStyle(TextInputStyle.Short) + .setPlaceholder(`e.g. ${baseValue || 100}`) + ) ); await interaction.showModal(modal); diff --git a/src/components/menus/ReforgeSelectMenu.ts b/src/components/menus/ReforgeSelectMenu.ts index 841527d..b72dd98 100644 --- a/src/components/menus/ReforgeSelectMenu.ts +++ b/src/components/menus/ReforgeSelectMenu.ts @@ -2,7 +2,7 @@ import { type AnySelectMenuInteraction, type Client } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class ReforgeSelectMenu extends SelectMenu { constructor() { @@ -66,7 +66,7 @@ export default class ReforgeSelectMenu extends SelectMenu { body.newStats && (reforgeType === 'stats' || reforgeType === 'full') ) { - const fmtStat = (label: string, old: number, now: number) => { + const fmtStat = (label: string, old: number, now: number): string => { const diff = now - old; const arrow = diff > 0 ? '🟢' : diff < 0 ? '🔴' : '⚪'; return `${arrow} ${label}: ${old} → **${now}** (${diff > 0 ? '+' : ''}${diff})`; diff --git a/src/components/menus/TravelSelectMenu.ts b/src/components/menus/TravelSelectMenu.ts index e092f84..35a8ae3 100644 --- a/src/components/menus/TravelSelectMenu.ts +++ b/src/components/menus/TravelSelectMenu.ts @@ -6,7 +6,7 @@ import { import SelectMenu from '../../structures/SelectMenu'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import { getZone } from '../../utilities/ZoneData'; export default class TravelSelectMenu extends SelectMenu { diff --git a/src/components/menus/UnequipMenu.ts b/src/components/menus/UnequipMenu.ts index 05d2c8e..1fb3ab2 100644 --- a/src/components/menus/UnequipMenu.ts +++ b/src/components/menus/UnequipMenu.ts @@ -7,11 +7,11 @@ import { StringSelectMenuOptionBuilder } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; import { type IPlayerJSON } from '../../interfaces/IPlayerJSON'; -import ImageService from '../../utilities/ImageService'; +import * as ImageService from '../../utilities/ImageService'; import { type EquipmentSlot } from '../../interfaces/IItemJSON'; export default class UnequipMenu extends SelectMenu { @@ -85,10 +85,10 @@ export default class UnequipMenu extends SelectMenu { options.length >= 1 ? options : [ - new StringSelectMenuOptionBuilder() - .setLabel('None') - .setValue('None') - ] + new StringSelectMenuOptionBuilder() + .setLabel('None') + .setValue('None') + ] ) .setMaxValues(1) .setPlaceholder('Unequip Slot'); diff --git a/src/components/modals/BulkCollectModal.ts b/src/components/modals/BulkCollectModal.ts index 7e84e2c..eaf3a9e 100644 --- a/src/components/modals/BulkCollectModal.ts +++ b/src/components/modals/BulkCollectModal.ts @@ -6,7 +6,7 @@ import { import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class BulkCollectModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/BulkDismantleModal.ts b/src/components/modals/BulkDismantleModal.ts index 4f9348c..3087c45 100644 --- a/src/components/modals/BulkDismantleModal.ts +++ b/src/components/modals/BulkDismantleModal.ts @@ -6,7 +6,7 @@ import { import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class BulkDismantleModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/BulkSellModal.ts b/src/components/modals/BulkSellModal.ts index 8dd03e9..b9b8a25 100644 --- a/src/components/modals/BulkSellModal.ts +++ b/src/components/modals/BulkSellModal.ts @@ -6,7 +6,7 @@ import { import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class BulkSellModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/CollectModal.ts b/src/components/modals/CollectModal.ts index b9adb43..7dabcf4 100644 --- a/src/components/modals/CollectModal.ts +++ b/src/components/modals/CollectModal.ts @@ -2,7 +2,7 @@ import { type ModalSubmitInteraction, type Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class CollectModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/ConsumeModal.ts b/src/components/modals/ConsumeModal.ts index 9c30a91..e135ff1 100644 --- a/src/components/modals/ConsumeModal.ts +++ b/src/components/modals/ConsumeModal.ts @@ -2,7 +2,7 @@ import { type ModalSubmitInteraction, type Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class ConsumeModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/MarketSellModal.ts b/src/components/modals/MarketSellModal.ts index c6ed897..b99edfb 100644 --- a/src/components/modals/MarketSellModal.ts +++ b/src/components/modals/MarketSellModal.ts @@ -6,8 +6,8 @@ import { import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; -import ItemManager from '../../managers/ItemManager'; +import * as Routes from '../../utilities/Routes'; +import * as ItemManager from '../../managers/ItemManager'; export default class MarketSellModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/SellModal.ts b/src/components/modals/SellModal.ts index 077de08..bd2be88 100644 --- a/src/components/modals/SellModal.ts +++ b/src/components/modals/SellModal.ts @@ -2,7 +2,7 @@ import { type ModalSubmitInteraction, type Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class SellModal extends ModalSubmit { constructor() { diff --git a/src/components/modals/SkillPointsModal.ts b/src/components/modals/SkillPointsModal.ts index d362b49..35d0c60 100644 --- a/src/components/modals/SkillPointsModal.ts +++ b/src/components/modals/SkillPointsModal.ts @@ -6,7 +6,7 @@ import { import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; -import Routes from '../../utilities/Routes'; +import * as Routes from '../../utilities/Routes'; export default class SkillPointsModal extends ModalSubmit { constructor() { diff --git a/src/events/ClientReadyEvent.ts b/src/events/ClientReadyEvent.ts index be743f9..62fb16a 100644 --- a/src/events/ClientReadyEvent.ts +++ b/src/events/ClientReadyEvent.ts @@ -1,9 +1,9 @@ import { type Client } from 'discord.js'; import Event from '../structures/Event'; import logger from '../utilities/Logger'; -import ItemManager from '../managers/ItemManager'; -import WorkerPool from '../utilities/WorkerPool'; -import PresenceManager from '../managers/PresenceManager'; +import * as ItemManager from '../managers/ItemManager'; +import * as WorkerPool from '../utilities/WorkerPool'; +import * as PresenceManager from '../managers/PresenceManager'; export default class ClientReadyEvent extends Event { constructor() { @@ -13,7 +13,7 @@ export default class ClientReadyEvent extends Event { }); } - public async execute(client: Client) { + public async execute(client: Client): Promise { // Use cluster id from hybrid sharding, fallback to shard id const clusterId = (client as any).cluster?.id ?? client.shard?.ids[0] ?? 0; logger.info( diff --git a/src/events/GuildCreateEvent.ts b/src/events/GuildCreateEvent.ts index e397404..2f47419 100644 --- a/src/events/GuildCreateEvent.ts +++ b/src/events/GuildCreateEvent.ts @@ -31,21 +31,16 @@ export default class GuildCreateEvent extends Event { if (logChannel && guild) { const container = new ContainerBuilder() .setAccentColor(Colors.Green) - .addSectionComponents((section) => - section - .setThumbnailAccessory((t) => - t.setURL(guild.iconURL() ?? client.user?.avatarURL()!) - ) - .addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent('## I Joined A New Server!'), - (textDisplay) => - textDisplay.setContent( - `Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.` - ), - (textDisplay) => - textDisplay.setContent(`-# ID: \`${guild.id}\``) - ) + .addSectionComponents((section) => section + .setThumbnailAccessory((t) => t.setURL(guild.iconURL() ?? client.user?.avatarURL()!) + ) + .addTextDisplayComponents( + (textDisplay) => textDisplay.setContent('## I Joined A New Server!'), + (textDisplay) => textDisplay.setContent( + `Joined the ${guild.name} server! It has ${guild.memberCount.toLocaleString()} members.` + ), + (textDisplay) => textDisplay.setContent(`-# ID: \`${guild.id}\``) + ) ); await logChannel.send({ @@ -64,8 +59,7 @@ export default class GuildCreateEvent extends Event { guild.systemChannel ?? (guild.channels.cache .filter( - (c) => - c.isTextBased() && + (c) => c.isTextBased() && c.permissionsFor(guild.members.me!)?.has('SendMessages') ) .first() as TextChannel | undefined); diff --git a/src/events/InteractionCreateEvent.ts b/src/events/InteractionCreateEvent.ts index 4a296cf..3464fa6 100644 --- a/src/events/InteractionCreateEvent.ts +++ b/src/events/InteractionCreateEvent.ts @@ -5,11 +5,11 @@ import { type Client } from 'discord.js'; import Event from '../structures/Event'; -import SlashCommandHandler from '../handlers/SlashCommandHandler'; +import * as SlashCommandHandler from '../handlers/SlashCommandHandler'; import logger from '../utilities/Logger'; -import ButtonHandler from '../handlers/ButtonHandler'; -import SelectMenuHandler from '../handlers/SelectMenuHandler'; -import ModalSubmitHandler from '../handlers/ModalSubmitHandler'; +import * as ButtonHandler from '../handlers/ButtonHandler'; +import * as SelectMenuHandler from '../handlers/SelectMenuHandler'; +import * as ModalSubmitHandler from '../handlers/ModalSubmitHandler'; import { formatError } from '../utilities/ErrorMessages'; import { ApiError } from '../utilities/ApiClient'; @@ -21,7 +21,10 @@ export default class InteractionCreateEvent extends Event { }); } - private async handleError(interaction: BaseInteraction, err: any) { + private async handleError( + interaction: BaseInteraction, + err: any + ): Promise { logger.error(err); if (interaction.isAutocomplete()) return; diff --git a/src/handlers/ButtonHandler.ts b/src/handlers/ButtonHandler.ts index ad83113..626ffa8 100644 --- a/src/handlers/ButtonHandler.ts +++ b/src/handlers/ButtonHandler.ts @@ -8,85 +8,76 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import Button from '../structures/Button'; import logger from '../utilities/Logger'; -import CooldownManager from '../managers/CooldownManager'; +import * as CooldownManager from '../managers/CooldownManager'; const filePath = join(__dirname, '../components/buttons'); -export default class ButtonHandler { - private static _cache: Collection = new Collection(); +const _cache: Collection = new Collection(); - public static load(): void { - const buttonFiles = readdirSync(filePath).filter( - (file) => - (file.endsWith('.ts') || file.endsWith('.js')) && - !file.endsWith('.d.ts') - ); - - if (buttonFiles.length < 1) { - logger.info( - `[ButtonHandler] No button executable data to cache. Skipping step` - ); - return; - } - - for (const file of buttonFiles) { - let button = require(join(filePath, file)); - button = new button.default(); - if (!(button instanceof Button)) continue; - this._cache.set(button.customId, button); - } +export function load(): void { + const buttonFiles = readdirSync(filePath).filter( + (file) => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + ); + if (buttonFiles.length < 1) { logger.info( - `[ButtonHandler] Cached ${this._cache.size} button executables` + `[ButtonHandler] No button executable data to cache. Skipping step` ); + return; + } + + for (const file of buttonFiles) { + let button = require(join(filePath, file)); + button = new button.default(); + if (!(button instanceof Button)) continue; + _cache.set(button.customId, button); } - public static async handle( - customId: string, - interaction: ButtonInteraction, - client: Client - ) { - let id = customId; - let target = null; - if (customId.startsWith('page_')) return; - if (customId.includes(':')) { - const [name, ...args] = customId.split(':'); - id = name; - target = args; - } - try { - const button = this._cache.get(id); - if (button == null) { - await interaction.reply({ - content: 'This button is no longer supported or has deprecated code!', - flags: MessageFlags.Ephemeral - }); - return; - } + logger.info(`[ButtonHandler] Cached ${_cache.size} button executables`); +} - if ( - button.isAuthorOnly && - interaction.user.id !== interaction.message.interactionMetadata?.user.id - ) - return; +export async function handle( + customId: string, + interaction: ButtonInteraction, + client: Client +): Promise { + let id = customId; + let target = null; + if (customId.startsWith('page_')) return; + if (customId.includes(':')) { + const [name, ...args] = customId.split(':'); + id = name; + target = args; + } - let key = `b-${id}-${interaction.user.id}`; - if (customId === 'startNewDay') key = `adventure-${interaction.user.id}`; - if (CooldownManager.onCooldown(key)) { - const expireAt = CooldownManager.getExpiration(key); - await interaction.reply({ - content: `⏳ You can use this button again .`, - flags: MessageFlags.Ephemeral - }); - return; - } + const button = _cache.get(id); + if (button == null) { + await interaction.reply({ + content: 'This button is no longer supported or has deprecated code!', + flags: MessageFlags.Ephemeral + }); + return; + } - await button.execute(interaction, client, target); - CooldownManager.addCooldown(key, button.cooldown); - logger.button( - `${interaction.user.username} (${interaction.user.id}) used '${customId}'` - ); - } catch (err) { - throw err; - } + if ( + button.isAuthorOnly && + interaction.user.id !== interaction.message.interactionMetadata?.user.id + ) + return; + + let key = `b-${id}-${interaction.user.id}`; + if (customId === 'startNewDay') key = `adventure-${interaction.user.id}`; + if (CooldownManager.onCooldown(key)) { + const expireAt = CooldownManager.getExpiration(key); + await interaction.reply({ + content: `⏳ You can use this button again .`, + flags: MessageFlags.Ephemeral + }); + return; } + + await button.execute(interaction, client, target); + CooldownManager.addCooldown(key, button.cooldown); + logger.button( + `${interaction.user.username} (${interaction.user.id}) used '${customId}'` + ); } diff --git a/src/handlers/EventHandler.ts b/src/handlers/EventHandler.ts index 386a54f..fd24a46 100644 --- a/src/handlers/EventHandler.ts +++ b/src/handlers/EventHandler.ts @@ -4,35 +4,21 @@ import { type Client } from 'discord.js'; import { join } from 'path'; const filePath = join(__dirname, '../events'); -export default class EventHandler { - private client: Client; +export default function initializeEventHandler(client: Client): void { + const eventFiles = readdirSync(filePath).filter( + (file) => file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts') + ); - constructor(client: Client) { - this.client = client; + for (const file of eventFiles) { + let event = require(join(filePath, file)); + event = new event.default(); + if (!(event instanceof Event)) continue; - this.initialize(); - } - - private initialize(): void { - const eventFiles = readdirSync(filePath).filter( - (file) => - file.endsWith('.ts') || file.endsWith('.js') || !file.endsWith('.d.ts') - ); - - for (const file of eventFiles) { - let event = require(join(filePath, file)); - event = new event.default(); - if (!(event instanceof Event)) continue; - - if (event.isOnce) { - this.client.once(event.name, (...args: any[]) => - event.execute(...args, this.client) - ); - } else { - this.client.on(event.name, (...args: any[]) => - event.execute(...args, this.client) - ); - } + if (event.isOnce) { + client.once(event.name, (...args: any[]) => event.execute(...args, client) + ); + } else { + client.on(event.name, (...args: any[]) => event.execute(...args, client)); } } } diff --git a/src/handlers/ModalSubmitHandler.ts b/src/handlers/ModalSubmitHandler.ts index a6a6361..e4f2774 100644 --- a/src/handlers/ModalSubmitHandler.ts +++ b/src/handlers/ModalSubmitHandler.ts @@ -7,66 +7,58 @@ import { import { readdirSync } from 'fs'; import { join } from 'path'; import logger from '../utilities/Logger'; -import CooldownManager from '../managers/CooldownManager'; +import * as CooldownManager from '../managers/CooldownManager'; const filePath = join(__dirname, '../components/modals'); -export default class ModalSubmitHandler { - private static _cache: Collection = new Collection(); +const _cache: Collection = new Collection(); - public static load(): void { - const modalFiles = readdirSync(filePath).filter( - (file) => - (file.endsWith('.ts') || file.endsWith('.js')) && - !file.endsWith('.d.ts') - ); - - if (modalFiles.length < 1) { - logger.info( - '[ModalSubmitHandler] No modal submit executable data to cache. Skipping step' - ); - return; - } - - for (const file of modalFiles) { - let modal = require(join(filePath, file)); - modal = new modal.default(); - if (!(modal instanceof ModalSubmit)) continue; - this._cache.set(modal.customId, modal); - } +export function load(): void { + const modalFiles = readdirSync(filePath).filter( + (file) => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + ); + if (modalFiles.length < 1) { logger.info( - `[ModalSubmitHandler] Cached a total of ${this._cache.size} modal executable data` + '[ModalSubmitHandler] No modal submit executable data to cache. Skipping step' ); + return; + } + + for (const file of modalFiles) { + let modal = require(join(filePath, file)); + modal = new modal.default(); + if (!(modal instanceof ModalSubmit)) continue; + _cache.set(modal.customId, modal); } - public static async handle( - customId: string, - interaction: ModalSubmitInteraction, - client: Client - ) { - let id = customId; - let target = null; - if (customId.includes(':')) { - const [name, ...args] = customId.split(':'); - id = name; - target = args; - } + logger.info( + `[ModalSubmitHandler] Cached a total of ${_cache.size} modal executable data` + ); +} - try { - const modal = this._cache.get(id); - if (modal == null) - throw new Error( - `No modal executable data could be found for the ID: ${customId}` - ); +export async function handle( + customId: string, + interaction: ModalSubmitInteraction, + client: Client +): Promise { + let id = customId; + let target = null; + if (customId.includes(':')) { + const [name, ...args] = customId.split(':'); + id = name; + target = args; + } - const key = `m-${customId}-${interaction.user.id}`; + const modal = _cache.get(id); + if (modal == null) + throw new Error( + `No modal executable data could be found for the ID: ${customId}` + ); - if (CooldownManager.onCooldown(key)) return; + const key = `m-${customId}-${interaction.user.id}`; - await modal.execute(interaction, client, target); - CooldownManager.addCooldown(key, modal.cooldown); - } catch (err) { - throw err; - } - } + if (CooldownManager.onCooldown(key)) return; + + await modal.execute(interaction, client, target); + CooldownManager.addCooldown(key, modal.cooldown); } diff --git a/src/handlers/SelectMenuHandler.ts b/src/handlers/SelectMenuHandler.ts index d9b2c68..70a3f75 100644 --- a/src/handlers/SelectMenuHandler.ts +++ b/src/handlers/SelectMenuHandler.ts @@ -6,73 +6,63 @@ import { } from 'discord.js'; import { readdirSync } from 'fs'; import logger from '../utilities/Logger'; -import CooldownManager from '../managers/CooldownManager'; +import * as CooldownManager from '../managers/CooldownManager'; import { join } from 'path'; const filePath = join(__dirname, '../components/menus'); -export default class SelectMenuHandler { - private static _cache: Collection = new Collection(); +const _cache: Collection = new Collection(); - public static load(): void { - const menuFiles = readdirSync(filePath).filter( - (file) => - (file.endsWith('.ts') || file.endsWith('.js')) && - !file.endsWith('.d.ts') - ); - - if (menuFiles.length < 1) { - logger.info( - `[SelectMenuHandler] No select menu executable data to cache. Skipping step` - ); - return; - } - - for (const file of menuFiles) { - let menu = require(join(filePath, file)); - menu = new menu.default(); - if (!(menu instanceof SelectMenu)) continue; - this._cache.set(menu.customId, menu); - } +export function load(): void { + const menuFiles = readdirSync(filePath).filter( + (file) => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + ); + if (menuFiles.length < 1) { logger.info( - `[SelectMenuHandler] Cached ${this._cache.size} menu executables` + `[SelectMenuHandler] No select menu executable data to cache. Skipping step` ); + return; + } + + for (const file of menuFiles) { + let menu = require(join(filePath, file)); + menu = new menu.default(); + if (!(menu instanceof SelectMenu)) continue; + _cache.set(menu.customId, menu); } - public static async handle( - customId: string, - interaction: AnySelectMenuInteraction, - client: Client - ) { - let id = customId; - let target = null; - if (customId.includes(':')) { - const [name, ...args] = customId.split(':'); - id = name; - target = args; - } + logger.info(`[SelectMenuHandler] Cached ${_cache.size} menu executables`); +} - try { - const menu = this._cache.get(id); - if (!menu) - throw new Error( - `No executable data could be found for menu with ID: ${customId}` - ); +export async function handle( + customId: string, + interaction: AnySelectMenuInteraction, + client: Client +): Promise { + let id = customId; + let target = null; + if (customId.includes(':')) { + const [name, ...args] = customId.split(':'); + id = name; + target = args; + } - if ( - menu.isAuthorOnly && - interaction.user.id !== interaction.message.interactionMetadata?.user.id - ) - return; + const menu = _cache.get(id); + if (!menu) + throw new Error( + `No executable data could be found for menu with ID: ${customId}` + ); - const key = `s-${customId}-${interaction.user.id}`; + if ( + menu.isAuthorOnly && + interaction.user.id !== interaction.message.interactionMetadata?.user.id + ) + return; - if (CooldownManager.onCooldown(key)) return; + const key = `s-${customId}-${interaction.user.id}`; - await menu.execute(interaction, client, target); - CooldownManager.addCooldown(key, menu.cooldown); - } catch (err) { - throw err; - } - } + if (CooldownManager.onCooldown(key)) return; + + await menu.execute(interaction, client, target); + CooldownManager.addCooldown(key, menu.cooldown); } diff --git a/src/handlers/SlashCommandHandler.ts b/src/handlers/SlashCommandHandler.ts index 45bcd4b..b607fac 100644 --- a/src/handlers/SlashCommandHandler.ts +++ b/src/handlers/SlashCommandHandler.ts @@ -9,88 +9,74 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import SlashCommand from '../structures/SlashCommand'; import logger from '../utilities/Logger'; -import CooldownManager from '../managers/CooldownManager'; +import * as CooldownManager from '../managers/CooldownManager'; const filePath = join(__dirname, '../commands'); -export default class SlashCommandHandler { - private static _cache: Collection = new Collection(); +export const cache: Collection = new Collection(); - public static getCache(): Collection { - return this._cache; +export function load(): void { + const commandFiles = readdirSync(filePath).filter( + (file) => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') + ); + + for (const file of commandFiles) { + let command = require(join(filePath, file)); + command = new command.default(); + if (!(command instanceof SlashCommand)) continue; + cache.set(command.data.name, command); } - public static load(): void { - const commandFiles = readdirSync(filePath).filter( - (file) => - (file.endsWith('.ts') || file.endsWith('.js')) && - !file.endsWith('.d.ts') - ); + logger.info(`[SlashCommandHandler] Cached a total of ${cache.size} commands`); +} - for (const file of commandFiles) { - let command = require(join(filePath, file)); - command = new command.default(); - if (!(command instanceof SlashCommand)) continue; - this._cache.set(command.data.name, command); - } +export async function handle( + name: string, + interaction: ChatInputCommandInteraction, + client: Client +): Promise { + const startTime = Date.now(); - logger.info( - `[SlashCommandHandler] Cached a total of ${this._cache.size} commands` - ); + const command = cache.get(name); + if (!command) { + await interaction.reply({ + content: 'This command is outdated or disabled', + flags: MessageFlags.Ephemeral + }); + return; } - public static async handle( - name: string, - interaction: ChatInputCommandInteraction, - client: Client - ): Promise { - const startTime = Date.now(); + const key = `${name}-${interaction.user.id}`; - try { - const command = this._cache.get(name); - if (!command) { - await interaction.reply({ - content: 'This command is outdated or disabled', - flags: MessageFlags.Ephemeral - }); - return; - } + if (CooldownManager.onCooldown(key)) { + const expiresAt = CooldownManager.getExpiration(key); + await interaction.reply({ + content: `⏳ You can use this command again .`, + flags: MessageFlags.Ephemeral + }); + return; + } - const key = `${name}-${interaction.user.id}`; + await command.execute(interaction, client); - if (CooldownManager.onCooldown(key)) { - const expiresAt = CooldownManager.getExpiration(key); - await interaction.reply({ - content: `⏳ You can use this command again .`, - flags: MessageFlags.Ephemeral - }); - return; - } + CooldownManager.addCooldown(key, command.cooldown); + logger.command( + `/${name} | ${interaction.user.username} (${interaction.user.id}) | ${interaction.guild?.name ?? 'DM'} | ${Date.now() - startTime}ms` + ); +} - await command.execute(interaction, client); +export async function autocomplete( + name: string, + interaction: AutocompleteInteraction, + client: Client +): Promise { + const command = cache.get(name); + if (!command) return; - CooldownManager.addCooldown(key, command.cooldown); - logger.command( - `/${name} | ${interaction.user.username} (${interaction.user.id}) | ${interaction.guild?.name ?? 'DM'} | ${Date.now() - startTime}ms` - ); + if (command.autocomplete) { + try { + await command.autocomplete(interaction, client); } catch (err) { - throw err; - } - } - - public static async autocomplete( - name: string, - interaction: AutocompleteInteraction, - client: Client - ): Promise { - const command = this._cache.get(name); - if (!command) return; - - if (command.autocomplete) { - try { - await command.autocomplete(interaction, client); - } catch (err) { - logger.error(`Autocomplete failed for ${name}: ${err}`); - } + logger.error(`Autocomplete failed for ${name}: ${err}`); } } } diff --git a/src/managers/CooldownManager.ts b/src/managers/CooldownManager.ts index a2faf78..ca2b613 100644 --- a/src/managers/CooldownManager.ts +++ b/src/managers/CooldownManager.ts @@ -1,37 +1,34 @@ import { Collection } from 'discord.js'; -export default class CooldownManager { - private static _cache: Collection = new Collection(); +const _cache: Collection = new Collection(); +const _interval = setInterval(() => prune(), 60_000); - private static _interval = setInterval(() => this.prune(), 60_000); +export function onCooldown(key: string): boolean { + const expiration = _cache.get(key); + if (!expiration) return false; - public static onCooldown(key: string): boolean { - const expiration = this._cache.get(key); - if (!expiration) return false; - - if (expiration > Date.now()) { - return true; - } - this._cache.delete(key); - return false; + if (expiration > Date.now()) { + return true; } + _cache.delete(key); + return false; +} - public static getExpiration(key: string): number { - const expiration = this._cache.get(key); - if (!expiration) return Math.floor(Date.now() / 1000); +export function getExpiration(key: string): number { + const expiration = _cache.get(key); + if (!expiration) return Math.floor(Date.now() / 1000); - return Math.floor(expiration / 1000); - } + return Math.floor(expiration / 1000); +} - public static addCooldown(key: string, durationInSeconds: number): void { - if (this.onCooldown(key)) return; +export function addCooldown(key: string, durationInSeconds: number): void { + if (onCooldown(key)) return; - const expiresAt = Date.now() + durationInSeconds * 1000; - this._cache.set(key, expiresAt); - } + const expiresAt = Date.now() + durationInSeconds * 1000; + _cache.set(key, expiresAt); +} - private static prune(): void { - const now = Date.now(); - this._cache.sweep((expiration) => expiration <= now); - } +function prune(): void { + const now = Date.now(); + _cache.sweep((expiration) => expiration <= now); } diff --git a/src/managers/ItemManager.ts b/src/managers/ItemManager.ts index b79fc0e..2cfa534 100644 --- a/src/managers/ItemManager.ts +++ b/src/managers/ItemManager.ts @@ -1,123 +1,121 @@ import { Collection } from 'discord.js'; import { type IItemJSON } from '../interfaces/IItemJSON'; import logger from '../utilities/Logger'; -import Routes from '../utilities/Routes'; +import * as Routes from '../utilities/Routes'; import 'dotenv/config'; const REFRESH_INTERVAL = 300_000; // 5 minutes const FETCH_TIMEOUT = 15_000; // 15 second timeout for API calls -export default class ItemManager { - public static cache: Collection = new Collection(); - private static isLoaded: boolean = false; - private static isRefreshing: boolean = false; - private static refreshTimer: NodeJS.Timeout | null = null; - - /** - * Fetch all items from the API and populate the cache. - * Uses atomic swap so the cache is never empty mid-refresh — - * live commands always read from a full dataset. - */ - public static async init(): Promise { - // Prevent overlapping fetches (e.g. slow API + interval fires again) - if (this.isRefreshing) { - logger.warn('[ItemManager] Refresh already in progress, skipping'); +export let cache: Collection = new Collection(); +let isLoaded: boolean = false; +let isRefreshing: boolean = false; +let refreshTimer: NodeJS.Timeout | null = null; + +/** + * Fetch all items from the API and populate the cache. + * Uses atomic swap so the cache is never empty mid-refresh — + * live commands always read from a full dataset. + */ +export async function init(): Promise { + // Prevent overlapping fetches (e.g. slow API + interval fires again) + if (isRefreshing) { + logger.warn('[ItemManager] Refresh already in progress, skipping'); + return; + } + + isRefreshing = true; + + try { + const res = await fetch(Routes.items(), { + headers: Routes.HEADERS(), + signal: AbortSignal.timeout(FETCH_TIMEOUT) + }); + + if (!res.ok) { + logger.error( + `[ItemManager] Failed to fetch items: HTTP ${res.status} ${res.statusText}` + ); return; } - this.isRefreshing = true; - - try { - const res = await fetch(Routes.items(), { - headers: Routes.HEADERS(), - signal: AbortSignal.timeout(FETCH_TIMEOUT) - }); - - if (!res.ok) { - logger.error( - `[ItemManager] Failed to fetch items: HTTP ${res.status} ${res.statusText}` - ); - return; - } - - const responseBody = await res.json(); - - if (!responseBody.success || !responseBody.data) { - logger.error('[ItemManager] API returned unexpected payload structure'); - return; - } - - const items: IItemJSON[] = responseBody.data; - - // Atomic swap: build the new cache fully before replacing the old one. - // This ensures any command reading mid-refresh still gets complete data. - const newCache = new Collection(); - for (const item of items) { - newCache.set(item.itemId, item); - } - - this.cache = newCache; - this.isLoaded = true; - - logger.info(`[ItemManager] Synced ${newCache.size} items`); - } catch (error: any) { - // Distinguish timeout from other errors for clearer debugging - if (error.name === 'TimeoutError' || error.name === 'AbortError') { - logger.error( - `[ItemManager] Fetch timed out after ${FETCH_TIMEOUT / 1000}s` - ); - } else { - logger.error(error, '[ItemManager] Critical fetch error:'); - } - } finally { - this.isRefreshing = false; + const responseBody = await res.json(); + + if (!responseBody.success || !responseBody.data) { + logger.error('[ItemManager] API returned unexpected payload structure'); + return; } - } - /** - * Start the auto-refresh loop. Safe to call multiple times — - * clears any existing interval before creating a new one. - */ - public static async refresh(): Promise { - // Clear any existing interval to prevent stacking - if (this.refreshTimer) { - clearInterval(this.refreshTimer); - this.refreshTimer = null; + const items: IItemJSON[] = responseBody.data; + + // Atomic swap: build the new cache fully before replacing the old one. + // This ensures any command reading mid-refresh still gets complete data. + const newCache = new Collection(); + for (const item of items) { + newCache.set(item.itemId, item); } - // Initial fetch - await this.init(); + cache = newCache; + isLoaded = true; - // Schedule recurring refreshes - this.refreshTimer = setInterval(() => this.init(), REFRESH_INTERVAL); + logger.info(`[ItemManager] Synced ${newCache.size} items`); + } catch (error: any) { + // Distinguish timeout from other errors for clearer debugging + if (error.name === 'TimeoutError' || error.name === 'AbortError') { + logger.error( + `[ItemManager] Fetch timed out after ${FETCH_TIMEOUT / 1000}s` + ); + } else { + logger.error(error, '[ItemManager] Critical fetch error:'); + } + } finally { + isRefreshing = false; } +} - /** - * Stop the auto-refresh loop. Call during shutdown. - */ - public static shutdown(): void { - if (this.refreshTimer) { - clearInterval(this.refreshTimer); - this.refreshTimer = null; - } +/** + * Start the auto-refresh loop. Safe to call multiple times — + * clears any existing interval before creating a new one. + */ +export async function refresh(): Promise { + // Clear any existing interval to prevent stacking + if (refreshTimer) { + clearInterval(refreshTimer); + refreshTimer = null; } - /** - * Retrieve a single item by ID. Returns undefined if not found. - */ - public static get(itemId: number): IItemJSON | undefined { - if (!this.isLoaded) { - logger.warn( - `[ItemManager] Attempted to get item ${itemId} before cache was loaded` - ); - } - return this.cache.get(itemId); + // Initial fetch + await init(); + + // Schedule recurring refreshes + refreshTimer = setInterval(() => init(), REFRESH_INTERVAL); +} + +/** + * Stop the auto-refresh loop. Call during shutdown. + */ +export function shutdown(): void { + if (refreshTimer) { + clearInterval(refreshTimer); + refreshTimer = null; } +} - /** - * Number of items currently cached. - */ - public static get size(): number { - return this.cache.size; +/** + * Retrieve a single item by ID. Returns undefined if not found. + */ +export function get(itemId: number): IItemJSON | undefined { + if (!isLoaded) { + logger.warn( + `[ItemManager] Attempted to get item ${itemId} before cache was loaded` + ); } + return cache.get(itemId); +} + +/** + * Number of items currently cached. + */ +export function size(): number { + return cache.size; } diff --git a/src/managers/PresenceManager.ts b/src/managers/PresenceManager.ts index ef35a74..ae67e96 100644 --- a/src/managers/PresenceManager.ts +++ b/src/managers/PresenceManager.ts @@ -1,7 +1,6 @@ import { ActivityType, type Client } from 'discord.js'; import logger from '../utilities/Logger'; -import Routes from '../utilities/Routes'; -import ItemManager from './ItemManager'; +import * as Routes from '../utilities/Routes'; import { apiFetch } from '../utilities/ApiClient'; interface GameStats { @@ -13,109 +12,107 @@ interface GameStats { const ROTATION_INTERVAL = 60_000; const FETCH_INTERVAL = 300_000; -export default class PresenceManager { - private static client: Client; - private static rotationIndex: number = 0; - private static rotationTimer: NodeJS.Timeout | null = null; - private static fetchTimer: NodeJS.Timeout | null = null; - private static totalGuilds: number = 0; +let client: Client; +let rotationIndex: number = 0; +let rotationTimer: NodeJS.Timeout | null = null; +let fetchTimer: NodeJS.Timeout | null = null; +let totalGuilds: number = 0; - private static stats: GameStats = { - players: 0, - items: 0, - scenarios: 0 - }; +const stats: GameStats = { + players: 0, + items: 0, + scenarios: 0 +}; - public static async init(client: Client): Promise { - this.client = client; +export async function init(newClient: Client): Promise { + client = newClient; - await this.fetchStats(); - this.fetchTimer = setInterval(() => this.fetchStats(), FETCH_INTERVAL); + await fetchStats(); + fetchTimer = setInterval(() => fetchStats(), FETCH_INTERVAL); - this.rotate(); - this.rotationTimer = setInterval(() => this.rotate(), ROTATION_INTERVAL); + rotate(); + rotationTimer = setInterval(() => rotate(), ROTATION_INTERVAL); - logger.info('[PresenceManager] Activity rotation started'); - } + logger.info('[PresenceManager] Activity rotation started'); +} - private static async fetchStats(): Promise { - try { - const telemetryRes = await apiFetch( - 'https://capi.gg/api/telemetry/db-stats' - ); +async function fetchStats(): Promise { + try { + const telemetryRes = await apiFetch( + 'https://capi.gg/api/telemetry/db-stats' + ); - if (telemetryRes.ok) { - const data = await telemetryRes.json(); - this.stats.players = data.players ?? this.stats.players; - this.stats.items = data.items ?? this.stats.items; - } + if (telemetryRes.ok) { + const data = await telemetryRes.json(); + stats.players = data.players ?? stats.players; + stats.items = data.items ?? stats.items; + } - const scenarioRes = await apiFetch(Routes.scenarios()); + const scenarioRes = await apiFetch(Routes.scenarios()); - if (scenarioRes.ok) { - const data = await scenarioRes.json(); - this.stats.scenarios = data.count ?? this.stats.scenarios; - } + if (scenarioRes.ok) { + const data = await scenarioRes.json(); + stats.scenarios = data.count ?? stats.scenarios; + } - // Fetch total guild count across all clusters - const cluster = (this.client as any).cluster; - if (cluster) { - try { - const results = await cluster.broadcastEval( - (c: any) => c.guilds.cache.size - ); - this.totalGuilds = results.reduce( - (acc: number, val: number) => acc + val, - 0 - ); - } catch { - this.totalGuilds = this.client.guilds.cache.size; - } - } else { - this.totalGuilds = this.client.guilds.cache.size; + // Fetch total guild count across all clusters + const cluster = (client! as any).cluster; + if (cluster) { + try { + const results = await cluster.broadcastEval( + (c: any) => c.guilds.cache.size + ); + totalGuilds = results.reduce( + (acc: number, val: number) => acc + val, + 0 + ); + } catch { + totalGuilds = client!.guilds.cache.size; } - } catch (err) { - logger.warn(`[PresenceManager] Failed to fetch stats: ${err}`); + } else { + totalGuilds = client!.guilds.cache.size; } + } catch (err) { + logger.warn(`[PresenceManager] Failed to fetch stats: ${err}`); } +} - private static rotate(): void { - if (!this.client.user) return; - - const activities = [ - { - type: ActivityType.Watching, - name: `${this.stats.players.toLocaleString()} players` - }, - { - type: ActivityType.Watching, - name: `${this.totalGuilds.toLocaleString()} servers` - }, - { - type: ActivityType.Watching, - name: `${this.stats.items.toLocaleString()} items` - }, - { - type: ActivityType.Watching, - name: `${this.stats.scenarios.toLocaleString()} scenarios` - }, - { type: ActivityType.Playing, name: `capi.gg` } - ]; - - const current = activities[this.rotationIndex % activities.length]; - - this.client.user.setPresence({ - activities: [{ name: current.name, type: current.type }], - status: 'online' - }); - - this.rotationIndex++; - } +function rotate(): void { + if (!client!.user) return; + + const activities = [ + { + type: ActivityType.Watching, + name: `${stats.players.toLocaleString()} players` + }, + { + type: ActivityType.Watching, + name: `${totalGuilds.toLocaleString()} servers` + }, + { + type: ActivityType.Watching, + name: `${stats.items.toLocaleString()} items` + }, + { + type: ActivityType.Watching, + name: `${stats.scenarios.toLocaleString()} scenarios` + }, + { type: ActivityType.Playing, name: `capi.gg` } + ]; + + const current = activities[rotationIndex % activities.length]; + + client!.user.setPresence({ + activities: [{ name: current.name, type: current.type }], + status: 'online' + }); + + rotationIndex++; +} - public static shutdown(): void { - if (this.rotationTimer) clearInterval(this.rotationTimer); - if (this.fetchTimer) clearInterval(this.fetchTimer); - this.rotationTimer = null; - this.fetchTimer = null; - } +export function shutdown(): void { + if (rotationTimer) clearInterval(rotationTimer); + if (fetchTimer) clearInterval(fetchTimer); + rotationTimer = null; + fetchTimer = null; } diff --git a/src/structures/containers/AttackContainer.ts b/src/structures/containers/AttackContainer.ts index b511554..f4588b8 100644 --- a/src/structures/containers/AttackContainer.ts +++ b/src/structures/containers/AttackContainer.ts @@ -20,24 +20,20 @@ export default class AttackContainer { '**$1**' ); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent(cleanFlavorText) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent(cleanFlavorText) ); if (!this.data.combatEnded && this.data.enemy) { container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent( - `**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\`` - ), - (textDisplay) => - textDisplay.setContent(`-# Use /attack to strike again!`) + (textDisplay) => textDisplay.setContent( + `**Your HP:** ❤️ \`${this.data.playerStats.stats.hp.toLocaleString()}/${this.data.playerStats.maxHp?.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**${this.data.enemy!.name}'s HP:** ❤️ \`${Math.max(0, this.data.enemy!.currentHp)}/${this.data.enemy!.maxHp}\`` + ), + (textDisplay) => textDisplay.setContent(`-# Use /attack to strike again!`) ); } @@ -52,16 +48,14 @@ export default class AttackContainer { rewardText.push(`🎒 Looted: **${this.data.rewards.item.name}**`); for (const reward of rewardText) { - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent(reward) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent(reward) ); } } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ExploreContainer.ts b/src/structures/containers/ExploreContainer.ts index 27c1ec5..24f0428 100644 --- a/src/structures/containers/ExploreContainer.ts +++ b/src/structures/containers/ExploreContainer.ts @@ -22,36 +22,29 @@ export default class ExploreContainer { container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent(cleanFlavorText), - (textDisplay) => - textDisplay.setContent( - `-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\`` - ) + (textDisplay) => textDisplay.setContent( + `-# **ID:** \`${this.data.scenarioId}\` | **Author:** \`${this.data.scenarioAuthor}\`` + ) ); if (this.data.enemy) { const enemy = this.data.enemy; container.addSeparatorComponents((s) => s); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent( - `**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), - (textDisplay) => - textDisplay.setContent(`-# Use the /attack command to fight`) + (textDisplay) => textDisplay.setContent( + `**Enemy**: \`LVL${enemy.level.toLocaleString()} ${enemy.name}\`` + ), + (textDisplay) => textDisplay.setContent( + `**HP:** \`${enemy.currentHp.toLocaleString()}/${enemy.maxHp.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent(`**ATK:** \`${enemy.atk.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`**DEF:** \`${enemy.def.toLocaleString()}\``), + (textDisplay) => textDisplay.setContent(`-# Use the /attack command to fight`) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -75,23 +68,20 @@ export default class ExploreContainer { if (rewardText.length >= 1) { container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent( - `-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\`` - ) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent( + `-# **Lvl:** \`${level.toLocaleString()}\` | **Exp:** \`${experience.toLocaleString()}/${expRequired.toLocaleString()}\`` + ) ); } for (const text of rewardText) { - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent(text) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent(text) ); } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; @@ -99,8 +89,7 @@ export default class ExploreContainer { container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ItemLookupContainer.ts b/src/structures/containers/ItemLookupContainer.ts index c98ce5d..682162b 100644 --- a/src/structures/containers/ItemLookupContainer.ts +++ b/src/structures/containers/ItemLookupContainer.ts @@ -14,33 +14,28 @@ export default class ItemLookupContainer { ); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), - (textDisplay) => - textDisplay.setContent( - `-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*` - ), + (textDisplay) => textDisplay.setContent(`## LVL${this.data.level} ${this.data.name}`), + (textDisplay) => textDisplay.setContent( + `-# *${this.data.rarity} ${this.data.slot === 'None' ? '' : this.data.slot} ${this.data.type}*` + ), (textDisplay) => textDisplay.setContent(`*${this.data.description}*`), - (textDisplay) => - textDisplay.setContent( - `-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\`` - ) + (textDisplay) => textDisplay.setContent( + `-# **Stats:**\n**ATK:** \`${this.data.stats.atk.toLocaleString()}\`, **DEF:** \`${this.data.stats.def.toLocaleString()}\`, **HP:** \`${this.data.stats.hp.toLocaleString()}\`` + ) ); if (this.data.affixes) { for (const affix of this.data.affixes) { - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent( - `**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\`` - ) + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent( + `**${affix.type}** \`${affix.value}${affix.type === 'THORNS' ? '' : '%'}\`` + ) ); } } container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/NPCLookupContainer.ts b/src/structures/containers/NPCLookupContainer.ts index 4e78251..6fbd9fa 100644 --- a/src/structures/containers/NPCLookupContainer.ts +++ b/src/structures/containers/NPCLookupContainer.ts @@ -12,8 +12,7 @@ export default class NPCLookupContainer { const container = new ContainerBuilder(); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), + (textDisplay) => textDisplay.setContent(`## 💀 (ID: ${this.data.id}) ${this.data.name}`), (textDisplay) => textDisplay.setContent(this.data.description) ); diff --git a/src/structures/containers/ProfileContainer.ts b/src/structures/containers/ProfileContainer.ts index 7413210..19e4a13 100644 --- a/src/structures/containers/ProfileContainer.ts +++ b/src/structures/containers/ProfileContainer.ts @@ -11,77 +11,61 @@ export default class ProfileContainer { public build(): ContainerBuilder { const container = new ContainerBuilder(); - container.addSectionComponents((section) => - section - .addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Username:** \`${this.data.username}\``), - (textDisplay) => - textDisplay.setContent( - `**Level:** \`${this.data.level.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**Experience:** \`${this.data.experience.toLocaleString()}\`` - ) - ) - .setThumbnailAccessory((tb) => - tb.setURL( - `https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png` - ) + container.addSectionComponents((section) => section + .addTextDisplayComponents( + (textDisplay) => textDisplay.setContent(`**Username:** \`${this.data.username}\``), + (textDisplay) => textDisplay.setContent( + `**Level:** \`${this.data.level.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**Experience:** \`${this.data.experience.toLocaleString()}\`` ) + ) + .setThumbnailAccessory((tb) => tb.setURL( + `https://cdn.discordapp.com/avatars/${this.data.id}/${this.data.avatar}.png` + ) + ) ); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), - (textDisplay) => - textDisplay.setContent( - `**Coins:** \`${this.data.coins.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**ATK:** \`${this.data.stats.atk.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**DEF:** \`${this.data.stats.def.toLocaleString()}\`` - ) + (textDisplay) => textDisplay.setContent(`**Privilege:** \`${this.data.privilege}\``), + (textDisplay) => textDisplay.setContent( + `**Coins:** \`${this.data.coins.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**HP:** \`${this.data.stats.hp}/${this.data.maxHp ?? 0}\`` + ), + (textDisplay) => textDisplay.setContent( + `**ATK:** \`${this.data.stats.atk.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**DEF:** \`${this.data.stats.def.toLocaleString()}\`` + ) ); container.addSeparatorComponents((separator) => separator); container.addTextDisplayComponents( - (textDisplay) => - textDisplay.setContent( - `**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\`` - ), - (textDisplay) => - textDisplay.setContent( - `**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\`` - ) + (textDisplay) => textDisplay.setContent( + `**Days Passed:** \`${this.data.statistics.daysPassed.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**Enemies Defeated:** \`${this.data.statistics.enemiesDefeated.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**Players Defeated:** \`${this.data.statistics.playersDefeated.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**Times Died:** \`${this.data.statistics.timesDied.toLocaleString()}\`` + ), + (textDisplay) => textDisplay.setContent( + `**Quests Done:** \`${this.data.statistics.questsDone.toLocaleString()}\`` + ) ); container.addSeparatorComponents((separator) => separator); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/structures/containers/ScenarioLookupContainer.ts b/src/structures/containers/ScenarioLookupContainer.ts index 35476c8..3b9abb8 100644 --- a/src/structures/containers/ScenarioLookupContainer.ts +++ b/src/structures/containers/ScenarioLookupContainer.ts @@ -14,20 +14,17 @@ export default class ScenarioLookupContainer { container.addTextDisplayComponents( (textDisplay) => textDisplay.setContent('## Scenario Viewer'), (textDisplay) => textDisplay.setContent(this.data.description), - (textDisplay) => - textDisplay.setContent( - `-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\`` - ), - (textDisplay) => - textDisplay.setContent( - `-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\`` - ) + (textDisplay) => textDisplay.setContent( + `-# **ID:** \`${this.data.id}\` | **Created By:** \`${this.data.createdBy}\`` + ), + (textDisplay) => textDisplay.setContent( + `-# **Created On:** \`${new Date(this.data.createdOn).toDateString()}\` | **Last Updated:** \`${new Date(this.data.lastUpdated).toDateString()}\`` + ) ); container.addSeparatorComponents((s) => s); - container.addTextDisplayComponents((textDisplay) => - textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') + container.addTextDisplayComponents((textDisplay) => textDisplay.setContent('-# ⚔️ DFO Cross-Platform Integration') ); return container; diff --git a/src/utilities/AdventureImageBuilder.ts b/src/utilities/AdventureImageBuilder.ts index 80ec9de..8a0e963 100644 --- a/src/utilities/AdventureImageBuilder.ts +++ b/src/utilities/AdventureImageBuilder.ts @@ -11,697 +11,686 @@ try { ); } catch (e) {} -export default class AdventureImageBuilder { - // Fully Upgraded Discord Markdown & Layout Engine - private static processText( - ctx: any, - text: string, - startX: number, - startY: number, - maxWidth: number, - baseLineHeight: number, - defaultColor: string, - draw: boolean - ): number { - const lines = text.split('\n'); - let currentY = startY; - - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - - // 1. Block-Level Modifiers (Headers, Quotes, Lists, Subtext) - let isQuote = false; - let isSubtext = false; - let headerLevel = 0; - let listPrefix = ''; - let lineIndent = 0; - - if (line.startsWith('>>> ')) { - isQuote = true; - line = line.substring(4); - } else if (line.startsWith('> ')) { - isQuote = true; - line = line.substring(2); - } +// Fully Upgraded Discord Markdown & Layout Engine +function processText( + ctx: any, + text: string, + startX: number, + startY: number, + maxWidth: number, + baseLineHeight: number, + defaultColor: string, + draw: boolean +): number { + const lines = text.split('\n'); + let currentY = startY; + + for (let line of lines) { + // 1. Block-Level Modifiers (Headers, Quotes, Lists, Subtext) + let isQuote = false; + let isSubtext = false; + let headerLevel = 0; + let listPrefix = ''; + let lineIndent = 0; + + if (line.startsWith('>>> ')) { + isQuote = true; + line = line.substring(4); + } else if (line.startsWith('> ')) { + isQuote = true; + line = line.substring(2); + } - if (line.startsWith('-# ')) { - isSubtext = true; - line = line.substring(3); - } else if (line.startsWith('### ')) { - headerLevel = 3; - line = line.substring(4); - } else if (line.startsWith('## ')) { - headerLevel = 2; - line = line.substring(3); - } else if (line.startsWith('# ')) { - headerLevel = 1; - line = line.substring(2); - } + if (line.startsWith('-# ')) { + isSubtext = true; + line = line.substring(3); + } else if (line.startsWith('### ')) { + headerLevel = 3; + line = line.substring(4); + } else if (line.startsWith('## ')) { + headerLevel = 2; + line = line.substring(3); + } else if (line.startsWith('# ')) { + headerLevel = 1; + line = line.substring(2); + } - const listMatch = line.match(/^(\s*[-*+]\s|\s*\d+\.\s)/); - if (listMatch) { - listPrefix = listMatch[0].trim(); - line = line.substring(listMatch[0].length); - lineIndent = 25; // Push list items inward - } + const listMatch = line.match(/^(\s*[-*+]\s|\s*\d+\.\s)/); + if (listMatch) { + listPrefix = listMatch[0].trim(); + line = line.substring(listMatch[0].length); + lineIndent = 25; // Push list items inward + } - if (isQuote) lineIndent = 20; - - // 2. Inline Markdown to Pseudo-HTML conversion - const parsedLine = line - .replace(/\|\|([^|]+)\|\|/g, '$1') // Strip Spoilers - .replace(/<@!?\d+>/g, '@User') // Format Mentions - .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // Bold Italic - .replace(/\*\*(.+?)\*\*/g, '$1') // Bold - .replace(/\*(.+?)\*/g, '$1') // Italic - .replace(/__(.+?)__/g, '$1') // Underline - .replace(/~~(.+?)~~/g, '$1') // Strikethrough - .replace(/`([^`]+)`/g, '$1') // Inline Code - .replace( - /\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, - '$1' - ); // Hex Colors - - // 3. Tokenize by our custom tags - const tokens = parsedLine - .split( - /(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g - ) - .filter(Boolean); - - const currentState = { - bold: headerLevel > 0, - italic: isQuote || isSubtext, - underline: false, - strike: false, - code: false, - color: isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor - }; + if (isQuote) lineIndent = 20; + + // 2. Inline Markdown to Pseudo-HTML conversion + const parsedLine = line + .replace(/\|\|([^|]+)\|\|/g, '$1') // Strip Spoilers + .replace(/<@!?\d+>/g, '@User') // Format Mentions + .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // Bold Italic + .replace(/\*\*(.+?)\*\*/g, '$1') // Bold + .replace(/\*(.+?)\*/g, '$1') // Italic + .replace(/__(.+?)__/g, '$1') // Underline + .replace(/~~(.+?)~~/g, '$1') // Strikethrough + .replace(/`([^`]+)`/g, '$1') // Inline Code + .replace( + /\[([^\]]+)\]\(color:(#[0-9a-fA-F]{3,6})\)/g, + '$1' + ); // Hex Colors + + // 3. Tokenize by our custom tags + const tokens = parsedLine + .split( + /(|<\/bi>||<\/b>||<\/i>||<\/u>||<\/s>||<\/c>||<\/col>)/g + ) + .filter(Boolean); + + const currentState = { + bold: headerLevel > 0, + italic: isQuote || isSubtext, + underline: false, + strike: false, + code: false, + color: isSubtext ? '#6b7280' : isQuote ? '#9ca3af' : defaultColor + }; - const wordObjects: { word: string; state: any }[] = []; + const wordObjects: { word: string; state: any }[] = []; - // Inject the bullet point/number if it's a list - if (listPrefix) { - wordObjects.push({ - word: `${listPrefix} `, - state: { ...currentState, color: '#10b981', bold: true } - }); - } + // Inject the bullet point/number if it's a list + if (listPrefix) { + wordObjects.push({ + word: `${listPrefix} `, + state: { ...currentState, color: '#10b981', bold: true } + }); + } - // Apply state toggles and chunk text into words - for (const token of tokens) { - if (token === '') { - currentState.bold = true; - currentState.italic = true; - } else if (token === '') { - currentState.bold = false; - currentState.italic = false; - } else if (token === '') currentState.bold = true; - else if (token === '') currentState.bold = false; - else if (token === '') currentState.italic = true; - else if (token === '') currentState.italic = false; - else if (token === '') currentState.underline = true; - else if (token === '') currentState.underline = false; - else if (token === '') currentState.strike = true; - else if (token === '') currentState.strike = false; - else if (token === '') { - currentState.code = true; - currentState.color = '#6ee7b7'; - } else if (token === '') { - currentState.code = false; - currentState.color = isSubtext - ? '#6b7280' - : isQuote - ? '#9ca3af' - : defaultColor; - } else if (token.startsWith('') { - currentState.color = isSubtext - ? '#6b7280' - : isQuote - ? '#9ca3af' - : defaultColor; - } else { - const textWords = token.split(' '); - for (let w = 0; w < textWords.length; w++) { - const wordStr = - textWords[w] + (w < textWords.length - 1 ? ' ' : ''); - if (wordStr.length > 0) { - wordObjects.push({ word: wordStr, state: { ...currentState } }); - } + // Apply state toggles and chunk text into words + for (const token of tokens) { + if (token === '') { + currentState.bold = true; + currentState.italic = true; + } else if (token === '') { + currentState.bold = false; + currentState.italic = false; + } else if (token === '') currentState.bold = true; + else if (token === '') currentState.bold = false; + else if (token === '') currentState.italic = true; + else if (token === '') currentState.italic = false; + else if (token === '') currentState.underline = true; + else if (token === '') currentState.underline = false; + else if (token === '') currentState.strike = true; + else if (token === '') currentState.strike = false; + else if (token === '') { + currentState.code = true; + currentState.color = '#6ee7b7'; + } else if (token === '') { + currentState.code = false; + currentState.color = isSubtext + ? '#6b7280' + : isQuote + ? '#9ca3af' + : defaultColor; + } else if (token.startsWith('') { + currentState.color = isSubtext + ? '#6b7280' + : isQuote + ? '#9ca3af' + : defaultColor; + } else { + const textWords = token.split(' '); + for (let w = 0; w < textWords.length; w++) { + const wordStr = textWords[w] + (w < textWords.length - 1 ? ' ' : ''); + if (wordStr.length > 0) { + wordObjects.push({ word: wordStr, state: { ...currentState } }); } } } + } - const getFont = (state: any) => { - const weight = state.bold ? 'bold ' : ''; - const style = state.italic ? 'italic ' : ''; - let size = 22; // Base size + const getFont = (state: any): string => { + const weight = state.bold ? 'bold ' : ''; + const style = state.italic ? 'italic ' : ''; + let size = 22; // Base size - if (headerLevel === 1) size = 32; - else if (headerLevel === 2) size = 28; - else if (headerLevel === 3) size = 24; - else if (isSubtext) size = 16; - else if (state.code) size = 20; + if (headerLevel === 1) size = 32; + else if (headerLevel === 2) size = 28; + else if (headerLevel === 3) size = 24; + else if (isSubtext) size = 16; + else if (state.code) size = 20; - let family = state.code ? 'monospace' : 'sans-serif'; - if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; + let family = state.code ? 'monospace' : 'sans-serif'; + if (!state.code && !headerLevel && !isSubtext) family = 'monospace'; - return `${style}${weight}${size}px ${family}`; - }; + return `${style}${weight}${size}px ${family}`; + }; - let lineWords: any[] = []; - let currentLineWidth = 0; - const startYOfParagraph = currentY; + let lineWords: any[] = []; + let currentLineWidth = 0; + const startYOfParagraph = currentY; - let increment = baseLineHeight; - if (headerLevel === 1) increment = 44; - else if (headerLevel === 2) increment = 38; - else if (headerLevel === 3) increment = 32; - else if (isSubtext) increment = 22; + let increment = baseLineHeight; + if (headerLevel === 1) increment = 44; + else if (headerLevel === 2) increment = 38; + else if (headerLevel === 3) increment = 32; + else if (isSubtext) increment = 22; - if (wordObjects.length === 0) { - currentY += baseLineHeight; - continue; - } + if (wordObjects.length === 0) { + currentY += baseLineHeight; + continue; + } - // Draws a full wrapped line to the canvas - const flushLine = () => { - if (lineWords.length === 0) return; + // Draws a full wrapped line to the canvas + const flushLine = (): void => { + if (lineWords.length === 0) return; - if (draw) { - let drawX = startX + lineIndent; - for (const lw of lineWords) { - ctx.font = getFont(lw.state); - ctx.fillStyle = lw.state.color; + if (draw) { + let drawX = startX + lineIndent; + for (const lw of lineWords) { + ctx.font = getFont(lw.state); + ctx.fillStyle = lw.state.color; + + const m = ctx.measureText(lw.word); - const m = ctx.measureText(lw.word); - - if (lw.state.code) { - ctx.fillStyle = '#ffffff1a'; - ctx.fillRect( - drawX, - currentY - increment * 0.7, - m.width, - increment - ); - ctx.fillStyle = lw.state.color; - } - - ctx.fillText(lw.word, drawX, currentY); - - // Underlines and Strikethroughs - if (lw.state.underline) { - ctx.fillRect( - drawX, - currentY + 4, - m.width - (lw.word.endsWith(' ') ? 8 : 0), - 2 - ); - } - if (lw.state.strike) { - ctx.fillRect( - drawX, - currentY - increment * 0.3, - m.width - (lw.word.endsWith(' ') ? 8 : 0), - 2 - ); - } - - drawX += m.width; + if (lw.state.code) { + ctx.fillStyle = '#ffffff1a'; + ctx.fillRect(drawX, currentY - increment * 0.7, m.width, increment); + ctx.fillStyle = lw.state.color; } - } - currentY += increment; - lineWords = []; - currentLineWidth = 0; - }; - // Measure & Wrap loop - for (let w = 0; w < wordObjects.length; w++) { - const wObj = wordObjects[w]; - ctx.font = getFont(wObj.state); - let metrics = ctx.measureText(wObj.word); - - if ( - currentLineWidth + metrics.width > maxWidth - lineIndent && - lineWords.length > 0 - ) { - flushLine(); - // Strip leading space on wrap - if (wObj.word.startsWith(' ')) { - wObj.word = wObj.word.substring(1); - metrics = ctx.measureText(wObj.word); + ctx.fillText(lw.word, drawX, currentY); + + // Underlines and Strikethroughs + if (lw.state.underline) { + ctx.fillRect( + drawX, + currentY + 4, + m.width - (lw.word.endsWith(' ') ? 8 : 0), + 2 + ); + } + if (lw.state.strike) { + ctx.fillRect( + drawX, + currentY - increment * 0.3, + m.width - (lw.word.endsWith(' ') ? 8 : 0), + 2 + ); } - } - lineWords.push(wObj); - currentLineWidth += metrics.width; + drawX += m.width; + } } + currentY += increment; + lineWords = []; + currentLineWidth = 0; + }; - flushLine(); - - // Draw Block Quote bar across the entire paragraph block - if (isQuote && draw) { - ctx.fillStyle = '#10b98180'; - ctx.fillRect( - startX, - startYOfParagraph - increment * 0.7, - 4, - currentY - startYOfParagraph - ); + // Measure & Wrap loop + for (const wObj of wordObjects) { + ctx.font = getFont(wObj.state); + let metrics = ctx.measureText(wObj.word); + + if ( + currentLineWidth + metrics.width > maxWidth - lineIndent && + lineWords.length > 0 + ) { + flushLine(); + // Strip leading space on wrap + if (wObj.word.startsWith(' ')) { + wObj.word = wObj.word.substring(1); + metrics = ctx.measureText(wObj.word); + } } - if (headerLevel > 0) currentY += 10; - else currentY += baseLineHeight * 0.3; // Paragraph margin + lineWords.push(wObj); + currentLineWidth += metrics.width; } - return currentY - startY; - } + flushLine(); - public static async build(data: IStepJSON | ICombatJSON): Promise { - // --- NORMALIZE DATA PAYLOADS --- - const flavorText = data.flavorText || 'Waiting for input...'; - const enemyStats = data.enemy; + // Draw Block Quote bar across the entire paragraph block + if (isQuote && draw) { + ctx.fillStyle = '#10b98180'; + ctx.fillRect( + startX, + startYOfParagraph - increment * 0.7, + 4, + currentY - startYOfParagraph + ); + } - const scenarioMeta = { - id: (data as IStepJSON).scenarioId || '0', - author: (data as IStepJSON).scenarioAuthor || 'SYSTEM' - }; + if (headerLevel > 0) currentY += 10; + else currentY += baseLineHeight * 0.3; // Paragraph margin + } - const pStats = data.playerStats || {}; - const level = pStats.level ?? 1; - const mappedStats = { - hp: Math.floor(pStats.stats?.hp ?? pStats.hp ?? 0), - maxHp: pStats.maxHp ?? 100, - level, - exp: Math.floor(pStats.experience ?? pStats.exp ?? 0), - gold: pStats.coins ?? pStats.gold ?? 0, - expRequired: - pStats.expRequired ?? Math.floor(50 * Math.max(1, level) ** 1.3), - activeBonuses: pStats.activeBonuses || {} - }; + return currentY - startY; +} - const inCombat = !!enemyStats; - const isDead = mappedStats.hp <= 0; +export async function build(data: IStepJSON | ICombatJSON): Promise { + // --- NORMALIZE DATA PAYLOADS --- + const flavorText = data.flavorText || 'Waiting for input...'; + const enemyStats = data.enemy; + + const scenarioMeta = { + id: (data as IStepJSON).scenarioId || '0', + author: (data as IStepJSON).scenarioAuthor || 'SYSTEM' + }; + + const pStats = data.playerStats || {}; + const level = pStats.level ?? 1; + const mappedStats = { + hp: Math.floor(pStats.stats?.hp ?? pStats.hp ?? 0), + maxHp: pStats.maxHp ?? 100, + level, + exp: Math.floor(pStats.experience ?? pStats.exp ?? 0), + gold: pStats.coins ?? pStats.gold ?? 0, + expRequired: + pStats.expRequired ?? Math.floor(50 * Math.max(1, level) ** 1.3), + activeBonuses: pStats.activeBonuses || {} + }; + + const inCombat = !!enemyStats; + const isDead = mappedStats.hp <= 0; + + const b = mappedStats.activeBonuses; + const hasBonuses = + b && (b.critChance > 5 || b.lifeSteal > 0 || b.dodge > 0 || b.thorns > 0); + + // --- PRE-CALCULATE TEXT HEIGHT --- + const dummyCanvas = createCanvas(800, 10); + const dummyCtx = dummyCanvas.getContext('2d'); + const termW = 720; + + // Measure without drawing + const requiredTextHeight = processText( + dummyCtx, + flavorText, + 0, + 0, + termW - 60, + 32, + '#ffffff', + false + ); - const b = mappedStats.activeBonuses; - const hasBonuses = - b && (b.critChance > 5 || b.lifeSteal > 0 || b.dodge > 0 || b.thorns > 0); + let extraHeight = 0; + const baseTextSpace = 160; + if (requiredTextHeight > baseTextSpace) { + extraHeight = requiredTextHeight - baseTextSpace + 20; // Stretch canvas + } - // --- PRE-CALCULATE TEXT HEIGHT --- - const dummyCanvas = createCanvas(800, 10); - const dummyCtx = dummyCanvas.getContext('2d'); - const termW = 720; + // --- DYNAMIC CANVAS SIZING --- + let canvasHeight = 560 + extraHeight; + if (inCombat) canvasHeight += 75; + if (hasBonuses) canvasHeight += 40; - // Measure without drawing - const requiredTextHeight = this.processText( - dummyCtx, - flavorText, - 0, - 0, - termW - 60, - 32, - '#ffffff', - false - ); + const canvas = createCanvas(800, canvasHeight); + const ctx = canvas.getContext('2d'); - let extraHeight = 0; - const baseTextSpace = 160; - if (requiredTextHeight > baseTextSpace) { - extraHeight = requiredTextHeight - baseTextSpace + 20; // Stretch canvas - } + const themeColor = inCombat || isDead ? '#ef4444' : '#10b981'; + const themeColorDim = inCombat || isDead ? '#ef444433' : '#10b98133'; - // --- DYNAMIC CANVAS SIZING --- - let canvasHeight = 560 + extraHeight; - if (inCombat) canvasHeight += 75; - if (hasBonuses) canvasHeight += 40; + // 1. Background + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); - const canvas = createCanvas(800, canvasHeight); - const ctx = canvas.getContext('2d'); + ctx.strokeStyle = '#ffffff05'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.height; i += 20) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); + } - const themeColor = inCombat || isDead ? '#ef4444' : '#10b981'; - const themeColorDim = inCombat || isDead ? '#ef444433' : '#10b98133'; + // 2. Header + ctx.fillStyle = themeColor; + ctx.font = 'bold 36px sans-serif'; + ctx.textAlign = 'center'; + const headerText = isDead + ? 'SYSTEM FAILURE' + : inCombat + ? 'COMBAT ENGAGED' + : 'ADVENTURE'; + ctx.fillText(headerText, canvas.width / 2, 60); + + // 3. Terminal Window + const termX = 40; + const termY = 90; + const termH = 280 + extraHeight; // Stretched dynamically + + ctx.fillStyle = '#000000cc'; + ctx.beginPath(); + ctx.roundRect(termX, termY, termW, termH, 12); + ctx.fill(); + ctx.lineWidth = 2; + ctx.strokeStyle = themeColorDim; + ctx.stroke(); + + ctx.fillStyle = '#ffffff0a'; + ctx.beginPath(); + ctx.roundRect(termX, termY, termW, 30, [12, 12, 0, 0]); + ctx.fill(); + + const dotY = termY + 15; + ctx.fillStyle = isDead ? '#dc2626' : '#ef4444'; + ctx.beginPath(); + ctx.arc(termX + 20, dotY, 6, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#f59e0b'; + ctx.beginPath(); + ctx.arc(termX + 40, dotY, 6, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#10b981'; + ctx.beginPath(); + ctx.arc(termX + 60, dotY, 6, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#6b7280'; + ctx.font = '12px monospace'; + ctx.textAlign = 'left'; + ctx.fillText( + inCombat + ? 'combat_protocol.exe' + : isDead + ? 'system_dump.log' + : 'adventure_logs.sh', + termX + 80, + termY + 20 + ); - // 1. Background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#ffffff0a'; + ctx.fillRect(termX, termY + termH - 25, termW, 25); + ctx.fillStyle = '#4b5563'; + ctx.font = '10px monospace'; + ctx.fillText( + `ID: ${scenarioMeta.id.toString().padStart(6, '0')}`, + termX + 15, + termY + termH - 8 + ); + ctx.textAlign = 'right'; + ctx.fillText( + `Author: ${scenarioMeta.author}`, + termX + termW - 15, + termY + termH - 8 + ); - ctx.strokeStyle = '#ffffff05'; - ctx.lineWidth = 1; - for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); - ctx.moveTo(0, i); - ctx.lineTo(canvas.width, i); - ctx.stroke(); - } + const textColor = isDead ? '#fca5a5' : inCombat ? '#fca5a5' : themeColor; + ctx.fillStyle = textColor; + ctx.font = '22px monospace'; + ctx.textAlign = 'left'; + ctx.fillText('>', termX + 20, termY + 60); + + // Execute the final drawing with markdown support! + processText( + ctx, + flavorText, + termX + 40, + termY + 60, + termW - 60, + 32, + textColor, + true + ); - // 2. Header - ctx.fillStyle = themeColor; - ctx.font = 'bold 36px sans-serif'; - ctx.textAlign = 'center'; - const headerText = isDead - ? 'SYSTEM FAILURE' - : inCombat - ? 'COMBAT ENGAGED' - : 'ADVENTURE'; - ctx.fillText(headerText, canvas.width / 2, 60); - - // 3. Terminal Window - const termX = 40; - const termY = 90; - const termH = 280 + extraHeight; // Stretched dynamically - - ctx.fillStyle = '#000000cc'; + let yOffset = termY + termH + 25; + + // 4. Enemy Stats + if (inCombat && enemyStats && !isDead) { + ctx.fillStyle = '#450a0a'; + ctx.strokeStyle = '#ef44444d'; ctx.beginPath(); - ctx.roundRect(termX, termY, termW, termH, 12); + ctx.roundRect(termX, yOffset, 60, 45, 6); ctx.fill(); - ctx.lineWidth = 2; - ctx.strokeStyle = themeColorDim; ctx.stroke(); + ctx.fillStyle = '#ef4444b3'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('ATK', termX + 30, yOffset + 18); + ctx.fillStyle = '#f87171'; + ctx.font = 'bold 16px monospace'; + ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); - ctx.fillStyle = '#ffffff0a'; + ctx.fillStyle = '#172554'; + ctx.strokeStyle = '#3b82f64d'; ctx.beginPath(); - ctx.roundRect(termX, termY, termW, 30, [12, 12, 0, 0]); + ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); ctx.fill(); + ctx.stroke(); + ctx.fillStyle = '#3b82f6b3'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('DEF', termX + termW - 30, yOffset + 18); + ctx.fillStyle = '#60a5fa'; + ctx.font = 'bold 16px monospace'; + ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); - const dotY = termY + 15; - ctx.fillStyle = isDead ? '#dc2626' : '#ef4444'; - ctx.beginPath(); - ctx.arc(termX + 20, dotY, 6, 0, Math.PI * 2); - ctx.fill(); - ctx.fillStyle = '#f59e0b'; + const eBarX = termX + 75; + const eBarW = termW - 150; + ctx.fillStyle = '#f87171'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(enemyStats.name, eBarX, yOffset + 15); + ctx.textAlign = 'right'; + ctx.fillText( + `${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, + eBarX + eBarW, + yOffset + 15 + ); + + ctx.fillStyle = '#ffffff1a'; ctx.beginPath(); - ctx.arc(termX + 40, dotY, 6, 0, Math.PI * 2); + ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); ctx.fill(); - ctx.fillStyle = '#10b981'; + const eHpPercent = Math.max( + 0, + Math.min(enemyStats.currentHp / enemyStats.maxHp, 1) + ); + ctx.fillStyle = '#dc2626'; ctx.beginPath(); - ctx.arc(termX + 60, dotY, 6, 0, Math.PI * 2); + ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); ctx.fill(); - ctx.fillStyle = '#6b7280'; - ctx.font = '12px monospace'; - ctx.textAlign = 'left'; - ctx.fillText( - inCombat - ? 'combat_protocol.exe' - : isDead - ? 'system_dump.log' - : 'adventure_logs.sh', - termX + 80, - termY + 20 - ); + yOffset += 75; + } - ctx.fillStyle = '#ffffff0a'; - ctx.fillRect(termX, termY + termH - 25, termW, 25); - ctx.fillStyle = '#4b5563'; - ctx.font = '10px monospace'; - ctx.fillText( - `ID: ${scenarioMeta.id.toString().padStart(6, '0')}`, - termX + 15, - termY + termH - 8 - ); - ctx.textAlign = 'right'; - ctx.fillText( - `Author: ${scenarioMeta.author}`, - termX + termW - 15, - termY + termH - 8 - ); + // 5. Player Stats + const hpPercent = Math.max( + 0, + Math.min(mappedStats.hp / mappedStats.maxHp, 1) + ); + const expPercent = Math.max( + 0, + Math.min(mappedStats.exp / mappedStats.expRequired, 1) + ); - const textColor = isDead ? '#fca5a5' : inCombat ? '#fca5a5' : themeColor; - ctx.fillStyle = textColor; - ctx.font = '22px monospace'; - ctx.textAlign = 'left'; - ctx.fillText('>', termX + 20, termY + 60); - - // Execute the final drawing with markdown support! - this.processText( - ctx, - flavorText, - termX + 40, - termY + 60, - termW - 60, - 32, - textColor, - true - ); + ctx.fillStyle = isDead ? '#ef4444' : '#34d399'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Player HP', termX, yOffset); + ctx.textAlign = 'right'; + ctx.fillText( + `${mappedStats.hp} / ${mappedStats.maxHp}`, + termX + termW, + yOffset + ); - let yOffset = termY + termH + 25; + ctx.fillStyle = '#ffffff1a'; + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW, 12, 6); + ctx.fill(); + ctx.fillStyle = isDead ? '#dc2626' : '#10b981'; + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW * hpPercent, 12, 6); + ctx.fill(); + + yOffset += 45; + + ctx.fillStyle = '#60a5fa'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`Level ${mappedStats.level}`, termX, yOffset); + ctx.fillStyle = '#6b7280'; + ctx.textAlign = 'right'; + ctx.fillText( + `${mappedStats.exp} / ${mappedStats.expRequired} XP`, + termX + termW, + yOffset + ); - // 4. Enemy Stats - if (inCombat && enemyStats && !isDead) { - ctx.fillStyle = '#450a0a'; - ctx.strokeStyle = '#ef44444d'; - ctx.beginPath(); - ctx.roundRect(termX, yOffset, 60, 45, 6); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = '#ef4444b3'; + ctx.fillStyle = '#ffffff1a'; + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW, 12, 6); + ctx.fill(); + ctx.fillStyle = '#3b82f6'; + ctx.beginPath(); + ctx.roundRect(termX, yOffset + 12, termW * expPercent, 12, 6); + ctx.fill(); + + yOffset += 40; + + // 6. Active Bonuses + if (hasBonuses) { + let pillX = termX; + + const drawBonusPill = ( + label: string, + value: string, + bgColor: string, + borderColor: string, + textColor: string + ): void => { ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('ATK', termX + 30, yOffset + 18); - ctx.fillStyle = '#f87171'; - ctx.font = 'bold 16px monospace'; - ctx.fillText(enemyStats.atk.toString(), termX + 30, yOffset + 38); + const text = `${label}: ${value}`; + const textWidth = ctx.measureText(text).width; - ctx.fillStyle = '#172554'; - ctx.strokeStyle = '#3b82f64d'; + ctx.fillStyle = bgColor; + ctx.strokeStyle = borderColor; ctx.beginPath(); - ctx.roundRect(termX + termW - 60, yOffset, 60, 45, 6); + ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#3b82f6b3'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('DEF', termX + termW - 30, yOffset + 18); - ctx.fillStyle = '#60a5fa'; - ctx.font = 'bold 16px monospace'; - ctx.fillText(enemyStats.def.toString(), termX + termW - 30, yOffset + 38); - - const eBarX = termX + 75; - const eBarW = termW - 150; - ctx.fillStyle = '#f87171'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(enemyStats.name, eBarX, yOffset + 15); - ctx.textAlign = 'right'; - ctx.fillText( - `${Math.max(0, enemyStats.currentHp)} / ${enemyStats.maxHp} HP`, - eBarX + eBarW, - yOffset + 15 - ); - - ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); - ctx.roundRect(eBarX, yOffset + 25, eBarW, 12, 6); - ctx.fill(); - const eHpPercent = Math.max( - 0, - Math.min(enemyStats.currentHp / enemyStats.maxHp, 1) - ); - ctx.fillStyle = '#dc2626'; - ctx.beginPath(); - ctx.roundRect(eBarX, yOffset + 25, eBarW * eHpPercent, 12, 6); - ctx.fill(); - - yOffset += 75; - } - - // 5. Player Stats - const hpPercent = Math.max( - 0, - Math.min(mappedStats.hp / mappedStats.maxHp, 1) - ); - const expPercent = Math.max( - 0, - Math.min(mappedStats.exp / mappedStats.expRequired, 1) - ); - ctx.fillStyle = isDead ? '#ef4444' : '#34d399'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText('Player HP', termX, yOffset); - ctx.textAlign = 'right'; - ctx.fillText( - `${mappedStats.hp} / ${mappedStats.maxHp}`, - termX + termW, - yOffset - ); + ctx.fillStyle = textColor; + ctx.textAlign = 'center'; + ctx.fillText(text, pillX + (textWidth + 16) / 2, yOffset + 14); - ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); - ctx.roundRect(termX, yOffset + 12, termW, 12, 6); - ctx.fill(); - ctx.fillStyle = isDead ? '#dc2626' : '#10b981'; - ctx.beginPath(); - ctx.roundRect(termX, yOffset + 12, termW * hpPercent, 12, 6); - ctx.fill(); + pillX += textWidth + 24; + }; - yOffset += 45; + if (b.critChance > 5) + drawBonusPill( + 'Crit', + `${b.critChance}%`, + '#713f1233', + '#eab30833', + '#facc15' + ); + if (b.lifeSteal > 0) + drawBonusPill( + 'Vamp', + `${b.lifeSteal}%`, + '#450a0a33', + '#ef444433', + '#f87171' + ); + if (b.dodge > 0) + drawBonusPill( + 'Dodge', + `${b.dodge}%`, + '#17255433', + '#3b82f633', + '#93c5fd' + ); + if (b.thorns > 0) + drawBonusPill( + 'Thorns', + `${b.thorns}`, + '#7c2d1233', + '#f9731633', + '#fb923c' + ); - ctx.fillStyle = '#60a5fa'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(`Level ${mappedStats.level}`, termX, yOffset); - ctx.fillStyle = '#6b7280'; - ctx.textAlign = 'right'; - ctx.fillText( - `${mappedStats.exp} / ${mappedStats.expRequired} XP`, - termX + termW, - yOffset - ); + yOffset += 35; + } - ctx.fillStyle = '#ffffff1a'; - ctx.beginPath(); - ctx.roundRect(termX, yOffset + 12, termW, 12, 6); - ctx.fill(); - ctx.fillStyle = '#3b82f6'; - ctx.beginPath(); - ctx.roundRect(termX, yOffset + 12, termW * expPercent, 12, 6); - ctx.fill(); + // 7. Footer (Gold) + ctx.fillStyle = '#fbbf24'; + ctx.textAlign = 'left'; + ctx.font = '16px "NotoEmoji", sans-serif'; + ctx.fillText('🪙', termX, yOffset + 15); + + ctx.font = 'bold 16px sans-serif'; + ctx.fillText( + ` GOLD ${mappedStats.gold.toLocaleString()}`, + termX + 22, + yOffset + 15 + ); - yOffset += 40; - - // 6. Active Bonuses - if (hasBonuses) { - let pillX = termX; - - const drawBonusPill = ( - label: string, - value: string, - bgColor: string, - borderColor: string, - textColor: string - ) => { - ctx.font = 'bold 10px sans-serif'; - const text = `${label}: ${value}`; - const textWidth = ctx.measureText(text).width; - - ctx.fillStyle = bgColor; - ctx.strokeStyle = borderColor; - ctx.beginPath(); - ctx.roundRect(pillX, yOffset, textWidth + 16, 20, 4); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = textColor; - ctx.textAlign = 'center'; - ctx.fillText(text, pillX + (textWidth + 16) / 2, yOffset + 14); - - pillX += textWidth + 24; + // 8. TOAST NOTIFICATIONS (Rewards) + const rewards = (data as any).rewards; + if (rewards) { + const toasts: { msg: string; color: string; icon: string }[] = []; + + if (rewards.xp) + toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); + if (rewards.gold) + toasts.push({ + msg: `+${rewards.gold} Gold`, + color: '#eab308', + icon: '🪙' + }); + if (rewards.levelsGained > 0) + toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); + if (rewards.item) { + const RARITY_COLORS: Record = { + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' }; - - if (b.critChance > 5) - drawBonusPill( - 'Crit', - `${b.critChance}%`, - '#713f1233', - '#eab30833', - '#facc15' - ); - if (b.lifeSteal > 0) - drawBonusPill( - 'Vamp', - `${b.lifeSteal}%`, - '#450a0a33', - '#ef444433', - '#f87171' - ); - if (b.dodge > 0) - drawBonusPill( - 'Dodge', - `${b.dodge}%`, - '#17255433', - '#3b82f633', - '#93c5fd' - ); - if (b.thorns > 0) - drawBonusPill( - 'Thorns', - `${b.thorns}`, - '#7c2d1233', - '#f9731633', - '#fb923c' - ); - - yOffset += 35; + const itemColor = RARITY_COLORS[rewards.item.rarity] || '#ffffff'; + toasts.push({ msg: rewards.item.name, color: itemColor, icon: '🎒' }); } - // 7. Footer (Gold) - ctx.fillStyle = '#fbbf24'; - ctx.textAlign = 'left'; - ctx.font = '16px "NotoEmoji", sans-serif'; - ctx.fillText('🪙', termX, yOffset + 15); - - ctx.font = 'bold 16px sans-serif'; - ctx.fillText( - ` GOLD ${mappedStats.gold.toLocaleString()}`, - termX + 22, - yOffset + 15 - ); - - // 8. TOAST NOTIFICATIONS (Rewards) - const rewards = (data as any).rewards; - if (rewards) { - const toasts: { msg: string; color: string; icon: string }[] = []; - - if (rewards.xp) - toasts.push({ msg: `+${rewards.xp} XP`, color: '#3b82f6', icon: '✨' }); - if (rewards.gold) - toasts.push({ - msg: `+${rewards.gold} Gold`, - color: '#eab308', - icon: '🪙' - }); - if (rewards.levelsGained > 0) - toasts.push({ msg: 'LEVEL UP!', color: '#10b981', icon: '🆙' }); - if (rewards.item) { - const RARITY_COLORS: Record = { - Common: '#b0b0b0', - Uncommon: '#2ecc71', - Rare: '#3498db', - Elite: '#e67e22', - Epic: '#9b59b6', - Legendary: '#f1c40f', - Divine: '#00e5ff', - Exotic: '#ff00cc' - }; - const itemColor = RARITY_COLORS[rewards.item.rarity] || '#ffffff'; - toasts.push({ msg: rewards.item.name, color: itemColor, icon: '🎒' }); - } - - let toastY = 30; - for (const toast of toasts) { - ctx.font = 'bold 14px sans-serif'; - const msgWidth = ctx.measureText(toast.msg).width; - const toastW = msgWidth + 60; - const toastH = 40; + let toastY = 30; + for (const toast of toasts) { + ctx.font = 'bold 14px sans-serif'; + const msgWidth = ctx.measureText(toast.msg).width; + const toastW = msgWidth + 60; + const toastH = 40; - ctx.fillStyle = '#0a0a0ae6'; - ctx.beginPath(); - ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); - ctx.fill(); + ctx.fillStyle = '#0a0a0ae6'; + ctx.beginPath(); + ctx.roundRect(0, toastY, toastW, toastH, [0, 8, 8, 0]); + ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = `${toast.color}40`; - ctx.stroke(); + ctx.lineWidth = 1; + ctx.strokeStyle = `${toast.color}40`; + ctx.stroke(); - ctx.fillStyle = toast.color; - ctx.fillRect(0, toastY, 4, toastH); + ctx.fillStyle = toast.color; + ctx.fillRect(0, toastY, 4, toastH); - ctx.font = '16px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(toast.icon, 24, toastY + 26); + ctx.font = '16px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(toast.icon, 24, toastY + 26); - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(toast.msg, 44, toastY + 25); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(toast.msg, 44, toastY + 25); - toastY += toastH + 10; - } + toastY += toastH + 10; } - - return canvas.toBuffer('image/png'); } + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/ApiClient.ts b/src/utilities/ApiClient.ts index 9ab0e86..91ef1c4 100644 --- a/src/utilities/ApiClient.ts +++ b/src/utilities/ApiClient.ts @@ -1,4 +1,4 @@ -import Routes from './Routes'; +import * as Routes from './Routes'; import logger from './Logger'; const DEFAULT_TIMEOUT = 10_000; // 10 seconds diff --git a/src/utilities/ChestsImageBuilder.ts b/src/utilities/ChestsImageBuilder.ts index ee43ba8..80d06c6 100644 --- a/src/utilities/ChestsImageBuilder.ts +++ b/src/utilities/ChestsImageBuilder.ts @@ -36,187 +36,185 @@ export interface ChestsPageConfig { totalOpened: number; } -export default class ChestsImageBuilder { - public static async build( - chests: IChestSlot[], - config: ChestsPageConfig - ): Promise { - const slotW = 160; - const slotH = 200; - const cols = 4; - const rows = Math.ceil(config.maxSlots / cols); - const padding = 30; - const gap = 15; - const headerH = 100; - const canvasW = padding * 2 + cols * slotW + (cols - 1) * gap; - const canvasH = headerH + rows * (slotH + gap) + 60; - - const canvas = createCanvas(canvasW, canvasH); - const ctx = canvas.getContext('2d'); - - // Background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Header gradient - const grad = ctx.createLinearGradient(0, 0, 0, 100); - grad.addColorStop(0, '#1a1a1a'); - grad.addColorStop(1, '#0a0a0a'); - ctx.fillStyle = grad; - ctx.fillRect(0, 0, canvas.width, 100); - - // Title - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 28px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText('🎁', padding, 45); - ctx.fillText('CHEST VAULT', padding + 42, 45); - - // Stats line - ctx.fillStyle = '#6b7280'; - ctx.font = '11px sans-serif'; - ctx.fillText( - `${chests.length} / ${config.maxSlots} slots • ${config.totalOpened} opened`, - padding, - 70 - ); - - // Pity progress - ctx.textAlign = 'right'; - ctx.fillStyle = '#00e5ff'; - ctx.font = 'bold 11px sans-serif'; - ctx.fillText( - `Divine Pity: ${config.divinePity}/${config.pityThreshold}`, - canvas.width - padding, - 40 - ); - - // Pity bar - const pityBarX = canvas.width - padding - 200; - const pityBarY = 52; - const pityBarW = 200; - const pityPct = Math.min(1, config.divinePity / config.pityThreshold); - - ctx.fillStyle = '#ffffff10'; - ctx.beginPath(); - ctx.roundRect(pityBarX, pityBarY, pityBarW, 8, 4); - ctx.fill(); - - if (pityPct > 0) { - ctx.fillStyle = '#00e5ff'; - ctx.beginPath(); - ctx.roundRect(pityBarX, pityBarY, pityBarW * pityPct, 8, 4); - ctx.fill(); - } +export async function build( + chests: IChestSlot[], + config: ChestsPageConfig +): Promise { + const slotW = 160; + const slotH = 200; + const cols = 4; + const rows = Math.ceil(config.maxSlots / cols); + const padding = 30; + const gap = 15; + const headerH = 100; + const canvasW = padding * 2 + cols * slotW + (cols - 1) * gap; + const canvasH = headerH + rows * (slotH + gap) + 60; + + const canvas = createCanvas(canvasW, canvasH); + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Header gradient + const grad = ctx.createLinearGradient(0, 0, 0, 100); + grad.addColorStop(0, '#1a1a1a'); + grad.addColorStop(1, '#0a0a0a'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, 100); + + // Title + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 28px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('🎁', padding, 45); + ctx.fillText('CHEST VAULT', padding + 42, 45); + + // Stats line + ctx.fillStyle = '#6b7280'; + ctx.font = '11px sans-serif'; + ctx.fillText( + `${chests.length} / ${config.maxSlots} slots • ${config.totalOpened} opened`, + padding, + 70 + ); - // Divider - ctx.beginPath(); - ctx.moveTo(padding, 85); - ctx.lineTo(canvas.width - padding, 85); - ctx.strokeStyle = '#ffffff1a'; - ctx.lineWidth = 1; - ctx.stroke(); + // Pity progress + ctx.textAlign = 'right'; + ctx.fillStyle = '#00e5ff'; + ctx.font = 'bold 11px sans-serif'; + ctx.fillText( + `Divine Pity: ${config.divinePity}/${config.pityThreshold}`, + canvas.width - padding, + 40 + ); - // Chest Slots - for (let i = 0; i < config.maxSlots; i++) { - const col = i % cols; - const row = Math.floor(i / cols); - const x = padding + col * (slotW + gap); - const y = headerH + row * (slotH + gap); + // Pity bar + const pityBarX = canvas.width - padding - 200; + const pityBarY = 52; + const pityBarW = 200; + const pityPct = Math.min(1, config.divinePity / config.pityThreshold); - const chest = chests[i]; + ctx.fillStyle = '#ffffff10'; + ctx.beginPath(); + ctx.roundRect(pityBarX, pityBarY, pityBarW, 8, 4); + ctx.fill(); - // Slot background - ctx.fillStyle = chest ? '#ffffff08' : '#ffffff03'; - ctx.beginPath(); - ctx.roundRect(x, y, slotW, slotH, 12); - ctx.fill(); + if (pityPct > 0) { + ctx.fillStyle = '#00e5ff'; + ctx.beginPath(); + ctx.roundRect(pityBarX, pityBarY, pityBarW * pityPct, 8, 4); + ctx.fill(); + } - if (!chest) { - // Empty slot - ctx.strokeStyle = '#ffffff0a'; - ctx.lineWidth = 1; - ctx.stroke(); - - ctx.fillStyle = '#ffffff15'; - ctx.font = '40px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('+', x + slotW / 2, y + slotH / 2 + 12); - continue; - } + // Divider + ctx.beginPath(); + ctx.moveTo(padding, 85); + ctx.lineTo(canvas.width - padding, 85); + ctx.strokeStyle = '#ffffff1a'; + ctx.lineWidth = 1; + ctx.stroke(); + + // Chest Slots + for (let i = 0; i < config.maxSlots; i++) { + const col = i % cols; + const row = Math.floor(i / cols); + const x = padding + col * (slotW + gap); + const y = headerH + row * (slotH + gap); + + const chest = chests[i]; + + // Slot background + ctx.fillStyle = chest ? '#ffffff08' : '#ffffff03'; + ctx.beginPath(); + ctx.roundRect(x, y, slotW, slotH, 12); + ctx.fill(); - const color = TIER_COLORS[chest.tier] || '#ffffff'; - const emoji = chest.emoji || TIER_EMOJIS[chest.tier] || '📦'; - - // Border glow based on status - if (chest.status === 'ready') { - ctx.strokeStyle = `${color}88`; - ctx.lineWidth = 2; - } else if (chest.status === 'unlocking') { - ctx.strokeStyle = `${color}44`; - ctx.lineWidth = 1; - } else { - ctx.strokeStyle = '#ffffff15'; - ctx.lineWidth = 1; - } + if (!chest) { + // Empty slot + ctx.strokeStyle = '#ffffff0a'; + ctx.lineWidth = 1; ctx.stroke(); - // Chest emoji - ctx.font = '50px "NotoEmoji", sans-serif'; + ctx.fillStyle = '#ffffff15'; + ctx.font = '40px sans-serif'; ctx.textAlign = 'center'; - ctx.globalAlpha = chest.status === 'locked' ? 0.5 : 1; - ctx.fillText(emoji, x + slotW / 2, y + 75); - ctx.globalAlpha = 1; - - // Tier name - ctx.fillStyle = color; - ctx.font = 'bold 14px sans-serif'; - ctx.fillText(chest.tier, x + slotW / 2, y + 110); - - // Status - ctx.font = 'bold 10px sans-serif'; - if (chest.status === 'ready') { - ctx.fillStyle = '#34d399'; - ctx.fillText('✓ READY TO OPEN', x + slotW / 2, y + 135); - } else if (chest.status === 'unlocking') { - const remainSec = Math.max(0, Math.floor(chest.remainingMs / 1000)); - const h = Math.floor(remainSec / 3600); - const m = Math.floor((remainSec % 3600) / 60); - const s = remainSec % 60; - const timeStr = - h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; + ctx.fillText('+', x + slotW / 2, y + slotH / 2 + 12); + continue; + } - ctx.fillStyle = '#eab308'; - ctx.fillText(`⏳ ${timeStr}`, x + slotW / 2, y + 135); + const color = TIER_COLORS[chest.tier] || '#ffffff'; + const emoji = chest.emoji || TIER_EMOJIS[chest.tier] || '📦'; + + // Border glow based on status + if (chest.status === 'ready') { + ctx.strokeStyle = `${color}88`; + ctx.lineWidth = 2; + } else if (chest.status === 'unlocking') { + ctx.strokeStyle = `${color}44`; + ctx.lineWidth = 1; + } else { + ctx.strokeStyle = '#ffffff15'; + ctx.lineWidth = 1; + } + ctx.stroke(); - // Unlock progress bar - const totalSec = chest.unlockSeconds; - const elapsed = totalSec - remainSec; - const pct = Math.min(1, elapsed / totalSec); + // Chest emoji + ctx.font = '50px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.globalAlpha = chest.status === 'locked' ? 0.5 : 1; + ctx.fillText(emoji, x + slotW / 2, y + 75); + ctx.globalAlpha = 1; + + // Tier name + ctx.fillStyle = color; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText(chest.tier, x + slotW / 2, y + 110); + + // Status + ctx.font = 'bold 10px sans-serif'; + if (chest.status === 'ready') { + ctx.fillStyle = '#34d399'; + ctx.fillText('✓ READY TO OPEN', x + slotW / 2, y + 135); + } else if (chest.status === 'unlocking') { + const remainSec = Math.max(0, Math.floor(chest.remainingMs / 1000)); + const h = Math.floor(remainSec / 3600); + const m = Math.floor(remainSec % 3600 / 60); + const s = remainSec % 60; + const timeStr = + h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; + + ctx.fillStyle = '#eab308'; + ctx.fillText(`⏳ ${timeStr}`, x + slotW / 2, y + 135); + + // Unlock progress bar + const totalSec = chest.unlockSeconds; + const elapsed = totalSec - remainSec; + const pct = Math.min(1, elapsed / totalSec); + + ctx.fillStyle = '#ffffff10'; + ctx.beginPath(); + ctx.roundRect(x + 20, y + 148, slotW - 40, 6, 3); + ctx.fill(); - ctx.fillStyle = '#ffffff10'; + if (pct > 0) { + ctx.fillStyle = '#eab308'; ctx.beginPath(); - ctx.roundRect(x + 20, y + 148, slotW - 40, 6, 3); + ctx.roundRect(x + 20, y + 148, (slotW - 40) * pct, 6, 3); ctx.fill(); - - if (pct > 0) { - ctx.fillStyle = '#eab308'; - ctx.beginPath(); - ctx.roundRect(x + 20, y + 148, (slotW - 40) * pct, 6, 3); - ctx.fill(); - } - } else { - ctx.fillStyle = '#6b7280'; - ctx.fillText('🔒 LOCKED', x + slotW / 2, y + 135); } - - // Slot number - ctx.fillStyle = '#4b5563'; - ctx.font = '9px monospace'; - ctx.textAlign = 'right'; - ctx.fillText(`#${i + 1}`, x + slotW - 10, y + slotH - 10); + } else { + ctx.fillStyle = '#6b7280'; + ctx.fillText('🔒 LOCKED', x + slotW / 2, y + 135); } - return canvas.toBuffer('image/png'); + // Slot number + ctx.fillStyle = '#4b5563'; + ctx.font = '9px monospace'; + ctx.textAlign = 'right'; + ctx.fillText(`#${i + 1}`, x + slotW - 10, y + slotH - 10); } + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/CombatResponseBuilder.ts b/src/utilities/CombatResponseBuilder.ts index 61da416..90e3d53 100644 --- a/src/utilities/CombatResponseBuilder.ts +++ b/src/utilities/CombatResponseBuilder.ts @@ -7,7 +7,7 @@ import { } from 'discord.js'; import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { type IStepJSON } from '../interfaces/IStepJSON'; -import ImageService from './ImageService'; +import * as ImageService from './ImageService'; export interface CombatResponse { embeds: EmbedBuilder[]; diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index 64bacb1..b0462f8 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -5,8 +5,8 @@ import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IItemJSON } from '../interfaces/IItemJSON'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; import { type ITaskJSON, type IChestSlot } from '../interfaces/IGameJSON'; -import ItemManager from '../managers/ItemManager'; -import WorkerPool from './WorkerPool'; +import * as ItemManager from '../managers/ItemManager'; +import * as WorkerPool from './WorkerPool'; import type { LeaderboardEntry, LeaderboardConfig @@ -15,81 +15,75 @@ import type { MarketListing, MarketPageConfig } from './MarketImageBuilder'; import type { TasksPageConfig } from './TasksImageBuilder'; import type { ChestsPageConfig } from './ChestsImageBuilder'; -/** - * High-level image generation API. - * Routes all canvas work through the WorkerPool. - */ -export default class ImageService { - private static serializeItemCache(): Record { - const cache: Record = {}; - for (const [id, item] of ItemManager.cache) { - cache[id] = item; - } - return cache; +function serializeItemCache(): Record { + const cache: Record = {}; + for (const [id, item] of ItemManager.cache) { + cache[id] = item; } + return cache; +} - public static adventure(data: IStepJSON | ICombatJSON): Promise { - return WorkerPool.run('adventure', { data }); - } +export function adventure(data: IStepJSON | ICombatJSON): Promise { + return WorkerPool.run('adventure', { data }); +} - public static profile( - player: IPlayerJSON, - discordUser: User - ): Promise { - return WorkerPool.run('profile', { - player, - avatarUrl: discordUser.displayAvatarURL({ extension: 'png', size: 256 }), - itemCache: this.serializeItemCache() - }); - } +export function profile( + player: IPlayerJSON, + discordUser: User +): Promise { + return WorkerPool.run('profile', { + player, + avatarUrl: discordUser.displayAvatarURL({ extension: 'png', size: 256 }), + itemCache: serializeItemCache() + }); +} - public static inventory( - chunk: IInventoryItem[], - player: IPlayerJSON - ): Promise { - return WorkerPool.run('inventory', { - chunk, - player, - itemCache: this.serializeItemCache() - }); - } +export function inventory( + chunk: IInventoryItem[], + player: IPlayerJSON +): Promise { + return WorkerPool.run('inventory', { + chunk, + player, + itemCache: serializeItemCache() + }); +} - public static item(itemData: IItemJSON): Promise { - return WorkerPool.run('item', { item: itemData }); - } +export function item(itemData: IItemJSON): Promise { + return WorkerPool.run('item', { item: itemData }); +} - public static leaderboard( - entries: LeaderboardEntry[], - config: LeaderboardConfig - ): Promise { - return WorkerPool.run('leaderboard', { entries, config }); - } +export function leaderboard( + entries: LeaderboardEntry[], + config: LeaderboardConfig +): Promise { + return WorkerPool.run('leaderboard', { entries, config }); +} - public static market( - listings: MarketListing[], - config: MarketPageConfig - ): Promise { - return WorkerPool.run('market', { listings, config }); - } +export function market( + listings: MarketListing[], + config: MarketPageConfig +): Promise { + return WorkerPool.run('market', { listings, config }); +} - public static travel( - playerLevel: number, - currentZoneId: number - ): Promise { - return WorkerPool.run('travel', { playerLevel, currentZoneId }); - } +export function travel( + playerLevel: number, + currentZoneId: number +): Promise { + return WorkerPool.run('travel', { playerLevel, currentZoneId }); +} - public static tasks( - tasks: ITaskJSON[], - config: TasksPageConfig - ): Promise { - return WorkerPool.run('tasks', { tasks, config }); - } +export function tasks( + tasks: ITaskJSON[], + config: TasksPageConfig +): Promise { + return WorkerPool.run('tasks', { tasks, config }); +} - public static chests( - chests: IChestSlot[], - config: ChestsPageConfig - ): Promise { - return WorkerPool.run('chests', { chests, config }); - } +export function chests( + chests: IChestSlot[], + config: ChestsPageConfig +): Promise { + return WorkerPool.run('chests', { chests, config }); } diff --git a/src/utilities/ImageWorker.ts b/src/utilities/ImageWorker.ts index 7c6d6d2..4df8b5a 100644 --- a/src/utilities/ImageWorker.ts +++ b/src/utilities/ImageWorker.ts @@ -1,13 +1,13 @@ import { parentPort } from 'worker_threads'; -import AdventureImageBuilder from './AdventureImageBuilder'; -import ProfileImageBuilder from './ProfileImageBuilder'; -import InventoryImageBuilder from './InventoryImageBuilder'; -import ItemImageBuilder from './ItemImageBuilder'; -import LeaderboardImageBuilder from './LeaderboardImageBuilder'; -import MarketImageBuilder from './MarketImageBuilder'; -import TravelImageBuilder from './TravelImageBuilder'; -import TasksImageBuilder from './TasksImageBuilder'; -import ChestsImageBuilder from './ChestsImageBuilder'; +import * as AdventureImageBuilder from './AdventureImageBuilder'; +import * as ProfileImageBuilder from './ProfileImageBuilder'; +import * as InventoryImageBuilder from './InventoryImageBuilder'; +import * as ItemImageBuilder from './ItemImageBuilder'; +import * as LeaderboardImageBuilder from './LeaderboardImageBuilder'; +import * as MarketImageBuilder from './MarketImageBuilder'; +import * as TravelImageBuilder from './TravelImageBuilder'; +import * as TasksImageBuilder from './TasksImageBuilder'; +import * as ChestsImageBuilder from './ChestsImageBuilder'; if (!parentPort) { throw new Error('ImageWorker must be run as a worker thread'); @@ -18,67 +18,67 @@ parentPort.on('message', async (msg: { builderName: string; payload: any }) => { let buffer: Buffer; switch (msg.builderName) { - case 'adventure': - buffer = await AdventureImageBuilder.build(msg.payload.data); - break; + case 'adventure': + buffer = await AdventureImageBuilder.build(msg.payload.data); + break; - case 'profile': - buffer = await ProfileImageBuilder.build( - msg.payload.player, - msg.payload.avatarUrl, - msg.payload.itemCache - ); - break; + case 'profile': + buffer = await ProfileImageBuilder.build( + msg.payload.player, + msg.payload.avatarUrl, + msg.payload.itemCache + ); + break; - case 'inventory': - buffer = await InventoryImageBuilder.build( - msg.payload.chunk, - msg.payload.player, - msg.payload.itemCache - ); - break; + case 'inventory': + buffer = await InventoryImageBuilder.build( + msg.payload.chunk, + msg.payload.player, + msg.payload.itemCache + ); + break; - case 'item': - buffer = await ItemImageBuilder.build(msg.payload.item); - break; + case 'item': + buffer = await ItemImageBuilder.build(msg.payload.item); + break; - case 'leaderboard': - buffer = await LeaderboardImageBuilder.build( - msg.payload.entries, - msg.payload.config - ); - break; + case 'leaderboard': + buffer = await LeaderboardImageBuilder.build( + msg.payload.entries, + msg.payload.config + ); + break; - case 'market': - buffer = await MarketImageBuilder.build( - msg.payload.listings, - msg.payload.config - ); - break; + case 'market': + buffer = await MarketImageBuilder.build( + msg.payload.listings, + msg.payload.config + ); + break; - case 'travel': - buffer = await TravelImageBuilder.build( - msg.payload.playerLevel, - msg.payload.currentZoneId - ); - break; + case 'travel': + buffer = await TravelImageBuilder.build( + msg.payload.playerLevel, + msg.payload.currentZoneId + ); + break; - case 'tasks': - buffer = await TasksImageBuilder.build( - msg.payload.tasks, - msg.payload.config - ); - break; + case 'tasks': + buffer = await TasksImageBuilder.build( + msg.payload.tasks, + msg.payload.config + ); + break; - case 'chests': - buffer = await ChestsImageBuilder.build( - msg.payload.chests, - msg.payload.config - ); - break; + case 'chests': + buffer = await ChestsImageBuilder.build( + msg.payload.chests, + msg.payload.config + ); + break; - default: - throw new Error(`Unknown builder: ${msg.builderName}`); + default: + throw new Error(`Unknown builder: ${msg.builderName}`); } const arrayBuffer = new ArrayBuffer(buffer.byteLength); diff --git a/src/utilities/InventoryImageBuilder.ts b/src/utilities/InventoryImageBuilder.ts index 97f4f90..f50d549 100644 --- a/src/utilities/InventoryImageBuilder.ts +++ b/src/utilities/InventoryImageBuilder.ts @@ -2,7 +2,7 @@ import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IItemJSON } from '../interfaces/IItemJSON'; -import ItemManager from '../managers/ItemManager'; +import * as ItemManager from '../managers/ItemManager'; import { join } from 'path'; try { @@ -47,184 +47,178 @@ const CATEGORY_ICONS: Record = { Collectible: '🗿' }; -function getItemIcon(item: any) { +function getItemIcon(item: any): string { if (item.slot && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; return CATEGORY_ICONS[item.type] || '📦'; } -export default class InventoryImageBuilder { - public static async build( - chunk: IInventoryItem[], - player: IPlayerJSON, - itemCache?: Record - ): Promise { - const getItem = (id: number): IItemJSON | undefined => { - if (itemCache) return itemCache[id]; - return ItemManager.get(id); - }; - - const canvas = createCanvas(900, 720); - const ctx = canvas.getContext('2d'); - - // Background - ctx.fillStyle = '#111111'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const bgGradient = ctx.createLinearGradient(0, 0, 0, 200); - bgGradient.addColorStop(0, '#1a1a1a'); - bgGradient.addColorStop(1, '#111111'); - ctx.fillStyle = bgGradient; - ctx.fillRect(0, 0, canvas.width, 200); - - // Header - ctx.fillStyle = '#ffffff'; - ctx.textAlign = 'left'; - ctx.font = '32px "NotoEmoji", sans-serif'; - ctx.fillText('💼', 40, 60); - ctx.font = 'bold 32px sans-serif'; - ctx.fillText(`${player.username.toUpperCase()}'S INVENTORY`, 85, 60, 500); - - ctx.fillStyle = '#10b981'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'right'; - const goldFormatted = new Intl.NumberFormat('en-US').format( - player.coins || 0 - ); - ctx.fillText(`LVL ${player.level} • ${goldFormatted} GOLD`, 860, 55); - - // Divider +export async function build( + chunk: IInventoryItem[], + player: IPlayerJSON, + itemCache?: Record +): Promise { + const getItem = (id: number): IItemJSON | undefined => { + if (itemCache) return itemCache[id]; + return ItemManager.get(id); + }; + + const canvas = createCanvas(900, 720); + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = '#111111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const bgGradient = ctx.createLinearGradient(0, 0, 0, 200); + bgGradient.addColorStop(0, '#1a1a1a'); + bgGradient.addColorStop(1, '#111111'); + ctx.fillStyle = bgGradient; + ctx.fillRect(0, 0, canvas.width, 200); + + // Header + ctx.fillStyle = '#ffffff'; + ctx.textAlign = 'left'; + ctx.font = '32px "NotoEmoji", sans-serif'; + ctx.fillText('💼', 40, 60); + ctx.font = 'bold 32px sans-serif'; + ctx.fillText(`${player.username.toUpperCase()}'S INVENTORY`, 85, 60, 500); + + ctx.fillStyle = '#10b981'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'right'; + const goldFormatted = new Intl.NumberFormat('en-US').format( + player.coins || 0 + ); + ctx.fillText(`LVL ${player.level} • ${goldFormatted} GOLD`, 860, 55); + + // Divider + ctx.beginPath(); + ctx.moveTo(40, 80); + ctx.lineTo(860, 80); + ctx.lineWidth = 1; + ctx.strokeStyle = '#ffffff1a'; + ctx.stroke(); + + // Grid (5 cols x 3 rows = 15 items) + const startX = 40; + const startY = 110; + const boxW = 150; + const boxH = 180; + const gapX = 17.5; + const gapY = 20; + + for (let i = 0; i < chunk.length; i++) { + const invEntry = chunk[i]; + const itemData = getItem(invEntry.itemId); + + const col = i % 5; + const row = Math.floor(i / 5); + const boxX = startX + col * (boxW + gapX); + const boxY = startY + row * (boxH + gapY); + + // Box BG + ctx.fillStyle = '#00000066'; ctx.beginPath(); - ctx.moveTo(40, 80); - ctx.lineTo(860, 80); + ctx.roundRect(boxX, boxY, boxW, boxH, 12); + ctx.fill(); ctx.lineWidth = 1; ctx.strokeStyle = '#ffffff1a'; ctx.stroke(); - // Grid (5 cols x 3 rows = 15 items) - const startX = 40; - const startY = 110; - const boxW = 150; - const boxH = 180; - const gapX = 17.5; - const gapY = 20; - - for (let i = 0; i < chunk.length; i++) { - const invEntry = chunk[i]; - const itemData = getItem(invEntry.itemId); - - const col = i % 5; - const row = Math.floor(i / 5); - const boxX = startX + col * (boxW + gapX); - const boxY = startY + row * (boxH + gapY); - - // Box BG - ctx.fillStyle = '#00000066'; - ctx.beginPath(); - ctx.roundRect(boxX, boxY, boxW, boxH, 12); - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = '#ffffff1a'; - ctx.stroke(); + if (itemData) { + const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; + const enhanceLevel = invEntry.enhanceLevel || 0; - if (itemData) { - const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - const enhanceLevel = invEntry.enhanceLevel || 0; - - // Top Left: Lock Icon - if (invEntry.isLocked) { - ctx.fillStyle = '#ffffff'; - ctx.font = '14px "NotoEmoji", sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText('🔒', boxX + 10, boxY + 25); - } - - // Top Left (after lock): Enhancement Badge - if (enhanceLevel > 0) { - const badgeX = invEntry.isLocked ? boxX + 30 : boxX + 8; - const badgeText = `+${enhanceLevel}`; - ctx.font = 'bold 10px sans-serif'; - const badgeW = ctx.measureText(badgeText).width + 8; - - ctx.fillStyle = '#92400e88'; // amber-900/50 - ctx.beginPath(); - ctx.roundRect(badgeX, boxY + 10, badgeW, 16, 3); - ctx.fill(); - ctx.strokeStyle = '#f59e0b66'; - ctx.lineWidth = 1; - ctx.stroke(); - - ctx.fillStyle = '#fbbf24'; - ctx.textAlign = 'center'; - ctx.fillText(badgeText, badgeX + badgeW / 2, boxY + 22); - } - - // Top Right: Quantity Pill - ctx.fillStyle = '#00000099'; - ctx.beginPath(); - ctx.roundRect(boxX + boxW - 40, boxY + 10, 30, 18, 4); - ctx.fill(); + // Top Left: Lock Icon + if (invEntry.isLocked) { ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(`x${invEntry.quantity}`, boxX + boxW - 25, boxY + 23); + ctx.font = '14px "NotoEmoji", sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('🔒', boxX + 10, boxY + 25); + } - // Center: Emoji - ctx.fillStyle = '#ffffff'; - ctx.font = '45px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(getItemIcon(itemData), boxX + boxW / 2, boxY + 85); + // Top Left (after lock): Enhancement Badge + if (enhanceLevel > 0) { + const badgeX = invEntry.isLocked ? boxX + 30 : boxX + 8; + const badgeText = `+${enhanceLevel}`; + ctx.font = 'bold 10px sans-serif'; + const badgeW = ctx.measureText(badgeText).width + 8; - // Bottom Panel BG - ctx.fillStyle = '#00000099'; + ctx.fillStyle = '#92400e88'; // amber-900/50 ctx.beginPath(); - ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); + ctx.roundRect(badgeX, boxY + 10, badgeW, 16, 3); ctx.fill(); - - // Item Name (with +level suffix if enhanced) - ctx.fillStyle = color; - ctx.font = 'bold 12px sans-serif'; - const displayName = - enhanceLevel > 0 - ? `${itemData.name} +${enhanceLevel}` - : itemData.name; - ctx.fillText(displayName, boxX + boxW / 2, boxY + 132, boxW - 10); - - // Type & Level - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.fillText( - `${itemData.type.toUpperCase()} | LVL ${itemData.level}`, - boxX + boxW / 2, - boxY + 148 - ); - - // Value - ctx.fillStyle = '#eab308'; - ctx.font = '10px sans-serif'; - const totalValue = Math.floor( - (itemData.value || 0) * invEntry.quantity - ); - ctx.fillText( - `${totalValue.toLocaleString()}g`, - boxX + boxW / 2, - boxY + 164 - ); - - // Bottom Rarity Border - ctx.beginPath(); - ctx.moveTo(boxX + 10, boxY + boxH); - ctx.lineTo(boxX + boxW - 10, boxY + boxH); - ctx.lineWidth = 4; - ctx.strokeStyle = color; + ctx.strokeStyle = '#f59e0b66'; + ctx.lineWidth = 1; ctx.stroke(); - } else { - ctx.fillStyle = '#374151'; - ctx.font = 'italic 12px sans-serif'; + + ctx.fillStyle = '#fbbf24'; ctx.textAlign = 'center'; - ctx.fillText('Unknown Item', boxX + boxW / 2, boxY + boxH / 2); + ctx.fillText(badgeText, badgeX + badgeW / 2, boxY + 22); } - } - return canvas.toBuffer('image/png'); + // Top Right: Quantity Pill + ctx.fillStyle = '#00000099'; + ctx.beginPath(); + ctx.roundRect(boxX + boxW - 40, boxY + 10, 30, 18, 4); + ctx.fill(); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`x${invEntry.quantity}`, boxX + boxW - 25, boxY + 23); + + // Center: Emoji + ctx.fillStyle = '#ffffff'; + ctx.font = '45px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(getItemIcon(itemData), boxX + boxW / 2, boxY + 85); + + // Bottom Panel BG + ctx.fillStyle = '#00000099'; + ctx.beginPath(); + ctx.roundRect(boxX, boxY + 110, boxW, 70, [0, 0, 12, 12]); + ctx.fill(); + + // Item Name (with +level suffix if enhanced) + ctx.fillStyle = color; + ctx.font = 'bold 12px sans-serif'; + const displayName = + enhanceLevel > 0 ? `${itemData.name} +${enhanceLevel}` : itemData.name; + ctx.fillText(displayName, boxX + boxW / 2, boxY + 132, boxW - 10); + + // Type & Level + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.fillText( + `${itemData.type.toUpperCase()} | LVL ${itemData.level}`, + boxX + boxW / 2, + boxY + 148 + ); + + // Value + ctx.fillStyle = '#eab308'; + ctx.font = '10px sans-serif'; + const totalValue = Math.floor((itemData.value || 0) * invEntry.quantity); + ctx.fillText( + `${totalValue.toLocaleString()}g`, + boxX + boxW / 2, + boxY + 164 + ); + + // Bottom Rarity Border + ctx.beginPath(); + ctx.moveTo(boxX + 10, boxY + boxH); + ctx.lineTo(boxX + boxW - 10, boxY + boxH); + ctx.lineWidth = 4; + ctx.strokeStyle = color; + ctx.stroke(); + } else { + ctx.fillStyle = '#374151'; + ctx.font = 'italic 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Unknown Item', boxX + boxW / 2, boxY + boxH / 2); + } } + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/ItemImageBuilder.ts b/src/utilities/ItemImageBuilder.ts index 2bddc9c..5a54ac8 100644 --- a/src/utilities/ItemImageBuilder.ts +++ b/src/utilities/ItemImageBuilder.ts @@ -44,223 +44,216 @@ const CATEGORY_ICONS: Record = { Collectible: '🗿' }; -function getItemIcon(item: IItemJSON) { +function getItemIcon(item: IItemJSON): string { if (item.slot && item.slot !== 'None' && SLOT_ICONS[item.slot]) return SLOT_ICONS[item.slot]; return CATEGORY_ICONS[item.type] || '📦'; } -export default class ItemImageBuilder { - public static async build(item: IItemJSON): Promise { - const affixesCount = item.affixes?.length || 0; - const enhanceLevel = (item as any).enhanceLevel || 0; - const canvasHeight = affixesCount > 0 ? 430 + affixesCount * 45 : 400; - - const canvas = createCanvas(600, canvasHeight); - const ctx = canvas.getContext('2d'); - const color = RARITY_COLORS[item.rarity] || '#ffffff'; - - // 1. Background & Rarity Glow - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.lineWidth = 4; - ctx.strokeStyle = `${color}44`; - ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); - - const bgGradient = ctx.createLinearGradient(0, 0, 0, 150); - bgGradient.addColorStop(0, `${color}11`); - bgGradient.addColorStop(1, '#0a0a0a00'); - ctx.fillStyle = bgGradient; - ctx.fillRect(0, 0, canvas.width, 150); - - // 2. Icon & Name - ctx.fillStyle = '#ffffff'; - ctx.font = '60px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(getItemIcon(item), canvas.width / 2, 85); - - // Item name with enhance level - const displayName = - enhanceLevel > 0 ? `${item.name} +${enhanceLevel}` : item.name; - ctx.fillStyle = color; - ctx.font = 'bold 32px sans-serif'; - ctx.fillText(displayName, canvas.width / 2, 135, 560); - - // 3. Badges (Rarity, Type, and Enhancement) - ctx.font = 'bold 10px sans-serif'; - const rarityText = item.rarity.toUpperCase(); - let typeText = item.type.toUpperCase(); - if (item.slot && item.slot !== 'None') - typeText += ` • ${item.slot.toUpperCase()}`; - - const rWidth = ctx.measureText(rarityText).width + 20; - const tWidth = ctx.measureText(typeText).width + 20; - - // Enhancement badge dimensions - let enhText = ''; - let enhWidth = 0; - if (enhanceLevel > 0) { - enhText = `+${enhanceLevel} ENHANCED`; - enhWidth = ctx.measureText(enhText).width + 20; - } - - const totalBadgeWidth = - rWidth + tWidth + (enhWidth > 0 ? enhWidth + 10 : 0) + 10; - let currentX = (canvas.width - totalBadgeWidth) / 2; +export async function build(item: IItemJSON): Promise { + const affixesCount = item.affixes?.length || 0; + const enhanceLevel = (item as any).enhanceLevel || 0; + const canvasHeight = affixesCount > 0 ? 430 + affixesCount * 45 : 400; + + const canvas = createCanvas(600, canvasHeight); + const ctx = canvas.getContext('2d'); + const color = RARITY_COLORS[item.rarity] || '#ffffff'; + + // 1. Background & Rarity Glow + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.lineWidth = 4; + ctx.strokeStyle = `${color}44`; + ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); + + const bgGradient = ctx.createLinearGradient(0, 0, 0, 150); + bgGradient.addColorStop(0, `${color}11`); + bgGradient.addColorStop(1, '#0a0a0a00'); + ctx.fillStyle = bgGradient; + ctx.fillRect(0, 0, canvas.width, 150); + + // 2. Icon & Name + ctx.fillStyle = '#ffffff'; + ctx.font = '60px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(getItemIcon(item), canvas.width / 2, 85); + + // Item name with enhance level + const displayName = + enhanceLevel > 0 ? `${item.name} +${enhanceLevel}` : item.name; + ctx.fillStyle = color; + ctx.font = 'bold 32px sans-serif'; + ctx.fillText(displayName, canvas.width / 2, 135, 560); + + // 3. Badges (Rarity, Type, and Enhancement) + ctx.font = 'bold 10px sans-serif'; + const rarityText = item.rarity.toUpperCase(); + let typeText = item.type.toUpperCase(); + if (item.slot && item.slot !== 'None') + typeText += ` • ${item.slot.toUpperCase()}`; + + const rWidth = ctx.measureText(rarityText).width + 20; + const tWidth = ctx.measureText(typeText).width + 20; + + // Enhancement badge dimensions + let enhText = ''; + let enhWidth = 0; + if (enhanceLevel > 0) { + enhText = `+${enhanceLevel} ENHANCED`; + enhWidth = ctx.measureText(enhText).width + 20; + } - // Rarity Badge - ctx.fillStyle = `${color}1a`; - ctx.strokeStyle = `${color}66`; + const totalBadgeWidth = + rWidth + tWidth + (enhWidth > 0 ? enhWidth + 10 : 0) + 10; + let currentX = (canvas.width - totalBadgeWidth) / 2; + + // Rarity Badge + ctx.fillStyle = `${color}1a`; + ctx.strokeStyle = `${color}66`; + ctx.beginPath(); + ctx.roundRect(currentX, 155, rWidth, 24, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = color; + ctx.fillText(rarityText, currentX + rWidth / 2, 171); + + currentX += rWidth + 10; + + // Type/Slot Badge + ctx.fillStyle = '#ffffff0a'; + ctx.strokeStyle = '#ffffff20'; + ctx.beginPath(); + ctx.roundRect(currentX, 155, tWidth, 24, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = '#9ca3af'; + ctx.fillText(typeText, currentX + tWidth / 2, 171); + + // Enhancement Badge (amber) + if (enhanceLevel > 0) { + currentX += tWidth + 10; + ctx.fillStyle = '#92400e44'; // amber-900/25 + ctx.strokeStyle = '#f59e0b66'; // amber-500/40 ctx.beginPath(); - ctx.roundRect(currentX, 155, rWidth, 24, 4); + ctx.roundRect(currentX, 155, enhWidth, 24, 4); ctx.fill(); ctx.stroke(); - ctx.fillStyle = color; - ctx.fillText(rarityText, currentX + rWidth / 2, 171); - - currentX += rWidth + 10; + ctx.fillStyle = '#fbbf24'; // amber-400 + ctx.fillText(enhText, currentX + enhWidth / 2, 171); + } - // Type/Slot Badge - ctx.fillStyle = '#ffffff0a'; - ctx.strokeStyle = '#ffffff20'; + // 4. Description + ctx.fillStyle = '#9ca3af'; + ctx.font = 'italic 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`"${item.description}"`, canvas.width / 2, 220, 540); + + // 5. Stats or Consumable Effect + let yOffset = 260; + if (item.type === 'Consumable') { + ctx.fillStyle = '#713f1233'; + ctx.strokeStyle = '#eab3084d'; ctx.beginPath(); - ctx.roundRect(currentX, 155, tWidth, 24, 4); + ctx.roundRect(50, yOffset, 500, 70, 8); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#9ca3af'; - ctx.fillText(typeText, currentX + tWidth / 2, 171); - - // Enhancement Badge (amber) - if (enhanceLevel > 0) { - currentX += tWidth + 10; - ctx.fillStyle = '#92400e44'; // amber-900/25 - ctx.strokeStyle = '#f59e0b66'; // amber-500/40 + + ctx.fillStyle = '#eab308'; + ctx.font = 'bold 10px sans-serif'; + ctx.fillText('EFFECT', canvas.width / 2, yOffset + 25); + + let effectText = 'Unknown Effect'; + let effectColor = '#ffffff'; + if (item.action?.effect === 'HEAL_HP') { + effectText = `Restores ${item.action.amount} HP`; + effectColor = '#4ade80'; + } else if (item.action?.effect === 'GRANT_XP') { + effectText = `Grants ${item.action.amount} XP`; + effectColor = '#c084fc'; + } else if (item.action?.effect === 'GRANT_GOLD') { + effectText = `Grants ${item.action.amount} Gold`; + effectColor = '#fbbf24'; + } + + ctx.fillStyle = effectColor; + ctx.font = 'bold 20px sans-serif'; + ctx.fillText(effectText, canvas.width / 2, yOffset + 50); + } else if (item.stats) { + const boxW = 150; + const gap = 20; + const statX = (canvas.width - (boxW * 3 + gap * 2)) / 2; + + const drawStatBox = ( + x: number, + label: string, + val: number, + valColor: string + ): void => { + ctx.fillStyle = '#ffffff0a'; + ctx.strokeStyle = '#ffffff1a'; ctx.beginPath(); - ctx.roundRect(currentX, 155, enhWidth, 24, 4); + ctx.roundRect(x, yOffset, boxW, 70, 8); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#fbbf24'; // amber-400 - ctx.fillText(enhText, currentX + enhWidth / 2, 171); - } - // 4. Description - ctx.fillStyle = '#9ca3af'; - ctx.font = 'italic 16px sans-serif'; + ctx.fillStyle = '#6b7280'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(label, x + boxW / 2, yOffset + 25); + + ctx.fillStyle = valColor; + ctx.font = 'bold 24px monospace'; + ctx.fillText(val.toString(), x + boxW / 2, yOffset + 55); + }; + + drawStatBox(statX, 'ATK', item.stats.atk || 0, '#f87171'); + drawStatBox(statX + boxW + gap, 'DEF', item.stats.def || 0, '#60a5fa'); + drawStatBox(statX + (boxW + gap) * 2, 'HP', item.stats.hp || 0, '#4ade80'); + } + + // 6. Affixes + if (affixesCount > 0) { + yOffset += 110; + + ctx.fillStyle = '#c084fc'; + ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(`"${item.description}"`, canvas.width / 2, 220, 540); + ctx.fillText('SPECIAL EFFECTS', canvas.width / 2, yOffset); - // 5. Stats or Consumable Effect - let yOffset = 260; - if (item.type === 'Consumable') { - ctx.fillStyle = '#713f1233'; - ctx.strokeStyle = '#eab3084d'; + yOffset += 15; + item.affixes!.forEach((affix) => { + ctx.fillStyle = '#581c8733'; + ctx.strokeStyle = '#a855f733'; ctx.beginPath(); - ctx.roundRect(50, yOffset, 500, 70, 8); + ctx.roundRect(150, yOffset, 300, 32, 4); ctx.fill(); ctx.stroke(); - ctx.fillStyle = '#eab308'; - ctx.font = 'bold 10px sans-serif'; - ctx.fillText('EFFECT', canvas.width / 2, yOffset + 25); - - let effectText = 'Unknown Effect'; - let effectColor = '#ffffff'; - if (item.action?.effect === 'HEAL_HP') { - effectText = `Restores ${item.action.amount} HP`; - effectColor = '#4ade80'; - } else if (item.action?.effect === 'GRANT_XP') { - effectText = `Grants ${item.action.amount} XP`; - effectColor = '#c084fc'; - } else if (item.action?.effect === 'GRANT_GOLD') { - effectText = `Grants ${item.action.amount} Gold`; - effectColor = '#fbbf24'; - } - - ctx.fillStyle = effectColor; - ctx.font = 'bold 20px sans-serif'; - ctx.fillText(effectText, canvas.width / 2, yOffset + 50); - } else if (item.stats) { - const boxW = 150; - const gap = 20; - const statX = (canvas.width - (boxW * 3 + gap * 2)) / 2; - - const drawStatBox = ( - x: number, - label: string, - val: number, - valColor: string - ) => { - ctx.fillStyle = '#ffffff0a'; - ctx.strokeStyle = '#ffffff1a'; - ctx.beginPath(); - ctx.roundRect(x, yOffset, boxW, 70, 8); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#6b7280'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(label, x + boxW / 2, yOffset + 25); - - ctx.fillStyle = valColor; - ctx.font = 'bold 24px monospace'; - ctx.fillText(val.toString(), x + boxW / 2, yOffset + 55); - }; - - drawStatBox(statX, 'ATK', item.stats.atk || 0, '#f87171'); - drawStatBox(statX + boxW + gap, 'DEF', item.stats.def || 0, '#60a5fa'); - drawStatBox( - statX + (boxW + gap) * 2, - 'HP', - item.stats.hp || 0, - '#4ade80' - ); - } + ctx.fillStyle = '#e9d5ff'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); - // 6. Affixes - if (affixesCount > 0) { - yOffset += 110; + const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px monospace'; + ctx.textAlign = 'right'; + ctx.fillText(valText, 435, yOffset + 22); - ctx.fillStyle = '#c084fc'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('SPECIAL EFFECTS', canvas.width / 2, yOffset); - - yOffset += 15; - item.affixes!.forEach((affix) => { - ctx.fillStyle = '#581c8733'; - ctx.strokeStyle = '#a855f733'; - ctx.beginPath(); - ctx.roundRect(150, yOffset, 300, 32, 4); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#e9d5ff'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(affix.type.replace('_', ' '), 165, yOffset + 21); - - const valText = `+${affix.value}${affix.type === 'THORNS' ? '' : '%'}`; - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px monospace'; - ctx.textAlign = 'right'; - ctx.fillText(valText, 435, yOffset + 22); - - yOffset += 40; - }); - } + yOffset += 40; + }); + } - // 7. Footer - ctx.fillStyle = '#4b5563'; - ctx.font = '10px monospace'; - ctx.textAlign = 'center'; + // 7. Footer + ctx.fillStyle = '#4b5563'; + ctx.font = '10px monospace'; + ctx.textAlign = 'center'; - let footerText = `ID: ${item.itemId}`; - if (item.level > 1) footerText += ` | REQ LVL: ${item.level}`; + let footerText = `ID: ${item.itemId}`; + if (item.level > 1) footerText += ` | REQ LVL: ${item.level}`; - ctx.fillText(footerText, canvas.width / 2, canvas.height - 20); + ctx.fillText(footerText, canvas.width / 2, canvas.height - 20); - return canvas.toBuffer('image/png'); - } + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/ItemViewBuilder.ts b/src/utilities/ItemViewBuilder.ts index 869676b..de73fae 100644 --- a/src/utilities/ItemViewBuilder.ts +++ b/src/utilities/ItemViewBuilder.ts @@ -6,8 +6,8 @@ import { type EmbedBuilder } from 'discord.js'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; -import ItemManager from '../managers/ItemManager'; -import ImageService from './ImageService'; +import * as ItemManager from '../managers/ItemManager'; +import * as ImageService from './ImageService'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IItemJSON } from '../interfaces/IItemJSON'; diff --git a/src/utilities/LeaderboardImageBuilder.ts b/src/utilities/LeaderboardImageBuilder.ts index 4cd9923..e502871 100644 --- a/src/utilities/LeaderboardImageBuilder.ts +++ b/src/utilities/LeaderboardImageBuilder.ts @@ -30,133 +30,131 @@ const FOOTER_HEIGHT = 50; const PADDING = 40; const CANVAS_WIDTH = 800; -export default class LeaderboardImageBuilder { - public static async build( - entries: LeaderboardEntry[], - config: LeaderboardConfig - ): Promise { - const rowCount = Math.min(entries.length, 10); - const canvasHeight = - HEADER_HEIGHT + rowCount * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; - - const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); - const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - PADDING * 2; - - // --- 1. Background --- - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Subtle scanlines - ctx.strokeStyle = '#ffffff05'; - ctx.lineWidth = 1; - for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); - ctx.moveTo(0, i); - ctx.lineTo(canvas.width, i); - ctx.stroke(); - } +export async function build( + entries: LeaderboardEntry[], + config: LeaderboardConfig +): Promise { + const rowCount = Math.min(entries.length, 10); + const canvasHeight = + HEADER_HEIGHT + rowCount * ROW_HEIGHT + FOOTER_HEIGHT + PADDING; + + const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); + const ctx = canvas.getContext('2d'); + const contentWidth = CANVAS_WIDTH - PADDING * 2; + + // --- 1. Background --- + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Subtle scanlines + ctx.strokeStyle = '#ffffff05'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.height; i += 20) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); + } - // Top accent gradient - const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); - headerGrad.addColorStop(0, config.accentColorDim); - headerGrad.addColorStop(1, '#0a0a0a00'); - ctx.fillStyle = headerGrad; - ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); - - // --- 2. Header --- - ctx.textAlign = 'center'; - - // Emoji - ctx.font = '32px "NotoEmoji", sans-serif'; - ctx.fillStyle = '#ffffff'; - ctx.fillText(config.emoji, canvas.width / 2, 45); - - // Title - ctx.fillStyle = config.accentColor; - ctx.font = 'bold 28px sans-serif'; - ctx.fillText(config.title, canvas.width / 2, 82); - - // Subtitle - ctx.fillStyle = '#6b7280'; - ctx.font = '14px sans-serif'; - ctx.fillText( - `Top ${rowCount} Players — Ranked by ${config.stat}`, - canvas.width / 2, - 105 - ); + // Top accent gradient + const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); + headerGrad.addColorStop(0, config.accentColorDim); + headerGrad.addColorStop(1, '#0a0a0a00'); + ctx.fillStyle = headerGrad; + ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); + + // --- 2. Header --- + ctx.textAlign = 'center'; + + // Emoji + ctx.font = '32px "NotoEmoji", sans-serif'; + ctx.fillStyle = '#ffffff'; + ctx.fillText(config.emoji, canvas.width / 2, 45); + + // Title + ctx.fillStyle = config.accentColor; + ctx.font = 'bold 28px sans-serif'; + ctx.fillText(config.title, canvas.width / 2, 82); + + // Subtitle + ctx.fillStyle = '#6b7280'; + ctx.font = '14px sans-serif'; + ctx.fillText( + `Top ${rowCount} Players — Ranked by ${config.stat}`, + canvas.width / 2, + 105 + ); + + // --- 3. Rows --- + const startY = HEADER_HEIGHT + 10; - // --- 3. Rows --- - const startY = HEADER_HEIGHT + 10; + for (let i = 0; i < rowCount; i++) { + const entry = entries[i]; + const rowY = startY + i * ROW_HEIGHT; + const isTop3 = i < 3; - for (let i = 0; i < rowCount; i++) { - const entry = entries[i]; - const rowY = startY + i * ROW_HEIGHT; - const isTop3 = i < 3; + // Row background — alternating subtle stripes + ctx.fillStyle = i % 2 === 0 ? '#ffffff06' : '#00000000'; + ctx.beginPath(); + ctx.roundRect(PADDING, rowY, contentWidth, ROW_HEIGHT - 4, 8); + ctx.fill(); - // Row background — alternating subtle stripes - ctx.fillStyle = i % 2 === 0 ? '#ffffff06' : '#00000000'; + // Top 3 get a colored left accent bar + if (isTop3) { + ctx.fillStyle = MEDAL_COLORS[i]; ctx.beginPath(); - ctx.roundRect(PADDING, rowY, contentWidth, ROW_HEIGHT - 4, 8); + ctx.roundRect(PADDING, rowY, 4, ROW_HEIGHT - 4, [4, 0, 0, 4]); ctx.fill(); + } - // Top 3 get a colored left accent bar - if (isTop3) { - ctx.fillStyle = MEDAL_COLORS[i]; - ctx.beginPath(); - ctx.roundRect(PADDING, rowY, 4, ROW_HEIGHT - 4, [4, 0, 0, 4]); - ctx.fill(); - } - - // Rank number - const rankX = PADDING + 30; - if (isTop3) { - // Medal emoji for top 3 - const medals = ['🥇', '🥈', '🥉']; - ctx.font = '24px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(medals[i], rankX, rowY + 40); - } else { - ctx.fillStyle = '#4b5563'; - ctx.font = 'bold 20px monospace'; - ctx.textAlign = 'center'; - ctx.fillText(`${i + 1}`, rankX, rowY + 40); - } - - // Username - ctx.textAlign = 'left'; - ctx.fillStyle = isTop3 ? '#ffffff' : '#d1d5db'; - ctx.font = `${isTop3 ? 'bold ' : ''}18px sans-serif`; - ctx.fillText(entry.username, PADDING + 65, rowY + 34); - - // Level badge (small, under the name) - ctx.fillStyle = '#374151'; - ctx.font = '11px sans-serif'; - ctx.fillText(`LVL ${entry.level}`, PADDING + 65, rowY + 52); - - // Stat value — right aligned - ctx.textAlign = 'right'; - ctx.fillStyle = isTop3 ? config.accentColor : '#9ca3af'; - ctx.font = `bold ${isTop3 ? '22' : '18'}px monospace`; - ctx.fillText( - entry.value.toLocaleString(), - PADDING + contentWidth - 15, - rowY + 40 - ); + // Rank number + const rankX = PADDING + 30; + if (isTop3) { + // Medal emoji for top 3 + const medals = ['🥇', '🥈', '🥉']; + ctx.font = '24px "NotoEmoji", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(medals[i], rankX, rowY + 40); + } else { + ctx.fillStyle = '#4b5563'; + ctx.font = 'bold 20px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(`${i + 1}`, rankX, rowY + 40); } - // --- 4. Footer --- - const footerY = startY + rowCount * ROW_HEIGHT + 15; + // Username + ctx.textAlign = 'left'; + ctx.fillStyle = isTop3 ? '#ffffff' : '#d1d5db'; + ctx.font = `${isTop3 ? 'bold ' : ''}18px sans-serif`; + ctx.fillText(entry.username, PADDING + 65, rowY + 34); - ctx.textAlign = 'center'; + // Level badge (small, under the name) ctx.fillStyle = '#374151'; ctx.font = '11px sans-serif'; + ctx.fillText(`LVL ${entry.level}`, PADDING + 65, rowY + 52); + + // Stat value — right aligned + ctx.textAlign = 'right'; + ctx.fillStyle = isTop3 ? config.accentColor : '#9ca3af'; + ctx.font = `bold ${isTop3 ? '22' : '18'}px monospace`; ctx.fillText( - '⚔️ DFO Cross-Platform Integration — capi.gg', - canvas.width / 2, - footerY + entry.value.toLocaleString(), + PADDING + contentWidth - 15, + rowY + 40 ); - - return canvas.toBuffer('image/png'); } + + // --- 4. Footer --- + const footerY = startY + rowCount * ROW_HEIGHT + 15; + + ctx.textAlign = 'center'; + ctx.fillStyle = '#374151'; + ctx.font = '11px sans-serif'; + ctx.fillText( + '⚔️ DFO Cross-Platform Integration — capi.gg', + canvas.width / 2, + footerY + ); + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/MarketImageBuilder.ts b/src/utilities/MarketImageBuilder.ts index 2c4e698..b69a2ff 100644 --- a/src/utilities/MarketImageBuilder.ts +++ b/src/utilities/MarketImageBuilder.ts @@ -78,172 +78,170 @@ const FOOTER_HEIGHT = 45; const PADDING = 30; const CANVAS_WIDTH = 850; -export default class MarketImageBuilder { - public static async build( - listings: MarketListing[], - config: MarketPageConfig - ): Promise { - const rowCount = listings.length; - const canvasHeight = - HEADER_HEIGHT + - Math.max(rowCount, 1) * ROW_HEIGHT + - FOOTER_HEIGHT + - PADDING; - - const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); - const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - PADDING * 2; - - // --- Background --- - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.strokeStyle = '#ffffff05'; - ctx.lineWidth = 1; - for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); - ctx.moveTo(0, i); - ctx.lineTo(canvas.width, i); - ctx.stroke(); - } +export async function build( + listings: MarketListing[], + config: MarketPageConfig +): Promise { + const rowCount = listings.length; + const canvasHeight = + HEADER_HEIGHT + + Math.max(rowCount, 1) * ROW_HEIGHT + + FOOTER_HEIGHT + + PADDING; + + const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); + const ctx = canvas.getContext('2d'); + const contentWidth = CANVAS_WIDTH - PADDING * 2; + + // --- Background --- + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = '#ffffff05'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.height; i += 20) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); + } + + const accentColor = config.mode === 'my_listings' ? '#3b82f6' : '#10b981'; + const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); + headerGrad.addColorStop(0, `${accentColor}25`); + headerGrad.addColorStop(1, '#0a0a0a00'); + ctx.fillStyle = headerGrad; + ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); + + // --- Header (text only, no emoji overlap) --- + ctx.textAlign = 'center'; + ctx.fillStyle = accentColor; + ctx.font = 'bold 28px sans-serif'; + ctx.fillText( + config.mode === 'my_listings' ? 'My Listings' : 'Global Market', + canvas.width / 2, + 42 + ); - const accentColor = config.mode === 'my_listings' ? '#3b82f6' : '#10b981'; - const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); - headerGrad.addColorStop(0, `${accentColor}25`); - headerGrad.addColorStop(1, '#0a0a0a00'); - ctx.fillStyle = headerGrad; - ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); + ctx.fillStyle = '#6b7280'; + ctx.font = '13px sans-serif'; + ctx.fillText( + `${config.totalItems.toLocaleString()} listing${config.totalItems !== 1 ? 's' : ''} — Page ${config.page} of ${Math.max(1, config.totalPages)}`, + canvas.width / 2, + 68 + ); + + // --- Empty state --- + const startY = HEADER_HEIGHT; - // --- Header (text only, no emoji overlap) --- + if (rowCount === 0) { + ctx.fillStyle = '#4b5563'; + ctx.font = 'italic 18px sans-serif'; ctx.textAlign = 'center'; - ctx.fillStyle = accentColor; - ctx.font = 'bold 28px sans-serif'; ctx.fillText( - config.mode === 'my_listings' ? 'My Listings' : 'Global Market', + config.mode === 'my_listings' + ? 'You have no active listings.' + : 'No listings found.', canvas.width / 2, - 42 + startY + 36 ); + } + // --- Listing rows --- + for (let i = 0; i < rowCount; i++) { + const listing = listings[i]; + const item = listing.item; + const rowY = startY + i * ROW_HEIGHT; + const rarityColor = RARITY_COLORS[item.rarity] || '#ffffff'; + const displayIndex = i + 1; + + // Row bg + ctx.fillStyle = i % 2 === 0 ? '#ffffff06' : '#00000000'; + ctx.beginPath(); + ctx.roundRect(PADDING, rowY, contentWidth, ROW_HEIGHT - 4, 8); + ctx.fill(); + + // Rarity accent bar + ctx.fillStyle = rarityColor; + ctx.beginPath(); + ctx.roundRect(PADDING, rowY, 4, ROW_HEIGHT - 4, [4, 0, 0, 4]); + ctx.fill(); + + // --- Number badge --- + ctx.fillStyle = '#ffffff12'; + ctx.beginPath(); + ctx.roundRect(PADDING + 14, rowY + 18, 32, 32, 6); + ctx.fill(); + + ctx.fillStyle = accentColor; + ctx.font = 'bold 18px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(`${displayIndex}`, PADDING + 30, rowY + 40); + + // Item icon + ctx.font = '22px "NotoEmoji", sans-serif'; + ctx.fillStyle = '#ffffff'; + ctx.fillText(getItemIcon(item), PADDING + 70, rowY + 42); + + // Item name + ctx.textAlign = 'left'; + ctx.fillStyle = rarityColor; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText(item.name, PADDING + 95, rowY + 28, 300); + + // Meta line ctx.fillStyle = '#6b7280'; - ctx.font = '13px sans-serif'; + ctx.font = '11px sans-serif'; + let meta = `${item.rarity} ${item.type} • Lvl ${item.level}`; + if (config.mode === 'browse') meta += ` • by ${listing.sellerName}`; + ctx.fillText(meta, PADDING + 95, rowY + 46, 300); + + // Quantity pill + const qtyText = `x${listing.quantity}`; + ctx.font = 'bold 12px sans-serif'; + const qtyWidth = ctx.measureText(qtyText).width + 16; + const qtyX = PADDING + contentWidth - 215; + + ctx.fillStyle = '#ffffff0f'; + ctx.beginPath(); + ctx.roundRect(qtyX, rowY + 20, qtyWidth, 22, 4); + ctx.fill(); + + ctx.fillStyle = '#d1d5db'; + ctx.textAlign = 'center'; + ctx.fillText(qtyText, qtyX + qtyWidth / 2, rowY + 35); + + // Price + ctx.textAlign = 'right'; + ctx.font = 'bold 18px monospace'; + ctx.fillStyle = '#fbbf24'; ctx.fillText( - `${config.totalItems.toLocaleString()} listing${config.totalItems !== 1 ? 's' : ''} — Page ${config.page} of ${Math.max(1, config.totalPages)}`, - canvas.width / 2, - 68 + `${listing.pricePerUnit.toLocaleString()}g`, + PADDING + contentWidth - 12, + rowY + 34 ); - // --- Empty state --- - const startY = HEADER_HEIGHT; - - if (rowCount === 0) { - ctx.fillStyle = '#4b5563'; - ctx.font = 'italic 18px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText( - config.mode === 'my_listings' - ? 'You have no active listings.' - : 'No listings found.', - canvas.width / 2, - startY + 36 - ); - } - - // --- Listing rows --- - for (let i = 0; i < rowCount; i++) { - const listing = listings[i]; - const item = listing.item; - const rowY = startY + i * ROW_HEIGHT; - const rarityColor = RARITY_COLORS[item.rarity] || '#ffffff'; - const displayIndex = i + 1; - - // Row bg - ctx.fillStyle = i % 2 === 0 ? '#ffffff06' : '#00000000'; - ctx.beginPath(); - ctx.roundRect(PADDING, rowY, contentWidth, ROW_HEIGHT - 4, 8); - ctx.fill(); - - // Rarity accent bar - ctx.fillStyle = rarityColor; - ctx.beginPath(); - ctx.roundRect(PADDING, rowY, 4, ROW_HEIGHT - 4, [4, 0, 0, 4]); - ctx.fill(); - - // --- Number badge --- - ctx.fillStyle = '#ffffff12'; - ctx.beginPath(); - ctx.roundRect(PADDING + 14, rowY + 18, 32, 32, 6); - ctx.fill(); - - ctx.fillStyle = accentColor; - ctx.font = 'bold 18px monospace'; - ctx.textAlign = 'center'; - ctx.fillText(`${displayIndex}`, PADDING + 30, rowY + 40); - - // Item icon - ctx.font = '22px "NotoEmoji", sans-serif'; - ctx.fillStyle = '#ffffff'; - ctx.fillText(getItemIcon(item), PADDING + 70, rowY + 42); - - // Item name - ctx.textAlign = 'left'; - ctx.fillStyle = rarityColor; - ctx.font = 'bold 16px sans-serif'; - ctx.fillText(item.name, PADDING + 95, rowY + 28, 300); - - // Meta line + if (listing.quantity > 1) { ctx.fillStyle = '#6b7280'; ctx.font = '11px sans-serif'; - let meta = `${item.rarity} ${item.type} • Lvl ${item.level}`; - if (config.mode === 'browse') meta += ` • by ${listing.sellerName}`; - ctx.fillText(meta, PADDING + 95, rowY + 46, 300); - - // Quantity pill - const qtyText = `x${listing.quantity}`; - ctx.font = 'bold 12px sans-serif'; - const qtyWidth = ctx.measureText(qtyText).width + 16; - const qtyX = PADDING + contentWidth - 215; - - ctx.fillStyle = '#ffffff0f'; - ctx.beginPath(); - ctx.roundRect(qtyX, rowY + 20, qtyWidth, 22, 4); - ctx.fill(); - - ctx.fillStyle = '#d1d5db'; - ctx.textAlign = 'center'; - ctx.fillText(qtyText, qtyX + qtyWidth / 2, rowY + 35); - - // Price - ctx.textAlign = 'right'; - ctx.font = 'bold 18px monospace'; - ctx.fillStyle = '#fbbf24'; ctx.fillText( - `${listing.pricePerUnit.toLocaleString()}g`, + `Total: ${(listing.pricePerUnit * listing.quantity).toLocaleString()}g`, PADDING + contentWidth - 12, - rowY + 34 + rowY + 52 ); - - if (listing.quantity > 1) { - ctx.fillStyle = '#6b7280'; - ctx.font = '11px sans-serif'; - ctx.fillText( - `Total: ${(listing.pricePerUnit * listing.quantity).toLocaleString()}g`, - PADDING + contentWidth - 12, - rowY + 52 - ); - } } + } - // --- Footer --- - const footerY = startY + Math.max(rowCount, 1) * ROW_HEIGHT + 12; - ctx.textAlign = 'center'; - ctx.fillStyle = '#374151'; - ctx.font = '11px sans-serif'; - ctx.fillText( - '⚔️ DFO Cross-Platform Market — capi.gg', - canvas.width / 2, - footerY - ); + // --- Footer --- + const footerY = startY + Math.max(rowCount, 1) * ROW_HEIGHT + 12; + ctx.textAlign = 'center'; + ctx.fillStyle = '#374151'; + ctx.font = '11px sans-serif'; + ctx.fillText( + '⚔️ DFO Cross-Platform Market — capi.gg', + canvas.width / 2, + footerY + ); - return canvas.toBuffer('image/png'); - } + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/PaginatorBuilder.ts b/src/utilities/PaginatorBuilder.ts index a5d4043..41a5065 100644 --- a/src/utilities/PaginatorBuilder.ts +++ b/src/utilities/PaginatorBuilder.ts @@ -89,7 +89,7 @@ export default class PaginatorBuilder { .setLabel('⏩') .setStyle(ButtonStyle.Secondary); - const getNavRow = (index: number) => { + const getNavRow = (index: number): ActionRowBuilder => { return new ActionRowBuilder().addComponents( firstBtn.setDisabled(index === 0), prevBtn.setDisabled(index === 0), @@ -108,7 +108,7 @@ export default class PaginatorBuilder { return rows.slice(0, 5); }; - const getEmbed = (index: number) => { + const getEmbed = (index: number): EmbedBuilder => { const originalEmbed = this.pages[index]; const embed = EmbedBuilder.from(originalEmbed); const currentFooter = originalEmbed.data.footer?.text || ''; @@ -154,18 +154,18 @@ export default class PaginatorBuilder { collector.on('collect', async (i) => { collector.resetTimer(); switch (i.customId) { - case 'page_first': - currentPage = 0; - break; - case 'page_prev': - currentPage = Math.max(0, currentPage - 1); - break; - case 'page_next': - currentPage = Math.min(this.pages.length - 1, currentPage + 1); - break; - case 'page_last': - currentPage = this.pages.length - 1; - break; + case 'page_first': + currentPage = 0; + break; + case 'page_prev': + currentPage = Math.max(0, currentPage - 1); + break; + case 'page_next': + currentPage = Math.min(this.pages.length - 1, currentPage + 1); + break; + case 'page_last': + currentPage = this.pages.length - 1; + break; } await i.update({ embeds: [getEmbed(currentPage)], @@ -176,8 +176,7 @@ export default class PaginatorBuilder { collector.on('end', async () => { const finalComponents = getComponents(currentPage); - finalComponents.forEach((row) => - row.components.forEach((c) => c.setDisabled(true)) + finalComponents.forEach((row) => row.components.forEach((c) => c.setDisabled(true)) ); try { await interaction.editReply({ components: finalComponents }); diff --git a/src/utilities/PlayerGuard.ts b/src/utilities/PlayerGuard.ts index 2a9334a..37d253f 100644 --- a/src/utilities/PlayerGuard.ts +++ b/src/utilities/PlayerGuard.ts @@ -3,7 +3,7 @@ import { type ButtonInteraction, MessageFlags } from 'discord.js'; -import Routes from './Routes'; +import * as Routes from './Routes'; import { apiFetch } from './ApiClient'; /** @@ -14,38 +14,37 @@ import { apiFetch } from './ApiClient'; * const playerData = await PlayerGuard.check(interaction); * if (!playerData) return; // Guard already replied with a helpful message */ -export default class PlayerGuard { - /** - * Verify a player exists via the API. If not, reply with a themed onboarding message. - * Designed for commands that call deferReply() first. - */ - public static async check( - interaction: ChatInputCommandInteraction | ButtonInteraction, - discordId?: string - ): Promise { - const id = discordId ?? interaction.user.id; - try { - const res = await apiFetch(Routes.player(id)); - - if (res.status === 404) { - const content = - '📜 **Adventurer not found!**\nYou need to register before you can play. Use the `/register` command to begin your journey!'; +/** + * Verify a player exists via the API. If not, reply with a themed onboarding message. + * Designed for commands that call deferReply() first. + */ +export async function check( + interaction: ChatInputCommandInteraction | ButtonInteraction, + discordId?: string +): Promise { + const id = discordId ?? interaction.user.id; - if (interaction.deferred || interaction.replied) { - await interaction.editReply({ content }); - } else { - await interaction.reply({ content, flags: MessageFlags.Ephemeral }); - } - return null; - } + try { + const res = await apiFetch(Routes.player(id)); - if (!res.ok) return null; + if (res.status === 404) { + const content = + '📜 **Adventurer not found!**\nYou need to register before you can play. Use the `/register` command to begin your journey!'; - const body = await res.json(); - return body.data ?? body; - } catch { + if (interaction.deferred || interaction.replied) { + await interaction.editReply({ content }); + } else { + await interaction.reply({ content, flags: MessageFlags.Ephemeral }); + } return null; } + + if (!res.ok) return null; + + const body = await res.json(); + return body.data ?? body; + } catch { + return null; } } diff --git a/src/utilities/ProfileImageBuilder.ts b/src/utilities/ProfileImageBuilder.ts index 100a4d6..f9f8e4d 100644 --- a/src/utilities/ProfileImageBuilder.ts +++ b/src/utilities/ProfileImageBuilder.ts @@ -2,7 +2,7 @@ import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IItemJSON } from '../interfaces/IItemJSON'; import { type User } from 'discord.js'; -import ItemManager from '../managers/ItemManager'; +import * as ItemManager from '../managers/ItemManager'; import { join } from 'path'; // --- CRITICAL FIX: Load the font directly from the project files --- @@ -12,354 +12,341 @@ GlobalFonts.registerFromPath( 'NotoEmoji' ); -export default class ProfileImageBuilder { - /** - * Build a profile image. - * @param player - Player data from the API - * @param discordUser - Discord User object OR a plain avatar URL string (for worker threads) - * @param itemCache - Optional item lookup map. If omitted, falls back to ItemManager (main thread only). - */ - public static async build( - player: IPlayerJSON, - discordUser: User | string, - itemCache?: Record - ): Promise { - // Resolve item lookup: use provided cache if available, otherwise fall back to ItemManager - const getItem = (id: number): IItemJSON | undefined => { - if (itemCache) return itemCache[id]; - return ItemManager.get(id); - }; - - // Increased height to 880 to comfortably fit the equipment panel - const canvas = createCanvas(800, 880); - const ctx = canvas.getContext('2d'); - - // --- Svelte Logic Conversions --- - const xpToNext = Math.floor(50 * (player.level || 1) ** 1.3); - const xpProgress = Math.min(player.experience / xpToNext, 1); - const totalAtk = player.stats?.atk || 0; - const totalDef = player.stats?.def || 0; - const currentHp = Math.floor(player.stats?.hp || 0); - const maxHp = player.maxHp || 100; - - // --- 1. Background (Dark Theme) --- - ctx.fillStyle = '#111111'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const bgGradient = ctx.createLinearGradient(0, 0, 0, 150); - bgGradient.addColorStop(0, '#1a1a1a'); - bgGradient.addColorStop(1, '#111111'); - ctx.fillStyle = bgGradient; - ctx.fillRect(0, 0, canvas.width, 150); - - // --- 2. Avatar --- - const avatarSize = 120; - const avatarX = 40; - const avatarY = 40; - - ctx.save(); - ctx.beginPath(); - ctx.arc( - avatarX + avatarSize / 2, - avatarY + avatarSize / 2, - avatarSize / 2, - 0, - Math.PI * 2, - true - ); - ctx.closePath(); - ctx.clip(); - - // Support both a Discord User object (main thread) and a plain URL string (worker thread) - const avatarUrl = - typeof discordUser === 'string' - ? discordUser - : discordUser.displayAvatarURL({ extension: 'png', size: 256 }); - const avatarImage = await loadImage(avatarUrl); - ctx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize); - ctx.restore(); - - ctx.beginPath(); - ctx.arc( - avatarX + avatarSize / 2, - avatarY + avatarSize / 2, - avatarSize / 2, - 0, - Math.PI * 2, - true - ); - ctx.lineWidth = 4; - ctx.strokeStyle = '#10b981'; - ctx.stroke(); - - // --- 3. User Info (Name, Privilege, ID) --- - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 36px sans-serif'; - ctx.fillText(player.username, 180, 75); - - // Fixed: Measure width while font is 36px! - const nameWidth = ctx.measureText(player.username).width; - - ctx.fillStyle = '#10b981'; - ctx.font = 'bold 16px sans-serif'; - ctx.fillText( - `[${player.privilege.toUpperCase()}]`, - 180 + nameWidth + 15, - 72 - ); - - ctx.fillStyle = '#9ca3af'; - ctx.font = '14px monospace'; - ctx.fillText(`ID: ${player.id}`, 180, 100); - - // --- 4. Stats Grid --- - const drawGridBox = ( - x: number, - y: number, - label: string, - value: string, - borderColor: string, - valueColor: string - ) => { - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(x, y, 180, 70); - ctx.fillStyle = borderColor; - ctx.fillRect(x, y, 4, 70); - - ctx.fillStyle = '#6b7280'; - ctx.font = '12px sans-serif'; - ctx.fillText(label.toUpperCase(), x + 15, y + 25); - - ctx.fillStyle = valueColor; - ctx.font = 'bold 24px monospace'; - ctx.fillText(value, x + 15, y + 55); - }; - - drawGridBox( - 180, - 130, - 'Level', - player.level.toString(), - '#eab308', - '#ffffff' - ); - drawGridBox( - 375, - 130, - 'Skill Points', - player.skillPoints.toString(), - '#3b82f6', - '#ffffff' - ); - drawGridBox( - 570, - 130, - 'Coins', - player.coins.toLocaleString(), - '#f59e0b', - '#fbbf24' - ); - - // --- 5. XP Bar --- - const barX = 180; - const barY = 220; - const barWidth = 570; - const barHeight = 24; - - ctx.fillStyle = '#1f2937'; - ctx.beginPath(); - ctx.roundRect(barX, barY, barWidth, barHeight, 12); - ctx.fill(); - - ctx.fillStyle = '#059669'; - ctx.beginPath(); - ctx.roundRect(barX, barY, barWidth * xpProgress, barHeight, 12); - ctx.fill(); - - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText( - `${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, - barX + barWidth / 2, - barY + 16 - ); - ctx.textAlign = 'left'; - - // --- 6. Combat Stats Panel --- - const panelY = 280; +/** + * Build a profile image. + * @param player - Player data from the API + * @param discordUser - Discord User object OR a plain avatar URL string (for worker threads) + * @param itemCache - Optional item lookup map. If omitted, falls back to ItemManager (main thread only). + */ +export async function build( + player: IPlayerJSON, + discordUser: User | string, + itemCache?: Record +): Promise { + // Resolve item lookup: use provided cache if available, otherwise fall back to ItemManager + const getItem = (id: number): IItemJSON | undefined => { + if (itemCache) return itemCache[id]; + return ItemManager.get(id); + }; + + // Increased height to 880 to comfortably fit the equipment panel + const canvas = createCanvas(800, 880); + const ctx = canvas.getContext('2d'); + + // --- Svelte Logic Conversions --- + const xpToNext = Math.floor(50 * (player.level || 1) ** 1.3); + const xpProgress = Math.min(player.experience / xpToNext, 1); + const totalAtk = player.stats?.atk || 0; + const totalDef = player.stats?.def || 0; + const currentHp = Math.floor(player.stats?.hp || 0); + const maxHp = player.maxHp || 100; + + // --- 1. Background (Dark Theme) --- + ctx.fillStyle = '#111111'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const bgGradient = ctx.createLinearGradient(0, 0, 0, 150); + bgGradient.addColorStop(0, '#1a1a1a'); + bgGradient.addColorStop(1, '#111111'); + ctx.fillStyle = bgGradient; + ctx.fillRect(0, 0, canvas.width, 150); + + // --- 2. Avatar --- + const avatarSize = 120; + const avatarX = 40; + const avatarY = 40; + + ctx.save(); + ctx.beginPath(); + ctx.arc( + avatarX + avatarSize / 2, + avatarY + avatarSize / 2, + avatarSize / 2, + 0, + Math.PI * 2, + true + ); + ctx.closePath(); + ctx.clip(); + + // Support both a Discord User object (main thread) and a plain URL string (worker thread) + const avatarUrl = + typeof discordUser === 'string' + ? discordUser + : discordUser.displayAvatarURL({ extension: 'png', size: 256 }); + const avatarImage = await loadImage(avatarUrl); + ctx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize); + ctx.restore(); + + ctx.beginPath(); + ctx.arc( + avatarX + avatarSize / 2, + avatarY + avatarSize / 2, + avatarSize / 2, + 0, + Math.PI * 2, + true + ); + ctx.lineWidth = 4; + ctx.strokeStyle = '#10b981'; + ctx.stroke(); + + // --- 3. User Info (Name, Privilege, ID) --- + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 36px sans-serif'; + ctx.fillText(player.username, 180, 75); + + // Fixed: Measure width while font is 36px! + const nameWidth = ctx.measureText(player.username).width; + + ctx.fillStyle = '#10b981'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText(`[${player.privilege.toUpperCase()}]`, 180 + nameWidth + 15, 72); + + ctx.fillStyle = '#9ca3af'; + ctx.font = '14px monospace'; + ctx.fillText(`ID: ${player.id}`, 180, 100); + + // --- 4. Stats Grid --- + const drawGridBox = ( + x: number, + y: number, + label: string, + value: string, + borderColor: string, + valueColor: string + ): void => { ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(x, y, 180, 70); + ctx.fillStyle = borderColor; + ctx.fillRect(x, y, 4, 70); + + ctx.fillStyle = '#6b7280'; + ctx.font = '12px sans-serif'; + ctx.fillText(label.toUpperCase(), x + 15, y + 25); + + ctx.fillStyle = valueColor; + ctx.font = 'bold 24px monospace'; + ctx.fillText(value, x + 15, y + 55); + }; + + drawGridBox(180, 130, 'Level', player.level.toString(), '#eab308', '#ffffff'); + drawGridBox( + 375, + 130, + 'Skill Points', + player.skillPoints.toString(), + '#3b82f6', + '#ffffff' + ); + drawGridBox( + 570, + 130, + 'Coins', + player.coins.toLocaleString(), + '#f59e0b', + '#fbbf24' + ); + + // --- 5. XP Bar --- + const barX = 180; + const barY = 220; + const barWidth = 570; + const barHeight = 24; + + ctx.fillStyle = '#1f2937'; + ctx.beginPath(); + ctx.roundRect(barX, barY, barWidth, barHeight, 12); + ctx.fill(); + + ctx.fillStyle = '#059669'; + ctx.beginPath(); + ctx.roundRect(barX, barY, barWidth * xpProgress, barHeight, 12); + ctx.fill(); + + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText( + `${player.experience.toLocaleString()} XP / ${xpToNext.toLocaleString()} XP`, + barX + barWidth / 2, + barY + 16 + ); + ctx.textAlign = 'left'; + + // --- 6. Combat Stats Panel --- + const panelY = 280; + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.roundRect(40, panelY, 710, 130, 8); + ctx.fill(); + + ctx.fillStyle = '#f87171'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('HEALTH POINTS', 60, panelY + 30); + ctx.textAlign = 'right'; + ctx.fillText(`${currentHp} / ${maxHp}`, 730, panelY + 30); + ctx.textAlign = 'left'; + + const hpProgress = Math.min(currentHp / maxHp, 1); + ctx.fillStyle = '#1f2937'; + ctx.beginPath(); + ctx.roundRect(60, panelY + 45, 670, 12, 6); + ctx.fill(); + ctx.fillStyle = '#ef4444'; + ctx.beginPath(); + ctx.roundRect(60, panelY + 45, 670 * hpProgress, 12, 6); + ctx.fill(); + + const drawStatBox = ( + x: number, + y: number, + label: string, + value: string, + color: string + ): void => { + ctx.fillStyle = '#00000066'; ctx.beginPath(); - ctx.roundRect(40, panelY, 710, 130, 8); + ctx.roundRect(x, y, 325, 50, 6); ctx.fill(); - ctx.fillStyle = '#f87171'; + ctx.fillStyle = '#6b7280'; ctx.font = 'bold 14px sans-serif'; - ctx.fillText('HEALTH POINTS', 60, panelY + 30); - ctx.textAlign = 'right'; - ctx.fillText(`${currentHp} / ${maxHp}`, 730, panelY + 30); - ctx.textAlign = 'left'; + ctx.fillText(label, x + 15, y + 30); + + ctx.fillStyle = color; + ctx.font = 'bold 22px monospace'; + ctx.fillText(value, x + 100, y + 33); + }; + + drawStatBox(60, panelY + 70, 'ATK', totalAtk.toString(), '#f87171'); + drawStatBox(405, panelY + 70, 'DEF', totalDef.toString(), '#60a5fa'); + + // --- 7. Equipment Grid Panel --- + const equipPanelY = 440; + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.roundRect(40, equipPanelY, 710, 400, 8); + ctx.fill(); + + // Equipment Header + ctx.fillStyle = '#facc15'; + ctx.beginPath(); + ctx.arc(60, equipPanelY + 30, 4, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#facc15'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('EQUIPMENT', 75, equipPanelY + 35); + + // Grid Settings (4 columns, 3 rows) + const equipSlots = [ + { key: 'Head', icon: '⛑️' }, + { key: 'Necklace', icon: '📿' }, + { key: 'Chest', icon: '👕' }, + { key: 'MainHand', icon: '⚔️' }, + { key: 'Legs', icon: '👖' }, + { key: 'OffHand', icon: '🛡️' }, + { key: 'Hands', icon: '🧤' }, + { key: 'RingA', icon: '💍' }, + { key: 'Feet', icon: '👢' }, + { key: 'RingB', icon: '💍' }, + { key: 'Pet', icon: '🐾' }, + { key: 'Special', icon: '✨' } + ]; + + const RARITY_COLORS: Record = { + Common: '#b0b0b0', + Uncommon: '#2ecc71', + Rare: '#3498db', + Elite: '#e67e22', + Epic: '#9b59b6', + Legendary: '#f1c40f', + Divine: '#00e5ff', + Exotic: '#ff00cc' + }; + + const gridStartX = 60; + const gridStartY = equipPanelY + 65; + const boxWidth = 160; + const boxHeight = 95; + const gapX = 10; + const gapY = 15; + + for (let i = 0; i < equipSlots.length; i++) { + const slot = equipSlots[i]; + const col = i % 4; + const row = Math.floor(i / 4); + + const boxX = gridStartX + col * (boxWidth + gapX); + const boxY = gridStartY + row * (boxHeight + gapY); + + // Fetch item data if equipped + const equippedRef = player.equipment + ? (player.equipment as any)[slot.key] + : null; + let itemData = null; + if (equippedRef?.itemId) { + itemData = getItem(equippedRef.itemId) ?? null; + } - const hpProgress = Math.min(currentHp / maxHp, 1); - ctx.fillStyle = '#1f2937'; - ctx.beginPath(); - ctx.roundRect(60, panelY + 45, 670, 12, 6); - ctx.fill(); - ctx.fillStyle = '#ef4444'; + // Box BG & Outline + ctx.fillStyle = '#00000066'; // bg-black/40 ctx.beginPath(); - ctx.roundRect(60, panelY + 45, 670 * hpProgress, 12, 6); + ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 8); ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = '#ffffff11'; + ctx.stroke(); - const drawStatBox = ( - x: number, - y: number, - label: string, - value: string, - color: string - ) => { - ctx.fillStyle = '#00000066'; - ctx.beginPath(); - ctx.roundRect(x, y, 325, 50, 6); - ctx.fill(); - - ctx.fillStyle = '#6b7280'; - ctx.font = 'bold 14px sans-serif'; - ctx.fillText(label, x + 15, y + 30); + ctx.textAlign = 'center'; - ctx.fillStyle = color; - ctx.font = 'bold 22px monospace'; - ctx.fillText(value, x + 100, y + 33); - }; + // --- UPDATED EMOJI RENDERING --- + ctx.fillStyle = '#ffffff66'; // Muted icon + // Use the NotoEmoji font we registered above + ctx.font = '20px "NotoEmoji", sans-serif'; + ctx.fillText(slot.icon, boxX + boxWidth / 2, boxY + 30); - drawStatBox(60, panelY + 70, 'ATK', totalAtk.toString(), '#f87171'); - drawStatBox(405, panelY + 70, 'DEF', totalDef.toString(), '#60a5fa'); + // Slot Key + ctx.fillStyle = '#4b5563'; // text-gray-600 + // Instantly reset the font back to standard sans-serif for regular text + ctx.font = 'bold 10px sans-serif'; + ctx.fillText(slot.key.toUpperCase(), boxX + boxWidth / 2, boxY + 45); - // --- 7. Equipment Grid Panel --- - const equipPanelY = 440; - ctx.fillStyle = '#1a1a1a'; - ctx.beginPath(); - ctx.roundRect(40, equipPanelY, 710, 400, 8); - ctx.fill(); + if (itemData) { + const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - // Equipment Header - ctx.fillStyle = '#facc15'; - ctx.beginPath(); - ctx.arc(60, equipPanelY + 30, 4, 0, Math.PI * 2); - ctx.fill(); + // Truncate name if it's too long (using canvas maxWidth param) + ctx.fillStyle = color; + ctx.font = 'bold 13px sans-serif'; + ctx.fillText( + itemData.name, + boxX + boxWidth / 2, + boxY + 65, + boxWidth - 10 + ); + + // Item Level + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.fillText(`Lvl ${itemData.level}`, boxX + boxWidth / 2, boxY + 80); - ctx.fillStyle = '#facc15'; - ctx.font = 'bold 14px sans-serif'; - ctx.fillText('EQUIPMENT', 75, equipPanelY + 35); - - // Grid Settings (4 columns, 3 rows) - const equipSlots = [ - { key: 'Head', icon: '⛑️' }, - { key: 'Necklace', icon: '📿' }, - { key: 'Chest', icon: '👕' }, - { key: 'MainHand', icon: '⚔️' }, - { key: 'Legs', icon: '👖' }, - { key: 'OffHand', icon: '🛡️' }, - { key: 'Hands', icon: '🧤' }, - { key: 'RingA', icon: '💍' }, - { key: 'Feet', icon: '👢' }, - { key: 'RingB', icon: '💍' }, - { key: 'Pet', icon: '🐾' }, - { key: 'Special', icon: '✨' } - ]; - - const RARITY_COLORS: Record = { - Common: '#b0b0b0', - Uncommon: '#2ecc71', - Rare: '#3498db', - Elite: '#e67e22', - Epic: '#9b59b6', - Legendary: '#f1c40f', - Divine: '#00e5ff', - Exotic: '#ff00cc' - }; - - const gridStartX = 60; - const gridStartY = equipPanelY + 65; - const boxWidth = 160; - const boxHeight = 95; - const gapX = 10; - const gapY = 15; - - for (let i = 0; i < equipSlots.length; i++) { - const slot = equipSlots[i]; - const col = i % 4; - const row = Math.floor(i / 4); - - const boxX = gridStartX + col * (boxWidth + gapX); - const boxY = gridStartY + row * (boxHeight + gapY); - - // Fetch item data if equipped - const equippedRef = player.equipment - ? (player.equipment as any)[slot.key] - : null; - let itemData = null; - if (equippedRef?.itemId) { - itemData = getItem(equippedRef.itemId) ?? null; - } - - // Box BG & Outline - ctx.fillStyle = '#00000066'; // bg-black/40 + // Bottom Border (matches rarity) ctx.beginPath(); - ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 8); - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = '#ffffff11'; + ctx.moveTo(boxX + 15, boxY + boxHeight); + ctx.lineTo(boxX + boxWidth - 15, boxY + boxHeight); + ctx.lineWidth = 3; + ctx.strokeStyle = color; ctx.stroke(); - - ctx.textAlign = 'center'; - - // --- UPDATED EMOJI RENDERING --- - ctx.fillStyle = '#ffffff66'; // Muted icon - // Use the NotoEmoji font we registered above - ctx.font = '20px "NotoEmoji", sans-serif'; - ctx.fillText(slot.icon, boxX + boxWidth / 2, boxY + 30); - - // Slot Key - ctx.fillStyle = '#4b5563'; // text-gray-600 - // Instantly reset the font back to standard sans-serif for regular text - ctx.font = 'bold 10px sans-serif'; - ctx.fillText(slot.key.toUpperCase(), boxX + boxWidth / 2, boxY + 45); - - if (itemData) { - const color = RARITY_COLORS[itemData.rarity] || '#ffffff'; - - // Truncate name if it's too long (using canvas maxWidth param) - ctx.fillStyle = color; - ctx.font = 'bold 13px sans-serif'; - ctx.fillText( - itemData.name, - boxX + boxWidth / 2, - boxY + 65, - boxWidth - 10 - ); - - // Item Level - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.fillText(`Lvl ${itemData.level}`, boxX + boxWidth / 2, boxY + 80); - - // Bottom Border (matches rarity) - ctx.beginPath(); - ctx.moveTo(boxX + 15, boxY + boxHeight); - ctx.lineTo(boxX + boxWidth - 15, boxY + boxHeight); - ctx.lineWidth = 3; - ctx.strokeStyle = color; - ctx.stroke(); - } else { - // Empty State - ctx.fillStyle = '#374151'; // text-gray-700 - ctx.font = 'italic 12px sans-serif'; - ctx.fillText('Empty', boxX + boxWidth / 2, boxY + 70); - } - - ctx.textAlign = 'left'; // Reset alignment for next loop iteration + } else { + // Empty State + ctx.fillStyle = '#374151'; // text-gray-700 + ctx.font = 'italic 12px sans-serif'; + ctx.fillText('Empty', boxX + boxWidth / 2, boxY + 70); } - return canvas.toBuffer('image/png'); + ctx.textAlign = 'left'; // Reset alignment for next loop iteration } + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/Routes.ts b/src/utilities/Routes.ts index 645cbf0..089a6c1 100644 --- a/src/utilities/Routes.ts +++ b/src/utilities/Routes.ts @@ -1,200 +1,200 @@ -export default class Routes { - public static HEADERS = () => ({ +export function HEADERS(): Record { + return { Authorization: `Bearer ${process.env.BOT_TOKEN}`, 'Content-Type': 'application/json' - }); + }; +} - // ========== PLAYER ========== +// ========== PLAYER ========== - public static player(userId: string): string { - return `https://capi.gg/api/bot/player/${userId}`; - } +export function player(userId: string): string { + return `https://capi.gg/api/bot/player/${userId}`; +} - public static registerPlayer(): string { - return 'https://capi.gg/api/bot/player/register'; - } +export function registerPlayer(): string { + return 'https://capi.gg/api/bot/player/register'; +} - // ========== INVENTORY ========== +// ========== INVENTORY ========== - public static inventory(userId: string): string { - return `https://capi.gg/api/bot/player/inventory/${userId}/all`; - } +export function inventory(userId: string): string { + return `https://capi.gg/api/bot/player/inventory/${userId}/all`; +} - public static inventoryItem(userId: string, itemId: number) { - return `https://capi.gg/api/bot/player/inventory/${userId}/${itemId}`; - } +export function inventoryItem(userId: string, itemId: number): string { + return `https://capi.gg/api/bot/player/inventory/${userId}/${itemId}`; +} - // ========== ITEMS ========== +// ========== ITEMS ========== - public static item(itemId: number): string { - return `https://capi.gg/api/bot/items/${itemId}`; - } +export function item(itemId: number): string { + return `https://capi.gg/api/bot/items/${itemId}`; +} - public static items(): string { - return 'https://capi.gg/api/bot/items/all'; - } +export function items(): string { + return 'https://capi.gg/api/bot/items/all'; +} - // ========== SCENARIOS ========== +// ========== SCENARIOS ========== - public static scenario(scenarioId: number): string { - return `https://capi.gg/api/bot/scenarios/${scenarioId}`; - } +export function scenario(scenarioId: number): string { + return `https://capi.gg/api/bot/scenarios/${scenarioId}`; +} - public static scenarios(): string { - return 'https://capi.gg/api/bot/scenarios/all'; - } +export function scenarios(): string { + return 'https://capi.gg/api/bot/scenarios/all'; +} - // ========== NPCS ========== +// ========== NPCS ========== - public static npc(npcId: number): string { - return `https://capi.gg/api/bot/npcs/${npcId}`; - } +export function npc(npcId: number): string { + return `https://capi.gg/api/bot/npcs/${npcId}`; +} - public static npcs(): string { - return `https://capi.gg/api/bot/npcs/all`; - } +export function npcs(): string { + return `https://capi.gg/api/bot/npcs/all`; +} - // ========== INVENTORY ACTIONS ========== +// ========== INVENTORY ACTIONS ========== - public static equip(): string { - return 'https://capi.gg/api/inventory/equip'; - } +export function equip(): string { + return 'https://capi.gg/api/inventory/equip'; +} - public static unequip(): string { - return 'https://capi.gg/api/inventory/unequip'; - } +export function unequip(): string { + return 'https://capi.gg/api/inventory/unequip'; +} - public static lock(): string { - return 'https://capi.gg/api/inventory/lock'; - } +export function lock(): string { + return 'https://capi.gg/api/inventory/lock'; +} - public static consume(): string { - return 'https://capi.gg/api/inventory/consume'; - } +export function consume(): string { + return 'https://capi.gg/api/inventory/consume'; +} - public static sell(): string { - return 'https://capi.gg/api/inventory/sell'; - } +export function sell(): string { + return 'https://capi.gg/api/inventory/sell'; +} - public static enhance(): string { - return 'https://capi.gg/api/inventory/enhance'; - } +export function enhance(): string { + return 'https://capi.gg/api/inventory/enhance'; +} - public static reforge(): string { - return 'https://capi.gg/api/inventory/reforge'; - } +export function reforge(): string { + return 'https://capi.gg/api/inventory/reforge'; +} - public static dismantle(): string { - return 'https://capi.gg/api/inventory/dismantle'; - } +export function dismantle(): string { + return 'https://capi.gg/api/inventory/dismantle'; +} - public static collectionAdd(): string { - return 'https://capi.gg/api/collection/add'; - } +export function collectionAdd(): string { + return 'https://capi.gg/api/collection/add'; +} - // ========== ADVENTURE ========== +// ========== ADVENTURE ========== - public static explore(): string { - return 'https://capi.gg/api/adventure/step'; - } +export function explore(): string { + return 'https://capi.gg/api/adventure/step'; +} - public static combat(): string { - return 'https://capi.gg/api/adventure/combat'; - } +export function combat(): string { + return 'https://capi.gg/api/adventure/combat'; +} - public static rest(): string { - return 'https://capi.gg/api/adventure/rest'; - } +export function rest(): string { + return 'https://capi.gg/api/adventure/rest'; +} - public static travel(): string { - return 'https://capi.gg/api/adventure/travel'; - } +export function travel(): string { + return 'https://capi.gg/api/adventure/travel'; +} - // ========== TASKS ========== +// ========== TASKS ========== - public static tasks(): string { - return 'https://capi.gg/api/tasks'; - } +export function tasks(): string { + return 'https://capi.gg/api/tasks'; +} - // ========== CHESTS ========== +// ========== CHESTS ========== - public static chests(): string { - return 'https://capi.gg/api/chests'; - } +export function chests(): string { + return 'https://capi.gg/api/chests'; +} - // ========== LEADERBOARD ========== +// ========== LEADERBOARD ========== - public static leaderboard(stat: string): string { - return `https://capi.gg/api/bot/leaderboard?stat=${stat}`; - } +export function leaderboard(stat: string): string { + return `https://capi.gg/api/bot/leaderboard?stat=${stat}`; +} - // ========== TELEMETRY ========== +// ========== TELEMETRY ========== - public static telemetry(): string { - return 'https://capi.gg/api/telemetry/db-stats'; - } +export function telemetry(): string { + return 'https://capi.gg/api/telemetry/db-stats'; +} - // ========== MARKET ========== - - public static marketBrowse( - discordId: string, - params?: { - page?: number; - search?: string; - rarity?: string; - type?: string; - sort?: string; - } - ): string { - const base = `https://capi.gg/api/market?discordId=${discordId}&limit=8`; - const qs = new URLSearchParams(); - if (params?.page) qs.set('page', String(params.page)); - if (params?.search) qs.set('search', params.search); - if (params?.rarity && params.rarity !== 'All') - qs.set('rarity', params.rarity); - if (params?.type && params.type !== 'All') qs.set('type', params.type); - if (params?.sort) qs.set('sort', params.sort); - const extra = qs.toString(); - return extra ? `${base}&${extra}` : base; - } +// ========== MARKET ========== + +export function marketBrowse( + discordId: string, + params?: { + page?: number; + search?: string; + rarity?: string; + type?: string; + sort?: string; + } +): string { + const base = `https://capi.gg/api/market?discordId=${discordId}&limit=8`; + const qs = new URLSearchParams(); + if (params?.page) qs.set('page', String(params.page)); + if (params?.search) qs.set('search', params.search); + if (params?.rarity && params.rarity !== 'All') + qs.set('rarity', params.rarity); + if (params?.type && params.type !== 'All') qs.set('type', params.type); + if (params?.sort) qs.set('sort', params.sort); + const extra = qs.toString(); + return extra ? `${base}&${extra}` : base; +} - public static marketMyListings(discordId: string, page: number = 1): string { - return `https://capi.gg/api/market?sellerId=${discordId}&discordId=${discordId}&limit=8&page=${page}`; - } +export function marketMyListings(discordId: string, page: number = 1): string { + return `https://capi.gg/api/market?sellerId=${discordId}&discordId=${discordId}&limit=8&page=${page}`; +} - public static marketBuy(): string { - return 'https://capi.gg/api/market/buy'; - } +export function marketBuy(): string { + return 'https://capi.gg/api/market/buy'; +} - public static marketList(): string { - return 'https://capi.gg/api/market/list'; - } +export function marketList(): string { + return 'https://capi.gg/api/market/list'; +} - public static marketCancel(): string { - return 'https://capi.gg/api/market/cancel'; - } +export function marketCancel(): string { + return 'https://capi.gg/api/market/cancel'; +} - public static marketTrend(itemId: number): string { - return `https://capi.gg/api/market/trend?itemId=${itemId}`; - } +export function marketTrend(itemId: number): string { + return `https://capi.gg/api/market/trend?itemId=${itemId}`; +} - // ========== PROFILE ========== +// ========== PROFILE ========== - public static allocate(): string { - return 'https://capi.gg/api/profile/allocate'; - } +export function allocate(): string { + return 'https://capi.gg/api/profile/allocate'; +} - // ========== BULK OPERATIONS ========== +// ========== BULK OPERATIONS ========== - public static bulkSell(): string { - return 'https://capi.gg/api/inventory/bulk-sell'; - } +export function bulkSell(): string { + return 'https://capi.gg/api/inventory/bulk-sell'; +} - public static bulkCollect(): string { - return 'https://capi.gg/api/collection/bulk-add'; - } +export function bulkCollect(): string { + return 'https://capi.gg/api/collection/bulk-add'; +} - public static bulkDismantle(): string { - return 'https://capi.gg/api/inventory/bulk-dismantle'; - } +export function bulkDismantle(): string { + return 'https://capi.gg/api/inventory/bulk-dismantle'; } diff --git a/src/utilities/TasksImageBuilder.ts b/src/utilities/TasksImageBuilder.ts index 5b816ac..0329c3d 100644 --- a/src/utilities/TasksImageBuilder.ts +++ b/src/utilities/TasksImageBuilder.ts @@ -24,191 +24,185 @@ export interface TasksPageConfig { playerEmbers: number; } -export default class TasksImageBuilder { - public static async build( - tasks: ITaskJSON[], - config: TasksPageConfig - ): Promise { - const rowH = 90; - const headerH = 100; - const footerH = 40; - const canvasH = headerH + tasks.length * rowH + footerH + 20; - const canvas = createCanvas(800, Math.max(300, canvasH)); - const ctx = canvas.getContext('2d'); - - // Background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Header gradient - const grad = ctx.createLinearGradient(0, 0, 0, 100); - grad.addColorStop(0, '#1a1a1a'); - grad.addColorStop(1, '#0a0a0a'); - ctx.fillStyle = grad; - ctx.fillRect(0, 0, canvas.width, 100); - - // Title - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 28px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText('📋', 30, 45); - ctx.font = 'bold 28px sans-serif'; - ctx.fillText('TASK BOARD', 70, 45); - - // Period badge - const pc = PERIOD_COLORS[config.period] || PERIOD_COLORS.daily; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'right'; - const periodText = config.period.toUpperCase(); - const pw = ctx.measureText(periodText).width + 20; - ctx.fillStyle = pc.bg; - ctx.strokeStyle = pc.border; +export async function build( + tasks: ITaskJSON[], + config: TasksPageConfig +): Promise { + const rowH = 90; + const headerH = 100; + const footerH = 40; + const canvasH = headerH + tasks.length * rowH + footerH + 20; + const canvas = createCanvas(800, Math.max(300, canvasH)); + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Header gradient + const grad = ctx.createLinearGradient(0, 0, 0, 100); + grad.addColorStop(0, '#1a1a1a'); + grad.addColorStop(1, '#0a0a0a'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, 100); + + // Title + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 28px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('📋', 30, 45); + ctx.font = 'bold 28px sans-serif'; + ctx.fillText('TASK BOARD', 70, 45); + + // Period badge + const pc = PERIOD_COLORS[config.period] || PERIOD_COLORS.daily; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'right'; + const periodText = config.period.toUpperCase(); + const pw = ctx.measureText(periodText).width + 20; + ctx.fillStyle = pc.bg; + ctx.strokeStyle = pc.border; + ctx.beginPath(); + ctx.roundRect(canvas.width - 30 - pw, 25, pw, 28, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = pc.text; + ctx.textAlign = 'center'; + ctx.fillText(periodText, canvas.width - 30 - pw / 2, 44); + + // Reset timer + const resetMin = Math.floor(config.resetIn / 60000); + const resetH = Math.floor(resetMin / 60); + const resetStr = resetH > 0 ? `${resetH}h ${resetMin % 60}m` : `${resetMin}m`; + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(`RESETS IN: ${resetStr}`, canvas.width - 30, 70); + + // Embers display + ctx.fillStyle = '#f97316'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`🔥 ${config.playerEmbers.toLocaleString()} EMBERS`, 30, 70); + + // Divider + ctx.beginPath(); + ctx.moveTo(30, 85); + ctx.lineTo(canvas.width - 30, 85); + ctx.strokeStyle = '#ffffff1a'; + ctx.lineWidth = 1; + ctx.stroke(); + + // Task rows + let y = headerH; + + for (const task of tasks) { + const pct = Math.min(100, Math.floor(task.progress / task.target * 100)); + const isComplete = task.progress >= task.target; + const isClaimed = task.claimed; + + // Row background + ctx.fillStyle = isClaimed + ? '#00000020' + : isComplete + ? '#064e3b22' + : '#ffffff06'; ctx.beginPath(); - ctx.roundRect(canvas.width - 30 - pw, 25, pw, 28, 6); + ctx.roundRect(30, y, canvas.width - 60, rowH - 10, 10); ctx.fill(); + ctx.strokeStyle = isClaimed + ? '#ffffff0a' + : isComplete + ? '#10b98133' + : '#ffffff10'; + ctx.lineWidth = 1; ctx.stroke(); - ctx.fillStyle = pc.text; - ctx.textAlign = 'center'; - ctx.fillText(periodText, canvas.width - 30 - pw / 2, 44); - - // Reset timer - const resetMin = Math.floor(config.resetIn / 60000); - const resetH = Math.floor(resetMin / 60); - const resetStr = - resetH > 0 ? `${resetH}h ${resetMin % 60}m` : `${resetMin}m`; - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.textAlign = 'right'; - ctx.fillText(`RESETS IN: ${resetStr}`, canvas.width - 30, 70); - // Embers display - ctx.fillStyle = '#f97316'; - ctx.font = 'bold 10px sans-serif'; + // Icon + ctx.font = '24px "NotoEmoji", sans-serif'; ctx.textAlign = 'left'; - ctx.fillText(`🔥 ${config.playerEmbers.toLocaleString()} EMBERS`, 30, 70); - - // Divider + ctx.globalAlpha = isClaimed ? 0.3 : 1; + ctx.fillText(task.icon || '📋', 50, y + 35); + + // Description + ctx.font = isClaimed ? 'italic 14px sans-serif' : 'bold 14px sans-serif'; + ctx.fillStyle = isClaimed ? '#6b7280' : '#ffffff'; + ctx.fillText(task.label, 90, y + 30, 400); + + // Progress text + ctx.font = '11px monospace'; + ctx.fillStyle = isComplete ? '#34d399' : '#9ca3af'; + ctx.fillText( + `${task.progress.toLocaleString()} / ${task.target.toLocaleString()}`, + 90, + y + 50 + ); + + // Progress bar + const barX = 90; + const barY = y + 58; + const barW = 350; + const barH = 8; + + ctx.fillStyle = '#ffffff10'; ctx.beginPath(); - ctx.moveTo(30, 85); - ctx.lineTo(canvas.width - 30, 85); - ctx.strokeStyle = '#ffffff1a'; - ctx.lineWidth = 1; - ctx.stroke(); - - // Task rows - let y = headerH; - - for (const task of tasks) { - const pct = Math.min( - 100, - Math.floor((task.progress / task.target) * 100) - ); - const isComplete = task.progress >= task.target; - const isClaimed = task.claimed; + ctx.roundRect(barX, barY, barW, barH, 4); + ctx.fill(); - // Row background + if (pct > 0) { ctx.fillStyle = isClaimed - ? '#00000020' + ? '#4b5563' : isComplete - ? '#064e3b22' - : '#ffffff06'; + ? '#10b981' + : '#3b82f6'; ctx.beginPath(); - ctx.roundRect(30, y, canvas.width - 60, rowH - 10, 10); - ctx.fill(); - ctx.strokeStyle = isClaimed - ? '#ffffff0a' - : isComplete - ? '#10b98133' - : '#ffffff10'; - ctx.lineWidth = 1; - ctx.stroke(); - - // Icon - ctx.font = '24px "NotoEmoji", sans-serif'; - ctx.textAlign = 'left'; - ctx.globalAlpha = isClaimed ? 0.3 : 1; - ctx.fillText(task.icon || '📋', 50, y + 35); - - // Description - ctx.font = isClaimed ? 'italic 14px sans-serif' : 'bold 14px sans-serif'; - ctx.fillStyle = isClaimed ? '#6b7280' : '#ffffff'; - ctx.fillText(task.label, 90, y + 30, 400); - - // Progress text - ctx.font = '11px monospace'; - ctx.fillStyle = isComplete ? '#34d399' : '#9ca3af'; - ctx.fillText( - `${task.progress.toLocaleString()} / ${task.target.toLocaleString()}`, - 90, - y + 50 - ); - - // Progress bar - const barX = 90; - const barY = y + 58; - const barW = 350; - const barH = 8; - - ctx.fillStyle = '#ffffff10'; - ctx.beginPath(); - ctx.roundRect(barX, barY, barW, barH, 4); + ctx.roundRect(barX, barY, barW * (pct / 100), barH, 4); ctx.fill(); + } - if (pct > 0) { - ctx.fillStyle = isClaimed - ? '#4b5563' - : isComplete - ? '#10b981' - : '#3b82f6'; - ctx.beginPath(); - ctx.roundRect(barX, barY, barW * (pct / 100), barH, 4); - ctx.fill(); - } + // Percentage + ctx.font = 'bold 10px sans-serif'; + ctx.fillStyle = isComplete ? '#34d399' : '#6b7280'; + ctx.textAlign = 'left'; + ctx.fillText(`${pct}%`, barX + barW + 10, barY + 8); - // Percentage - ctx.font = 'bold 10px sans-serif'; - ctx.fillStyle = isComplete ? '#34d399' : '#6b7280'; - ctx.textAlign = 'left'; - ctx.fillText(`${pct}%`, barX + barW + 10, barY + 8); - - // Rewards (right side) - ctx.textAlign = 'right'; - const rewardX = canvas.width - 50; - - if (isClaimed) { - ctx.fillStyle = '#4b5563'; - ctx.font = 'bold 12px sans-serif'; - ctx.fillText('CLAIMED ✓', rewardX, y + 40); - } else { - ctx.fillStyle = '#eab308'; - ctx.font = 'bold 11px sans-serif'; - ctx.fillText(`${task.reward.gold.toLocaleString()}g`, rewardX, y + 25); - - ctx.fillStyle = '#60a5fa'; - ctx.font = '10px sans-serif'; - ctx.fillText(`${task.reward.xp.toLocaleString()} XP`, rewardX, y + 42); - - if (task.reward.embers > 0) { - ctx.fillStyle = '#f97316'; - ctx.fillText(`${task.reward.embers} 🔥`, rewardX, y + 57); - } + // Rewards (right side) + ctx.textAlign = 'right'; + const rewardX = canvas.width - 50; + + if (isClaimed) { + ctx.fillStyle = '#4b5563'; + ctx.font = 'bold 12px sans-serif'; + ctx.fillText('CLAIMED ✓', rewardX, y + 40); + } else { + ctx.fillStyle = '#eab308'; + ctx.font = 'bold 11px sans-serif'; + ctx.fillText(`${task.reward.gold.toLocaleString()}g`, rewardX, y + 25); + + ctx.fillStyle = '#60a5fa'; + ctx.font = '10px sans-serif'; + ctx.fillText(`${task.reward.xp.toLocaleString()} XP`, rewardX, y + 42); + + if (task.reward.embers > 0) { + ctx.fillStyle = '#f97316'; + ctx.fillText(`${task.reward.embers} 🔥`, rewardX, y + 57); } - - ctx.globalAlpha = 1; - y += rowH; } - if (tasks.length === 0) { - ctx.fillStyle = '#6b7280'; - ctx.font = 'italic 16px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText( - 'No tasks available for this period.', - canvas.width / 2, - headerH + 50 - ); - } + ctx.globalAlpha = 1; + y += rowH; + } - return canvas.toBuffer('image/png'); + if (tasks.length === 0) { + ctx.fillStyle = '#6b7280'; + ctx.font = 'italic 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText( + 'No tasks available for this period.', + canvas.width / 2, + headerH + 50 + ); } + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/TravelImageBuilder.ts b/src/utilities/TravelImageBuilder.ts index 7b0cf6a..37619a4 100644 --- a/src/utilities/TravelImageBuilder.ts +++ b/src/utilities/TravelImageBuilder.ts @@ -34,172 +34,170 @@ const FOOTER_HEIGHT = 40; const PADDING = 30; const CANVAS_WIDTH = 800; -export default class TravelImageBuilder { - public static async build( - playerLevel: number, - currentZoneId: number - ): Promise { - // Group zones by tier - const tiers = new Map(); - for (const zone of ZONES) { - const tier = zone.tier; - if (!tiers.has(tier)) tiers.set(tier, []); - tiers.get(tier)!.push(zone); - } +export async function build( + playerLevel: number, + currentZoneId: number +): Promise { + // Group zones by tier + const tiers = new Map(); + for (const zone of ZONES) { + const tier = zone.tier; + if (!tiers.has(tier)) tiers.set(tier, []); + tiers.get(tier)!.push(zone); + } - // Calculate canvas height - let totalRows = 0; - let tierCount = 0; - for (const zones of tiers.values()) { - tierCount++; - totalRows += zones.length; - } + // Calculate canvas height + let totalRows = 0; + let tierCount = 0; + for (const zones of tiers.values()) { + tierCount++; + totalRows += zones.length; + } - const canvasHeight = - HEADER_HEIGHT + - tierCount * TIER_HEADER_HEIGHT + - totalRows * ROW_HEIGHT + - FOOTER_HEIGHT + - PADDING; + const canvasHeight = + HEADER_HEIGHT + + tierCount * TIER_HEADER_HEIGHT + + totalRows * ROW_HEIGHT + + FOOTER_HEIGHT + + PADDING; + + const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); + const ctx = canvas.getContext('2d'); + const contentWidth = CANVAS_WIDTH - PADDING * 2; + + // --- Background --- + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = '#ffffff05'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.height; i += 20) { + ctx.beginPath(); + ctx.moveTo(0, i); + ctx.lineTo(canvas.width, i); + ctx.stroke(); + } - const canvas = createCanvas(CANVAS_WIDTH, canvasHeight); - const ctx = canvas.getContext('2d'); - const contentWidth = CANVAS_WIDTH - PADDING * 2; + // Header gradient + const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); + headerGrad.addColorStop(0, '#10b98125'); + headerGrad.addColorStop(1, '#0a0a0a00'); + ctx.fillStyle = headerGrad; + ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); + + // --- Header --- + ctx.textAlign = 'center'; + ctx.fillStyle = '#10b981'; + ctx.font = 'bold 26px sans-serif'; + ctx.fillText('Zone Map', canvas.width / 2, 38); + + const currentZone = ZONES.find((z) => z.id === currentZoneId); + ctx.fillStyle = '#6b7280'; + ctx.font = '13px sans-serif'; + ctx.fillText( + `Current: ${currentZone?.name ?? 'Unknown'} • Level ${playerLevel}`, + canvas.width / 2, + 62 + ); - // --- Background --- - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + // --- Zone rows grouped by tier --- + let y = HEADER_HEIGHT; - ctx.strokeStyle = '#ffffff05'; - ctx.lineWidth = 1; - for (let i = 0; i < canvas.height; i += 20) { - ctx.beginPath(); - ctx.moveTo(0, i); - ctx.lineTo(canvas.width, i); - ctx.stroke(); - } + for (const [tierName, zones] of tiers) { + const tierColor = TIER_COLORS[tierName] || '#ffffff'; - // Header gradient - const headerGrad = ctx.createLinearGradient(0, 0, 0, HEADER_HEIGHT); - headerGrad.addColorStop(0, '#10b98125'); - headerGrad.addColorStop(1, '#0a0a0a00'); - ctx.fillStyle = headerGrad; - ctx.fillRect(0, 0, canvas.width, HEADER_HEIGHT); - - // --- Header --- - ctx.textAlign = 'center'; - ctx.fillStyle = '#10b981'; - ctx.font = 'bold 26px sans-serif'; - ctx.fillText('Zone Map', canvas.width / 2, 38); - - const currentZone = ZONES.find((z) => z.id === currentZoneId); - ctx.fillStyle = '#6b7280'; - ctx.font = '13px sans-serif'; - ctx.fillText( - `Current: ${currentZone?.name ?? 'Unknown'} • Level ${playerLevel}`, - canvas.width / 2, - 62 - ); - - // --- Zone rows grouped by tier --- - let y = HEADER_HEIGHT; - - for (const [tierName, zones] of tiers) { - const tierColor = TIER_COLORS[tierName] || '#ffffff'; - - // Tier header - ctx.fillStyle = `${tierColor}15`; - ctx.fillRect(PADDING, y, contentWidth, TIER_HEADER_HEIGHT - 2); - - ctx.fillStyle = tierColor; - ctx.font = 'bold 13px sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(tierName.toUpperCase(), PADDING + 12, y + 22); - - y += TIER_HEADER_HEIGHT; - - // Zone rows - for (const zone of zones) { - const isCurrentZone = zone.id === currentZoneId; - const isAccessible = playerLevel >= zone.levelReq; - const rarityColor = RARITY_COLORS[zone.rarityCap] || '#6b7280'; - - // Row bg - if (isCurrentZone) { - ctx.fillStyle = '#10b98118'; - } else { - ctx.fillStyle = zone.id % 2 === 0 ? '#ffffff04' : '#00000000'; - } - ctx.beginPath(); - ctx.roundRect(PADDING, y, contentWidth, ROW_HEIGHT - 2, 6); - ctx.fill(); + // Tier header + ctx.fillStyle = `${tierColor}15`; + ctx.fillRect(PADDING, y, contentWidth, TIER_HEADER_HEIGHT - 2); + + ctx.fillStyle = tierColor; + ctx.font = 'bold 13px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(tierName.toUpperCase(), PADDING + 12, y + 22); + + y += TIER_HEADER_HEIGHT; - // Current zone indicator - if (isCurrentZone) { - ctx.fillStyle = '#10b981'; - ctx.beginPath(); - ctx.roundRect(PADDING, y, 4, ROW_HEIGHT - 2, [4, 0, 0, 4]); - ctx.fill(); - - ctx.font = '14px "NotoEmoji", sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('📍', PADDING + 22, y + 33); - } - - const textX = isCurrentZone ? PADDING + 42 : PADDING + 16; - - // Zone name - ctx.textAlign = 'left'; - ctx.fillStyle = isAccessible ? '#ffffff' : '#4b5563'; - ctx.font = `${isCurrentZone ? 'bold ' : ''}15px sans-serif`; - ctx.fillText( - `${isAccessible ? '' : '🔒 '}${zone.name}`, - textX, - y + 22, - 340 - ); - - // Description - ctx.fillStyle = isAccessible ? '#6b7280' : '#374151'; - ctx.font = '11px sans-serif'; - ctx.fillText(zone.description, textX, y + 38, 340); - - // Level req (right side) - ctx.textAlign = 'right'; - ctx.fillStyle = isAccessible ? '#4b5563' : '#ef4444'; - ctx.font = '11px sans-serif'; - ctx.fillText( - `Lvl ${zone.levelReq}+`, - PADDING + contentWidth - 100, - y + 22 - ); - - // Rarity cap pill - ctx.fillStyle = `${rarityColor}20`; - ctx.font = '10px sans-serif'; - const pillText = zone.rarityCap; - const pillWidth = ctx.measureText(pillText).width + 14; - const pillX = PADDING + contentWidth - pillWidth - 8; + // Zone rows + for (const zone of zones) { + const isCurrentZone = zone.id === currentZoneId; + const isAccessible = playerLevel >= zone.levelReq; + const rarityColor = RARITY_COLORS[zone.rarityCap] || '#6b7280'; + // Row bg + if (isCurrentZone) { + ctx.fillStyle = '#10b98118'; + } else { + ctx.fillStyle = zone.id % 2 === 0 ? '#ffffff04' : '#00000000'; + } + ctx.beginPath(); + ctx.roundRect(PADDING, y, contentWidth, ROW_HEIGHT - 2, 6); + ctx.fill(); + + // Current zone indicator + if (isCurrentZone) { + ctx.fillStyle = '#10b981'; ctx.beginPath(); - ctx.roundRect(pillX, y + 28, pillWidth, 16, 3); + ctx.roundRect(PADDING, y, 4, ROW_HEIGHT - 2, [4, 0, 0, 4]); ctx.fill(); - ctx.fillStyle = isAccessible ? rarityColor : '#4b5563'; + ctx.font = '14px "NotoEmoji", sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(pillText, pillX + pillWidth / 2, y + 40); - - y += ROW_HEIGHT; + ctx.fillText('📍', PADDING + 22, y + 33); } - } - // --- Footer --- - y += 10; - ctx.textAlign = 'center'; - ctx.fillStyle = '#374151'; - ctx.font = '11px sans-serif'; - ctx.fillText('⚔️ DFO Zone Map — capi.gg', canvas.width / 2, y); + const textX = isCurrentZone ? PADDING + 42 : PADDING + 16; + + // Zone name + ctx.textAlign = 'left'; + ctx.fillStyle = isAccessible ? '#ffffff' : '#4b5563'; + ctx.font = `${isCurrentZone ? 'bold ' : ''}15px sans-serif`; + ctx.fillText( + `${isAccessible ? '' : '🔒 '}${zone.name}`, + textX, + y + 22, + 340 + ); + + // Description + ctx.fillStyle = isAccessible ? '#6b7280' : '#374151'; + ctx.font = '11px sans-serif'; + ctx.fillText(zone.description, textX, y + 38, 340); + + // Level req (right side) + ctx.textAlign = 'right'; + ctx.fillStyle = isAccessible ? '#4b5563' : '#ef4444'; + ctx.font = '11px sans-serif'; + ctx.fillText( + `Lvl ${zone.levelReq}+`, + PADDING + contentWidth - 100, + y + 22 + ); + + // Rarity cap pill + ctx.fillStyle = `${rarityColor}20`; + ctx.font = '10px sans-serif'; + const pillText = zone.rarityCap; + const pillWidth = ctx.measureText(pillText).width + 14; + const pillX = PADDING + contentWidth - pillWidth - 8; - return canvas.toBuffer('image/png'); + ctx.beginPath(); + ctx.roundRect(pillX, y + 28, pillWidth, 16, 3); + ctx.fill(); + + ctx.fillStyle = isAccessible ? rarityColor : '#4b5563'; + ctx.textAlign = 'center'; + ctx.fillText(pillText, pillX + pillWidth / 2, y + 40); + + y += ROW_HEIGHT; + } } + + // --- Footer --- + y += 10; + ctx.textAlign = 'center'; + ctx.fillStyle = '#374151'; + ctx.font = '11px sans-serif'; + ctx.fillText('⚔️ DFO Zone Map — capi.gg', canvas.width / 2, y); + + return canvas.toBuffer('image/png'); } diff --git a/src/utilities/WorkerPool.ts b/src/utilities/WorkerPool.ts index 950894d..e2ecbc9 100644 --- a/src/utilities/WorkerPool.ts +++ b/src/utilities/WorkerPool.ts @@ -10,151 +10,155 @@ interface QueuedTask { reject: (reason: any) => void; } -export default class WorkerPool { - private static workers: Worker[] = []; - private static available: Worker[] = []; - private static queue: QueuedTask[] = []; - private static isInitialized = false; - - /** - * Spin up the pool. Call once at startup (e.g. in ClientReadyEvent). - * Defaults to (CPU cores - 1), minimum 2, capped at 8. - */ - public static init(size?: number): void { - if (this.isInitialized) return; - - const coreCount = cpus().length; - const poolSize = size ?? Math.max(2, Math.min(coreCount - 1, 8)); - - // Resolve to the compiled .js worker in production, or .ts in development - const isCompiled = __filename.endsWith('.js'); - const workerFile = join( - __dirname, - isCompiled ? 'ImageWorker.js' : 'ImageWorker.ts' - ); +export interface Stats { + total: number; + available: number; + queued: number; +} - for (let i = 0; i < poolSize; i++) { - const worker = new Worker(workerFile, { - // If running raw TypeScript, we need ts-node to compile the worker file - execArgv: isCompiled ? [] : ['-r', 'ts-node/register'] - }); +let workers: Worker[] = []; +let available: Worker[] = []; +let queue: QueuedTask[] = []; +let isInitialized = false; + +/** + * Spin up the pool. Call once at startup (e.g. in ClientReadyEvent). + * Defaults to (CPU cores - 1), minimum 2, capped at 8. + */ +export function init(size?: number): void { + if (isInitialized) return; + + const coreCount = cpus().length; + const poolSize = size ?? Math.max(2, Math.min(coreCount - 1, 8)); + + // Resolve to the compiled .js worker in production, or .ts in development + const isCompiled = __filename.endsWith('.js'); + const workerFile = join( + __dirname, + isCompiled ? 'ImageWorker.js' : 'ImageWorker.ts' + ); + + for (let i = 0; i < poolSize; i++) { + const worker = new Worker(workerFile, { + // If running raw TypeScript, we need ts-node to compile the worker file + execArgv: isCompiled ? [] : ['-r', 'ts-node/register'] + }); - worker.on('error', (err) => { - logger.error(err, `[WorkerPool] Worker ${i} encountered an error`); - }); + worker.on('error', (err) => { + logger.error(err, `[WorkerPool] Worker ${i} encountered an error`); + }); - worker.on('exit', (code) => { - if (code !== 0) { - logger.error(`[WorkerPool] Worker ${i} exited with code ${code}`); - } - }); + worker.on('exit', (code) => { + if (code !== 0) { + logger.error(`[WorkerPool] Worker ${i} exited with code ${code}`); + } + }); - this.workers.push(worker); - this.available.push(worker); - } + workers.push(worker); + available.push(worker); + } - this.isInitialized = true; - logger.info( - `[WorkerPool] Initialized ${poolSize} workers (${coreCount} CPU cores detected)` + isInitialized = true; + logger.info( + `[WorkerPool] Initialized ${poolSize} workers (${coreCount} CPU cores detected)` + ); +} + +/** + * Submit a rendering task to the pool. + * If a worker is free it runs immediately; otherwise it queues. + */ +export function run(builderName: string, payload: any): Promise { + if (!isInitialized) { + throw new Error( + '[WorkerPool] Pool not initialized — call WorkerPool.init() first' ); } - /** - * Submit a rendering task to the pool. - * If a worker is free it runs immediately; otherwise it queues. - */ - public static run(builderName: string, payload: any): Promise { - if (!this.isInitialized) { - throw new Error( - '[WorkerPool] Pool not initialized — call WorkerPool.init() first' - ); - } - - return new Promise((resolve, reject) => { - const task: QueuedTask = { builderName, payload, resolve, reject }; - const worker = this.available.pop(); + return new Promise((resolve, reject) => { + const task: QueuedTask = { builderName, payload, resolve, reject }; + const worker = available.pop(); - if (worker) { - this.execute(worker, task); - } else { - this.queue.push(task); - } - }); - } + if (worker) { + execute(worker, task); + } else { + queue.push(task); + } + }); +} - /** - * Send a task to a specific worker and listen for the result. - */ - private static execute(worker: Worker, task: QueuedTask): void { - const onMessage = (result: { - success: boolean; - buffer?: ArrayBuffer; - error?: string; - }) => { - // Clean up this specific listener - worker.off('message', onMessage); - worker.off('error', onError); - - // Return worker to the available pool - this.release(worker); - - if (result.success && result.buffer) { - task.resolve(Buffer.from(result.buffer)); - } else { - task.reject(new Error(result.error ?? 'Unknown worker error')); - } - }; +/** + * Send a task to a specific worker and listen for the result. + */ +function execute(worker: Worker, task: QueuedTask): void { + const onMessage = (result: { + success: boolean; + buffer?: ArrayBuffer; + error?: string; + }): void => { + // Clean up this specific listener + worker.off('message', onMessage); + worker.off('error', onError); + + // Return worker to the available pool + release(worker); + + if (result.success && result.buffer) { + task.resolve(Buffer.from(result.buffer)); + } else { + task.reject(new Error(result.error ?? 'Unknown worker error')); + } + }; - const onError = (err: Error) => { - worker.off('message', onMessage); - worker.off('error', onError); + const onError = (err: Error): void => { + worker.off('message', onMessage); + worker.off('error', onError); - this.release(worker); - task.reject(err); - }; + release(worker); + task.reject(err); + }; - worker.on('message', onMessage); - worker.on('error', onError); + worker.on('message', onMessage); + worker.on('error', onError); - worker.postMessage({ - builderName: task.builderName, - payload: task.payload - }); - } + worker.postMessage({ + builderName: task.builderName, + payload: task.payload + }); +} - /** - * Return a worker to the pool and drain the queue if tasks are waiting. - */ - private static release(worker: Worker): void { - const next = this.queue.shift(); - if (next) { - this.execute(worker, next); - } else { - this.available.push(worker); - } +/** + * Return a worker to the pool and drain the queue if tasks are waiting. + */ +function release(worker: Worker): void { + const next = queue.shift(); + if (next) { + execute(worker, next); + } else { + available.push(worker); } +} - /** - * Gracefully terminate all workers. Call during shutdown. - */ - public static async shutdown(): Promise { - const terminations = this.workers.map((w) => w.terminate()); - await Promise.all(terminations); - this.workers = []; - this.available = []; - this.queue = []; - this.isInitialized = false; - logger.info('[WorkerPool] All workers terminated'); - } +/** + * Gracefully terminate all workers. Call during shutdown. + */ +export async function shutdown(): Promise { + const terminations = workers.map((w) => w.terminate()); + await Promise.all(terminations); + workers = []; + available = []; + queue = []; + isInitialized = false; + logger.info('[WorkerPool] All workers terminated'); +} - /** - * Current diagnostics. - */ - public static stats() { - return { - total: this.workers.length, - available: this.available.length, - queued: this.queue.length - }; - } +/** + * Current diagnostics. + */ +export function stats(): Stats { + return { + total: workers.length, + available: available.length, + queued: queue.length + }; } From de384e76843deaffa9c8ed88402b92a1cc02e350 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:29:03 +0700 Subject: [PATCH 5/7] refactor: simplify typing imports --- src/commands/AttackCommand.ts | 2 +- src/commands/ExploreCommand.ts | 2 +- src/commands/FleeCommand.ts | 2 +- src/commands/RestCommand.ts | 2 +- src/components/buttons/ChestBuyButton.ts | 2 +- src/components/buttons/ChestOpenButton.ts | 2 +- src/components/buttons/ChestStartButton.ts | 2 +- src/components/buttons/DismantleButton.ts | 2 +- src/components/buttons/EmbedAttackButton.ts | 2 +- src/components/buttons/EmbedFleeButton.ts | 2 +- src/components/buttons/EnhanceButton.ts | 2 +- src/components/buttons/EquipButton.ts | 2 +- src/components/buttons/ExploreAgainButton.ts | 2 +- src/components/buttons/GuideNavButton.ts | 2 +- src/components/buttons/LockButton.ts | 2 +- src/components/buttons/MarketNextButton.ts | 2 +- src/components/buttons/MarketSellPageButton.ts | 2 +- src/components/buttons/RegisterDeclineButton.ts | 2 +- src/components/buttons/RestButton.ts | 2 +- src/components/buttons/TaskClaimButton.ts | 2 +- src/components/menus/InvSelectMenu.ts | 2 +- src/components/menus/ReforgeSelectMenu.ts | 2 +- src/components/modals/CollectModal.ts | 2 +- src/components/modals/ConsumeModal.ts | 2 +- src/components/modals/SellModal.ts | 2 +- src/structures/Button.ts | 2 +- src/structures/ModalSubmit.ts | 2 +- src/structures/SelectMenu.ts | 2 +- src/utilities/ImageService.ts | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/commands/AttackCommand.ts b/src/commands/AttackCommand.ts index 821118e..9eeea3c 100644 --- a/src/commands/AttackCommand.ts +++ b/src/commands/AttackCommand.ts @@ -1,4 +1,4 @@ -import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import type { ChatInputCommandInteraction, Client } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { apiFetch } from '../utilities/ApiClient'; diff --git a/src/commands/ExploreCommand.ts b/src/commands/ExploreCommand.ts index f996368..24e3c2e 100644 --- a/src/commands/ExploreCommand.ts +++ b/src/commands/ExploreCommand.ts @@ -1,4 +1,4 @@ -import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import type { ChatInputCommandInteraction, Client } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { type IStepJSON } from '../interfaces/IStepJSON'; import { apiFetch } from '../utilities/ApiClient'; diff --git a/src/commands/FleeCommand.ts b/src/commands/FleeCommand.ts index 9126606..4d28603 100644 --- a/src/commands/FleeCommand.ts +++ b/src/commands/FleeCommand.ts @@ -1,4 +1,4 @@ -import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import type { ChatInputCommandInteraction, Client } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { apiFetch } from '../utilities/ApiClient'; diff --git a/src/commands/RestCommand.ts b/src/commands/RestCommand.ts index b3b9543..f558419 100644 --- a/src/commands/RestCommand.ts +++ b/src/commands/RestCommand.ts @@ -1,4 +1,4 @@ -import { type ChatInputCommandInteraction, type Client } from 'discord.js'; +import type { ChatInputCommandInteraction, Client } from 'discord.js'; import SlashCommand from '../structures/SlashCommand'; import { apiFetch } from '../utilities/ApiClient'; import { formatError } from '../utilities/ErrorMessages'; diff --git a/src/components/buttons/ChestBuyButton.ts b/src/components/buttons/ChestBuyButton.ts index bae1e0e..b2b2e59 100644 --- a/src/components/buttons/ChestBuyButton.ts +++ b/src/components/buttons/ChestBuyButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/ChestOpenButton.ts b/src/components/buttons/ChestOpenButton.ts index 112996b..0fd98c9 100644 --- a/src/components/buttons/ChestOpenButton.ts +++ b/src/components/buttons/ChestOpenButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/ChestStartButton.ts b/src/components/buttons/ChestStartButton.ts index 8fec692..84bc2a1 100644 --- a/src/components/buttons/ChestStartButton.ts +++ b/src/components/buttons/ChestStartButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/DismantleButton.ts b/src/components/buttons/DismantleButton.ts index e02b7d0..f1e98be 100644 --- a/src/components/buttons/DismantleButton.ts +++ b/src/components/buttons/DismantleButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/EmbedAttackButton.ts b/src/components/buttons/EmbedAttackButton.ts index 1dbfad9..46a3b99 100644 --- a/src/components/buttons/EmbedAttackButton.ts +++ b/src/components/buttons/EmbedAttackButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; diff --git a/src/components/buttons/EmbedFleeButton.ts b/src/components/buttons/EmbedFleeButton.ts index 6b00e61..ab6fc13 100644 --- a/src/components/buttons/EmbedFleeButton.ts +++ b/src/components/buttons/EmbedFleeButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { type ICombatJSON } from '../../interfaces/ICombatJSON'; import { apiFetch } from '../../utilities/ApiClient'; diff --git a/src/components/buttons/EnhanceButton.ts b/src/components/buttons/EnhanceButton.ts index 5cd543e..36b33f6 100644 --- a/src/components/buttons/EnhanceButton.ts +++ b/src/components/buttons/EnhanceButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/EquipButton.ts b/src/components/buttons/EquipButton.ts index 31f4bbd..8b2c3e2 100644 --- a/src/components/buttons/EquipButton.ts +++ b/src/components/buttons/EquipButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/ExploreAgainButton.ts b/src/components/buttons/ExploreAgainButton.ts index f579b0a..3cbd0ef 100644 --- a/src/components/buttons/ExploreAgainButton.ts +++ b/src/components/buttons/ExploreAgainButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { buildCombatResponse } from '../../utilities/CombatResponseBuilder'; diff --git a/src/components/buttons/GuideNavButton.ts b/src/components/buttons/GuideNavButton.ts index b225dbd..21286f9 100644 --- a/src/components/buttons/GuideNavButton.ts +++ b/src/components/buttons/GuideNavButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; const SECTIONS: Record< diff --git a/src/components/buttons/LockButton.ts b/src/components/buttons/LockButton.ts index 86ce7fd..5037c3a 100644 --- a/src/components/buttons/LockButton.ts +++ b/src/components/buttons/LockButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/MarketNextButton.ts b/src/components/buttons/MarketNextButton.ts index dab0e45..7af5941 100644 --- a/src/components/buttons/MarketNextButton.ts +++ b/src/components/buttons/MarketNextButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { handleMarketPage } from './MarketPrevButton'; diff --git a/src/components/buttons/MarketSellPageButton.ts b/src/components/buttons/MarketSellPageButton.ts index d91952e..66b0fa6 100644 --- a/src/components/buttons/MarketSellPageButton.ts +++ b/src/components/buttons/MarketSellPageButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { formatError } from '../../utilities/ErrorMessages'; import { buildSellPage } from '../../commands/MarketCommand'; diff --git a/src/components/buttons/RegisterDeclineButton.ts b/src/components/buttons/RegisterDeclineButton.ts index db46546..bb6e984 100644 --- a/src/components/buttons/RegisterDeclineButton.ts +++ b/src/components/buttons/RegisterDeclineButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; export default class RegisterDeclineButton extends Button { diff --git a/src/components/buttons/RestButton.ts b/src/components/buttons/RestButton.ts index fcc4677..7ab9df6 100644 --- a/src/components/buttons/RestButton.ts +++ b/src/components/buttons/RestButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/buttons/TaskClaimButton.ts b/src/components/buttons/TaskClaimButton.ts index 7157402..f44d37f 100644 --- a/src/components/buttons/TaskClaimButton.ts +++ b/src/components/buttons/TaskClaimButton.ts @@ -1,4 +1,4 @@ -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; import Button from '../../structures/Button'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/menus/InvSelectMenu.ts b/src/components/menus/InvSelectMenu.ts index d43ff64..4616a5e 100644 --- a/src/components/menus/InvSelectMenu.ts +++ b/src/components/menus/InvSelectMenu.ts @@ -1,4 +1,4 @@ -import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import type { AnySelectMenuInteraction, Client } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/menus/ReforgeSelectMenu.ts b/src/components/menus/ReforgeSelectMenu.ts index b72dd98..36839c2 100644 --- a/src/components/menus/ReforgeSelectMenu.ts +++ b/src/components/menus/ReforgeSelectMenu.ts @@ -1,4 +1,4 @@ -import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import type { AnySelectMenuInteraction, Client } from 'discord.js'; import SelectMenu from '../../structures/SelectMenu'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/modals/CollectModal.ts b/src/components/modals/CollectModal.ts index 7dabcf4..0792e31 100644 --- a/src/components/modals/CollectModal.ts +++ b/src/components/modals/CollectModal.ts @@ -1,4 +1,4 @@ -import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import type { ModalSubmitInteraction, Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/modals/ConsumeModal.ts b/src/components/modals/ConsumeModal.ts index e135ff1..b38eb80 100644 --- a/src/components/modals/ConsumeModal.ts +++ b/src/components/modals/ConsumeModal.ts @@ -1,4 +1,4 @@ -import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import type { ModalSubmitInteraction, Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/components/modals/SellModal.ts b/src/components/modals/SellModal.ts index bd2be88..c18991c 100644 --- a/src/components/modals/SellModal.ts +++ b/src/components/modals/SellModal.ts @@ -1,4 +1,4 @@ -import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import type { ModalSubmitInteraction, Client } from 'discord.js'; import ModalSubmit from '../../structures/ModalSubmit'; import { apiFetch } from '../../utilities/ApiClient'; import { formatError } from '../../utilities/ErrorMessages'; diff --git a/src/structures/Button.ts b/src/structures/Button.ts index d9a193b..119095c 100644 --- a/src/structures/Button.ts +++ b/src/structures/Button.ts @@ -1,5 +1,5 @@ import type IExecutable from '../interfaces/IExecutable'; -import { type ButtonInteraction, type Client } from 'discord.js'; +import type { ButtonInteraction, Client } from 'discord.js'; export interface ButtonOptions { customId: string; diff --git a/src/structures/ModalSubmit.ts b/src/structures/ModalSubmit.ts index 854da1e..016dfa8 100644 --- a/src/structures/ModalSubmit.ts +++ b/src/structures/ModalSubmit.ts @@ -1,4 +1,4 @@ -import { type ModalSubmitInteraction, type Client } from 'discord.js'; +import type { ModalSubmitInteraction, Client } from 'discord.js'; import type IExecutable from '../interfaces/IExecutable'; export interface ModalSubmitOptions { diff --git a/src/structures/SelectMenu.ts b/src/structures/SelectMenu.ts index cb8a117..9b72fd5 100644 --- a/src/structures/SelectMenu.ts +++ b/src/structures/SelectMenu.ts @@ -1,4 +1,4 @@ -import { type AnySelectMenuInteraction, type Client } from 'discord.js'; +import type { AnySelectMenuInteraction, Client } from 'discord.js'; import type IExecutable from '../interfaces/IExecutable'; export interface SelectMenuOptions { diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index b0462f8..139cda4 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -4,7 +4,7 @@ import { type ICombatJSON } from '../interfaces/ICombatJSON'; import { type IPlayerJSON } from '../interfaces/IPlayerJSON'; import { type IItemJSON } from '../interfaces/IItemJSON'; import { type IInventoryItem } from '../interfaces/IInventoryJSON'; -import { type ITaskJSON, type IChestSlot } from '../interfaces/IGameJSON'; +import type { ITaskJSON, IChestSlot } from '../interfaces/IGameJSON'; import * as ItemManager from '../managers/ItemManager'; import * as WorkerPool from './WorkerPool'; import type { From 175db36babc2635ccebedc145dde6c35fdbbf962 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:39:12 +0700 Subject: [PATCH 6/7] refactor: mark all functions returning a Promise as async --- src/utilities/ImageService.ts | 18 +++++++++--------- src/utilities/WorkerPool.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index 139cda4..e9c0881 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -23,11 +23,11 @@ function serializeItemCache(): Record { return cache; } -export function adventure(data: IStepJSON | ICombatJSON): Promise { +export async function adventure(data: IStepJSON | ICombatJSON): Promise { return WorkerPool.run('adventure', { data }); } -export function profile( +export async function profile( player: IPlayerJSON, discordUser: User ): Promise { @@ -38,7 +38,7 @@ export function profile( }); } -export function inventory( +export async function inventory( chunk: IInventoryItem[], player: IPlayerJSON ): Promise { @@ -49,39 +49,39 @@ export function inventory( }); } -export function item(itemData: IItemJSON): Promise { +export async function item(itemData: IItemJSON): Promise { return WorkerPool.run('item', { item: itemData }); } -export function leaderboard( +export async function leaderboard( entries: LeaderboardEntry[], config: LeaderboardConfig ): Promise { return WorkerPool.run('leaderboard', { entries, config }); } -export function market( +export async function market( listings: MarketListing[], config: MarketPageConfig ): Promise { return WorkerPool.run('market', { listings, config }); } -export function travel( +export async function travel( playerLevel: number, currentZoneId: number ): Promise { return WorkerPool.run('travel', { playerLevel, currentZoneId }); } -export function tasks( +export async function tasks( tasks: ITaskJSON[], config: TasksPageConfig ): Promise { return WorkerPool.run('tasks', { tasks, config }); } -export function chests( +export async function chests( chests: IChestSlot[], config: ChestsPageConfig ): Promise { diff --git a/src/utilities/WorkerPool.ts b/src/utilities/WorkerPool.ts index e2ecbc9..c103c13 100644 --- a/src/utilities/WorkerPool.ts +++ b/src/utilities/WorkerPool.ts @@ -68,7 +68,7 @@ export function init(size?: number): void { * Submit a rendering task to the pool. * If a worker is free it runs immediately; otherwise it queues. */ -export function run(builderName: string, payload: any): Promise { +export async function run(builderName: string, payload: any): Promise { if (!isInitialized) { throw new Error( '[WorkerPool] Pool not initialized — call WorkerPool.init() first' From 836b927faf6d45a1b0096b42e93dfcf4cdb33550 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:54:13 +0700 Subject: [PATCH 7/7] refactor: move chunkArray to its own helper file to prevent duplicates --- src/commands/ChestsCommand.ts | 9 +-------- src/commands/MarketCommand.ts | 9 +-------- src/utilities/ImageService.ts | 4 +++- src/utilities/helpers.ts | 7 +++++++ 4 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 src/utilities/helpers.ts diff --git a/src/commands/ChestsCommand.ts b/src/commands/ChestsCommand.ts index 2c5bbb9..92386c0 100644 --- a/src/commands/ChestsCommand.ts +++ b/src/commands/ChestsCommand.ts @@ -13,6 +13,7 @@ import { formatError } from '../utilities/ErrorMessages'; import * as Routes from '../utilities/Routes'; import * as ImageService from '../utilities/ImageService'; import type { IChestSlot } from '../interfaces/IGameJSON'; +import { chunkArray } from '../utilities/helpers'; export default class ChestsCommand extends SlashCommand { constructor() { @@ -140,11 +141,3 @@ export default class ChestsCommand extends SlashCommand { } } } - -function chunkArray(arr: T[], size: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < arr.length; i += size) { - chunks.push(arr.slice(i, i + size)); - } - return chunks; -} diff --git a/src/commands/MarketCommand.ts b/src/commands/MarketCommand.ts index 38712e8..ee4224a 100644 --- a/src/commands/MarketCommand.ts +++ b/src/commands/MarketCommand.ts @@ -21,6 +21,7 @@ import { import * as ImageService from '../utilities/ImageService'; import * as ItemManager from '../managers/ItemManager'; import type { IInventoryItem } from '../interfaces/IInventoryJSON'; +import { chunkArray } from '../utilities/helpers'; const RARITY_CHOICES = [ { name: 'All', value: 'All' }, @@ -429,11 +430,3 @@ function buildMarketButtons( return rows; } - -function chunkArray(arr: T[], size: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < arr.length; i += size) { - chunks.push(arr.slice(i, i + size)); - } - return chunks; -} diff --git a/src/utilities/ImageService.ts b/src/utilities/ImageService.ts index e9c0881..2904e1d 100644 --- a/src/utilities/ImageService.ts +++ b/src/utilities/ImageService.ts @@ -23,7 +23,9 @@ function serializeItemCache(): Record { return cache; } -export async function adventure(data: IStepJSON | ICombatJSON): Promise { +export async function adventure( + data: IStepJSON | ICombatJSON +): Promise { return WorkerPool.run('adventure', { data }); } diff --git a/src/utilities/helpers.ts b/src/utilities/helpers.ts new file mode 100644 index 0000000..db6b5f5 --- /dev/null +++ b/src/utilities/helpers.ts @@ -0,0 +1,7 @@ +export function chunkArray(arr: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + chunks.push(arr.slice(i, i + size)); + } + return chunks; +}