From 65c13e8a338241213ca5592b8b6758339d5e39c2 Mon Sep 17 00:00:00 2001 From: YoruNoken Date: Fri, 12 Apr 2024 18:58:52 +0300 Subject: [PATCH 1/4] update --- src/commands/osu/simulate.ts | 149 +++++++++++++++++++++++++++++++++++ src/types/commandArgs.ts | 17 ++++ src/utils/args.ts | 34 +++++--- 3 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/commands/osu/simulate.ts diff --git a/src/commands/osu/simulate.ts b/src/commands/osu/simulate.ts new file mode 100644 index 00000000..c0608d1e --- /dev/null +++ b/src/commands/osu/simulate.ts @@ -0,0 +1,149 @@ +import { getCommandArgs } from "@utils/args"; +import { getBeatmapIdFromContext } from "@utils/osu"; +import { mapBuilder } from "@builders/map"; +import { EmbedBuilderType } from "@type/embedBuilders"; +import { ApplicationCommandOptionType, EmbedType } from "lilybird"; +import type { Mod } from "osu-web.js"; +import type { ApplicationCommandData, Interaction } from "@lilybird/transformers"; +import type { SlashCommand } from "@lilybird/handlers"; + +export default { + post: "GLOBAL", + data: { + name: "simulate", + description: "Simulate a score on a beatmap..", + options: [ + { + type: ApplicationCommandOptionType.STRING, + name: "map", + description: "Specify a beatmap link (eg: https://osu.ppy.sh/b/72727)" + }, + { + type: ApplicationCommandOptionType.STRING, + name: "mods", + description: "Specify a mods combination.", + min_length: 2 + }, + { + type: ApplicationCommandOptionType.STRING, + name: "mode", + description: "Specify a gamemode." + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "combo", + description: "Specify a combo.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "acc", + description: "Specify an accuracy.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "clock_rate", + description: "Specify a custom clockrate that overwrites any other rate changes." + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "bpm", + description: "Specify a BPM instead of a clock rate.", + min_value: 0, + max_value: 999 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "n300", + description: "Specify the amount of 300s.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "n100", + description: "Specify the amount of 100s.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "n50s", + description: "Specify the amount of 50s.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "nmisses", + description: "Specify the amount of misses.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "ngeki", + description: "Specify the amount of gekis, aka n320.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.NUMBER, + name: "nkatu", + description: "Specify the amount of katus, aka n200.", + min_value: 0 + }, + { + type: ApplicationCommandOptionType.INTEGER, + name: "ar", + description: "Overwrite the map's approach rate.", + min_value: 0, + max_value: 11 + }, + { + type: ApplicationCommandOptionType.INTEGER, + name: "od", + description: "Overwrite the map's overall difficulty.", + min_value: 0, + max_value: 11.11 + }, + { + type: ApplicationCommandOptionType.INTEGER, + name: "cs", + description: "Overwrite the map's circle size.", + min_value: 0, + max_value: 10 + } + ] + }, + run +} satisfies SlashCommand; + +async function run(interaction: Interaction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply(); + + const args = getCommandArgs(interaction, true); + + if (typeof args === "undefined") return; + const { user, mods } = args; + + const beatmapId = user.beatmapId ?? await getBeatmapIdFromContext({ channelId: interaction.channelId, client: interaction.client }); + if (typeof beatmapId === "undefined" || beatmapId === null) { + await interaction.editReply({ + embeds: [ + { + type: EmbedType.Rich, + title: "Uh oh! :x:", + description: "It seems like the beatmap ID couldn't be found :(\n" + } + ] + }); + return; + } + + const embeds = await mapBuilder({ + type: EmbedBuilderType.MAP, + initiatorId: interaction.member.user.id, + beatmapId: Number(beatmapId), + mods: | null>mods.name?.match(/.{1,2}/g) ?? null + }); + await interaction.editReply({ embeds }); +} + diff --git a/src/types/commandArgs.ts b/src/types/commandArgs.ts index 8711cebd..3935bee0 100644 --- a/src/types/commandArgs.ts +++ b/src/types/commandArgs.ts @@ -25,11 +25,28 @@ interface FailUser extends BaseUser { failMessage: string; } +export interface DifficultyAttributes { + combo?: number; + acc?: number; + clock_rate?: number; + bpm?: number; + n300?: number; + n100?: number; + n50?: number; + nmisses?: number; + ngeki?: number; + nkatu?: number; + ar?: number; + od?: number; + cs?: number; +} + export type User = SuccessUser | FailUser; export interface CommandArgs { user: User; mods: Mods; + difficultySettings?: DifficultyAttributes; } export interface Mods { diff --git a/src/utils/args.ts b/src/utils/args.ts index c7ba7a9d..872c904d 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -3,7 +3,7 @@ import { slashCommandsIds } from "./cache"; import { Mode } from "@type/osu"; import { UserType } from "@type/commandArgs"; import { ModsEnum } from "osu-web.js"; -import type { CommandArgs, Mods, ParsedArgs, User } from "@type/commandArgs"; +import type { CommandArgs, DifficultyAttributes, Mods, ParsedArgs, User } from "@type/commandArgs"; import type { Mod } from "osu-web.js"; import type { ApplicationCommandData, Interaction, Message } from "@lilybird/transformers"; @@ -71,14 +71,26 @@ function linkCommand(): string | undefined { return slashCommandsIds.get("link"); } -export function getCommandArgs(interaction: Interaction): CommandArgs | undefined { +export function getCommandArgs(interaction: Interaction, getAttributes?: boolean): CommandArgs | undefined { if (!interaction.isApplicationCommandInteraction() || !interaction.inGuild()) return; + const { data } = interaction; - const userArg = interaction.data.getString("username"); + let difficultySettings: DifficultyAttributes | undefined; + if (getAttributes) { + difficultySettings = {}; + const attributesArray: Array = ["combo", "acc", "clock_rate", "bpm", "n300", "n100", "n50", "nmisses", "ngeki", "nkatu", "ar", "cs", "od"]; + + for (let i = 0; i < attributesArray.length; i++) { + const attribute = attributesArray[i]; + difficultySettings[attribute] = data.getNumber(attribute); + } + } + + const userArg = data.getString("username"); const userAuthor = getUser(interaction.member.user.id); - const discordUserId = interaction.data.getUser("discord"); + const discordUserId = data.getUser("discord"); const discordUser = getUser(discordUserId ?? ""); - const mode = interaction.data.getString("mode") ?? Mode.OSU; + const mode = data.getString("mode") ?? Mode.OSU; let mods: Mods = { exclude: null, @@ -87,18 +99,18 @@ export function getCommandArgs(interaction: Interaction) name: null }; - const modsValue = interaction.data.getString("mods"); + const modsValue = data.getString("mods"); const modSections = modsValue?.toUpperCase().match(/.{1,2}/g); if (modSections && !modSections.every((selectedMod) => selectedMod in ModsEnum || modsValue === "NM")) { mods = { - exclude: interaction.data.getBoolean("exclude") ?? null, - include: interaction.data.getBoolean("include") ?? null, - forceInclude: interaction.data.getBoolean("force_include") ?? null, + exclude: data.getBoolean("exclude") ?? null, + include: data.getBoolean("include") ?? null, + forceInclude: data.getBoolean("force_include") ?? null, name: modsValue ?? null }; } - const urlMatch = parseURL(interaction.data.getString("map") ?? ""); + const urlMatch = parseURL(data.getString("map") ?? ""); let beatmapId: string | null = null; if (urlMatch && "id" in urlMatch) beatmapId = urlMatch.id; @@ -120,7 +132,7 @@ export function getCommandArgs(interaction: Interaction) ? { type: UserType.SUCCESS, banchoId: userAuthor.banchoId, mode, beatmapId, authorDb: userAuthor } : { type: UserType.FAIL, beatmapId, authorDb: userAuthor, failMessage: "Please link your account to the bot using /link!" }; - return { user, mods }; + return { user, mods, difficultySettings }; } export function parseOsuArguments(message: Message, args: Array, mode: Mode): ParsedArgs { From 4962ac69a1edf7f99d24cee4504af5c13557cb36 Mon Sep 17 00:00:00 2001 From: YoruNoken Date: Sun, 14 Apr 2024 17:21:28 +0300 Subject: [PATCH 2/4] will continue later --- src/commands/osu/simulate.ts | 9 ++-- src/embed-builders/simulate.ts | 95 ++++++++++++++++++++++++++++++++++ src/types/commandArgs.ts | 8 +-- src/types/embedBuilders.ts | 14 ++++- src/utils/args.ts | 20 ++++--- 5 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/embed-builders/simulate.ts diff --git a/src/commands/osu/simulate.ts b/src/commands/osu/simulate.ts index c0608d1e..eb429a00 100644 --- a/src/commands/osu/simulate.ts +++ b/src/commands/osu/simulate.ts @@ -1,6 +1,6 @@ import { getCommandArgs } from "@utils/args"; import { getBeatmapIdFromContext } from "@utils/osu"; -import { mapBuilder } from "@builders/map"; +import { simulateBuilder } from "@builders/simulate"; import { EmbedBuilderType } from "@type/embedBuilders"; import { ApplicationCommandOptionType, EmbedType } from "lilybird"; import type { Mod } from "osu-web.js"; @@ -119,10 +119,10 @@ async function run(interaction: Interaction): Promise(interaction); if (typeof args === "undefined") return; - const { user, mods } = args; + const { user, mods, difficultySettings } = args; const beatmapId = user.beatmapId ?? await getBeatmapIdFromContext({ channelId: interaction.channelId, client: interaction.client }); if (typeof beatmapId === "undefined" || beatmapId === null) { @@ -138,9 +138,10 @@ async function run(interaction: Interaction): Promise | null>mods.name?.match(/.{1,2}/g) ?? null }); diff --git a/src/embed-builders/simulate.ts b/src/embed-builders/simulate.ts new file mode 100644 index 00000000..e8a257ff --- /dev/null +++ b/src/embed-builders/simulate.ts @@ -0,0 +1,95 @@ +import { client } from "@utils/initalize"; +import { downloadBeatmap, getPerformanceResults } from "@utils/osu"; +import { getMap } from "@utils/database"; +import { rulesets } from "@utils/emotes"; +import { EmbedType } from "lilybird"; +import type { SimulateBuilderOptions } from "@type/embedBuilders"; +import type { EmbedStructure } from "lilybird"; + +export async function simulateBuilder({ + beatmapId, + mods, + difficultyOptions +}: SimulateBuilderOptions): Promise> { + const beatmapRequest = await client.safeParse(client.beatmaps.getBeatmap(Number(beatmapId))); + if (!beatmapRequest.success) { + return [ + { + type: EmbedType.Rich, + title: "Uh oh! :x:", + description: "It seems like this beatmap couldn't be found :(" + } + ]; + } + const map = beatmapRequest.data; + + const { beatmapset: mapset, mode, version } = map; + + const mapData = getMap(beatmapId)?.data ?? (await downloadBeatmap(beatmapId)).contents; + + const performancesAsync = []; + const accuracyList = [98, 97, 95]; + for (let i = 0; i < accuracyList.length; i++) { + const accuracy = accuracyList[i]; + const performance = getPerformanceResults({ beatmapId, setId: map.mode_int, mapData, accuracy, mods: mods ?? 0 }); + performancesAsync.push(performance); + } + const performances = await Promise.all(performancesAsync); + + const [a98, a97, a95] = performances; + if (a98 === null || a97 === null || a95 === null) { + return [ + { + title: "ERROR", + description: "Oops, sorry about that, it seems there was an error. Maybe try again?\n\nPERFORMANCES IS NULL" + } + ]; + } + + const drainLengthInSeconds = map.total_length / a98.difficultyAttrs.clockRate; + const drainMinutes = Math.floor(drainLengthInSeconds / 60); + const drainSeconds = Math.ceil(drainLengthInSeconds % 60); + + const objects = map.count_circles + map.count_sliders + map.count_spinners; + + const infoField = [ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `**Stars:** **\`${a98.current.difficulty.stars.toFixed(2)}\`** **Mods:** \`+${mods ?? "NM"}\` **BPM:** \`${a98.mapValues.bpm.toFixed(0)}\``, + `**Length:** \`${drainMinutes}:${drainSeconds < 10 ? `0${drainSeconds}` : drainSeconds}\` **Max Combo:** \`${a98.current.difficulty.maxCombo}\` **Objects:** \`${objects.toLocaleString()}\``, + `**AR:** \`${a98.mapValues.ar.toFixed(1)}\` **OD:** \`${a98.mapValues.od.toFixed(1)}\` **CS:** \`${a98.mapValues.cs.toFixed(1)}\` **HP:** \`${a98.mapValues.hp.toFixed(1)}\``, + `\n:heart: **${mapset.favourite_count.toLocaleString()}** :play_pause: **${mapset.play_count.toLocaleString()}**` + ]; + + const ppField = [ + "```Acc | PP", + `100% ${a98.perfect.pp.toFixed(2)}`, + `98% ${a98.current.pp.toFixed(2)}`, + `97% ${a97.current.pp.toFixed(2)}`, + `95% ${a95.current.pp.toFixed(2)}\`\`\`` + ]; + + const linksField = [ + `<:chimu:1117792339549761576>[Chimu](https://chimu.moe/d/${map.beatmapset_id})`, + `<:beatconnect:1075915329512931469>[Beatconnect](https://beatconnect.io/b/${map.beatmapset_id})`, + `:notes:[Song Preview](https://b.ppy.sh/preview/${map.beatmapset_id}.mp3)`, + `🖼️[Full Background](https://assets.ppy.sh/beatmaps/${map.beatmapset_id}/covers/raw.jpg)` + ]; + + return [ + { + title: `${mapset.artist} - ${mapset.title}`, + url: `https://osu.ppy.sh/b/${beatmapId}`, + thumbnail: { url: `https://assets.ppy.sh/beatmaps/${mapset.id}/covers/list.jpg` }, + author: { name: `${mapset.status.charAt(0).toUpperCase()}${mapset.status.slice(1)} mapset by ${mapset.creator}`, icon_url: `https://a.ppy.sh/${mapset.user_id}` }, + fields: [ + { + name: `${rulesets[mode]} ${version}`, + value: infoField.join("\n"), + inline: false + }, + { name: "PP", value: ppField.join("\n"), inline: true }, + { name: "Links", value: linksField.join("\n"), inline: true } + ] + } + ]; +} diff --git a/src/types/commandArgs.ts b/src/types/commandArgs.ts index 3935bee0..70dc7b08 100644 --- a/src/types/commandArgs.ts +++ b/src/types/commandArgs.ts @@ -25,7 +25,7 @@ interface FailUser extends BaseUser { failMessage: string; } -export interface DifficultyAttributes { +export interface DifficultyOptions { combo?: number; acc?: number; clock_rate?: number; @@ -43,10 +43,10 @@ export interface DifficultyAttributes { export type User = SuccessUser | FailUser; -export interface CommandArgs { +export interface SlashCommandArgs { user: User; mods: Mods; - difficultySettings?: DifficultyAttributes; + difficultySettings: T extends true ? Required : DifficultyOptions; } export interface Mods { @@ -56,7 +56,7 @@ export interface Mods { name: Mod | null; } -export interface ParsedArgs { +export interface PrefixCommandArgs { tempUser: Array | null; user: User; flags: Record; diff --git a/src/types/embedBuilders.ts b/src/types/embedBuilders.ts index b3f0ab14..e65c5469 100644 --- a/src/types/embedBuilders.ts +++ b/src/types/embedBuilders.ts @@ -1,3 +1,4 @@ +import type { DifficultyOptions } from "./commandArgs"; import type { DatabaseUser } from "./database"; import type { UserScore, UserBestScore, Beatmap, LeaderboardScores, Mode, Score } from "./osu"; import type { UserExtended, Mod } from "osu-web.js"; @@ -10,7 +11,8 @@ export const enum EmbedBuilderType { PROFILE = "profileBuilder", AVATAR = "avatarBuilder", BACKGROUND = "backgroundBuilder", - BANNER = "bannerBuilder" + BANNER = "bannerBuilder", + SIMULATE = "simulateBuilder" } interface ModStructure { @@ -42,6 +44,13 @@ export interface LeaderboardBuilderOptions extends BuilderOptions { page: number | undefined; } +export interface SimulateBuilderOptions extends BuilderOptions { + type: EmbedBuilderType.MAP; + beatmapId: number; + mods: Array | null; + difficultyOptions: DifficultyOptions; +} + export interface MapBuilderOptions extends BuilderOptions { type: EmbedBuilderType.MAP; beatmapId: number; @@ -90,4 +99,5 @@ export type EmbedBuilderOptions = CompareBuilderOptions | PlaysBuilderOptions | ProfileBuilderOptions | AvatarBuilderOptions - | BackgroundBuilderOptions; + | BackgroundBuilderOptions + | SimulateBuilderOptions; diff --git a/src/utils/args.ts b/src/utils/args.ts index 872c904d..51fc331f 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -3,7 +3,7 @@ import { slashCommandsIds } from "./cache"; import { Mode } from "@type/osu"; import { UserType } from "@type/commandArgs"; import { ModsEnum } from "osu-web.js"; -import type { CommandArgs, DifficultyAttributes, Mods, ParsedArgs, User } from "@type/commandArgs"; +import type { SlashCommandArgs, DifficultyOptions, Mods, PrefixCommandArgs, User } from "@type/commandArgs"; import type { Mod } from "osu-web.js"; import type { ApplicationCommandData, Interaction, Message } from "@lilybird/transformers"; @@ -71,14 +71,14 @@ function linkCommand(): string | undefined { return slashCommandsIds.get("link"); } -export function getCommandArgs(interaction: Interaction, getAttributes?: boolean): CommandArgs | undefined { +export function getCommandArgs(interaction: Interaction, getAttributes?: T): SlashCommandArgs | undefined { if (!interaction.isApplicationCommandInteraction() || !interaction.inGuild()) return; const { data } = interaction; - let difficultySettings: DifficultyAttributes | undefined; + let difficultySettings: DifficultyOptions | undefined; if (getAttributes) { - difficultySettings = {}; - const attributesArray: Array = ["combo", "acc", "clock_rate", "bpm", "n300", "n100", "n50", "nmisses", "ngeki", "nkatu", "ar", "cs", "od"]; + difficultySettings = {} as DifficultyOptions; + const attributesArray: Array = ["combo", "acc", "clock_rate", "bpm", "n300", "n100", "n50", "nmisses", "ngeki", "nkatu", "ar", "cs", "od"]; for (let i = 0; i < attributesArray.length; i++) { const attribute = attributesArray[i]; @@ -112,10 +112,8 @@ export function getCommandArgs(interaction: Interaction, const urlMatch = parseURL(data.getString("map") ?? ""); let beatmapId: string | null = null; - if (urlMatch && "id" in urlMatch) - beatmapId = urlMatch.id; - else if (urlMatch && "setId" in urlMatch) - beatmapId = urlMatch.difficultyId; + if (urlMatch && "id" in urlMatch) beatmapId = urlMatch.id; + else if (urlMatch && "setId" in urlMatch) beatmapId = urlMatch.difficultyId; const user: User = discordUserId ? discordUser?.banchoId @@ -135,8 +133,8 @@ export function getCommandArgs(interaction: Interaction, return { user, mods, difficultySettings }; } -export function parseOsuArguments(message: Message, args: Array, mode: Mode): ParsedArgs { - const result: ParsedArgs = { +export function parseOsuArguments(message: Message, args: Array, mode: Mode): PrefixCommandArgs { + const result: PrefixCommandArgs = { tempUser: null, user: { beatmapId: null, From 760d81254adaf542db5f8a3c4f3580f9f62a4d29 Mon Sep 17 00:00:00 2001 From: Yoru Date: Mon, 15 Apr 2024 12:37:44 +0300 Subject: [PATCH 3/4] setup simulate --- src/commands-message/osu/simulate.ts | 61 ++++++++++++++++++++++++++++ src/commands/osu/simulate.ts | 9 ++-- src/types/commandArgs.ts | 4 +- src/types/embedBuilders.ts | 2 +- src/utils/args.ts | 9 ++-- 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 src/commands-message/osu/simulate.ts diff --git a/src/commands-message/osu/simulate.ts b/src/commands-message/osu/simulate.ts new file mode 100644 index 00000000..3ba8ee02 --- /dev/null +++ b/src/commands-message/osu/simulate.ts @@ -0,0 +1,61 @@ +import { parseOsuArguments } from "@utils/args"; +import { getBeatmapIdFromContext } from "@utils/osu"; +import { Mode } from "@type/osu"; +import { EmbedBuilderType } from "@type/embedBuilders"; +import { simulateBuilder } from "@builders/simulate"; +import { EmbedType } from "lilybird"; +import type { DifficultyOptions } from "@type/commandArgs"; +import type { Mod } from "osu-web.js"; +import type { GuildTextChannel, Message } from "@lilybird/transformers"; +import type { MessageCommand } from "@type/commands"; + +export default { + name: "simulate", + aliases: ["simulate", "sim", "s"], + description: "Display statistics of a beatmap.", + cooldown: 1000, + run +} satisfies MessageCommand; + +async function run({ message, args, channel }: { message: Message, args: Array, channel: GuildTextChannel }): Promise { + const { user, mods, flags } = parseOsuArguments(message, args, Mode.OSU); + + const difficultyOptions: DifficultyOptions = { + acc: Number(flags.acc ?? flags.accuracy) || undefined, + bpm: Number(flags.bpm) || undefined, + clock_rate: Number(flags.clockrate ?? flags.clockRate ?? flags.clock_rate ?? flags.cr) || undefined, + combo: Number(flags.combo) || undefined, + ar: Number(flags.ar) || undefined, + cs: Number(flags.cs) || undefined, + od: Number(flags.od) || undefined, + n300: Number(flags.n300 ?? flags["300"]) || undefined, + n100: Number(flags.n100 ?? flags["100"]) || undefined, + n50: Number(flags.n50 ?? flags["50"]) || undefined, + ngeki: Number(flags.ngeki ?? flags.geki) || undefined, + nkatu: Number(flags.natu ?? flags.katu) || undefined, + nmisses: Number(flags.nmisses ?? flags.misses ?? flags.miss ?? flags.nmiss) || undefined + }; + + const beatmapId = user.beatmapId ?? await getBeatmapIdFromContext({ message, client: message.client }); + if (typeof beatmapId === "undefined" || beatmapId === null) { + await channel.send({ + embeds: [ + { + type: EmbedType.Rich, + title: "Uh oh! :x:", + description: "It seems like the beatmap ID couldn't be found :(\n" + } + ] + }); + return; + } + + const embeds = await simulateBuilder({ + type: EmbedBuilderType.SIMULATE, + initiatorId: message.author.id, + beatmapId: Number(beatmapId), + difficultyOptions, + mods: | null>mods.name?.match(/.{1,2}/g) ?? null + }); + await channel.send({ embeds }); +} diff --git a/src/commands/osu/simulate.ts b/src/commands/osu/simulate.ts index eb429a00..ae953e06 100644 --- a/src/commands/osu/simulate.ts +++ b/src/commands/osu/simulate.ts @@ -3,12 +3,11 @@ import { getBeatmapIdFromContext } from "@utils/osu"; import { simulateBuilder } from "@builders/simulate"; import { EmbedBuilderType } from "@type/embedBuilders"; import { ApplicationCommandOptionType, EmbedType } from "lilybird"; +import type { SlashCommand } from "@type/commands"; import type { Mod } from "osu-web.js"; import type { ApplicationCommandData, Interaction } from "@lilybird/transformers"; -import type { SlashCommand } from "@lilybird/handlers"; export default { - post: "GLOBAL", data: { name: "simulate", description: "Simulate a score on a beatmap..", @@ -119,7 +118,7 @@ async function run(interaction: Interaction): Promise(interaction); + const args = getCommandArgs(interaction, true); if (typeof args === "undefined") return; const { user, mods, difficultySettings } = args; @@ -139,9 +138,9 @@ async function run(interaction: Interaction): Promise | null>mods.name?.match(/.{1,2}/g) ?? null }); diff --git a/src/types/commandArgs.ts b/src/types/commandArgs.ts index 70dc7b08..4f7aa2e9 100644 --- a/src/types/commandArgs.ts +++ b/src/types/commandArgs.ts @@ -43,10 +43,10 @@ export interface DifficultyOptions { export type User = SuccessUser | FailUser; -export interface SlashCommandArgs { +export interface SlashCommandArgs { user: User; mods: Mods; - difficultySettings: T extends true ? Required : DifficultyOptions; + difficultySettings?: DifficultyOptions; } export interface Mods { diff --git a/src/types/embedBuilders.ts b/src/types/embedBuilders.ts index e65c5469..68a14d44 100644 --- a/src/types/embedBuilders.ts +++ b/src/types/embedBuilders.ts @@ -45,7 +45,7 @@ export interface LeaderboardBuilderOptions extends BuilderOptions { } export interface SimulateBuilderOptions extends BuilderOptions { - type: EmbedBuilderType.MAP; + type: EmbedBuilderType.SIMULATE; beatmapId: number; mods: Array | null; difficultyOptions: DifficultyOptions; diff --git a/src/utils/args.ts b/src/utils/args.ts index 51fc331f..64601599 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -71,14 +71,15 @@ function linkCommand(): string | undefined { return slashCommandsIds.get("link"); } -export function getCommandArgs(interaction: Interaction, getAttributes?: T): SlashCommandArgs | undefined { +export function getCommandArgs(interaction: Interaction, getAttributes?: boolean): SlashCommandArgs | undefined { if (!interaction.isApplicationCommandInteraction() || !interaction.inGuild()) return; const { data } = interaction; - let difficultySettings: DifficultyOptions | undefined; - if (getAttributes) { - difficultySettings = {} as DifficultyOptions; + // This is so fucking annoying holy shit I can't get it right + let difficultySettings = getAttributes ? {} as DifficultyOptions : undefined; + if (getAttributes === true) { const attributesArray: Array = ["combo", "acc", "clock_rate", "bpm", "n300", "n100", "n50", "nmisses", "ngeki", "nkatu", "ar", "cs", "od"]; + difficultySettings = {} as DifficultyOptions; for (let i = 0; i < attributesArray.length; i++) { const attribute = attributesArray[i]; From 9ccb14e631b25d51200d0581302331a6a0dd67c1 Mon Sep 17 00:00:00 2001 From: Yoru Date: Tue, 16 Apr 2024 10:37:56 +0300 Subject: [PATCH 4/4] finish command --- src/commands-message/osu/simulate.ts | 4 +- src/commands/osu/simulate.ts | 2 +- src/embed-builders/simulate.ts | 110 ++++++++++++++++++--------- src/types/embedBuilders.ts | 2 +- src/utils/osu.ts | 36 ++++++--- 5 files changed, 103 insertions(+), 51 deletions(-) diff --git a/src/commands-message/osu/simulate.ts b/src/commands-message/osu/simulate.ts index 3ba8ee02..622be2b5 100644 --- a/src/commands-message/osu/simulate.ts +++ b/src/commands-message/osu/simulate.ts @@ -20,7 +20,7 @@ export default { async function run({ message, args, channel }: { message: Message, args: Array, channel: GuildTextChannel }): Promise { const { user, mods, flags } = parseOsuArguments(message, args, Mode.OSU); - const difficultyOptions: DifficultyOptions = { + const options: DifficultyOptions = { acc: Number(flags.acc ?? flags.accuracy) || undefined, bpm: Number(flags.bpm) || undefined, clock_rate: Number(flags.clockrate ?? flags.clockRate ?? flags.clock_rate ?? flags.cr) || undefined, @@ -54,7 +54,7 @@ async function run({ message, args, channel }: { message: Message, args: Array | null>mods.name?.match(/.{1,2}/g) ?? null }); await channel.send({ embeds }); diff --git a/src/commands/osu/simulate.ts b/src/commands/osu/simulate.ts index ae953e06..5e898be7 100644 --- a/src/commands/osu/simulate.ts +++ b/src/commands/osu/simulate.ts @@ -140,7 +140,7 @@ async function run(interaction: Interaction): Promise | null>mods.name?.match(/.{1,2}/g) ?? null }); diff --git a/src/embed-builders/simulate.ts b/src/embed-builders/simulate.ts index e8a257ff..203ccd94 100644 --- a/src/embed-builders/simulate.ts +++ b/src/embed-builders/simulate.ts @@ -1,17 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { client } from "@utils/initalize"; -import { downloadBeatmap, getPerformanceResults } from "@utils/osu"; +import { accuracyCalculator, downloadBeatmap, getPerformanceResults } from "@utils/osu"; import { getMap } from "@utils/database"; import { rulesets } from "@utils/emotes"; +import { SPACE } from "@utils/constants"; import { EmbedType } from "lilybird"; +import type { Mode } from "@type/osu"; import type { SimulateBuilderOptions } from "@type/embedBuilders"; import type { EmbedStructure } from "lilybird"; export async function simulateBuilder({ beatmapId, mods, - difficultyOptions + options }: SimulateBuilderOptions): Promise> { - const beatmapRequest = await client.safeParse(client.beatmaps.getBeatmap(Number(beatmapId))); + const beatmapRequest = await client.safeParse(client.beatmaps.getBeatmap(beatmapId)); if (!beatmapRequest.success) { return [ { @@ -22,22 +26,25 @@ export async function simulateBuilder({ ]; } const map = beatmapRequest.data; + const { acc, ar, bpm, clock_rate: clockRate, combo, cs, n100, n300, n50, ngeki, nkatu, nmisses, od } = options; const { beatmapset: mapset, mode, version } = map; const mapData = getMap(beatmapId)?.data ?? (await downloadBeatmap(beatmapId)).contents; - const performancesAsync = []; - const accuracyList = [98, 97, 95]; - for (let i = 0; i < accuracyList.length; i++) { - const accuracy = accuracyList[i]; - const performance = getPerformanceResults({ beatmapId, setId: map.mode_int, mapData, accuracy, mods: mods ?? 0 }); - performancesAsync.push(performance); - } - const performances = await Promise.all(performancesAsync); + const performance = await getPerformanceResults({ + beatmapId, + setId: map.mode_int, + mapData, + mapSettings: { ar, cs, od }, + maxCombo: combo, + hitValues: { count_100: n100, count_300: n300, count_50: n50, count_geki: ngeki, count_katu: nkatu, count_miss: nmisses }, + clockRate: clockRate ?? (bpm && map.bpm ? bpm / map.bpm : undefined), + accuracy: acc, + mods: mods ?? 0 + }); - const [a98, a97, a95] = performances; - if (a98 === null || a97 === null || a95 === null) { + if (performance === null) { return [ { title: "ERROR", @@ -45,34 +52,62 @@ export async function simulateBuilder({ } ]; } + const { current, mapValues, difficultyAttrs, perfect, fc } = performance; + + console.log(current.state); + + const order = ["count_geki", "count_300", "count_katu", "count_100", "count_50", "count_miss"]; + + // rosu-pp exposes `state` as a Map and not an object, so you have to get them like this. + // will be fixed in the future and I will remove the eslint error things. + const hitValues = { + count_300: current.state?.get("n300"), + count_100: current.state?.get("n100"), + count_50: current.state?.get("n50"), + count_miss: current.state?.get("misses"), + count_geki: current.state?.get("nGeki"), + count_katu: current.state?.get("nKatu") + }; + + let hitValuesString = ""; + for (let i = 0; i < order.length; i++) { + const count = order[i]; + const countKey = count as keyof typeof hitValues; + const countValue = hitValues[countKey]; + if (typeof countValue !== "undefined") { + if (hitValuesString.length > 0) + hitValuesString += "/"; + + hitValuesString += countValue; + } + } + + // same thing here + const comboValue = current.state?.get("maxCombo"); + const comboValues = `**${comboValue}**/${map.max_combo}x`; + const comboDifference = (comboValue ?? 0) / map.max_combo; + console.log(comboDifference); - const drainLengthInSeconds = map.total_length / a98.difficultyAttrs.clockRate; + const accuracy = accuracyCalculator(map.mode as Mode, hitValues); + + const drainLengthInSeconds = map.total_length / difficultyAttrs.clockRate; const drainMinutes = Math.floor(drainLengthInSeconds / 60); const drainSeconds = Math.ceil(drainLengthInSeconds % 60); const objects = map.count_circles + map.count_sliders + map.count_spinners; - const infoField = [ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `**Stars:** **\`${a98.current.difficulty.stars.toFixed(2)}\`** **Mods:** \`+${mods ?? "NM"}\` **BPM:** \`${a98.mapValues.bpm.toFixed(0)}\``, - `**Length:** \`${drainMinutes}:${drainSeconds < 10 ? `0${drainSeconds}` : drainSeconds}\` **Max Combo:** \`${a98.current.difficulty.maxCombo}\` **Objects:** \`${objects.toLocaleString()}\``, - `**AR:** \`${a98.mapValues.ar.toFixed(1)}\` **OD:** \`${a98.mapValues.od.toFixed(1)}\` **CS:** \`${a98.mapValues.cs.toFixed(1)}\` **HP:** \`${a98.mapValues.hp.toFixed(1)}\``, - `\n:heart: **${mapset.favourite_count.toLocaleString()}** :play_pause: **${mapset.play_count.toLocaleString()}**` + const newBpm = difficultyAttrs.clockRate * mapValues.bpm; + const statsField = [ + `**Stars:** **\`${current.difficulty.stars.toFixed(2)}\`** **Mods:** \`+${mods ? mods.join("") : "NM"}\` **BPM:** \`${newBpm.toFixed(0)}\``, + `**Length:** \`${drainMinutes}:${drainSeconds < 10 ? `0${drainSeconds}` : drainSeconds}\` **Max Combo:** \`${current.difficulty.maxCombo}\` **Objects:** \`${objects.toLocaleString()}\``, + `**AR:** \`${difficultyAttrs.ar.toFixed(1)}\` **OD:** \`${difficultyAttrs.od.toFixed(1)}\` **CS:** \`${difficultyAttrs.cs.toFixed(1)}\` **HP:** \`${difficultyAttrs.hp.toFixed(1)}\`` ]; - const ppField = [ - "```Acc | PP", - `100% ${a98.perfect.pp.toFixed(2)}`, - `98% ${a98.current.pp.toFixed(2)}`, - `97% ${a97.current.pp.toFixed(2)}`, - `95% ${a95.current.pp.toFixed(2)}\`\`\`` - ]; - - const linksField = [ - `<:chimu:1117792339549761576>[Chimu](https://chimu.moe/d/${map.beatmapset_id})`, - `<:beatconnect:1075915329512931469>[Beatconnect](https://beatconnect.io/b/${map.beatmapset_id})`, - `:notes:[Song Preview](https://b.ppy.sh/preview/${map.beatmapset_id}.mp3)`, - `🖼️[Full Background](https://assets.ppy.sh/beatmaps/${map.beatmapset_id}/covers/raw.jpg)` + const scoreField = [ + `**${current.pp.toFixed(2)}**/${perfect.pp.toFixed(2)}pp ${typeof current.effectiveMissCount !== "undefined" && current.effectiveMissCount > 1 || comboDifference < 0.99 + ? `~~[**${fc.pp.toFixed(2)}**]~~` + : ""} ${SPACE} ${accuracy.toFixed(2)}% `, + `[${comboValues}] ${SPACE} {${hitValuesString}}` ]; return [ @@ -84,11 +119,14 @@ export async function simulateBuilder({ fields: [ { name: `${rulesets[mode]} ${version}`, - value: infoField.join("\n"), + value: scoreField.join("\n"), inline: false }, - { name: "PP", value: ppField.join("\n"), inline: true }, - { name: "Links", value: linksField.join("\n"), inline: true } + { + name: "Stats", + value: statsField.join("\n"), + inline: false + } ] } ]; diff --git a/src/types/embedBuilders.ts b/src/types/embedBuilders.ts index 68a14d44..bf379ff8 100644 --- a/src/types/embedBuilders.ts +++ b/src/types/embedBuilders.ts @@ -48,7 +48,7 @@ export interface SimulateBuilderOptions extends BuilderOptions { type: EmbedBuilderType.SIMULATE; beatmapId: number; mods: Array | null; - difficultyOptions: DifficultyOptions; + options: DifficultyOptions; } export interface MapBuilderOptions extends BuilderOptions { diff --git a/src/utils/osu.ts b/src/utils/osu.ts index 9b967d51..5ada48c1 100644 --- a/src/utils/osu.ts +++ b/src/utils/osu.ts @@ -152,7 +152,7 @@ export function isNewMods(mods: Array | Array): mods is Array typeof mod === "object" && "acronym" in mod); } -export async function getPerformanceResults({ play, setId, beatmapId, maxCombo, accuracy, clockRate, hitValues, mods, mapData }: +export async function getPerformanceResults({ play, setId, beatmapId, maxCombo, accuracy, clockRate, mapSettings, hitValues, mods, mapData }: { play?: UserBestScore | UserScore | Score | LeaderboardScore, setId?: number, @@ -160,7 +160,8 @@ export async function getPerformanceResults({ play, setId, beatmapId, maxCombo, maxCombo?: number, accuracy?: number, clockRate?: number, - hitValues?: { count_100: number | null, count_300: number | null, count_50: number | null, count_geki: number | null, count_katu: number | null, count_miss: number | null }, + mapSettings?: { ar?: number, od?: number, cs?: number }, + hitValues?: { count_100?: number | null, count_300?: number | null, count_50?: number | null, count_geki?: number | null, count_katu?: number | null, count_miss?: number | null }, mods: Array | Array | number, mapData?: string }): Promise { @@ -197,9 +198,22 @@ export async function getPerformanceResults({ play, setId, beatmapId, maxCombo, const beatmap = new Beatmap(mapData); beatmap.convert(rulesetId); - const difficultyAttrs = new BeatmapAttributesBuilder({ map: beatmap }).build(); - - const perfect = new Performance({ mods: modsInt, clockRate }).calculate(beatmap); + const difficultyAttrs = new BeatmapAttributesBuilder({ + map: beatmap, + ar: mapSettings?.ar, + cs: mapSettings?.cs, + od: mapSettings?.od, + mods: modsInt, + clockRate + }).build(); + + const perfect = new Performance({ + ar: mapSettings?.ar, + cs: mapSettings?.cs, + od: mapSettings?.od, + mods: modsInt, + clockRate + }).calculate(beatmap); let { count_100: n100, @@ -266,12 +280,12 @@ export async function downloadBeatmap(id: string | number, timeoutMs = 6000): Pr } export function accuracyCalculator(mode: Mode, hits: { - count_300: number | null, - count_100: number | null, - count_50: number | null, - count_miss: number | null, - count_geki: number | null, - count_katu: number | null + count_300?: number | null, + count_100?: number | null, + count_50?: number | null, + count_miss?: number | null, + count_geki?: number | null, + count_katu?: number | null }): number { let { count_100: count100,