Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 177 additions & 71 deletions apps/discord-bot/src/commands/ratios/ratios.command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ApiService,
Command,
CommandContext,
type LocalizeFunction,
Page,
paginate,
PlayerArgument,
Expand All @@ -57,6 +58,12 @@ import { render } from "@statsify/rendering";

const args = [PlayerArgument];

type RatioMode<T extends GamesWithBackgrounds> = {
mode: GameModeWithSubModes<T>;
formatted: string;
submode?: GameModeWithSubModes<T>["submodes"][number];
};

@Command({ description: (t) => t("commands.ratios") })
export class RatiosCommand {
public constructor(private readonly apiService: ApiService) {}
Expand Down Expand Up @@ -89,7 +96,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);

Expand Down Expand Up @@ -183,7 +190,7 @@ export class RatiosCommand {
private async run<T extends GamesWithBackgrounds>(
context: CommandContext,
modes: GameModes<T>,
filterModes?: (player: Player, modes: GameModeWithSubModes<T>[]) => GameModeWithSubModes<T>[]
filterModes?: (player: Player, modes: RatioMode<T>[]) => RatioMode<T>[]
) {
const user = context.getUser();
const player = await this.apiService.getPlayer(context.option("player"), user);
Expand All @@ -200,59 +207,109 @@ 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 pages: Page[] = displayedModes.map((mode, index) => ({
label: mode.formatted,
generator: async (t) => {
const background = await getBackground(...mapBackground(modes, mode.api));

const game = player.stats[key];
const stats = this.getModeStats(game, mode);

const ratios = ratiosPerMode[index][1];

const props: RatiosProfileProps = {
player,
skin,
background,
logo,
t,
user,
badge,
mode: { ...mode, submode: mode.submodes.length === 0 ? undefined : mode.submodes[0] },
gameName: MODES_TO_FORMATTED.get(modes)!,
ratios: ratios.map((r) => [
stats[r[0] as keyof typeof stats],
stats[r[1] as keyof typeof stats],
prettify(r[0]),
r[3],
]),
};

const canvas = render(<RatiosProfile {...props} />, getTheme(user));
const buffer = await canvas.toBuffer("png");
const displayedModeOrder = new Map(
displayedModes.map((mode, index) => [this.getRatioModeKey(mode), index])
);
const displayedRatiosPerMode = ratiosPerMode
.flatMap((ratio) => {
const index = displayedModeOrder.get(this.getRatioModeKey(ratio.ratioMode));
return index === undefined ? [] : [{ ...ratio, index }];
})
.sort((a, b) => a.index - b.index);

const getGenerator = (ratioMode: RatioMode<T>, ratios: Ratio[]) => async (t: LocalizeFunction) => {
const background = await getBackground(...mapBackground(modes, ratioMode.mode.api));

const game = player.stats[key];
const stats = this.getModeStats(game, ratioMode);

const props: RatiosProfileProps = {
player,
skin,
background,
logo,
t,
user,
badge,
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],
stats[r[1] as keyof typeof stats],
prettify(r[0]),
r[3],
]),
};

const canvas = render(<RatiosProfile {...props} />, getTheme(user));
const buffer = await canvas.toBuffer("png");

return {
files: [{ name: "ratios.png", data: buffer, type: "image/png" }],
attachments: [],
};
};

const pages: Page[] = this.groupRatioModes(displayedRatiosPerMode).map((group) => {
if (group.length === 1) {
const [{ ratioMode, ratios }] = group;

return {
files: [{ name: "ratios.png", data: buffer, type: "image/png" }],
attachments: [],
label: ratioMode.formatted,
generator: getGenerator(ratioMode, ratios),
};
},
}));
}

return {
label: group[0].ratioMode.mode.formatted,
subPages: group.map(({ ratioMode, ratios }) => ({
label: this.getSubPageLabel(ratioMode),
generator: getGenerator(ratioMode, ratios),
})),
};
});

return paginate(context, pages);
}

