diff --git a/apps/discord-bot/src/commands/ratios/ratios.command.tsx b/apps/discord-bot/src/commands/ratios/ratios.command.tsx index bdd522891..931cf1ae9 100644 --- a/apps/discord-bot/src/commands/ratios/ratios.command.tsx +++ b/apps/discord-bot/src/commands/ratios/ratios.command.tsx @@ -57,6 +57,12 @@ import { render } from "@statsify/rendering"; const args = [PlayerArgument]; +type RatioMode = { + mode: GameModeWithSubModes; + formatted: string; + submode?: GameModeWithSubModes["submodes"][number]; +}; + @Command({ description: (t) => t("commands.ratios") }) export class RatiosCommand { public constructor( @@ -92,7 +98,7 @@ export class RatiosCommand { const filteredKits = kits .sort( (a, b) => - (blitzsg[b.api] as BlitzSGKit).exp - (blitzsg[a.api] as BlitzSGKit).exp + (blitzsg[b.mode.api] as BlitzSGKit).exp - (blitzsg[a.mode.api] as BlitzSGKit).exp ) .slice(0, 24); @@ -186,7 +192,7 @@ export class RatiosCommand { private async run( context: CommandContext, modes: GameModes, - filterModes?: (player: Player, modes: GameModeWithSubModes[]) => GameModeWithSubModes[] + filterModes?: (player: Player, modes: RatioMode[]) => RatioMode[] ) { const user = context.getUser(); const player = await this.apiService.getPlayer(context.option("player"), user); @@ -203,18 +209,20 @@ export class RatiosCommand { const ratiosPerMode = this.getRatiosPerMode(key, modes); - const allModes = ratiosPerMode.map(([mode]) => mode); + const allModes = ratiosPerMode.map(({ ratioMode }) => ratioMode); const displayedModes = filterModes ? filterModes(player, allModes) : allModes; + const displayedModeKeys = new Set(displayedModes.map((mode) => this.getRatioModeKey(mode))); + const displayedRatiosPerMode = ratiosPerMode.filter(({ ratioMode }) => + displayedModeKeys.has(this.getRatioModeKey(ratioMode)) + ); - const pages: Page[] = displayedModes.map((mode, index) => ({ - label: mode.formatted, + const pages: Page[] = displayedRatiosPerMode.map(({ ratioMode, ratios }) => ({ + label: ratioMode.formatted, generator: async (t) => { - const background = await getBackground(...mapBackground(modes, mode.api)); + const background = await getBackground(...mapBackground(modes, ratioMode.mode.api)); const game = player.stats[key]; - const stats = this.getModeStats(game, mode); - - const ratios = ratiosPerMode[index][1]; + const stats = this.getModeStats(game, ratioMode); const props: RatiosProfileProps = { player, @@ -224,7 +232,11 @@ export class RatiosCommand { t, user, badge, - mode: { ...mode, submode: mode.submodes.length === 0 ? undefined : mode.submodes[0] }, + mode: { + ...ratioMode.mode, + formatted: ratioMode.formatted, + submode: ratioMode.submode, + }, gameName: MODES_TO_FORMATTED.get(modes)!, ratios: ratios.map((r) => [ stats[r[0] as keyof typeof stats], @@ -247,11 +259,13 @@ export class RatiosCommand { return this.paginateService.paginate(context, pages); } - private getModeStats(game: PlayerStats[keyof PlayerStats], mode: GameModeWithSubModes) { - if (mode.submodes.length !== 0) { - let stats = game[mode.api as keyof typeof game]; - stats = stats[mode.submodes[0].api as keyof typeof game]; - return mode.submodes[0].api === "overall" ? stats || game : stats; + private getModeStats(game: PlayerStats[keyof PlayerStats], ratioMode: RatioMode) { + const { mode, submode } = ratioMode; + + if (submode) { + const modeStats = game[mode.api as keyof typeof game]; + const submodeStats = modeStats?.[submode.api as keyof typeof modeStats]; + return submode.api === "overall" ? submodeStats || modeStats : submodeStats; } const stats = game[mode.api as keyof typeof game]; @@ -264,47 +278,78 @@ export class RatiosCommand { ) { const gameClass = Reflect.getMetadata("design:type", PlayerStats.prototype, key); - const ratioModes: [mode: GameModeWithSubModes, ratios: Ratio[]][] = []; + const ratioModes: { ratioMode: RatioMode; ratios: Ratio[] }[] = []; const gameModes = modes.getModes(); for (const mode of gameModes) { if (!mode.api) continue; - const modeClass = this.getModeClass(mode, gameClass); - if (!modeClass) continue; + for (const ratioMode of this.getRatioModes(mode)) { + const modeClass = this.getModeClass(ratioMode, gameClass); + if (!modeClass) continue; - const ratios = LEADERBOARD_RATIOS.filter(([numerator, denominator]) => { - const numeratorType = Reflect.getMetadata( - "design:type", - modeClass.prototype, - numerator - ); + const ratios = LEADERBOARD_RATIOS.filter(([numerator, denominator]) => { + const numeratorType = Reflect.getMetadata( + "design:type", + modeClass.prototype, + numerator + ); - const denominatorType = Reflect.getMetadata( - "design:type", - modeClass.prototype, - denominator - ); + const denominatorType = Reflect.getMetadata( + "design:type", + modeClass.prototype, + denominator + ); - return numeratorType === Number && denominatorType === Number; - }); + return numeratorType === Number && denominatorType === Number; + }); - if (!ratios.length) continue; + if (!ratios.length) continue; - ratioModes.push([mode, ratios]); + ratioModes.push({ ratioMode, ratios }); + } } return ratioModes; } - private getModeClass(mode: GameModeWithSubModes, gameClass: Constructor) { + private getModeClass(ratioMode: RatioMode, gameClass: Constructor) { + const { mode, submode } = ratioMode; const apiType = Reflect.getMetadata("design:type", gameClass.prototype, mode.api); const modeType = mode.api === "overall" ? apiType || gameClass : apiType; - if (mode.submodes.length === 0) return modeType; + if (!submode) return modeType; + + const submodeType = Reflect.getMetadata("design:type", modeType.prototype, submode.api); + return submode.api === "overall" ? submodeType || modeType : submodeType; + } + + private getRatioModes(mode: GameModeWithSubModes): RatioMode[] { + const baseMode = { + mode, + formatted: mode.formatted, + }; + + if (mode.submodes.length === 0) return [baseMode]; + + const submodes = mode.submodes + .filter((submode) => submode.api !== "stats" && submode.api !== "titles") + .map((submode) => ({ + mode, + submode, + formatted: this.formatSubmode(mode.formatted, submode), + })); + + return mode.api === "overall" ? [baseMode, ...submodes] : submodes; + } + + private formatSubmode(mode: string, submode: { api: string; formatted: string }) { + if (submode.api === "overall") return `${mode} Overall`; + if (submode.formatted.startsWith(mode)) return submode.formatted; + return `${mode} ${submode.formatted}`; + } - const submode = mode.submodes[0].api; - const submodeType = Reflect.getMetadata("design:type", modeType.prototype, submode); - return submode === "overall" ? submodeType || modeType : submodeType; + private getRatioModeKey(ratioMode: RatioMode) { + return `${ratioMode.mode.api}:${ratioMode.submode?.api ?? ""}`; } } diff --git a/packages/schemas/src/player/gamemodes/duels/index.ts b/packages/schemas/src/player/gamemodes/duels/index.ts index 7a2d60cc7..8db68805c 100644 --- a/packages/schemas/src/player/gamemodes/duels/index.ts +++ b/packages/schemas/src/player/gamemodes/duels/index.ts @@ -54,16 +54,51 @@ export const DUELS_MODES = new GameModes([ { api: "doubles" }, { api: "threes" }, { api: "fours" }, + { api: "twoVTwoVTwoVTwo", formatted: "2v2v2v2" }, + { api: "threeVThreeVThreeVThree", formatted: "3v3v3v3" }, + { api: "capture", formatted: "CTF" }, + ], + }, + { + api: "classic", + hypixel: "DUELS_CLASSIC_DUEL", + submodes: [ + { api: "overall" }, + { api: "solo" }, + { api: "doubles" }, ], }, - { api: "classic", hypixel: "DUELS_CLASSIC_DUEL" }, { api: "combo", hypixel: "DUELS_COMBO_DUEL" }, - { api: "megawalls", formatted: "MegaWalls" }, + { + api: "megawalls", + formatted: "MegaWalls", + submodes: [ + { api: "overall" }, + { api: "solo" }, + { api: "doubles" }, + ], + }, { api: "nodebuff", hypixel: "DUELS_POTION_DUEL", formatted: "NoDebuff" }, - { api: "op", formatted: "OP" }, + { + api: "op", + formatted: "OP", + submodes: [ + { api: "overall" }, + { api: "solo" }, + { api: "doubles" }, + ], + }, { api: "quake", hypixel: "DUELS_QUAKE_DUEL" }, { api: "parkour", hypixel: "DUELS_PARKOUR_EIGHT" }, - { api: "skywars", formatted: "SkyWars" }, + { + api: "skywars", + formatted: "SkyWars", + submodes: [ + { api: "overall" }, + { api: "solo" }, + { api: "doubles" }, + ], + }, { api: "spleef", submodes: [ diff --git a/packages/schemas/src/player/gamemodes/duels/mode.ts b/packages/schemas/src/player/gamemodes/duels/mode.ts index 57cd014a0..41fa84889 100644 --- a/packages/schemas/src/player/gamemodes/duels/mode.ts +++ b/packages/schemas/src/player/gamemodes/duels/mode.ts @@ -143,20 +143,32 @@ export class BridgeDuels { @Field() public fours: BridgeDuelsMode; + @Field() + public twoVTwoVTwoVTwo: BridgeDuelsMode; + + @Field() + public threeVThreeVThreeVThree: BridgeDuelsMode; + + @Field() + public capture: BridgeDuelsMode; + public constructor(data: APIData) { this.solo = new BridgeDuelsMode(data, "bridge_duel"); this.doubles = new BridgeDuelsMode(data, "bridge_doubles"); this.threes = new BridgeDuelsMode(data, "bridge_threes"); this.fours = new BridgeDuelsMode(data, "bridge_four"); + this.twoVTwoVTwoVTwo = new BridgeDuelsMode(data, "bridge_2v2v2v2"); + this.threeVThreeVThreeVThree = new BridgeDuelsMode(data, "bridge_3v3v3v3"); + this.capture = new BridgeDuelsMode(data, "capture_threes"); this.overall = deepAdd( this.solo, this.doubles, this.threes, this.fours, - new BridgeDuelsMode(data, "bridge_2v2v2v2"), - new BridgeDuelsMode(data, "bridge_3v3v3v3"), - new BridgeDuelsMode(data, "capture_threes") + this.twoVTwoVTwoVTwo, + this.threeVThreeVThreeVThree, + this.capture ); this.overall.winstreak = data.current_bridge_winstreak; @@ -591,16 +603,24 @@ export class ParkourDuels extends SingleDuelsGameMode { } export class MegaWallsDuels extends SinglePVPDuelsGameMode { + @Field() + public solo: PVPBaseDuelsGameMode; + + @Field() + public doubles: PVPBaseDuelsGameMode; + public constructor(data: APIData) { super(data, "Mega Walls", "mw_duel", "half"); // add back doubles stats - const doubles = new PVPBaseDuelsGameMode(data, "mw_doubles"); - this.wins = add(this.wins, doubles.wins); - this.losses = add(this.losses, doubles.losses); - this.kills = add(this.kills, doubles.kills); - this.deaths = add(this.deaths, doubles.deaths); - this.blocksPlaced = add(this.blocksPlaced, doubles.blocksPlaced); + this.solo = new PVPBaseDuelsGameMode(data, "mw_duel"); + this.doubles = new PVPBaseDuelsGameMode(data, "mw_doubles"); + + this.wins = add(this.solo.wins, this.doubles.wins); + this.losses = add(this.solo.losses, this.doubles.losses); + this.kills = add(this.solo.kills, this.doubles.kills); + this.deaths = add(this.solo.deaths, this.doubles.deaths); + this.blocksPlaced = add(this.solo.blocksPlaced, this.doubles.blocksPlaced); PVPBaseDuelsGameMode.applyRatios(this);