Skip to content

Commit 8c2df05

Browse files
authored
feat: add year option to /bws (#935)
1 parent 4011252 commit 8c2df05

File tree

3 files changed

+220
-208
lines changed

3 files changed

+220
-208
lines changed

bathbot/src/commands/osu/bws.rs

Lines changed: 219 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
use std::{borrow::Cow, mem};
1+
use std::{
2+
borrow::Cow,
3+
collections::{BTreeMap, HashSet},
4+
fmt::Write,
5+
iter, mem,
6+
};
27

38
use 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+
};
513
use eyre::{Report, Result};
14+
use rkyv::rancor::{Panic, ResultExt};
615
use rosu_v2::{model::GameMode, prelude::OsuError, request::UserId};
716
use twilight_interactions::command::{CommandModel, CreateCommand};
817
use twilight_model::id::{marker::UserMarker, Id};
918

1019
use super::{require_link, user_not_found};
1120
use 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

Comments
 (0)