diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs index 81f86da..fd41a0f 100644 --- a/src/arcaea/chart.rs +++ b/src/arcaea/chart.rs @@ -1,6 +1,5 @@ -use std::path::Path; // {{{ Imports -use std::{fmt::Display, num::NonZeroU16, path::PathBuf}; +use std::{fmt::Display, num::NonZeroU16}; use anyhow::anyhow; use image::{ImageBuffer, Rgb}; @@ -26,6 +25,8 @@ impl Difficulty { [Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD]; pub const DIFFICULTY_SHORTHANDS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"]; + pub const DIFFICULTY_SHORTHANDS_IN_BRACKETS: [&'static str; 5] = + ["[PST]", "[PRS]", "[FTR]", "[ETR]", "[BYD]"]; pub const DIFFICULTY_STRINGS: [&'static str; 5] = ["PAST", "PRESENT", "FUTURE", "ETERNAL", "BEYOND"]; @@ -53,11 +54,7 @@ impl FromSql for Difficulty { impl Display for Difficulty { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - Self::DIFFICULTY_SHORTHANDS[self.to_index()].to_lowercase() - ) + write!(f, "{}", Self::DIFFICULTY_SHORTHANDS[self.to_index()]) } } @@ -192,6 +189,24 @@ pub struct Song { pub pack: Option, pub side: Side, } + +impl Song { + /// Returns true if multiple songs are known to exist with the given title. + #[inline] + pub fn ambigous_name(&self) -> bool { + self.title == "Genesis" || self.title == "Quon" + } +} + +impl Display for Song { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.ambigous_name() { + write!(f, "{} ({})", self.title, self.artist) + } else { + write!(f, "{}", self.title) + } + } +} // }}} // {{{ Chart #[derive(Debug, Clone, Copy)] @@ -215,6 +230,10 @@ pub struct Chart { #[serde(skip)] pub cached_jacket: Option, + + /// If `None`, the default jacket is used. + /// Otherwise, a difficulty-specific jacket exists. + pub jacket_source: Option, } // }}} // {{{ Cached song @@ -374,6 +393,7 @@ impl SongCache { note_count: row.get("note_count")?, note_design: row.get("note_design")?, cached_jacket: None, + jacket_source: None, }) })?; diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs index 1597070..cf06e0b 100644 --- a/src/arcaea/jacket.rs +++ b/src/arcaea/jacket.rs @@ -106,6 +106,7 @@ impl JacketCache { Vec::new() } else { + let suffix = format!("_{BITMAP_IMAGE_SIZE}.jpg"); let songs_dir = get_asset_dir().join("songs/by_id"); let entries = fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?; @@ -127,7 +128,12 @@ impl JacketCache { for entry in entries { let file = entry?; let raw_name = file.file_name(); - let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap(); + let name = raw_name.to_str().unwrap(); + if !name.ends_with(&suffix) { + continue; + } + + let name = name.strip_suffix(&suffix).unwrap(); let difficulty = Difficulty::DIFFICULTY_SHORTHANDS .iter() @@ -146,6 +152,7 @@ impl JacketCache { let chart = song_cache .lookup_by_difficulty_mut(song_id, difficulty) .unwrap(); + chart.jacket_source = Some(difficulty); chart.cached_jacket = Some(Jacket { raw: contents, bitmap, @@ -153,11 +160,12 @@ impl JacketCache { } else { for chart_id in song_cache.lookup_song(song_id)?.charts() { let chart = song_cache.lookup_chart_mut(chart_id)?; - if chart.cached_jacket.is_none() { + if chart.jacket_source.is_none() { chart.cached_jacket = Some(Jacket { raw: contents, bitmap, }); + chart.jacket_source = None; } } } diff --git a/src/arcaea/rating.rs b/src/arcaea/rating.rs index 149d197..d0fb45a 100644 --- a/src/arcaea/rating.rs +++ b/src/arcaea/rating.rs @@ -1,4 +1,4 @@ -use num::Rational32; +use num::{Rational32, ToPrimitive}; pub type Rating = Rational32; @@ -13,7 +13,10 @@ pub fn rating_as_fixed(rating: Rating) -> i32 { /// Saves a rating rational as a float with precision 2. #[inline] pub fn rating_as_float(rating: Rating) -> f32 { - rating_as_fixed(rating) as f32 / 100.0 + let hundred = Rational32::from_integer(100); + let rounded = (rating * hundred).round(); + + (rounded / hundred).to_f32().unwrap() } /// The pseudo-inverse of `rating_as_fixed`. diff --git a/src/bin/cli/commands/prepare_jackets.rs b/src/bin/cli/commands/prepare_jackets.rs index 771aed3..17287c3 100644 --- a/src/bin/cli/commands/prepare_jackets.rs +++ b/src/bin/cli/commands/prepare_jackets.rs @@ -36,7 +36,7 @@ pub fn run() -> Result<(), Error> { let entries = fs::read_dir(&raw_songs_dir) .with_context(|| "Couldn't read songs directory")? .collect::, _>>() - .with_context(|| format!("Could not read member of `songs/raw`"))?; + .with_context(|| "Could not read member of `songs/raw`")?; for (i, dir) in entries.iter().enumerate() { let raw_dir_name = dir.file_name(); @@ -48,10 +48,7 @@ pub fn run() -> Result<(), Error> { } print!("{}/{}: {dir_name}", i, entries.len()); - - if i % 5 == 0 { - stdout().flush()?; - } + stdout().flush()?; // }}} let entries = fs::read_dir(dir.path()) @@ -126,13 +123,31 @@ pub fn run() -> Result<(), Error> { jacket_vectors.push((song.id, ImageVec::from_image(&image))); - let image = image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian); - let image_out_path = - out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg")); - image - // .blur(27.5) - .save(&image_out_path) - .with_context(|| format!("Could not save image to {image_out_path:?}"))?; + let small_image = + image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian); + + { + let image_small_path = + out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg")); + small_image + .save(&image_small_path) + .with_context(|| format!("Could not save image to {image_small_path:?}"))?; + } + + { + let image_full_path = out_dir.join(format!("{difficulty_string}_full.jpg")); + image + .save(&image_full_path) + .with_context(|| format!("Could not save image to {image_full_path:?}"))?; + } + + { + let blurred_out_path = out_dir.join(format!("{difficulty_string}_blurred.jpg")); + small_image + .blur(27.5) + .save(&blurred_out_path) + .with_context(|| format!("Could not save image to {blurred_out_path:?}"))?; + } } } @@ -152,9 +167,9 @@ pub fn run() -> Result<(), Error> { { println!("Encoded {} images", jacket_vectors.len()); let bytes = postcard::to_allocvec(&jacket_vectors) - .with_context(|| format!("Coult not encode jacket matrix"))?; + .with_context(|| "Coult not encode jacket matrix")?; fs::write(songs_dir.join("recognition_matrix"), bytes) - .with_context(|| format!("Could not write jacket matrix"))?; + .with_context(|| "Could not write jacket matrix")?; } Ok(()) diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs index 3090402..b2d1b4a 100644 --- a/src/bin/discord-bot/main.rs +++ b/src/bin/discord-bot/main.rs @@ -21,6 +21,7 @@ async fn main() { commands::score::score(), commands::stats::stats(), commands::chart::chart(), + commands::calc::calc(), ], prefix_options: poise::PrefixFrameworkOptions { stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { diff --git a/src/bin/discord-presence/main.rs b/src/bin/discord-presence/main.rs index 3be806a..4408949 100644 --- a/src/bin/discord-presence/main.rs +++ b/src/bin/discord-presence/main.rs @@ -21,7 +21,7 @@ async fn main() -> Result<(), Error> { ipc.connect().map_err(|e| anyhow!("{}", e))?; println!("Starting presence loop..."); - for i in 0.. { + loop { println!("Getting most recent score..."); let res = reqwest::get(format!("{}/plays/latest", server_url)).await; @@ -42,7 +42,6 @@ async fn main() -> Result<(), Error> { "{}/jackets/by_chart_id/{}.png", server_url, &triplet.chart.id ); - let jacket_url = "https://static.wikia.nocookie.net/iowiro/images/c/c2/Fracture_Ray.jpg/revision/latest?cb=20230928061927"; println!("Jacket url: {}", jacket_url); let jacket_text = format!("{} — {}", &triplet.song.title, &triplet.song.artist); @@ -68,6 +67,4 @@ async fn main() -> Result<(), Error> { ipc.set_activity(activity).map_err(|e| anyhow!("{}", e))?; tokio::time::sleep(Duration::from_secs(30)).await; } - - Ok(()) } diff --git a/src/commands/calc.rs b/src/commands/calc.rs new file mode 100644 index 0000000..663eb9d --- /dev/null +++ b/src/commands/calc.rs @@ -0,0 +1,184 @@ +// {{{ Imports +use num::{FromPrimitive, Rational32}; + +use crate::arcaea::play::{compute_b30_ptt, get_best_plays}; +use crate::arcaea::rating::{rating_as_float, rating_from_fixed, Rating}; +use crate::context::{Context, Error, TaggedError}; +use crate::recognition::fuzzy_song_name::guess_song_and_chart; +use crate::user::User; + +use crate::arcaea::score::{Score, ScoringSystem}; + +use super::discord::MessageContext; +// }}} + +// {{{ Top command +/// Compute different things +#[poise::command( + prefix_command, + slash_command, + subcommands("expected", "rating"), + subcommand_required +)] +pub async fn calc(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} +// }}} +// {{{ Expected +// {{{ Implementation +async fn expected_impl( + ctx: &mut impl MessageContext, + ptt: Option, + name: &str, +) -> Result { + let (song, chart) = guess_song_and_chart(ctx.data(), name)?; + + let ptt = if let Some(ptt) = ptt { + ptt + } else { + let user = User::from_context(ctx)?; + compute_b30_ptt( + ScoringSystem::Standard, + &get_best_plays(ctx.data(), user.id, ScoringSystem::Standard, 30, 30, None)?, + ) + }; + + let cc = rating_from_fixed(chart.chart_constant as i32); + + let score = if ptt >= cc + 2 { + Rational32::from_integer(chart.note_count as i32 + 10_000_000) + } else if ptt >= cc + 1 { + Rational32::from_integer(9_800_000) + + (ptt - cc - 1).reduced() * Rational32::from_integer(200_000) + } else { + Rational32::from_integer(9_500_000) + + (ptt - cc).reduced() * Rational32::from_integer(300_000) + }; + let score = Score(score.to_integer().max(0) as u32); + + ctx.reply(&format!( + "The expected score for a player of potential {:.2} on {} [{}] is {}", + rating_as_float(ptt), + song, + chart.difficulty, + score + )) + .await?; + + Ok(score) +} +// }}} +// {{{ Tests +#[cfg(test)] +mod expected_tests { + use crate::{ + commands::discord::mock::MockContext, context::testing::get_mock_context, golden_test, + }; + + use super::*; + + #[tokio::test] + async fn consistent_with_rating() -> Result<(), Error> { + let (mut ctx, _guard) = get_mock_context().await?; + ctx.save_messages = false; // We don't want to waste time writing to a vec + + for i in 0..1_000 { + let score = Score(i * 10_000); + let rating = score.play_rating(1140); + let res = expected_impl(&mut ctx, Some(rating), "Pentiment [BYD]") + .await + .map_err(|e| e.error)?; + assert_eq!( + score, res, + "Wrong expected score for starting score {score} and rating {rating}" + ); + } + + Ok(()) + } + + golden_test!(basic_usage, "commands/calc/expected/basic_usage"); + async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> { + expected_impl( + ctx, + Some(Rational32::from_f32(12.27).unwrap()), + "Vicious anti heorism", + ) + .await?; + + Ok(()) + } +} +// }}} +// {{{ Discord wrapper +/// Computes the expected score for a player of some potential on a given chart. +#[poise::command(prefix_command, slash_command, user_cooldown = 1)] +async fn expected( + mut ctx: Context<'_>, + #[description = "The potential to compute the expected score for"] ptt: Option, + #[rest] + #[description = "Name of chart (difficulty at the end)"] + name: String, +) -> Result<(), Error> { + let res = expected_impl(&mut ctx, ptt.and_then(Rational32::from_f32), &name).await; + ctx.handle_error(res).await?; + + Ok(()) +} +// }}} +// }}} +// {{{ Rating +// {{{ Implementation +async fn rating_impl( + ctx: &mut impl MessageContext, + score: Score, + name: &str, +) -> Result { + let (song, chart) = guess_song_and_chart(ctx.data(), name)?; + + let rating = score.play_rating(chart.chart_constant); + + ctx.reply(&format!( + "The score {} on {} [{}] yields a rating of {:.2}", + score, + song, + chart.difficulty, + rating_as_float(rating), + )) + .await?; + + Ok(rating) +} +// }}} +// {{{ Tests +#[cfg(test)] +mod rating_tests { + use crate::{commands::discord::mock::MockContext, golden_test}; + + use super::*; + + golden_test!(basic_usage, "commands/calc/rating/basic_usage"); + async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> { + rating_impl(ctx, Score(9_349_070), "Arcana Eden [PRS]").await?; + + Ok(()) + } +} +// }}} +// {{{ Discord wrapper +/// Computes the rating (potential) of a play on a given chart. +#[poise::command(prefix_command, slash_command, user_cooldown = 1)] +async fn rating( + mut ctx: Context<'_>, + score: u32, + #[rest] + #[description = "Name of chart (difficulty at the end)"] + name: String, +) -> Result<(), Error> { + let res = rating_impl(&mut ctx, Score(score), &name).await; + ctx.handle_error(res).await?; + + Ok(()) +} +// }}} +// }}} diff --git a/src/commands/chart.rs b/src/commands/chart.rs index 8db25e3..5c982f0 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -1,5 +1,5 @@ -use anyhow::anyhow; // {{{ Imports +use anyhow::anyhow; use poise::serenity_prelude::{CreateAttachment, CreateEmbed}; use crate::arcaea::{chart::Side, play::Play}; @@ -138,7 +138,7 @@ mod info_tests { async fn info( mut ctx: Context<'_>, #[rest] - #[description = "Name of chart to show (difficulty at the end)"] + #[description = "Name of chart (difficulty at the end)"] name: String, ) -> Result<(), Error> { let res = info_impl(&mut ctx, &name).await; @@ -251,7 +251,7 @@ mod best_tests { async fn best( mut ctx: Context<'_>, #[rest] - #[description = "Name of chart to show (difficulty at the end)"] + #[description = "Name of chart (difficulty at the end)"] name: String, ) -> Result<(), Error> { let res = best_impl(&mut ctx, &name).await; @@ -403,7 +403,7 @@ async fn plot( mut ctx: Context<'_>, scoring_system: Option, #[rest] - #[description = "Name of chart to show (difficulty at the end)"] + #[description = "Name of chart (difficulty at the end)"] name: String, ) -> Result<(), Error> { let res = plot_impl(&mut ctx, scoring_system, name).await; diff --git a/src/commands/discord.rs b/src/commands/discord.rs index e00ffca..bf8a27b 100644 --- a/src/commands/discord.rs +++ b/src/commands/discord.rs @@ -199,6 +199,9 @@ pub mod mock { pub user_id: u64, pub data: UserContext, + /// If true, messages will be saved in a vec. + pub save_messages: bool, + messages: Vec, } @@ -207,6 +210,7 @@ pub mod mock { Self { data, user_id: 666, + save_messages: true, messages: vec![], } } @@ -274,7 +278,10 @@ pub mod mock { } async fn send(&mut self, message: CreateReply) -> Result<(), Error> { - self.messages.push(ReplyEssence::from_reply(message)); + if self.save_messages { + self.messages.push(ReplyEssence::from_reply(message)); + } + Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0a49309..e14401a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,6 +5,7 @@ pub mod discord; pub mod score; pub mod stats; pub mod utils; +pub mod calc; // {{{ Help /// Show this help menu diff --git a/src/context.rs b/src/context.rs index 8f0c86b..3542ecf 100644 --- a/src/context.rs +++ b/src/context.rs @@ -141,6 +141,10 @@ impl UserContext { // {{{ Testing helpers #[cfg(test)] pub mod testing { + use tempfile::TempDir; + + use crate::commands::discord::mock::MockContext; + use super::*; pub async fn get_shared_context() -> &'static UserContext { @@ -165,6 +169,16 @@ pub mod testing { ); } + pub async fn get_mock_context() -> Result<(MockContext, TempDir), Error> { + let mut data = (*get_shared_context().await).clone(); + let dir = tempfile::tempdir()?; + data.db = connect_db(dir.path()); + import_songs_and_jackets_from(dir.path()); + + let ctx = MockContext::new(data); + Ok((ctx, dir)) + } + // rustfmt fucks up the formatting here, // but the skip attribute doesn't seem to work well on macros 🤔 #[macro_export] @@ -184,12 +198,7 @@ pub mod testing { ($test_path:expr, $f:expr) => {{ use std::str::FromStr; - let mut data = (*$crate::context::testing::get_shared_context().await).clone(); - let dir = tempfile::tempdir()?; - data.db = $crate::context::connect_db(dir.path()); - $crate::context::testing::import_songs_and_jackets_from(dir.path()); - - let mut ctx = $crate::commands::discord::mock::MockContext::new(data); + let (mut ctx, _guard) = $crate::context::testing::get_mock_context().await?; let res = $crate::user::User::create_from_context(&ctx); ctx.handle_error(res).await?; diff --git a/src/lib.rs b/src/lib.rs index 979f3f5..6470d20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ #![allow(async_fn_in_trait)] #![allow(clippy::needless_range_loop)] #![allow(clippy::redundant_closure)] +// This sometimes triggers for rationals, where it doesn't make sense +#![allow(clippy::int_plus_one)] pub mod arcaea; pub mod assets; diff --git a/src/recognition/fuzzy_song_name.rs b/src/recognition/fuzzy_song_name.rs index e56efd0..a8fda98 100644 --- a/src/recognition/fuzzy_song_name.rs +++ b/src/recognition/fuzzy_song_name.rs @@ -32,22 +32,23 @@ pub fn guess_song_and_chart<'a>( ctx: &'a UserContext, name: &'a str, ) -> Result<(&'a Song, &'a Chart), Error> { - let name = name.trim(); - let (name, difficulty) = name - .strip_suffix("PST") - .zip(Some(Difficulty::PST)) - .or_else(|| strip_case_insensitive_suffix(name, "[PST]").zip(Some(Difficulty::PST))) - .or_else(|| strip_case_insensitive_suffix(name, "PRS").zip(Some(Difficulty::PRS))) - .or_else(|| strip_case_insensitive_suffix(name, "[PRS]").zip(Some(Difficulty::PRS))) - .or_else(|| strip_case_insensitive_suffix(name, "FTR").zip(Some(Difficulty::FTR))) - .or_else(|| strip_case_insensitive_suffix(name, "[FTR]").zip(Some(Difficulty::FTR))) - .or_else(|| strip_case_insensitive_suffix(name, "ETR").zip(Some(Difficulty::ETR))) - .or_else(|| strip_case_insensitive_suffix(name, "[ETR]").zip(Some(Difficulty::ETR))) - .or_else(|| strip_case_insensitive_suffix(name, "BYD").zip(Some(Difficulty::BYD))) - .or_else(|| strip_case_insensitive_suffix(name, "[BYD]").zip(Some(Difficulty::BYD))) - .unwrap_or((name, Difficulty::FTR)); + let mut name = name.trim(); + let mut inferred_difficulty = None; - guess_chart_name(name, &ctx.song_cache, Some(difficulty), true) + for difficulty in Difficulty::DIFFICULTIES { + for shorthand in [ + Difficulty::DIFFICULTY_SHORTHANDS[difficulty.to_index()], + Difficulty::DIFFICULTY_SHORTHANDS_IN_BRACKETS[difficulty.to_index()], + ] { + if let Some(stripped) = strip_case_insensitive_suffix(name, shorthand) { + inferred_difficulty = Some(difficulty); + name = stripped; + break; + } + } + } + + guess_chart_name(name, &ctx.song_cache, inferred_difficulty, true) } // }}} // {{{ Guess chart by name @@ -74,11 +75,20 @@ pub fn guess_chart_name<'a>( let mut close_enough: Vec<_> = cache .charts() .filter_map(|chart| { - if difficulty.map_or(false, |d| d != chart.difficulty) { + let cached_song = &cache.lookup_song(chart.song_id).ok()?; + let song = &cached_song.song; + let plausible_difficulty = match difficulty { + Some(difficulty) => difficulty == chart.difficulty, + None => { + let chart_count = cached_song.charts().count(); + chart_count == 1 || chart.difficulty == Difficulty::FTR + } + }; + + if !plausible_difficulty { return None; } - let song = &cache.lookup_song(chart.song_id).ok()?.song; let song_title = &song.lowercase_title; distance_vec.clear(); diff --git a/test/commands/calc/expected/basic_usage/0.toml b/test/commands/calc/expected/basic_usage/0.toml new file mode 100644 index 0000000..22d9a00 --- /dev/null +++ b/test/commands/calc/expected/basic_usage/0.toml @@ -0,0 +1,4 @@ +reply = true +content = "The expected score for a player of potential 12.27 on Vicious [ANTi] Heroism [BYD] is 9'834'000" +embeds = [] +attachments = [] diff --git a/test/commands/calc/rating/basic_usage/0.toml b/test/commands/calc/rating/basic_usage/0.toml new file mode 100644 index 0000000..ed29156 --- /dev/null +++ b/test/commands/calc/rating/basic_usage/0.toml @@ -0,0 +1,4 @@ +reply = true +content = "The score 9'349'070 on Arcana Eden [PRS] yields a rating of 8.20" +embeds = [] +attachments = [] diff --git a/test/commands/commands/chart/info/no_suffix/0.toml b/test/commands/commands/chart/info/no_suffix/0.toml new file mode 100644 index 0000000..4ee7b3d --- /dev/null +++ b/test/commands/commands/chart/info/no_suffix/0.toml @@ -0,0 +1,52 @@ +reply = true + +[[embeds]] +title = "Pentiment [FTR 10]" +type = "rich" + +[embeds.thumbnail] +url = "attachment://chart.png" + +[[embeds.fields]] +name = "Note count" +value = "1345" +inline = true + +[[embeds.fields]] +name = "Chart constant" +value = "10.3" +inline = true + +[[embeds.fields]] +name = "Total plays" +value = "0" +inline = true + +[[embeds.fields]] +name = "BPM" +value = "200-222" +inline = true + +[[embeds.fields]] +name = "Side" +value = "conflict" +inline = true + +[[embeds.fields]] +name = "Artist" +value = "Pentiment" +inline = true + +[[embeds.fields]] +name = "Note design" +value = "Paradox Blight" +inline = true + +[[embeds.fields]] +name = "Pack" +value = "Final Verdict" +inline = true + +[[attachments]] +filename = "chart.png" +hash = "sha256_5ffda660ce1c6ddd7c60bbb7f34443a7772e608f930768a147e423cc62b7e25d" diff --git a/test/commands/commands/chart/info/specify_difficulty/0.toml b/test/commands/commands/chart/info/specify_difficulty/0.toml new file mode 100644 index 0000000..f3d63c0 --- /dev/null +++ b/test/commands/commands/chart/info/specify_difficulty/0.toml @@ -0,0 +1,52 @@ +reply = true + +[[embeds]] +title = "HELLOHELL [ETR 9]" +type = "rich" + +[embeds.thumbnail] +url = "attachment://chart.png" + +[[embeds.fields]] +name = "Note count" +value = "770" +inline = true + +[[embeds.fields]] +name = "Chart constant" +value = "9.4" +inline = true + +[[embeds.fields]] +name = "Total plays" +value = "0" +inline = true + +[[embeds.fields]] +name = "BPM" +value = "155" +inline = true + +[[embeds.fields]] +name = "Side" +value = "conflict" +inline = true + +[[embeds.fields]] +name = "Artist" +value = "HELLOHELL" +inline = true + +[[embeds.fields]] +name = "Note design" +value = "eién" +inline = true + +[[embeds.fields]] +name = "Pack" +value = "World Extend 3: Illusions" +inline = true + +[[attachments]] +filename = "chart.png" +hash = "sha256_e00a92ba1abbcf97c7b006447867914b9d95dc4dfb039e05260d71128b60eedb" diff --git a/test/score/magic/single_pic/0.toml b/test/score/magic/single_pic/0.toml new file mode 100644 index 0000000..054680a --- /dev/null +++ b/test/score/magic/single_pic/0.toml @@ -0,0 +1,57 @@ +reply = true + +[[embeds]] +title = "ALTER EGO [FTR 10]" +type = "rich" + +[embeds.thumbnail] +url = "attachment://416-9926250-0.png" + +[[embeds.fields]] +name = "Score" +value = "9'926'250" +inline = true + +[[embeds.fields]] +name = "Rating" +value = "12.13" +inline = true + +[[embeds.fields]] +name = "Grade" +value = "EX+" +inline = true + +[[embeds.fields]] +name = "ξ-Score" +value = "9'693'042" +inline = true + +[[embeds.fields]] +name = "ξ-Rating" +value = "11.14" +inline = true + +[[embeds.fields]] +name = "ξ-Grade" +value = "AA" +inline = true + +[[embeds.fields]] +name = "Status" +value = "C (-164/-12/-5)" +inline = true + +[[embeds.fields]] +name = "Max recall" +value = "397" +inline = true + +[[embeds.fields]] +name = "ID" +value = "1" +inline = true + +[[attachments]] +filename = "416-9926250-0.png" +hash = "sha256_5f1febcdf44bc22bf7ef5bff0cee197a65a221277ebe9615d469002b4324ada3" diff --git a/test/score/magic/weird_kerning/0.toml b/test/score/magic/weird_kerning/0.toml new file mode 100644 index 0000000..740214a --- /dev/null +++ b/test/score/magic/weird_kerning/0.toml @@ -0,0 +1,113 @@ +reply = true + +[[embeds]] +title = "Antithese [FTR 8+]" +type = "rich" + +[embeds.thumbnail] +url = "attachment://116-9983744-0.png" + +[[embeds.fields]] +name = "Score" +value = "9'983'744" +inline = true + +[[embeds.fields]] +name = "Rating" +value = "10.72" +inline = true + +[[embeds.fields]] +name = "Grade" +value = "EX+" +inline = true + +[[embeds.fields]] +name = "ξ-Score" +value = "9'920'182" +inline = true + +[[embeds.fields]] +name = "ξ-Rating" +value = "10.40" +inline = true + +[[embeds.fields]] +name = "ξ-Grade" +value = "EX+" +inline = true + +[[embeds.fields]] +name = "Status" +value = "C (-27/-1/-1)" +inline = true + +[[embeds.fields]] +name = "Max recall" +value = "479" +inline = true + +[[embeds.fields]] +name = "ID" +value = "1" +inline = true + +[[embeds]] +title = "GENOCIDER [FTR 10+]" +type = "rich" + +[embeds.thumbnail] +url = "attachment://243-9724775-1.png" + +[[embeds.fields]] +name = "Score" +value = "9'724'775" +inline = true + +[[embeds.fields]] +name = "Rating" +value = "11.45" +inline = true + +[[embeds.fields]] +name = "Grade" +value = "AA" +inline = true + +[[embeds.fields]] +name = "ξ-Score" +value = "9'453'809" +inline = true + +[[embeds.fields]] +name = "ξ-Rating" +value = "10.55" +inline = true + +[[embeds.fields]] +name = "ξ-Grade" +value = "A" +inline = true + +[[embeds.fields]] +name = "Status" +value = "C (-180/-40/-21)" +inline = true + +[[embeds.fields]] +name = "Max recall" +value = "347" +inline = true + +[[embeds.fields]] +name = "ID" +value = "2" +inline = true + +[[attachments]] +filename = "116-9983744-0.png" +hash = "sha256_75b03ac3392d4bcb9d377396a36708aea1298dd463c08d5d62ca1e1414bfeaef" + +[[attachments]] +filename = "243-9724775-1.png" +hash = "sha256_4a8ad7af5482104b3ec7c9a50091bb6f01df10a0cc758837cf47dcf7d73fd59d"