private getModeStats(game: PlayerStats[keyof PlayerStats], mode: GameModeWithSubModes<any>) {
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<any>) {
const { mode, submode } = ratioMode;

let stats: any;

if (submode) {
const modeStats = game[mode.api as keyof typeof game];
const submodeStats = modeStats?.[submode.api as keyof typeof modeStats];
stats = submode.api === "overall" ? submodeStats || modeStats : submodeStats;
} else {
const modeStats = game[mode.api as keyof typeof game];
stats = mode.api === "overall" ? modeStats || game : modeStats;
}

const stats = game[mode.api as keyof typeof game];
return mode.api === "overall" ? stats || game : stats;
// Some modes (e.g. Duels Classic, OP, SkyWars) only display a single
// image and don't expose their ratio fields directly, instead nesting
// them under an `overall` field combining their submodes
if (stats && this.getRatios(stats.constructor).length === 0 && stats.overall)
return stats.overall;

return stats;
}

private getRatios(modeClass?: Constructor<any>): Ratio[] {
if (!modeClass) return [];

return LEADERBOARD_RATIOS.filter(([numerator, denominator]) => {
const numeratorType = Reflect.getMetadata("design:type", modeClass.prototype, numerator);
const denominatorType = Reflect.getMetadata("design:type", modeClass.prototype, denominator);

return numeratorType === Number && denominatorType === Number;
});
}

private getRatiosPerMode<T extends GamesWithBackgrounds>(
Expand All @@ -261,47 +318,96 @@ export class RatiosCommand {
) {
const gameClass = Reflect.getMetadata("design:type", PlayerStats.prototype, key);

const ratioModes: [mode: GameModeWithSubModes<T>, ratios: Ratio[]][] = [];
const ratioModes: { ratioMode: RatioMode<T>; ratios: Ratio[] }[] = [];
const gameModes = modes.getModes();

for (const mode of gameModes) {
if (!mode.api) continue;

const modeClass = this.getModeClass(mode, gameClass);
if (!modeClass) continue;

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
);
for (const ratioMode of this.getRatioModes(mode)) {
const modeClass = this.getModeClass(ratioMode, gameClass);
if (!modeClass) continue;

return numeratorType === Number && denominatorType === Number;
});
const ratios = this.getRatios(modeClass);
if (ratios.length === 0) continue;

if (ratios.length === 0) continue;

ratioModes.push([mode, ratios]);
ratioModes.push({ ratioMode, ratios });
}
}

return ratioModes;
}

private getModeClass<T extends GamesWithBackgrounds>(mode: GameModeWithSubModes<T>, gameClass: Constructor<any>) {
private getModeClass<T extends GamesWithBackgrounds>(ratioMode: RatioMode<T>, gameClass: Constructor<any>) {
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;
const resolved = (() => {
if (!submode) return modeType;
if (!modeType) return undefined;

const submodeType = Reflect.getMetadata("design:type", modeType.prototype, submode.api);
return submode.api === "overall" ? submodeType || modeType : submodeType;
})();

if (!resolved) return resolved;

// Some modes (e.g. Duels Classic, OP, SkyWars) don't expose their ratio
// fields directly, instead nesting them under an `overall` field
// combining their submodes
if (this.getRatios(resolved).length === 0) {
const overallType = Reflect.getMetadata("design:type", resolved.prototype, "overall");
return overallType ?? resolved;
}

return resolved;
}

private getRatioModes<T extends GamesWithBackgrounds>(mode: GameModeWithSubModes<T>): RatioMode<T>[] {
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}`;
}

private getRatioModeKey<T extends GamesWithBackgrounds>(ratioMode: RatioMode<T>) {
return `${ratioMode.mode.api}:${ratioMode.submode?.api ?? ""}`;
}

private groupRatioModes<T extends GamesWithBackgrounds>(
ratiosPerMode: { ratioMode: RatioMode<T>; ratios: Ratio[] }[]
) {
const groups = new Map<string, { ratioMode: RatioMode<T>; ratios: Ratio[] }[]>();

for (const ratio of ratiosPerMode) {
const group = groups.get(ratio.ratioMode.mode.api) ?? [];
group.push(ratio);
groups.set(ratio.ratioMode.mode.api, group);
}

return [...groups.values()];
}

const submode = mode.submodes[0].api;
const submodeType = Reflect.getMetadata("design:type", modeType.prototype, submode);
return submode === "overall" ? submodeType || modeType : submodeType;
private getSubPageLabel<T extends GamesWithBackgrounds>(ratioMode: RatioMode<T>) {
return ratioMode.submode?.formatted ?? ratioMode.formatted;
}
}
Loading