diff --git a/apps/discord-bot/src/commands/gamecounts.command.ts b/apps/discord-bot/src/commands/gamecounts.command.ts index e6dc06ab0..46386519d 100644 --- a/apps/discord-bot/src/commands/gamecounts.command.ts +++ b/apps/discord-bot/src/commands/gamecounts.command.ts @@ -7,10 +7,16 @@ */ import { + APIApplicationCommandOptionChoice, + ApplicationCommandOptionType, +} from "discord-api-types/v10"; +import { + AbstractArgument, ApiService, Command, CommandContext, EmbedBuilder, + LocalizationString, Page, PaginateService, } from "@statsify/discord"; @@ -19,8 +25,77 @@ import { STATUS_COLORS } from "@statsify/logger"; import { mapGame } from "#constants"; import { removeFormatting } from "@statsify/util"; +const GAMECOUNT_GAME_IDS = [ + "ARCADE", + "ARENA_BRAWL", + "BEDWARS", + "BLITZSG", + "BUILD_BATTLE", + "COPS_AND_CRIMS", + "DUELS", + "HOUSING", + "IDLE", + "LIMBO", + "MAIN_LOBBY", + "MEGAWALLS", + "MURDER_MYSTERY", + "PAINTBALL", + "PIT", + "PROTOTYPE", + "QUAKE", + "QUEUE", + "REPLAY", + "SKYBLOCK", + "SKYWARS", + "SMASH_HEROES", + "SMP", + "SPEED_UHC", + "TNT_GAMES", + "TOURNAMENT_LOBBY", + "TURBO_KART_RACERS", + "UHC", + "VAMPIREZ", + "WALLS", + "WARLORDS", + "WOOLGAMES", +] as const satisfies readonly GameId[]; + +type GameCountGameId = (typeof GAMECOUNT_GAME_IDS)[number]; + +const GAMECOUNT_GAME_CHOICES = GAMECOUNT_GAME_IDS.map((id) => ({ + name: removeFormatting(FormattedGame[id]), + value: id, +})); + +class GameCountsModeArgument extends AbstractArgument { + public name = "mode"; + public type = ApplicationCommandOptionType.String; + public required = false; + public autocomplete = true; + public description: LocalizationString; + + public constructor() { + super(); + this.description = (t) => t("stats.mode"); + } + + public autocompleteHandler(context: CommandContext): APIApplicationCommandOptionChoice[] { + const currentValue = context.option(this.name, "").toLowerCase(); + + if (!currentValue) return GAMECOUNT_GAME_CHOICES.slice(0, 25); + + return GAMECOUNT_GAME_CHOICES + .filter(({ name, value }) => + name.toLowerCase().includes(currentValue) || + value.toLowerCase().includes(currentValue) + ) + .slice(0, 25); + } +} + @Command({ description: (t) => t("commands.game-counts"), + args: [GameCountsModeArgument], }) export class GameCountsCommand { public constructor( @@ -31,33 +106,19 @@ export class GameCountsCommand { public async run(context: CommandContext) { const t = context.t(); const gamecounts = await this.apiService.getGamecounts(); + const selectedGame = context.option("mode", undefined); - const gamecountEntries = Object.entries(gamecounts) as [GameId, GamePlayers][]; + const gamecountEntries = Object.entries(gamecounts) as [GameCountGameId, GamePlayers][]; const subGameGenerators = gamecountEntries - .filter(([, g]) => g.modes && Object.keys(g.modes).length > 1) - .map(([id, { players, modes }]) => { - const name = removeFormatting(FormattedGame[id]); - const list = Object.entries(modes!).sort((a, b) => Number(b[1]) - Number(a[1])); - - const embed = new EmbedBuilder() - .title((t) => `${name} ${t("players")}`) - .color(STATUS_COLORS.info) - .description( - (t) => - `${this.formatGameCount(t("stats.total"), t(players))}\n\n${list - .map(([mode, players]) => - this.formatGameCount(mapGame(id, mode), t(players)) - ) - .join("\n")}` - ); - - return { - label: name, - emoji: t(`emojis:games.${id}`), - generator: () => embed, - }; - }); + .filter(([, g]) => this.hasSubGames(g)) + .map(([id, game]) => this.createGameCountsPage(id, game)); + + if (selectedGame && selectedGame in gamecounts) { + return this.paginateService.paginate(context, [ + this.createGameCountsPage(selectedGame, gamecounts[selectedGame]), + ]); + } const total = gamecountEntries.reduce((acc, [, v]) => acc + (v.players ?? 0), 0); @@ -93,4 +154,35 @@ export class GameCountsCommand { private formatGameCount(name: string, count: string, emoji?: string) { return `\`•\` ${emoji ? `${emoji} ` : ""}**${name}**: \`${count || 0}\``; } + + private createGameCountsPage(id: GameId, { players, modes }: GamePlayers): Page { + const name = removeFormatting(FormattedGame[id]); + const list = Object.entries(modes ?? {}).sort((a, b) => Number(b[1]) - Number(a[1])); + + const embed = new EmbedBuilder() + .title((t) => `${name} ${t("players")}`) + .color(STATUS_COLORS.info) + .description( + (t) => { + const modeCounts = list + .map(([mode, players]) => this.formatGameCount(mapGame(id, mode), t(players))) + .join("\n"); + + return [ + this.formatGameCount(t("stats.total"), t(players)), + modeCounts, + ].filter(Boolean).join("\n\n"); + } + ); + + return { + label: name, + emoji: (t) => t(`emojis:games.${id}`), + generator: () => embed, + }; + } + + private hasSubGames({ modes }: GamePlayers) { + return modes && Object.keys(modes).length > 1; + } } diff --git a/packages/discord/src/command/command.builder.ts b/packages/discord/src/command/command.builder.ts index 8dcbe9e13..af1cd3b79 100644 --- a/packages/discord/src/command/command.builder.ts +++ b/packages/discord/src/command/command.builder.ts @@ -235,5 +235,26 @@ if (import.meta.vitest) { ], }); }); + + it("should detect added arguments", () => { + class Arg extends AbstractArgument { + public name = "test"; + public description = "test"; + public required = true; + public type = ApplicationCommandOptionType.String; + } + + @Command({ description: "test" }) + class BaseCommand {} + + @Command({ description: "test", args: [Arg] }) + class CommandWithArgs {} + + expect( + CommandBuilder.scan(new BaseCommand(), BaseCommand).equals( + CommandBuilder.scan(new CommandWithArgs(), CommandWithArgs) + ) + ).toBe(false); + }); }); } diff --git a/packages/discord/src/command/command.resolvable.ts b/packages/discord/src/command/command.resolvable.ts index 5279bae4c..870418791 100644 --- a/packages/discord/src/command/command.resolvable.ts +++ b/packages/discord/src/command/command.resolvable.ts @@ -134,9 +134,11 @@ export class CommandResolvable { return false; } - if (this.options?.length && other.options?.length) { - if (this.options.length !== other.options.length) return false; + if ((this.options?.length ?? 0) !== (other.options?.length ?? 0)) { + return false; + } + if (this.options?.length && other.options?.length) { for (let i = 0; i < this.options.length; i++) if (!this.options[i].equals(other.options[i])) return false; }