use hypixel::HypixelClient;
use hypixel::models::skyblock::SkyBlockMember;
use hypixel::util::leveling;
use serde_json::Value;
#[tokio::main]
async fn main() -> hypixel::Result<()> {
dotenvy::dotenv().ok();
let key = std::env::var("HYPIXEL_API_KEY").expect("set HYPIXEL_API_KEY");
let uuid = std::env::args()
.nth(1)
.unwrap_or_else(|| "be5c42d4ea454601a57b944c6413f3cd".to_string());
let member_id = uuid.replace('-', "");
let client = HypixelClient::new(key);
let profiles = client.skyblock_profiles(&uuid).await?;
let profile = profiles
.iter()
.find(|p| p.selected)
.or_else(|| profiles.first())
.expect("player has no SkyBlock profiles");
println!(
"Profile: {} [{}]",
profile.cute_name.as_deref().unwrap_or("?"),
profile.game_mode.as_deref().unwrap_or("normal")
);
if let Some(banking) = &profile.banking {
println!("Bank balance: {:.0}", banking.balance);
}
let member = profile
.members
.get(&member_id)
.expect("uuid is not a member of this profile");
print_currencies(member);
print_skills(member, &client).await?;
print_combat(member);
print_collections_summary(member);
Ok(())
}
fn print_currencies(member: &SkyBlockMember) {
if let Some(purse) = member
.currencies
.as_ref()
.and_then(|c| c.get("coin_purse"))
.and_then(Value::as_f64)
{
println!("Purse: {purse:.0}");
}
}
async fn print_skills(member: &SkyBlockMember, client: &HypixelClient) -> hypixel::Result<()> {
let Some(experience) = skill_experience(member) else {
return Ok(());
};
let skills = client.resource_skyblock_skills().await?;
println!("\nSkills:");
let mut entries: Vec<(&String, &Value)> = experience.iter().collect();
entries.sort_by_key(|(name, _)| name.to_owned());
for (raw_name, xp) in entries {
let name = raw_name.strip_prefix("SKILL_").unwrap_or(raw_name);
let Some(skill) = skills.skills.get(name) else {
continue;
};
let progress = leveling::skill_level(xp.as_f64().unwrap_or(0.0), skill);
println!(
" {name:<14} level {:>2}/{:<2} ({:.0} xp)",
progress.level, skill.max_level, progress.total_xp
);
}
Ok(())
}
fn skill_experience(member: &SkyBlockMember) -> Option<&serde_json::Map<String, Value>> {
member
.player_data
.as_ref()
.and_then(|d| d.get("experience"))
.and_then(Value::as_object)
}
fn print_combat(member: &SkyBlockMember) {
if let Some(catacombs) = member
.dungeons
.as_ref()
.and_then(|d| d.get("dungeon_types"))
.and_then(|t| t.get("catacombs"))
.and_then(|c| c.get("experience"))
.and_then(Value::as_f64)
{
println!("\nCatacombs experience: {catacombs:.0}");
}
if let Some(bosses) = member
.slayer
.as_ref()
.and_then(|s| s.get("slayer_bosses"))
.and_then(Value::as_object)
{
let total: f64 = bosses
.values()
.filter_map(|b| b.get("xp").and_then(Value::as_f64))
.sum();
println!("Total slayer experience: {total:.0}");
}
}
fn print_collections_summary(member: &SkyBlockMember) {
if let Some(count) = member
.extra
.get("collection")
.and_then(Value::as_object)
.map(|c| c.len())
{
println!("Tracked collections: {count}");
}
}