1- use std:: { borrow:: Cow , mem} ;
1+ use std:: {
2+ borrow:: Cow ,
3+ collections:: { BTreeMap , HashSet } ,
4+ fmt:: Write ,
5+ iter, mem,
6+ } ;
27
38use bathbot_macros:: { command, HasName , SlashCommand } ;
4- use bathbot_util:: { constants:: GENERAL_ISSUE , matcher, MessageBuilder , TourneyBadges } ;
9+ use bathbot_util:: {
10+ constants:: GENERAL_ISSUE , matcher, numbers:: WithComma , EmbedBuilder , IntHasher , MessageBuilder ,
11+ TourneyBadges ,
12+ } ;
513use eyre:: { Report , Result } ;
14+ use rkyv:: rancor:: { Panic , ResultExt } ;
615use rosu_v2:: { model:: GameMode , prelude:: OsuError , request:: UserId } ;
716use twilight_interactions:: command:: { CommandModel , CreateCommand } ;
817use twilight_model:: id:: { marker:: UserMarker , Id } ;
918
1019use super :: { require_link, user_not_found} ;
1120use crate :: {
1221 core:: commands:: { prefix:: Args , CommandOrigin } ,
13- embeds:: { BWSEmbed , EmbedData } ,
14- manager:: redis:: osu:: { UserArgs , UserArgsError } ,
15- util:: { interaction:: InteractionCommand , ChannelExt , InteractionCommandExt } ,
22+ manager:: redis:: osu:: { CachedUser , UserArgs , UserArgsError } ,
23+ util:: { interaction:: InteractionCommand , CachedUserExt , ChannelExt , InteractionCommandExt } ,
1624 Context ,
1725} ;
1826
@@ -44,6 +52,12 @@ pub struct Bws<'a> {
4452 If none is specified, it defaults to the current amount + 2."
4553 ) ]
4654 badges : Option < usize > ,
55+ #[ command(
56+ min_value = 0 ,
57+ max_value = 3000 ,
58+ desc = "Filter out badges before a certain year"
59+ ) ]
60+ year : Option < i32 > ,
4761 #[ command(
4862 desc = "Specify a linked discord user" ,
4963 help = "Instead of specifying an osu! username with the `name` option, \
@@ -101,6 +115,7 @@ impl<'m> Bws<'m> {
101115 name,
102116 rank,
103117 badges,
118+ year : None ,
104119 discord,
105120 } )
106121 }
@@ -152,7 +167,9 @@ async fn bws(orig: CommandOrigin<'_>, args: Bws<'_>) -> Result<()> {
152167 } ,
153168 } ;
154169
155- let Bws { rank, badges, .. } = args;
170+ let Bws {
171+ rank, badges, year, ..
172+ } = args;
156173
157174 let user_args = UserArgs :: rosu_id ( & user_id, GameMode :: Osu ) . await ;
158175
@@ -171,7 +188,18 @@ async fn bws(orig: CommandOrigin<'_>, args: Bws<'_>) -> Result<()> {
171188 }
172189 } ;
173190
174- let badges_curr = TourneyBadges :: count ( user. badges . iter ( ) . map ( |badge| & badge. description ) ) ;
191+ let badges_iter = user
192+ . badges
193+ . iter ( )
194+ . filter ( |badge| {
195+ let Some ( year) = year else { return true } ;
196+ let awarded_at = badge. awarded_at . try_deserialize :: < Panic > ( ) . always_ok ( ) ;
197+
198+ awarded_at. year ( ) >= year
199+ } )
200+ . map ( |badge| & badge. description ) ;
201+
202+ let badges_curr = TourneyBadges :: count ( badges_iter) ;
175203
176204 let ( badges_min, badges_max) = match badges {
177205 Some ( num) => {
@@ -189,10 +217,192 @@ async fn bws(orig: CommandOrigin<'_>, args: Bws<'_>) -> Result<()> {
189217 None => ( badges_curr, badges_curr + MIN_BADGES_OFFSET ) ,
190218 } ;
191219
192- let embed_data = BWSEmbed :: new ( & user, badges_curr, badges_min, badges_max, rank) ;
193- let embed = embed_data. build ( ) ;
220+ let embed = bws_embed ( & user, badges_curr, badges_min, badges_max, rank, year) ;
194221 let builder = MessageBuilder :: new ( ) . embed ( embed) ;
195222 orig. create_message ( builder) . await ?;
196223
197224 Ok ( ( ) )
198225}
226+
227+ fn bws_embed (
228+ user : & CachedUser ,
229+ badges_curr : usize ,
230+ badges_min : usize ,
231+ badges_max : usize ,
232+ rank : Option < u32 > ,
233+ year : Option < i32 > ,
234+ ) -> EmbedBuilder {
235+ let global_rank = user
236+ . statistics
237+ . as_ref ( )
238+ . expect ( "missing stats" )
239+ . global_rank
240+ . to_native ( ) ;
241+
242+ let dist_badges = badges_max - badges_min;
243+ let step_dist = 2 ;
244+
245+ let badges: Vec < _ > = ( badges_min..badges_max)
246+ . step_by ( dist_badges / step_dist)
247+ . take ( step_dist)
248+ . chain ( iter:: once ( badges_max) )
249+ . map ( |count| BadgeEntry {
250+ count,
251+ len : WithComma :: new ( count) . to_string ( ) . len ( ) ,
252+ } )
253+ . collect ( ) ;
254+
255+ let yellow = "\u{001b} [1;33m" ;
256+ let reset = "\u{001b} [0m" ;
257+
258+ let description = match rank {
259+ Some ( rank_arg) => {
260+ let mut min = rank_arg;
261+ let mut max = global_rank;
262+
263+ if min > max {
264+ mem:: swap ( & mut min, & mut max) ;
265+ }
266+
267+ let rank_len = max. to_string ( ) . len ( ) . max ( 6 ) + 1 ;
268+ let dist_rank = ( max - min) as usize ;
269+ let step_rank = 3 ;
270+
271+ let bwss: BTreeMap < _ , _ > = {
272+ let mut values = HashSet :: with_hasher ( IntHasher ) ;
273+
274+ ( min..max)
275+ . step_by ( ( dist_rank / step_rank) . max ( 1 ) )
276+ . take ( step_rank)
277+ . chain ( iter:: once ( max) )
278+ . filter ( |& n| values. insert ( n) )
279+ . map ( |rank| {
280+ let bwss: Vec < _ > = badges
281+ . iter ( )
282+ . map ( |entry| WithComma :: new ( bws_value ( rank, entry. count ) ) . to_string ( ) )
283+ . collect ( ) ;
284+
285+ ( rank, bwss)
286+ } )
287+ . collect ( )
288+ } ;
289+
290+ // Calculate the widths for each column
291+ let max: Vec < _ > = ( 0 ..=2 )
292+ . map ( |n| {
293+ bwss. values ( )
294+ . map ( |bwss| bwss. get ( n) . unwrap ( ) . len ( ) )
295+ . fold ( 0 , |max, next| max. max ( next) )
296+ . max ( 2 )
297+ . max ( badges[ n] . len )
298+ } )
299+ . collect ( ) ;
300+
301+ let mut content = String :: with_capacity ( 256 ) ;
302+ content. push_str ( "```ansi\n " ) ;
303+
304+ let _ = writeln ! (
305+ content,
306+ " {:>rank_len$} | {:^len1$} | {:^len2$} | {:^len3$}" ,
307+ "Badges>" ,
308+ badges[ 0 ] . count,
309+ badges[ 1 ] . count,
310+ badges[ 2 ] . count,
311+ len1 = max[ 0 ] ,
312+ len2 = max[ 1 ] ,
313+ len3 = max[ 2 ] ,
314+ ) ;
315+
316+ let _ = writeln ! (
317+ content,
318+ "-{0:->rank_len$}-+-{0:-^len1$}-+-{0:-^len2$}-+-{0:-^len3$}-" ,
319+ '-' ,
320+ len1 = max[ 0 ] ,
321+ len2 = max[ 1 ] ,
322+ len3 = max[ 2 ] ,
323+ ) ;
324+
325+ for ( rank, bwss) in bwss {
326+ let _ = writeln ! (
327+ content,
328+ " {:>rank_len$} | {ansi_left}{:^len1$}{reset} | {:^len2$} | {ansi_right}{:^len3$}{reset}" ,
329+ format!( "#{rank}" ) ,
330+ bwss[ 0 ] ,
331+ bwss[ 1 ] ,
332+ bwss[ 2 ] ,
333+ len1 = max[ 0 ] ,
334+ len2 = max[ 1 ] ,
335+ len3 = max[ 2 ] ,
336+ ansi_left = if rank == global_rank && badges_curr == badges[ 0 ] . count { yellow } else { reset } ,
337+ ansi_right = if rank == global_rank && badges_curr == badges[ 2 ] . count { yellow } else { reset } ,
338+ ) ;
339+ }
340+
341+ content. push_str ( "```" ) ;
342+
343+ content
344+ }
345+ None => {
346+ let bws1 = WithComma :: new ( bws_value ( global_rank, badges[ 0 ] . count ) ) . to_string ( ) ;
347+ let bws2 = WithComma :: new ( bws_value ( global_rank, badges[ 1 ] . count ) ) . to_string ( ) ;
348+ let bws3 = WithComma :: new ( bws_value ( global_rank, badges[ 2 ] . count ) ) . to_string ( ) ;
349+ let len1 = bws1. len ( ) . max ( 2 ) . max ( badges[ 0 ] . len ) ;
350+ let len2 = bws2. len ( ) . max ( 2 ) . max ( badges[ 1 ] . len ) ;
351+ let len3 = bws3. len ( ) . max ( 2 ) . max ( badges[ 2 ] . len ) ;
352+ let mut content = String :: with_capacity ( 128 ) ;
353+ content. push_str ( "```ansi\n " ) ;
354+
355+ let _ = writeln ! (
356+ content,
357+ "Badges | {:^len1$} | {:^len2$} | {:^len3$}" ,
358+ badges[ 0 ] . count, badges[ 1 ] . count, badges[ 2 ] . count,
359+ ) ;
360+
361+ let _ = writeln ! (
362+ content,
363+ "-------+-{0:-^len1$}-+-{0:-^len2$}-+-{0:-^len3$}-" ,
364+ '-'
365+ ) ;
366+
367+ let _ = writeln ! (
368+ content,
369+ " BWS | {ansi_left}{bws1:^len1$}{reset} | {bws2:^len2$} | {ansi_right}{bws3:^len3$}{reset}\n ```" ,
370+ ansi_left = if badges_curr == badges[ 0 ] . count { yellow } else { reset } ,
371+ ansi_right = if badges_curr == badges[ 2 ] . count { yellow } else { reset } ,
372+ ) ;
373+
374+ content
375+ }
376+ } ;
377+
378+ let title = format ! (
379+ "Current BWS for {badges_curr} badge{}: {}" ,
380+ if badges_curr == 1 { "" } else { "s" } ,
381+ WithComma :: new( bws_value( global_rank, badges_curr) )
382+ ) ;
383+
384+ let mut embed = EmbedBuilder :: new ( ) ;
385+
386+ if let Some ( year) = year {
387+ embed = embed. footer ( format ! ( "Badges from the year {year} onward" ) ) ;
388+ }
389+
390+ embed
391+ . author ( user. author_builder ( ) )
392+ . description ( description)
393+ . thumbnail ( user. avatar_url . as_ref ( ) . to_owned ( ) )
394+ . title ( title)
395+ }
396+
397+ struct BadgeEntry {
398+ count : usize ,
399+ /// Length of `count` when stringified
400+ len : usize ,
401+ }
402+
403+ fn bws_value ( rank : u32 , badges : usize ) -> u64 {
404+ let rank = rank as f64 ;
405+ let badges = badges as i32 ;
406+
407+ rank. powf ( 0.9937_f64 . powi ( badges * badges) ) . round ( ) as u64
408+ }
0 commit comments