From 84f70b41e2df19f39b62d60ba22ce46a1401732c Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 5 May 2026 00:22:22 -0400 Subject: [PATCH 1/8] feat: implement miscellaneous stat suggestions --- apps/api/src/dtos/guild-leaderboard.dto.ts | 7 ++ apps/api/src/dtos/player-leaderboard.dto.ts | 8 +- .../guild-leaderboard.controller.ts | 5 +- .../src/leaderboards/leaderboard.service.ts | 42 +++++++++ .../player-leaderboard.controller.ts | 5 +- .../src/commands/arcade/modes/farm-hunt.tsx | 2 + .../src/commands/arcade/modes/galaxy-wars.tsx | 3 + .../src/commands/config/badge.command.tsx | 94 +++++++++++++------ .../src/commands/duels/duels.profile.tsx | 1 + .../src/commands/general/general.profile.tsx | 12 +++ .../leaderboards/base.leaderboard-command.tsx | 81 +++++++++++++++- .../src/commands/pit/pit.profile.tsx | 22 +++++ .../src/commands/quake/quake.profile.tsx | 7 ++ .../src/commands/quests/quests.profile.tsx | 26 ++++- .../commands/rankings/rankings.profile.tsx | 5 +- .../commands/tntgames/tntgames.profile.tsx | 1 + .../commands/warlords/warlords.profile.tsx | 6 +- locales/en-US/default.json | 18 ++++ packages/api-client/src/constants.ts | 3 +- .../src/player/gamemodes/arcade/mode.ts | 14 ++- .../src/player/gamemodes/bedwars/index.ts | 85 ++++++++++++++--- .../src/player/gamemodes/general/index.ts | 6 ++ .../schemas/src/player/gamemodes/pit/index.ts | 18 ++++ .../schemas/src/player/gamemodes/pit/util.ts | 13 +++ .../src/player/gamemodes/quake/mode.ts | 6 +- .../src/player/gamemodes/quests/index.ts | 2 +- .../gamemodes/quests/modes/turbokartracers.ts | 4 +- .../src/player/gamemodes/quests/util.ts | 38 +++++++- .../src/player/gamemodes/tntgames/index.ts | 6 +- packages/schemas/src/player/stats.ts | 52 ++++++++++ 30 files changed, 525 insertions(+), 67 deletions(-) diff --git a/apps/api/src/dtos/guild-leaderboard.dto.ts b/apps/api/src/dtos/guild-leaderboard.dto.ts index bdb8e5b5c..bd15bd8ee 100644 --- a/apps/api/src/dtos/guild-leaderboard.dto.ts +++ b/apps/api/src/dtos/guild-leaderboard.dto.ts @@ -11,6 +11,7 @@ import { Guild, LeaderboardScanner } from "@statsify/schemas"; import { IsEnum, IsInt, + IsNumber, IsOptional, IsString, MaxLength, @@ -39,6 +40,12 @@ export class GuildLeaderboardDto { @ApiProperty({ minimum: 1, type: () => Number, required: false }) public position?: number; + @Transform((params) => +params.value) + @IsOptional() + @IsNumber() + @ApiProperty({ type: () => Number, required: false }) + public value?: number; + @IsOptional() @IsString() @MinLength(3) diff --git a/apps/api/src/dtos/player-leaderboard.dto.ts b/apps/api/src/dtos/player-leaderboard.dto.ts index 8b8706757..4033505dd 100644 --- a/apps/api/src/dtos/player-leaderboard.dto.ts +++ b/apps/api/src/dtos/player-leaderboard.dto.ts @@ -7,7 +7,7 @@ */ import { ApiProperty, PartialType } from "@nestjs/swagger"; -import { IsEnum, IsInt, IsOptional, Min } from "class-validator"; +import { IsEnum, IsInt, IsNumber, IsOptional, Min } from "class-validator"; import { LeaderboardScanner, Player } from "@statsify/schemas"; import { PlayerDto } from "./player.dto.js"; import { Transform } from "class-transformer"; @@ -31,4 +31,10 @@ export class PlayerLeaderboardDto extends PartialType(PlayerDto) { @Min(1) @ApiProperty({ minimum: 1, type: () => Number, required: false }) public position?: number; + + @Transform((params) => +params.value) + @IsOptional() + @IsNumber() + @ApiProperty({ type: () => Number, required: false }) + public value?: number; } diff --git a/apps/api/src/guild/leaderboards/guild-leaderboard.controller.ts b/apps/api/src/guild/leaderboards/guild-leaderboard.controller.ts index 70eeb3474..0e2edccb8 100644 --- a/apps/api/src/guild/leaderboards/guild-leaderboard.controller.ts +++ b/apps/api/src/guild/leaderboards/guild-leaderboard.controller.ts @@ -29,7 +29,7 @@ export class GuildLeaderboardController { @ApiBadRequestResponse({ type: ErrorResponse }) @Auth({ weight: 10 }) public async getGuildLeaderboard( - @Body() { field, page, guild, position }: GuildLeaderboardDto + @Body() { field, page, guild, position, value }: GuildLeaderboardDto ) { let input: number | string; let type: LeaderboardQuery; @@ -40,6 +40,9 @@ export class GuildLeaderboardController { } else if (position) { input = position; type = LeaderboardQuery.POSITION; + } else if (typeof value === "number") { + input = value; + type = LeaderboardQuery.VALUE; } else { input = page; type = LeaderboardQuery.PAGE; diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index fd054864b..fb32d61a2 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -117,6 +117,18 @@ export abstract class LeaderboardService { bottom = top + PAGE_SIZE; break; } + case LeaderboardQuery.VALUE: { + const ranking = await this.searchLeaderboardValue( + constructor, + field, + input as number, + sort + ); + highlight = ranking - 1; + top = highlight - (highlight % 10); + bottom = top + PAGE_SIZE; + break; + } } const leaderboard = await this.getLeaderboardFromRedis( @@ -302,6 +314,36 @@ export abstract class LeaderboardService { return response; } + private async searchLeaderboardValue( + constructor: Constructor, + field: string, + value: number, + sort = "DESC" + ): Promise { + const name = constructor.name.toLowerCase(); + const key = `${name}.${field}`; + + const result = await this.redis.zrevrangebyscore( + key, + value, + "-inf", + "LIMIT", + 0, + 1 + ); + + const fallback = await this.redis.zrange(key, 0, 0); + + const id = result[0] ?? fallback[0]; + if (!id) return 1; + + const rank = sort === "ASC" ? + await this.redis.zrank(key, id) : + await this.redis.zrevrank(key, id); + + return (rank ?? 0) + 1; + } + private getLeaderboardExpiryTime(leaderboard: LeaderboardEnabledMetadata): number { if (!leaderboard.resetEvery) throw new Error("To get a leaderboard expiry time, `resetEvery` must be specified"); diff --git a/apps/api/src/player/leaderboards/player-leaderboard.controller.ts b/apps/api/src/player/leaderboards/player-leaderboard.controller.ts index 567d5f4cf..c2f6ba641 100644 --- a/apps/api/src/player/leaderboards/player-leaderboard.controller.ts +++ b/apps/api/src/player/leaderboards/player-leaderboard.controller.ts @@ -37,7 +37,7 @@ export class PlayerLeaderboardsController { @ApiBadRequestResponse({ type: ErrorResponse }) @Auth({ weight: 3 }) public getPlayerLeaderboard( - @Body() { field, page, player, position }: PlayerLeaderboardDto + @Body() { field, page, player, position, value }: PlayerLeaderboardDto ) { let input: number | string; let type: LeaderboardQuery; @@ -48,6 +48,9 @@ export class PlayerLeaderboardsController { } else if (position) { input = position; type = LeaderboardQuery.POSITION; + } else if (typeof value === "number") { + input = value; + type = LeaderboardQuery.VALUE; } else { input = page; type = LeaderboardQuery.PAGE; diff --git a/apps/discord-bot/src/commands/arcade/modes/farm-hunt.tsx b/apps/discord-bot/src/commands/arcade/modes/farm-hunt.tsx index 55000d2df..101341589 100644 --- a/apps/discord-bot/src/commands/arcade/modes/farm-hunt.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/farm-hunt.tsx @@ -33,12 +33,14 @@ export const FarmHuntTable = ({ stats, t }: FarmHuntTableProps) => ( + + diff --git a/apps/discord-bot/src/commands/arcade/modes/galaxy-wars.tsx b/apps/discord-bot/src/commands/arcade/modes/galaxy-wars.tsx index 0ae58b7fd..d6fb3bd6c 100644 --- a/apps/discord-bot/src/commands/arcade/modes/galaxy-wars.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/galaxy-wars.tsx @@ -27,5 +27,8 @@ export const GalaxyWarsTable = ({ stats, t }: GalaxyWarsTableProps) => ( + + + ); diff --git a/apps/discord-bot/src/commands/config/badge.command.tsx b/apps/discord-bot/src/commands/config/badge.command.tsx index d04ea406a..6f00e4e86 100644 --- a/apps/discord-bot/src/commands/config/badge.command.tsx +++ b/apps/discord-bot/src/commands/config/badge.command.tsx @@ -16,6 +16,7 @@ import { IMessage, LocalizeFunction, SubCommand, + TextArgument, } from "@statsify/discord"; import { type Canvas, Image } from "skia-canvas"; import { DemoProfile } from "./demo.profile.js"; @@ -41,7 +42,10 @@ export class BadgeCommand { description: (t) => t("commands.badge-set"), tier: UserTier.GOLD, preview: "badge.png", - args: [new FileArgument("badge", true)], + args: [ + new FileArgument("badge"), + new TextArgument("emoji", (t) => t("arguments.emoji"), false), + ], }) public set(context: CommandContext) { return this.run(context, "set"); @@ -62,6 +66,7 @@ export class BadgeCommand { ): Promise { const userId = context.getInteraction().getUserId(); const file = context.option("badge"); + const emoji = context.option("emoji"); const user = context.getUser(); const t = context.t(); @@ -82,41 +87,13 @@ export class BadgeCommand { } case "set": { - if (!file) + if (!file && !emoji) throw new ErrorMessage( (t) => t("errors.unknown.title"), (t) => t("errors.unknown.description") ); - const canvas = createCanvas(32, 32); - const ctx = canvas.getContext("2d"); - ctx.imageSmoothingEnabled = false; - - if (!["image/png", "image/jpeg", "image/gif"].includes(file.content_type ?? "")) - throw new ErrorMessage( - (t) => t("errors.unsupportedFileType.title"), - (t) => t("errors.unsupportedFileType.description") - ); - - const badge = await loadImage(file.url); - - const ratio = Math.min(canvas.width / badge.width, canvas.height / badge.height); - const scaled = badge.width > 32 || badge.height > 32; - - const width = scaled ? badge.width * ratio : badge.width; - const height = scaled ? badge.height * ratio : badge.height; - - ctx.drawImage( - badge, - 0, - 0, - badge.width, - badge.height, - (canvas.width - width) / 2, - (canvas.height - height) / 2, - width, - height - ); + const canvas = file ? await this.getBadgeCanvas(file) : await this.getEmojiCanvas(emoji as string); await this.apiService.updateUserBadge(userId, await canvas.toBuffer("png")); const profile = await this.getProfile(t, user, canvas); @@ -141,6 +118,61 @@ export class BadgeCommand { } } + private async getBadgeCanvas(file: APIAttachment) { + const canvas = createCanvas(32, 32); + const ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + + if (!["image/png", "image/jpeg", "image/gif"].includes(file.content_type ?? "")) + throw new ErrorMessage( + (t) => t("errors.unsupportedFileType.title"), + (t) => t("errors.unsupportedFileType.description") + ); + + const badge = await loadImage(file.url); + + const ratio = Math.min(canvas.width / badge.width, canvas.height / badge.height); + const scaled = badge.width > 32 || badge.height > 32; + + const width = scaled ? badge.width * ratio : badge.width; + const height = scaled ? badge.height * ratio : badge.height; + + ctx.drawImage( + badge, + 0, + 0, + badge.width, + badge.height, + (canvas.width - width) / 2, + (canvas.height - height) / 2, + width, + height + ); + + return canvas; + } + + private async getEmojiCanvas(input: string) { + const canvas = createCanvas(32, 32); + const ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + + const customEmoji = input.trim().match(/^$/); + + if (customEmoji) { + const badge = await loadImage(`https://cdn.discordapp.com/emojis/${customEmoji[1]}.png?size=32&quality=lossless`); + ctx.drawImage(badge, 0, 0, badge.width, badge.height, 0, 0, 32, 32); + return canvas; + } + + ctx.font = "28px Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(input.trim(), 16, 17); + + return canvas; + } + private async getProfile(t: LocalizeFunction, user: User, badge?: Image | Canvas) { if (!user?.uuid) throw new ErrorMessage("errors.unknown"); diff --git a/apps/discord-bot/src/commands/duels/duels.profile.tsx b/apps/discord-bot/src/commands/duels/duels.profile.tsx index d45cfdd5a..01c1a3dd2 100644 --- a/apps/discord-bot/src/commands/duels/duels.profile.tsx +++ b/apps/discord-bot/src/commands/duels/duels.profile.tsx @@ -42,6 +42,7 @@ export const DuelsProfile = ({ const sidebar: SidebarItem[] = [ [t("stats.tokens"), t(duels.tokens), "§2"], [t("stats.pingRange"), `${t(duels.pingRange)}ms`, "§a"], + [t("stats.matchesPlayed"), t(duels.overall.wins + duels.overall.losses), "§e"], [t("stats.blocksPlaced"), t(duels.overall.blocksPlaced), "§9"], ]; diff --git a/apps/discord-bot/src/commands/general/general.profile.tsx b/apps/discord-bot/src/commands/general/general.profile.tsx index 60361afd2..d356588ac 100644 --- a/apps/discord-bot/src/commands/general/general.profile.tsx +++ b/apps/discord-bot/src/commands/general/general.profile.tsx @@ -112,6 +112,18 @@ export const GeneralProfile = ({ color="§a" /> + + + + Promise; +const parseLeaderboardValue = (input: string): number => { + const normalized = input.trim().replaceAll(",", "").toLowerCase(); + const match = normalized.match(/^(\d+(?:\.\d+)?)([kmbt])?$/); + + if (!match) return Number.NaN; + + const suffixes = { + k: 1000, + m: 1_000_000, + b: 1_000_000_000, + t: 1_000_000_000_000, + }; + + const [, value, suffix] = match; + const multiplier = suffix ? suffixes[suffix as keyof typeof suffixes] : 1; + + return Number(value) * multiplier; +}; + export interface CreateLeaderboardOptions { context: CommandContext; background: Image; @@ -96,6 +115,10 @@ export class BaseLeaderboardCommand { .emoji(t("emojis:search.position")) .style(ButtonStyle.Primary); + const searchValue = new ButtonBuilder() + .label((t) => t("leaderboard.valueInput.button")) + .style(ButtonStyle.Primary); + let currentPage = 0; const changePage = @@ -117,8 +140,14 @@ export class BaseLeaderboardCommand { if (interaction.getUserId() === userId && !message.ephemeral) { up.disable(page === 0); - currentPage = page || currentPage; - const row = new ActionRowBuilder([up, down, searchDocument, searchPosition]); + currentPage = page ?? currentPage; + const row = new ActionRowBuilder([ + up, + down, + searchDocument, + searchPosition, + searchValue, + ]); context.reply({ ...message, @@ -174,6 +203,19 @@ export class BaseLeaderboardCommand { ) ); + const valueModal = new ModalBuilder() + .title((t) => t("leaderboard.modal.title")) + .component( + new ActionRowBuilder().component( + new TextInputBuilder() + .label((t) => t("leaderboard.valueInput.label")) + .placeholder((t) => t("leaderboard.valueInput.placeholder")) + .minLength(1) + .maxLength(16) + .required(true) + ) + ); + listener.addHook(searchDocument.getCustomId(), () => ({ type: InteractionResponseType.Modal, data: documentModal.build(t), @@ -184,6 +226,11 @@ export class BaseLeaderboardCommand { data: positionModal.build(t), })); + listener.addHook(searchValue.getCustomId(), () => ({ + type: InteractionResponseType.Modal, + data: valueModal.build(t), + })); + listener.addHook(documentModal.getCustomId(), async (interaction) => { const data = interaction.getData(); const documentInput = data.components[0].components[0].value; @@ -214,7 +261,29 @@ export class BaseLeaderboardCommand { ); }); - const row = new ActionRowBuilder([up, down, searchDocument, searchPosition]); + listener.addHook(valueModal.getCustomId(), async (interaction) => { + const data = interaction.getData(); + const valueInput = data.components[0].components[0].value; + + const value = parseLeaderboardValue(valueInput); + + if (user?.locale) interaction.setLocale(user.locale); + + if (Number.isNaN(value) || value < 0) { + const error = new ErrorMessage("errors.leaderboardInvalidValue"); + + return interaction.sendFollowup({ + ...error, + ephemeral: true, + }); + } + + changePage(() => ({ input: value, type: LeaderboardQuery.VALUE }))( + interaction + ); + }); + + const row = new ActionRowBuilder([up, down, searchDocument, searchPosition, searchValue]); const [message, page] = await this.getLeaderboardMessage( user, @@ -234,14 +303,16 @@ export class BaseLeaderboardCommand { listener.removeHook(down.getCustomId()); listener.removeHook(searchDocument.getCustomId()); listener.removeHook(searchPosition.getCustomId()); + listener.removeHook(searchValue.getCustomId()); listener.removeHook(documentModal.getCustomId()); listener.removeHook(positionModal.getCustomId()); + listener.removeHook(valueModal.getCustomId()); context.reply({ embeds: [], components: [] }); cache.clear(); }, 300_000); - currentPage = page || currentPage; + currentPage = page ?? currentPage; return { ...message, components: [row] }; } @@ -271,7 +342,7 @@ export class BaseLeaderboardCommand { getLeaderboardDataIcon ); - if (params.type === LeaderboardQuery.PAGE && page) cache.set(page, message); + if (params.type === LeaderboardQuery.PAGE && page !== null) cache.set(page, message); return [message, page]; } diff --git a/apps/discord-bot/src/commands/pit/pit.profile.tsx b/apps/discord-bot/src/commands/pit/pit.profile.tsx index 76d5a5d9a..1f554e3e1 100644 --- a/apps/discord-bot/src/commands/pit/pit.profile.tsx +++ b/apps/discord-bot/src/commands/pit/pit.profile.tsx @@ -54,6 +54,13 @@ export const PitProfile = ({ currentLevel: pit.levelFormatted, nextLevel: pit.nextLevelFormatted, showLevel: true, + })}\n${formatProgression({ + t, + label: t("stats.progression.gold"), + progression: pit.goldProgression, + currentLevel: pit.levelFormatted, + nextLevel: pit.nextLevelFormatted, + showLevel: false, })}`} sidebar={sidebar} badge={badge} @@ -80,6 +87,21 @@ export const PitProfile = ({ /> + + + + + 0 ? t(pit.goldRequirement) : "§7N/A"} + color="§e" + /> + + + + + { const isOverall = time === QuestTime.Overall; + const progress = (quests as GameQuests & { __progress?: Record }).__progress ?? {}; const entries = Object.entries(quests) - .filter(([k, v]) => k !== "total" && v !== null) + .filter(([k, v]) => k !== "total" && k !== "__progress" && v !== null) .sort((a, b) => b[1] - a[1]) .map(([quest, completions]) => { const name = questMetadata[time][game][quest].leaderboard.name; + const questProgress = progress[quest]; + let progressText: string | undefined; + + if (questProgress?.max) { + progressText = `${t(questProgress.current)}/${t(questProgress.max)}`; + } else if (questProgress) { + progressText = t(questProgress.current); + } + + let status: JSX.Element; + + if (isOverall) { + status = {t(completions)}; + } else if (progressText && completions === 0) { + status = §e{progressText}; + } else { + status = ; + } return ( @@ -156,9 +176,7 @@ const GameTable = ({ quests, t, game, time, logos: [cross, check] }: GameTablePr {completions > 0 ? "§a" : "§c"}§l{name}
- {isOverall ? - {t(completions)} : - } + {status} ); }); diff --git a/apps/discord-bot/src/commands/rankings/rankings.profile.tsx b/apps/discord-bot/src/commands/rankings/rankings.profile.tsx index 26d4b7941..6b25c3170 100644 --- a/apps/discord-bot/src/commands/rankings/rankings.profile.tsx +++ b/apps/discord-bot/src/commands/rankings/rankings.profile.tsx @@ -57,7 +57,7 @@ export const RankingsProfile = ({ game, badge, }: RankingsProfileProps) => { - const listTitles = ["Statistic", "Pos", "Value"]; + const listTitles = ["Statistic", "Pos", "Page", "Value"]; if (!game) listTitles.unshift("Game"); const titles = listTitles.map((field) => ( @@ -86,6 +86,9 @@ export const RankingsProfile = ({ {formatPosition(t, d.rank)} + + {t(Math.floor((d.rank - 1) / 10) + 1)} + {typeof d.value === "string" ? d.value : t(d.value)} diff --git a/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx b/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx index 7e1f9d466..a0e529c62 100644 --- a/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx +++ b/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx @@ -31,6 +31,7 @@ export const TNTGamesProfile = ({ const sidebar: SidebarItem[] = [ [t("stats.coins"), t(tntgames.coins), "§6"], [t("stats.overallWins"), t(tntgames.wins), "§e"], + [t("stats.playtime"), formatTime(tntgames.playtime), "§a"], ]; let table; diff --git a/apps/discord-bot/src/commands/warlords/warlords.profile.tsx b/apps/discord-bot/src/commands/warlords/warlords.profile.tsx index 82d410c24..4561f703c 100644 --- a/apps/discord-bot/src/commands/warlords/warlords.profile.tsx +++ b/apps/discord-bot/src/commands/warlords/warlords.profile.tsx @@ -42,7 +42,11 @@ export const WarlordsProfile = ({ sidebar.push([t("stats.class"), prettify(warlords.class), "§e"]); const clazz = warlords.class as "mage" | "warrior" | "paladin" | "shaman"; // Verify that the cast is correct and the class is a valid class - if (clazz in warlords && typeof warlords[clazz] === "object") sidebar.push([t("stats.spec"), prettify(warlords[clazz].specification), "§a"]); + if (clazz in warlords && typeof warlords[clazz] === "object") + sidebar.push( + [t("stats.spec"), prettify(warlords[clazz].specification), "§a"], + [t("stats.level"), t(warlords[clazz].level), "§a"] + ); } let table: JSX.Element; diff --git a/locales/en-US/default.json b/locales/en-US/default.json index b66f3e884..7bfdd0ec2 100644 --- a/locales/en-US/default.json +++ b/locales/en-US/default.json @@ -1,6 +1,7 @@ { "arguments": { "choice": "Choose an option", + "emoji": "A Discord emoji", "file": "Upload a file", "gtbhelper": "The current hint", "guild-leaderboard": "$t(arguments.player-leaderboard)", @@ -427,6 +428,10 @@ "description": "The leaderboard position you entered is invalid. Enter a number greater than 1.", "title": "Invalid Position" }, + "leaderboardInvalidValue": { + "description": "The leaderboard value you entered is invalid. Enter a positive number. Suffixes like k, m, b, and t are supported.", + "title": "Invalid Value" + }, "leaderboardNotFound": { "description": "We can't find the `leaderboard` you are looking for!", "title": "Leaderboard Not Found" @@ -538,6 +543,11 @@ "positionInput": { "label": "Search by a Position", "placeholder": "Enter a position, eg: 5000" + }, + "valueInput": { + "button": "Value", + "label": "Search by a Value", + "placeholder": "Enter a value, eg: 5k" } }, "minecraft": { @@ -576,6 +586,7 @@ "blocksBroken": "Blocks Broken", "blocksPlaced": "Blocks Placed", "blocksRan": "Blocks Ran", + "blocksTravelled": "Blocks Travelled", "bombsDefused": "Bombs Defused", "bombsPlanted": "Bombs Planted", "bounty": "Bounty", @@ -629,6 +640,7 @@ "goldEarned": "Gold Earned", "goldPickedUp": "Gold Picked Up", "goldRate": "Gold Rate", + "goldRequirement": "Gold Requirement", "goldSpent": "Gold Spent", "goldTrophies": "Gold Trophies", "grandPrixTokens": "GP Tokens", @@ -680,6 +692,7 @@ "killsAsMurderer": "Kills As Murderer", "killsAsSurvivor": "Kills as Survivor", "killsAsAlpha": "Kills as Alpha", + "killsPerWin": "Kills Per Win", "killstreaks": "Killstreaks", "kit": "Current Kit", "knife": "Knife", @@ -704,6 +717,7 @@ "magicalKeys": "Magical Keys", "map": "Map", "mastery": "Mastery", + "matchesPlayed": "Matches Played", "maxPoints": "Max Score", "maxWave": "Max Wave", "mode": "Mode", @@ -728,10 +742,12 @@ "powerupActivations": "Power-Up Activations", "powerups": "Power-Ups", "prefix": "Prefix", + "prestigeGold": "Prestige Gold", "prevent": "Prevent", "progression": { "exp": "EXP Progress", "gexp": "GEXP Progress", + "gold": "Gold Progress", "goldTrophy": "Gold Trophy Progress", "kill": "Kill Progress", "score": "Score Progress", @@ -769,7 +785,9 @@ "title": "Title", "tokens": "Tokens", "total": "Total", + "totalKills": "Total Kills", "totalSlumberTickets": "Total Slumber Tickets", + "totalWins": "Total Wins", "totalTrophies": "Total Trophies", "trail": "Trail", "transfusion": "Transfusion", diff --git a/packages/api-client/src/constants.ts b/packages/api-client/src/constants.ts index 52310234b..a32dc88da 100644 --- a/packages/api-client/src/constants.ts +++ b/packages/api-client/src/constants.ts @@ -37,5 +37,6 @@ export enum CacheLevel { export enum LeaderboardQuery { PAGE = "page", INPUT = "input", - POSITION = "position" + POSITION = "position", + VALUE = "value" } diff --git a/packages/schemas/src/player/gamemodes/arcade/mode.ts b/packages/schemas/src/player/gamemodes/arcade/mode.ts index e2dd5a1a2..96db9dca7 100644 --- a/packages/schemas/src/player/gamemodes/arcade/mode.ts +++ b/packages/schemas/src/player/gamemodes/arcade/mode.ts @@ -802,6 +802,12 @@ export class FarmHunt { @Field() public hunterKills: number; + @Field() + public animalKillsPerWin: number; + + @Field() + public hunterKillsPerWin: number; + @Field() public tauntsUsed: number; @@ -815,6 +821,8 @@ export class FarmHunt { this.kills = data.kills_farm_hunt; this.animalKills = data.animal_kills_farm_hunt; this.hunterKills = data.hunter_kills_farm_hunt; + this.animalKillsPerWin = ratio(this.animalKills, this.animalWins); + this.hunterKillsPerWin = ratio(this.hunterKills, this.hunterWins); this.poopCollected = add( data.poop_collected, data.poop_collected_farm_hunt @@ -863,6 +871,9 @@ export class GalaxyWars { @Field() public rebelKills: number; + @Field() + public shotsFired: number; + public constructor(data: APIData) { this.wins = data.sw_game_wins; this.kills = data.sw_kills; @@ -870,6 +881,7 @@ export class GalaxyWars { this.kdr = ratio(this.kills, this.deaths); this.empireKills = data.sw_empire_kills; this.rebelKills = data.sw_rebel_kills; + this.shotsFired = data.sw_shots_fired; } } @@ -1377,7 +1389,7 @@ export class ZombiesMapDifficulty { }) public fastestWin: number; - @Field({ leaderboard: { enabled: false } }) + @Field() public kills: number; @Field({ leaderboard: { enabled: false } }) diff --git a/packages/schemas/src/player/gamemodes/bedwars/index.ts b/packages/schemas/src/player/gamemodes/bedwars/index.ts index f501a998a..1caa732ad 100644 --- a/packages/schemas/src/player/gamemodes/bedwars/index.ts +++ b/packages/schemas/src/player/gamemodes/bedwars/index.ts @@ -33,20 +33,20 @@ export const BEDWARS_MODES = new GameModes([ { api: "voidless" }, { api: "oneBlock", hypixel: "BEDWARS_EIGHT_ONE_ONEBLOCK" }, - { hypixel: "BEDWARS_EIGHT_TWO_ARMED", formatted: "Armed Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_ARMED", formatted: "Armed Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_LUCKY", formatted: "Lucky Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_LUCKY", formatted: "Lucky Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_RUSH", formatted: "Rush Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_RUSH", formatted: "Rush Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_SWAP", formatted: "Swap Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_SWAP", formatted: "Swap Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_ULTIMATE", formatted: "Ultimate Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_ULTIMATE", formatted: "Ultimate Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_UNDERWORLD", formatted: "Underworld Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_UNDERWORLD", formatted: "Underworld Fours" }, - { hypixel: "BEDWARS_EIGHT_TWO_VOIDLESS", formatted: "Voidless Doubles" }, - { hypixel: "BEDWARS_FOUR_FOUR_VOIDLESS", formatted: "Voidless Fours" }, + { api: "armedDoubles", hypixel: "BEDWARS_EIGHT_TWO_ARMED", formatted: "Armed Doubles" }, + { api: "armedFours", hypixel: "BEDWARS_FOUR_FOUR_ARMED", formatted: "Armed Fours" }, + { api: "luckyDoubles", hypixel: "BEDWARS_EIGHT_TWO_LUCKY", formatted: "Lucky Doubles" }, + { api: "luckyFours", hypixel: "BEDWARS_FOUR_FOUR_LUCKY", formatted: "Lucky Fours" }, + { api: "rushDoubles", hypixel: "BEDWARS_EIGHT_TWO_RUSH", formatted: "Rush Doubles" }, + { api: "rushFours", hypixel: "BEDWARS_FOUR_FOUR_RUSH", formatted: "Rush Fours" }, + { api: "swapDoubles", hypixel: "BEDWARS_EIGHT_TWO_SWAP", formatted: "Swap Doubles" }, + { api: "swapFours", hypixel: "BEDWARS_FOUR_FOUR_SWAP", formatted: "Swap Fours" }, + { api: "ultimateDoubles", hypixel: "BEDWARS_EIGHT_TWO_ULTIMATE", formatted: "Ultimate Doubles" }, + { api: "ultimateFours", hypixel: "BEDWARS_FOUR_FOUR_ULTIMATE", formatted: "Ultimate Fours" }, + { api: "underworldDoubles", hypixel: "BEDWARS_EIGHT_TWO_UNDERWORLD", formatted: "Underworld Doubles" }, + { api: "underworldFours", hypixel: "BEDWARS_FOUR_FOUR_UNDERWORLD", formatted: "Underworld Fours" }, + { api: "voidlessDoubles", hypixel: "BEDWARS_EIGHT_TWO_VOIDLESS", formatted: "Voidless Doubles" }, + { api: "voidlessFours", hypixel: "BEDWARS_FOUR_FOUR_VOIDLESS", formatted: "Voidless Fours" }, { hypixel: "BEDWARS_PRACTICE", formatted: "Practice" }, ] as const); @@ -139,6 +139,48 @@ export class BedWars { @Field() public oneBlock: BedWarsMode; + @Field() + public armedDoubles: BedWarsMode; + + @Field() + public armedFours: BedWarsMode; + + @Field() + public luckyDoubles: BedWarsMode; + + @Field() + public luckyFours: BedWarsMode; + + @Field() + public rushDoubles: BedWarsMode; + + @Field() + public rushFours: BedWarsMode; + + @Field() + public swapDoubles: BedWarsMode; + + @Field() + public swapFours: BedWarsMode; + + @Field() + public ultimateDoubles: BedWarsMode; + + @Field() + public ultimateFours: BedWarsMode; + + @Field() + public underworldDoubles: BedWarsMode; + + @Field() + public underworldFours: BedWarsMode; + + @Field() + public voidlessDoubles: BedWarsMode; + + @Field() + public voidlessFours: BedWarsMode; + @Field() public challenges: ChallengesBedWars; @@ -179,6 +221,21 @@ export class BedWars { this.voidless = DreamsBedWarsMode.new(data, "voidless"); this.oneBlock = new BedWarsMode(data, "eight_one_oneblock"); + this.armedDoubles = new BedWarsMode(data, "eight_two_armed"); + this.armedFours = new BedWarsMode(data, "four_four_armed"); + this.luckyDoubles = new BedWarsMode(data, "eight_two_lucky"); + this.luckyFours = new BedWarsMode(data, "four_four_lucky"); + this.rushDoubles = new BedWarsMode(data, "eight_two_rush"); + this.rushFours = new BedWarsMode(data, "four_four_rush"); + this.swapDoubles = new BedWarsMode(data, "eight_two_swap"); + this.swapFours = new BedWarsMode(data, "four_four_swap"); + this.ultimateDoubles = new BedWarsMode(data, "eight_two_ultimate"); + this.ultimateFours = new BedWarsMode(data, "four_four_ultimate"); + this.underworldDoubles = new BedWarsMode(data, "eight_two_underworld"); + this.underworldFours = new BedWarsMode(data, "four_four_underworld"); + this.voidlessDoubles = new BedWarsMode(data, "eight_two_voidless"); + this.voidlessFours = new BedWarsMode(data, "four_four_voidless"); + this.core = deepSub(this.overall, this["4v4"]); BedWarsMode.applyRatios(this.core); diff --git a/packages/schemas/src/player/gamemodes/general/index.ts b/packages/schemas/src/player/gamemodes/general/index.ts index f3e6dd85e..e6920dd44 100644 --- a/packages/schemas/src/player/gamemodes/general/index.ts +++ b/packages/schemas/src/player/gamemodes/general/index.ts @@ -20,6 +20,12 @@ export class General { @Field() public achievementPoints: number; + @Field() + public totalWins: number; + + @Field() + public totalKills: number; + @Field() public giftsSent: number; diff --git a/packages/schemas/src/player/gamemodes/pit/index.ts b/packages/schemas/src/player/gamemodes/pit/index.ts index b647ec65c..2e19f934f 100644 --- a/packages/schemas/src/player/gamemodes/pit/index.ts +++ b/packages/schemas/src/player/gamemodes/pit/index.ts @@ -16,6 +16,7 @@ import { getLevel, getLevelFormatted, getPrestige, + getPrestigeGoldReq, getPrestigeReq, getRenownShopCost, } from "./util.js"; @@ -79,6 +80,15 @@ export class Pit { }) public goldEarned: number; + @Field({ leaderboard: { enabled: false }, historical: { enabled: false } }) + public prestigeGold: number; + + @Field({ leaderboard: { enabled: false }, historical: { enabled: false } }) + public goldRequirement: number; + + @Field({ leaderboard: { enabled: false }, historical: { enabled: false } }) + public goldProgression: Progression; + @Field({ historical: { enabled: false } }) public renown: number; @@ -162,11 +172,19 @@ export class Pit { this.trueLevel = prestige * 120 + level; const lastPrestigeReq = getPrestigeReq(prestige - 1); + const prestigeGoldReq = getPrestigeGoldReq(prestige); + + this.prestigeGold = profile[`cash_during_prestige_${prestige}`] ?? 0; + this.goldRequirement = prestigeGoldReq; this.progression = new Progression( this.exp - lastPrestigeReq, Math.min(getPrestigeReq(prestige) - lastPrestigeReq, 11_787_293_080) ); + this.goldProgression = new Progression( + this.prestigeGold, + Math.max(prestigeGoldReq, 0) + ); this.levelFormatted = getLevelFormatted(level, prestige); this.nextLevelFormatted = diff --git a/packages/schemas/src/player/gamemodes/pit/util.ts b/packages/schemas/src/player/gamemodes/pit/util.ts index e7f5dc1d8..b9d6e16c2 100644 --- a/packages/schemas/src/player/gamemodes/pit/util.ts +++ b/packages/schemas/src/player/gamemodes/pit/util.ts @@ -29,6 +29,16 @@ const PRESTIGE_XP_REQUIREMENTS = [ 11_787_293_080, ]; +const PRESTIGE_GOLD_REQUIREMENTS = [ + 10_000, 20_000, 20_000, 20_000, 30_000, 35_000, 40_000, 45_000, 50_000, + 60_000, 70_000, 80_000, 90_000, 100_000, 125_000, 150_000, 175_000, 200_000, + 250_000, 300_000, 350_000, 400_000, 500_000, 600_000, 700_000, 800_000, + 900_000, 1_000_000, 1_000_000, 1_000_000, 1_000_000, 1_000_000, 1_000_000, + 1_000_000, 1_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000, + 2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000, + 2_000_000, 2_000_000, 2_000_000, -1, +]; + const PRESTIGE_COLORS = [ "7", "9", @@ -82,6 +92,9 @@ export const getPrestige = (xp: number) => { export const getPrestigeReq = (prestige: number) => prestige > -1 ? PRESTIGE_XP_REQUIREMENTS[prestige] : 0; +export const getPrestigeGoldReq = (prestige: number) => + prestige > -1 ? PRESTIGE_GOLD_REQUIREMENTS[prestige] : 0; + export const getLevel = (pres: number, xp: number) => { let level = 120; if ( diff --git a/packages/schemas/src/player/gamemodes/quake/mode.ts b/packages/schemas/src/player/gamemodes/quake/mode.ts index f0bf2a5cd..d14d43440 100644 --- a/packages/schemas/src/player/gamemodes/quake/mode.ts +++ b/packages/schemas/src/player/gamemodes/quake/mode.ts @@ -29,9 +29,12 @@ export class QuakeMode { @Field() public killstreaks: number; - @Field({ leaderboard: { enabled: false } }) + @Field() public shotsFired: number; + @Field({ leaderboard: { fieldName: "Blocks Travelled", name: "Blocks Travelled" } }) + public blocksTravelled: number; + @Field({ leaderboard: { fieldName: "2017+ Kills", name: "2017+ Kills" } }) public postUpdateKills: number; @@ -50,6 +53,7 @@ export class QuakeMode { this.headshots = data[`headshots${mode}`]; this.killstreaks = data[`killstreaks${mode}`]; this.shotsFired = data[`shots_fired${mode}`]; + this.blocksTravelled = data[`distance_travelled${mode}`]; this.postUpdateKills = data[`kills_since_update_feb_2017${mode}`]; QuakeMode.applyRatios(this); } diff --git a/packages/schemas/src/player/gamemodes/quests/index.ts b/packages/schemas/src/player/gamemodes/quests/index.ts index 3dcb0b5b4..25298ca05 100644 --- a/packages/schemas/src/player/gamemodes/quests/index.ts +++ b/packages/schemas/src/player/gamemodes/quests/index.ts @@ -163,4 +163,4 @@ export class Quests { } } -export { QuestTime } from "./util.js"; +export { QuestTime, type QuestProgress } from "./util.js"; diff --git a/packages/schemas/src/player/gamemodes/quests/modes/turbokartracers.ts b/packages/schemas/src/player/gamemodes/quests/modes/turbokartracers.ts index f43eb6af2..3e63e94bc 100644 --- a/packages/schemas/src/player/gamemodes/quests/modes/turbokartracers.ts +++ b/packages/schemas/src/player/gamemodes/quests/modes/turbokartracers.ts @@ -13,8 +13,8 @@ export const TurboKartRacersQuests = createGameModeQuests({ game: FormattedGame.TURBO_KART_RACERS, fieldPrefix: "gingerbread", daily: [ - { field: "bling_bling", propertyKey: "blingBling" }, - { field: "maps", propertyKey: "internationalChampionship" }, + { field: "bling_bling", propertyKey: "blingBling", objectives: { gingerbread_gold_pickedup: 50 } }, + { field: "maps", propertyKey: "internationalChampionship", objectives: { gingerbread_maps: 5 } }, { field: "racer", propertyKey: "racer" }, ], weekly: [{ field: "mastery", propertyKey: "turboKartRacers" }], diff --git a/packages/schemas/src/player/gamemodes/quests/util.ts b/packages/schemas/src/player/gamemodes/quests/util.ts index ddfdddd88..f48316749 100644 --- a/packages/schemas/src/player/gamemodes/quests/util.ts +++ b/packages/schemas/src/player/gamemodes/quests/util.ts @@ -18,6 +18,14 @@ import { FormattedGame } from "#game"; interface Quest { completions?: { time: number }[]; + active?: { + objectives?: Record; + }; +} + +export interface QuestProgress { + current: number; + max?: number; } export enum QuestTime { @@ -32,6 +40,7 @@ export interface QuestOption { propertyKey: TField; fieldName?: string; name?: string; + objectives?: Record; leaderboard?: false; overall?: { fieldName?: string; @@ -64,7 +73,7 @@ export interface CreateQuestsOptions< } const processQuests = ( - instance: Record, + instance: Record & { __progress?: Record }, quests: APIData, time: QuestTime, options: QuestOption[], @@ -76,6 +85,12 @@ const processQuests = ( instance[k] = getQuestCountDuring(time, quests[field]); instance.total += instance[k] ?? 0; + + const progress = getQuestProgress(quests[field], quest); + if (progress) { + instance.__progress ??= {}; + instance.__progress[k] = progress; + } }); }; @@ -272,3 +287,24 @@ export const getQuestCountDuring = (time: QuestTime, quest: Quest | undefined) = return quest.completions.filter((ms) => ms.time >= millis).length; }; + +export const getQuestProgress = ( + quest: Quest | undefined, + option: QuestOption +): QuestProgress | undefined => { + const objectives = quest?.active?.objectives; + if (!objectives) return undefined; + + const entries = Object.entries(objectives); + if (!entries.length) return undefined; + + const current = entries.reduce((total, [, value]) => total + (value ?? 0), 0); + const max = option.objectives ? + Object.entries(option.objectives).reduce( + (total, [objective, value]) => total + (objectives[objective] === undefined ? 0 : value), + 0 + ) : + undefined; + + return { current, max: max || undefined }; +}; diff --git a/packages/schemas/src/player/gamemodes/tntgames/index.ts b/packages/schemas/src/player/gamemodes/tntgames/index.ts index c8edc3e91..04f272373 100644 --- a/packages/schemas/src/player/gamemodes/tntgames/index.ts +++ b/packages/schemas/src/player/gamemodes/tntgames/index.ts @@ -6,10 +6,10 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import { type APIData, formatTime } from "@statsify/util"; import { BowSpleef, PVPRun, TNTRun, TNTTag, Wizards } from "./mode.js"; import { type ExtractGameModes, GameModes } from "#game"; import { Field } from "#metadata"; -import type { APIData } from "@statsify/util"; export const TNT_GAMES_MODES = new GameModes([ { api: "overall" }, @@ -45,6 +45,9 @@ export class TNTGames { @Field() public wins: number; + @Field({ leaderboard: { formatter: formatTime }, historical: { enabled: false } }) + public playtime: number; + @Field({ leaderboard: { fieldName: "TNT Run", extraDisplay: "this.tntRun.naturalPrefix" }, }) @@ -65,6 +68,7 @@ export class TNTGames { public constructor(data: APIData, ap: APIData) { this.coins = data.coins; this.wins = data.wins; + this.playtime = (ap.tntgames_tnt_triathlon ?? 0) * 60_000; this.tntRun = new TNTRun(data, ap); this.pvpRun = new PVPRun(data); diff --git a/packages/schemas/src/player/stats.ts b/packages/schemas/src/player/stats.ts index 60fa3b0e6..b24b1821c 100644 --- a/packages/schemas/src/player/stats.ts +++ b/packages/schemas/src/player/stats.ts @@ -36,6 +36,7 @@ import { } from "./gamemodes/index.js"; import { Field } from "#metadata"; import { FormattedGame } from "#game"; +import { add } from "@statsify/math"; import type { APIData } from "@statsify/util"; export class PlayerStats { @@ -253,5 +254,56 @@ export class PlayerStats { this.walls = new Walls(stats.Walls ?? {}, legacy); this.warlords = new Warlords(stats.Battleground ?? {}); this.woolgames = new WoolGames(stats.WoolGames ?? {}, achievements); + + this.general.totalWins = add( + this.arcade.wins, + this.arenabrawl.overall.wins, + this.bedwars.overall.wins, + this.blitzsg.overall.wins, + this.buildbattle.overall.wins, + this.copsandcrims.overall.wins, + this.duels.overall.wins, + this.megawalls.overall.wins, + this.murdermystery.overall.wins, + this.paintball.wins, + this.quake.overall.wins, + this.skywars.overall.wins, + this.smashheroes.overall.wins, + this.speeduhc.overall.wins, + this.tntgames.wins, + this.uhc.overall.wins, + this.vampirez.overallWins, + this.walls.wins, + this.warlords.wins, + this.woolgames.wins + ); + + this.general.totalKills = add( + this.arenabrawl.overall.kills, + this.bedwars.overall.kills, + this.blitzsg.overall.kills, + this.copsandcrims.overall.kills, + this.duels.overall.kills, + this.megawalls.overall.kills, + this.murdermystery.overall.kills, + this.paintball.kills, + this.pit.kills, + this.quake.overall.kills, + this.skywars.overall.kills, + this.smashheroes.overall.kills, + this.speeduhc.overall.kills, + this.tntgames.pvpRun.kills, + this.tntgames.tntTag.kills, + this.tntgames.wizards.kills, + this.uhc.overall.kills, + this.vampirez.human.kills, + this.vampirez.vampire.kills, + this.vampirez.zombieKills, + this.walls.kills, + this.warlords.kills, + this.woolgames.woolwars.overall.kills, + this.woolgames.sheepwars.kills, + this.woolgames.captureTheWool.kills + ); } } From 137275ed35b4a714b7957d98db2937cd13121f3e Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 5 May 2026 14:33:08 -0400 Subject: [PATCH 2/8] fix(profile): remove unnecessary table rows in GeneralProfile component --- apps/discord-bot/src/commands/general/general.profile.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/discord-bot/src/commands/general/general.profile.tsx b/apps/discord-bot/src/commands/general/general.profile.tsx index d356588ac..6b360c5f2 100644 --- a/apps/discord-bot/src/commands/general/general.profile.tsx +++ b/apps/discord-bot/src/commands/general/general.profile.tsx @@ -111,8 +111,6 @@ export const GeneralProfile = ({ value={t(challenges.total)} color="§a" /> - - Date: Wed, 27 May 2026 00:29:15 -0600 Subject: [PATCH 3/8] fix: prepare misc stats PR for review --- apps/api/src/leaderboards/leaderboard.service.ts | 15 ++++++--------- .../src/commands/rankings/rankings.command.tsx | 4 +++- .../src/commands/rankings/rankings.profile.tsx | 10 ++++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index fb32d61a2..899565a4a 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -323,16 +323,13 @@ export abstract class LeaderboardService { const name = constructor.name.toLowerCase(); const key = `${name}.${field}`; - const result = await this.redis.zrevrangebyscore( - key, - value, - "-inf", - "LIMIT", - 0, - 1 - ); + const result = sort === "ASC" ? + await this.redis.zrangebyscore(key, value, "+inf", "LIMIT", 0, 1) : + await this.redis.zrevrangebyscore(key, value, "-inf", "LIMIT", 0, 1); - const fallback = await this.redis.zrange(key, 0, 0); + const fallback = sort === "ASC" ? + await this.redis.zrevrange(key, 0, 0) : + await this.redis.zrange(key, 0, 0); const id = result[0] ?? fallback[0]; if (!id) return 1; diff --git a/apps/discord-bot/src/commands/rankings/rankings.command.tsx b/apps/discord-bot/src/commands/rankings/rankings.command.tsx index 2aa88ac6f..1c5376221 100644 --- a/apps/discord-bot/src/commands/rankings/rankings.command.tsx +++ b/apps/discord-bot/src/commands/rankings/rankings.command.tsx @@ -350,7 +350,7 @@ export class RankingsCommand { return this.paginateService.scrollingPagination( context, groups.map( - (group) => () => + (group, page) => () => render( , getTheme(user) ) diff --git a/apps/discord-bot/src/commands/rankings/rankings.profile.tsx b/apps/discord-bot/src/commands/rankings/rankings.profile.tsx index 6b25c3170..bdb9c314c 100644 --- a/apps/discord-bot/src/commands/rankings/rankings.profile.tsx +++ b/apps/discord-bot/src/commands/rankings/rankings.profile.tsx @@ -44,6 +44,8 @@ const formatStat = (stat: PostLeaderboardRankingsResponse, game?: string) => { export interface RankingsProfileProps extends Omit { data: PostLeaderboardRankingsResponse[]; game?: string; + page: number; + pageCount: number; } export const RankingsProfile = ({ @@ -56,8 +58,10 @@ export const RankingsProfile = ({ skin, game, badge, + page, + pageCount, }: RankingsProfileProps) => { - const listTitles = ["Statistic", "Pos", "Page", "Value"]; + const listTitles = ["Statistic", "Pos", "Value"]; if (!game) listTitles.unshift("Game"); const titles = listTitles.map((field) => ( @@ -86,9 +90,6 @@ export const RankingsProfile = ({ {formatPosition(t, d.rank)} - - {t(Math.floor((d.rank - 1) / 10) + 1)} - {typeof d.value === "string" ? d.value : t(d.value)} @@ -105,6 +106,7 @@ export const RankingsProfile = ({ skin={skin} time="LIVE" title={`§l§bLeaderboard Positions §r(§l${formattedGame}§r)`} + description={`§7Page ${t(page + 1)} / ${t(pageCount)}`} badge={badge} /> {titles}, ...items]} /> From d8168b7f1f16134166afd7d48df5eacad0855930 Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 27 May 2026 00:39:59 -0600 Subject: [PATCH 4/8] feat: show zombies map kills --- apps/discord-bot/src/commands/arcade/modes/zombies.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx index 78dc6a714..704d8d4eb 100644 --- a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx @@ -27,6 +27,7 @@ const ZombiesMapColumn = ({ title, stats, t, time }: ZombiesMapColumnProps) => { return ( + From b4ca84a5c00618e2b9d5ac20c2ad8be5a60ea950 Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 27 May 2026 00:53:36 -0600 Subject: [PATCH 5/8] fix: nest bedwars dream split modes --- .../src/commands/bedwars/bedwars.profile.tsx | 8 +- .../src/player/gamemodes/bedwars/index.ts | 78 +++++++++++++------ 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/apps/discord-bot/src/commands/bedwars/bedwars.profile.tsx b/apps/discord-bot/src/commands/bedwars/bedwars.profile.tsx index 452699499..5c1149f81 100644 --- a/apps/discord-bot/src/commands/bedwars/bedwars.profile.tsx +++ b/apps/discord-bot/src/commands/bedwars/bedwars.profile.tsx @@ -34,7 +34,11 @@ export const BedWarsProfile = ({ time, }: BedWarsProfileProps) => { const { bedwars } = player.stats; - const stats = bedwars[mode.api]; + const modeKey = mode.submode?.api ?? mode.api; + const formattedMode = mode.submode?.api === mode.api || !mode.submode ? + mode.formatted : + `${mode.formatted} ${mode.submode.formatted}`; + const stats = bedwars[modeKey]; const sidebar: SidebarItem[] = [ [t("stats.tokens"), t(bedwars.tokens), "§2"], @@ -58,7 +62,7 @@ export const BedWarsProfile = ({ name={player.prefixName} badge={badge} sidebar={sidebar} - title={`§l${FormattedGame.BEDWARS} §fStats §r(${mode.formatted})`} + title={`§l${FormattedGame.BEDWARS} §fStats §r(${formattedMode})`} description={`§7${t("stats.level")}: ${ bedwars.levelFormatted }\n${formatProgression({ diff --git a/packages/schemas/src/player/gamemodes/bedwars/index.ts b/packages/schemas/src/player/gamemodes/bedwars/index.ts index 1caa732ad..8163cd928 100644 --- a/packages/schemas/src/player/gamemodes/bedwars/index.ts +++ b/packages/schemas/src/player/gamemodes/bedwars/index.ts @@ -23,30 +23,64 @@ export const BEDWARS_MODES = new GameModes([ { api: "threes", hypixel: "BEDWARS_FOUR_THREE" }, { api: "fours", hypixel: "BEDWARS_FOUR_FOUR" }, { api: "4v4", hypixel: "BEDWARS_TWO_FOUR" }, - { api: "armed" }, + { + api: "armed", + submodes: [ + { api: "armed", formatted: "Overall" }, + { api: "armedDoubles", formatted: "Doubles" }, + { api: "armedFours", formatted: "Fours" }, + ], + }, { api: "castle", hypixel: "BEDWARS_CASTLE" }, - { api: "lucky" }, - { api: "rush" }, - { api: "swap" }, - { api: "ultimate" }, - { api: "underworld" }, - { api: "voidless" }, + { + api: "lucky", + submodes: [ + { api: "lucky", formatted: "Overall" }, + { api: "luckyDoubles", formatted: "Doubles" }, + { api: "luckyFours", formatted: "Fours" }, + ], + }, + { + api: "rush", + submodes: [ + { api: "rush", formatted: "Overall" }, + { api: "rushDoubles", formatted: "Doubles" }, + { api: "rushFours", formatted: "Fours" }, + ], + }, + { + api: "swap", + submodes: [ + { api: "swap", formatted: "Overall" }, + { api: "swapDoubles", formatted: "Doubles" }, + { api: "swapFours", formatted: "Fours" }, + ], + }, + { + api: "ultimate", + submodes: [ + { api: "ultimate", formatted: "Overall" }, + { api: "ultimateDoubles", formatted: "Doubles" }, + { api: "ultimateFours", formatted: "Fours" }, + ], + }, + { + api: "underworld", + submodes: [ + { api: "underworld", formatted: "Overall" }, + { api: "underworldDoubles", formatted: "Doubles" }, + { api: "underworldFours", formatted: "Fours" }, + ], + }, + { + api: "voidless", + submodes: [ + { api: "voidless", formatted: "Overall" }, + { api: "voidlessDoubles", formatted: "Doubles" }, + { api: "voidlessFours", formatted: "Fours" }, + ], + }, { api: "oneBlock", hypixel: "BEDWARS_EIGHT_ONE_ONEBLOCK" }, - - { api: "armedDoubles", hypixel: "BEDWARS_EIGHT_TWO_ARMED", formatted: "Armed Doubles" }, - { api: "armedFours", hypixel: "BEDWARS_FOUR_FOUR_ARMED", formatted: "Armed Fours" }, - { api: "luckyDoubles", hypixel: "BEDWARS_EIGHT_TWO_LUCKY", formatted: "Lucky Doubles" }, - { api: "luckyFours", hypixel: "BEDWARS_FOUR_FOUR_LUCKY", formatted: "Lucky Fours" }, - { api: "rushDoubles", hypixel: "BEDWARS_EIGHT_TWO_RUSH", formatted: "Rush Doubles" }, - { api: "rushFours", hypixel: "BEDWARS_FOUR_FOUR_RUSH", formatted: "Rush Fours" }, - { api: "swapDoubles", hypixel: "BEDWARS_EIGHT_TWO_SWAP", formatted: "Swap Doubles" }, - { api: "swapFours", hypixel: "BEDWARS_FOUR_FOUR_SWAP", formatted: "Swap Fours" }, - { api: "ultimateDoubles", hypixel: "BEDWARS_EIGHT_TWO_ULTIMATE", formatted: "Ultimate Doubles" }, - { api: "ultimateFours", hypixel: "BEDWARS_FOUR_FOUR_ULTIMATE", formatted: "Ultimate Fours" }, - { api: "underworldDoubles", hypixel: "BEDWARS_EIGHT_TWO_UNDERWORLD", formatted: "Underworld Doubles" }, - { api: "underworldFours", hypixel: "BEDWARS_FOUR_FOUR_UNDERWORLD", formatted: "Underworld Fours" }, - { api: "voidlessDoubles", hypixel: "BEDWARS_EIGHT_TWO_VOIDLESS", formatted: "Voidless Doubles" }, - { api: "voidlessFours", hypixel: "BEDWARS_FOUR_FOUR_VOIDLESS", formatted: "Voidless Fours" }, { hypixel: "BEDWARS_PRACTICE", formatted: "Practice" }, ] as const); From 068c98230951ca06ec552586ce5661303bf0d355 Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 27 May 2026 01:15:53 -0600 Subject: [PATCH 6/8] fix: show zombies fastest win seconds --- apps/discord-bot/src/commands/arcade/modes/zombies.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx index 704d8d4eb..dc62418b9 100644 --- a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx @@ -21,7 +21,7 @@ interface ZombiesMapColumnProps { const ZombiesMapColumn = ({ title, stats, t, time }: ZombiesMapColumnProps) => { const mapStat = stats.wins >= 1 ? - [t("stats.fastestWin"), stats.fastestWin ? formatTime(stats.fastestWin) : "N/A"] : + [t("stats.fastestWin"), stats.fastestWin ? formatTime(stats.fastestWin, { entries: 3 }) : "N/A"] : [t("stats.bestRound"), t(stats.bestRound)]; return ( @@ -90,7 +90,7 @@ export const ZombiesMapDifficultyTable = ({ stats, t, time }: ZombiesMapDifficul - + From dfcfdf1ff0529eb7e12da321ddc0b2959b8e924b Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 27 May 2026 01:20:24 -0600 Subject: [PATCH 7/8] fix: show zombies leaderboard seconds --- packages/schemas/src/player/gamemodes/arcade/mode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/schemas/src/player/gamemodes/arcade/mode.ts b/packages/schemas/src/player/gamemodes/arcade/mode.ts index 96db9dca7..714784437 100644 --- a/packages/schemas/src/player/gamemodes/arcade/mode.ts +++ b/packages/schemas/src/player/gamemodes/arcade/mode.ts @@ -16,6 +16,8 @@ import { import { Field } from "#metadata"; import { add, deepAdd, deepSub, ratio, sub } from "@statsify/math"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export class BlockingDead { @Field() public wins: number; @@ -1382,7 +1384,7 @@ export class ZombiesMapDifficulty { @Field({ leaderboard: { sort: "ASC", - formatter: formatTime, + formatter: formatTimeWithSeconds, additionalFields: ["this.wins"], }, historical: { enabled: false }, From 4b682f6e2a0ab809ecc47159d312b8234438581e Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 27 May 2026 01:39:37 -0600 Subject: [PATCH 8/8] fix: expand record time formatting --- apps/discord-bot/src/commands/arcade/modes/dropper.tsx | 4 +++- apps/discord-bot/src/commands/arcade/modes/zombies.tsx | 1 + .../src/commands/copsandcrims/copsandcrims.profile.tsx | 4 +++- apps/discord-bot/src/commands/duels/duels.profile.tsx | 4 +++- .../commands/murdermystery/murdermystery.profile.tsx | 6 ++++-- .../src/commands/parkour/parkour.profile.tsx | 4 +++- .../src/commands/tntgames/tntgames.profile.tsx | 8 +++++--- .../src/commands/woolgames/capture-the-wool.table.tsx | 8 +++++--- packages/schemas/src/player/gamemodes/arcade/mode.ts | 10 +++++++--- packages/schemas/src/player/gamemodes/duels/mode.ts | 4 +++- .../schemas/src/player/gamemodes/murdermystery/mode.ts | 6 ++++-- packages/schemas/src/player/gamemodes/parkour/index.ts | 3 ++- packages/schemas/src/player/gamemodes/tntgames/mode.ts | 7 ++++--- .../src/player/gamemodes/woolgames/capture-the-wool.ts | 8 +++++--- 14 files changed, 52 insertions(+), 25 deletions(-) diff --git a/apps/discord-bot/src/commands/arcade/modes/dropper.tsx b/apps/discord-bot/src/commands/arcade/modes/dropper.tsx index db404d1c4..ffe78b044 100644 --- a/apps/discord-bot/src/commands/arcade/modes/dropper.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/dropper.tsx @@ -12,6 +12,8 @@ import { arrayGroup, formatRaceTime, formatTime } from "@statsify/util"; import type { LocalizeFunction } from "@statsify/discord"; import type { ProfileTime } from "#commands/base.hypixel-command"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + interface DropperTableProps { stats: Dropper; submode: SubModeForMode; @@ -34,7 +36,7 @@ export const DropperTable = ({ stats, submode, t, time }: DropperTableProps) => 0}> - + diff --git a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx index dc62418b9..342c57f4f 100644 --- a/apps/discord-bot/src/commands/arcade/modes/zombies.tsx +++ b/apps/discord-bot/src/commands/arcade/modes/zombies.tsx @@ -51,6 +51,7 @@ export const ZombiesTable = ({ stats, t, time }: ZombiesTableProps) => { + diff --git a/apps/discord-bot/src/commands/copsandcrims/copsandcrims.profile.tsx b/apps/discord-bot/src/commands/copsandcrims/copsandcrims.profile.tsx index c49932ceb..ec50e4938 100644 --- a/apps/discord-bot/src/commands/copsandcrims/copsandcrims.profile.tsx +++ b/apps/discord-bot/src/commands/copsandcrims/copsandcrims.profile.tsx @@ -11,6 +11,8 @@ import { CopsAndCrimsModes, FormattedGame, type GameMode } from "@statsify/schem import { formatTime } from "@statsify/util"; import type { BaseProfileProps } from "#commands/base.hypixel-command"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export interface CopsAndCrimsProfileProps extends BaseProfileProps { mode: GameMode; } @@ -105,7 +107,7 @@ export const CopsAndCrimsProfile = ({ diff --git a/apps/discord-bot/src/commands/duels/duels.profile.tsx b/apps/discord-bot/src/commands/duels/duels.profile.tsx index 01c1a3dd2..a05298dd0 100644 --- a/apps/discord-bot/src/commands/duels/duels.profile.tsx +++ b/apps/discord-bot/src/commands/duels/duels.profile.tsx @@ -19,6 +19,8 @@ import { formatTime, prettify } from "@statsify/util"; import type { BaseProfileProps, ProfileTime } from "#commands/base.hypixel-command"; import type { DuelsModeIcons } from "./duels.command.js"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export type DuelsProfileProps = Omit & { mode: GameMode; time: T; @@ -62,7 +64,7 @@ export const DuelsProfile = ({ if (mode.api === "parkour") { sidebar.push( - [t("stats.bestTime"), duels.parkour.bestTime === 0 ? "N/A" : formatTime(duels.parkour.bestTime), "§d"], + [t("stats.bestTime"), duels.parkour.bestTime === 0 ? "N/A" : formatTimeWithSeconds(duels.parkour.bestTime), "§d"], [t("stats.checkpoints"), t(duels.parkour.checkpoints), "§5"] ); } diff --git a/apps/discord-bot/src/commands/murdermystery/murdermystery.profile.tsx b/apps/discord-bot/src/commands/murdermystery/murdermystery.profile.tsx index 4251fa621..91cff1f5b 100644 --- a/apps/discord-bot/src/commands/murdermystery/murdermystery.profile.tsx +++ b/apps/discord-bot/src/commands/murdermystery/murdermystery.profile.tsx @@ -22,6 +22,8 @@ import { import { formatTime } from "@statsify/util"; import type { BaseProfileProps } from "#commands/base.hypixel-command"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export interface MurderMysteryProfileProps extends BaseProfileProps { mode: GameMode; } @@ -199,7 +201,7 @@ export const MurderMysteryProfile = ({ title={t("stats.fastestMurdererWin")} value={ stats.fastestMurdererWin ? - formatTime(stats.fastestMurdererWin) : + formatTimeWithSeconds(stats.fastestMurdererWin) : "N/A" } color="§c" @@ -208,7 +210,7 @@ export const MurderMysteryProfile = ({ title={t("stats.fastestDetectiveWin")} value={ stats.fastestDetectiveWin ? - formatTime(stats.fastestDetectiveWin) : + formatTimeWithSeconds(stats.fastestDetectiveWin) : "N/A" } color="§b" diff --git a/apps/discord-bot/src/commands/parkour/parkour.profile.tsx b/apps/discord-bot/src/commands/parkour/parkour.profile.tsx index b5c9d185a..9d0bd739e 100644 --- a/apps/discord-bot/src/commands/parkour/parkour.profile.tsx +++ b/apps/discord-bot/src/commands/parkour/parkour.profile.tsx @@ -12,6 +12,8 @@ import { formatTime } from "@statsify/util"; import type { BaseProfileProps } from "#commands/base.hypixel-command"; import type { Image } from "skia-canvas"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + interface ParkourProfileProps extends BaseProfileProps { gameIcons: Record; } @@ -29,7 +31,7 @@ export const ParkourProfile = ({ const times: [GameId, any][] = Object.entries(parkour) .sort((a, b) => (a[1] || Number.MAX_VALUE) - (b[1] || Number.MAX_VALUE)) - .map(([field, time]) => [field as GameId, time ? formatTime(time) : "N/A"]); + .map(([field, time]) => [field as GameId, time ? formatTimeWithSeconds(time) : "N/A"]); return ( diff --git a/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx b/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx index a0e529c62..e998f1005 100644 --- a/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx +++ b/apps/discord-bot/src/commands/tntgames/tntgames.profile.tsx @@ -11,6 +11,8 @@ import { FormattedGame, type GameMode, type TNTGamesModes } from "@statsify/sche import { formatTime, prettify } from "@statsify/util"; import type { BaseProfileProps } from "#commands/base.hypixel-command"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export interface TNTGamesProfileProps extends BaseProfileProps { mode: GameMode; } @@ -48,7 +50,7 @@ export const TNTGamesProfile = ({ [ [t("stats.wins"), t(tntgames.tntRun.wins)], [t("stats.wlr"), t(tntgames.tntRun.wlr)], - [t("stats.bestTime"), formatTime(tntgames.tntRun.record)], + [t("stats.bestTime"), formatTimeWithSeconds(tntgames.tntRun.record)], ] : [ [t("stats.wins"), t(tntgames.tntRun.wins)], @@ -105,7 +107,7 @@ export const TNTGamesProfile = ({ - + @@ -125,7 +127,7 @@ export const TNTGamesProfile = ({ - + diff --git a/apps/discord-bot/src/commands/woolgames/capture-the-wool.table.tsx b/apps/discord-bot/src/commands/woolgames/capture-the-wool.table.tsx index 3514b8815..4fd0ac318 100644 --- a/apps/discord-bot/src/commands/woolgames/capture-the-wool.table.tsx +++ b/apps/discord-bot/src/commands/woolgames/capture-the-wool.table.tsx @@ -12,6 +12,8 @@ import type { CaptureTheWool } from "@statsify/schemas"; import type { LocalizeFunction } from "@statsify/discord"; import type { ProfileTime } from "#commands/base.hypixel-command"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + interface CaptureTheWoolTableProps { captureTheWool: CaptureTheWool; t: LocalizeFunction; @@ -54,21 +56,21 @@ export const CaptureTheWoolTable = ({ captureTheWool, t, time }: CaptureTheWoolT 0}> 0}> 0}> diff --git a/packages/schemas/src/player/gamemodes/arcade/mode.ts b/packages/schemas/src/player/gamemodes/arcade/mode.ts index 714784437..86fb1bd4c 100644 --- a/packages/schemas/src/player/gamemodes/arcade/mode.ts +++ b/packages/schemas/src/player/gamemodes/arcade/mode.ts @@ -735,7 +735,7 @@ export class Dropper { @Field() public flawlessGames: number; - @Field({ leaderboard: { formatter: formatTime, sort: "ASC" } }) + @Field({ leaderboard: { formatter: formatTimeWithSeconds, sort: "ASC" } }) public bestTime: number; @Field({ leaderboard: { name: "Maps:" } }) @@ -1391,12 +1391,15 @@ export class ZombiesMapDifficulty { }) public fastestWin: number; - @Field() + @Field({ leaderboard: { additionalFields: ["this.deaths", "this.kdr"] } }) public kills: number; - @Field({ leaderboard: { enabled: false } }) + @Field({ leaderboard: { additionalFields: ["this.kills", "this.kdr"] } }) public deaths: number; + @Field({ leaderboard: { enabled: false } }) + public kdr: number; + @Field({ leaderboard: { enabled: false } }) public bestRound: number; @@ -1413,6 +1416,7 @@ export class ZombiesMapDifficulty { this.fastestWin = (data[`fastest_time_30_zombies${mode}`] ?? 0) * 1000; this.kills = data[`zombie_kills_zombies${mode}`]; this.deaths = data[`deaths_zombies${mode}`]; + this.kdr = ratio(this.kills, this.deaths); this.bestRound = data[`best_round_zombies${mode}`]; this.doorsOpened = data[`doors_opened_zombies${mode}`]; this.totalRounds = data[`total_rounds_survived_zombies${mode}`]; diff --git a/packages/schemas/src/player/gamemodes/duels/mode.ts b/packages/schemas/src/player/gamemodes/duels/mode.ts index 57cd014a0..c8f9ed2a9 100644 --- a/packages/schemas/src/player/gamemodes/duels/mode.ts +++ b/packages/schemas/src/player/gamemodes/duels/mode.ts @@ -12,6 +12,8 @@ import { Progression } from "#progression"; import { TitleRequirement, getTitleAndProgression } from "./util.js"; import { add, deepAdd, ratio } from "@statsify/math"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export class BaseDuelsGameMode { @Field() public bestWinstreak: number; @@ -580,7 +582,7 @@ export class ParkourDuels extends SingleDuelsGameMode { @Field() public checkpoints: number; - @Field({ leaderboard: { formatter: formatTime, sort: "ASC" } }) + @Field({ leaderboard: { formatter: formatTimeWithSeconds, sort: "ASC" } }) public bestTime: number; public constructor(data: APIData) { diff --git a/packages/schemas/src/player/gamemodes/murdermystery/mode.ts b/packages/schemas/src/player/gamemodes/murdermystery/mode.ts index 547c120b5..610fab4d7 100644 --- a/packages/schemas/src/player/gamemodes/murdermystery/mode.ts +++ b/packages/schemas/src/player/gamemodes/murdermystery/mode.ts @@ -11,6 +11,8 @@ import { Field } from "#metadata"; import { Progression } from "#progression"; import { ratio } from "@statsify/math"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export class BaseMurderMysteryMode { @Field() public wins: number; @@ -98,13 +100,13 @@ export class OverallMurderMysteryMode extends StandardMurderMysteryMode { export class ClassicMurderMysteryMode extends StandardMurderMysteryMode { @Field({ - leaderboard: { sort: "ASC", formatter: formatTime }, + leaderboard: { sort: "ASC", formatter: formatTimeWithSeconds }, historical: { enabled: false }, }) public fastestDetectiveWin: number; @Field({ - leaderboard: { sort: "ASC", formatter: formatTime }, + leaderboard: { sort: "ASC", formatter: formatTimeWithSeconds }, historical: { enabled: false }, }) public fastestMurdererWin: number; diff --git a/packages/schemas/src/player/gamemodes/parkour/index.ts b/packages/schemas/src/player/gamemodes/parkour/index.ts index 8936c32f9..ff57df798 100644 --- a/packages/schemas/src/player/gamemodes/parkour/index.ts +++ b/packages/schemas/src/player/gamemodes/parkour/index.ts @@ -14,7 +14,8 @@ export const PARKOUR_MODES = new GameModes([{ api: "overall" }] as const); export type ParkourModes = ExtractGameModes; -const fieldOptions = { sort: "ASC", formatter: formatTime, fieldName: "Time" }; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); +const fieldOptions = { sort: "ASC", formatter: formatTimeWithSeconds, fieldName: "Time" }; const historical = { enabled: false }; export class Parkour { diff --git a/packages/schemas/src/player/gamemodes/tntgames/mode.ts b/packages/schemas/src/player/gamemodes/tntgames/mode.ts index abb824ba2..85782ce89 100644 --- a/packages/schemas/src/player/gamemodes/tntgames/mode.ts +++ b/packages/schemas/src/player/gamemodes/tntgames/mode.ts @@ -12,6 +12,8 @@ import { type GamePrefix, createPrefixProgression, cycleColors, defaultPrefix, g import { Progression } from "#progression"; import { ratio } from "@statsify/math"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + const tntgamesRainbow = (text: string) => cycleColors(text, ["c", "6", "e", "a", "b", "d", "5"]); // Prefixes for TNT Run, PVP Run and Bow Spleef @@ -95,7 +97,7 @@ export class PVPRun { @Field() public kdr: number; - @Field({ leaderboard: { formatter: formatTime }, historical: { enabled: false } }) + @Field({ leaderboard: { formatter: formatTimeWithSeconds }, historical: { enabled: false } }) public record: number; @Field() @@ -149,7 +151,7 @@ export class TNTRun { @Field() public wlr: number; - @Field({ leaderboard: { formatter: formatTime }, historical: { enabled: false } }) + @Field({ leaderboard: { formatter: formatTimeWithSeconds }, historical: { enabled: false } }) public record: number; @Field() @@ -413,4 +415,3 @@ export class Wizards { this.arcaneWizard = new WizardsClass(data, "arcane_wizard"); } } - diff --git a/packages/schemas/src/player/gamemodes/woolgames/capture-the-wool.ts b/packages/schemas/src/player/gamemodes/woolgames/capture-the-wool.ts index 477c45511..92cee381c 100644 --- a/packages/schemas/src/player/gamemodes/woolgames/capture-the-wool.ts +++ b/packages/schemas/src/player/gamemodes/woolgames/capture-the-wool.ts @@ -10,6 +10,8 @@ import { type APIData, formatTime } from "@statsify/util"; import { Field } from "#metadata"; import { ratio } from "@statsify/math"; +const formatTimeWithSeconds = (time: number) => formatTime(time, { entries: 3 }); + export class CaptureTheWool { @Field() public wins: number; @@ -44,7 +46,7 @@ export class CaptureTheWool { @Field({ leaderboard: { sort: "ASC", - formatter: formatTime, + formatter: formatTimeWithSeconds, additionalFields: ["this.wins"], }, historical: { enabled: false }, @@ -54,7 +56,7 @@ export class CaptureTheWool { @Field({ leaderboard: { sort: "ASC", - formatter: formatTime, + formatter: formatTimeWithSeconds, additionalFields: ["this.woolCaptured"], }, historical: { enabled: false }, @@ -62,7 +64,7 @@ export class CaptureTheWool { public fastestWoolCapture: number; @Field({ - leaderboard: { formatter: formatTime }, + leaderboard: { formatter: formatTimeWithSeconds }, historical: { enabled: false }, }) public longestGame: number;