rs_pixel/util/
skyblock_profile.rs

1use super::utils::parse_nbt;
2use crate::util::constants::{
3    BLAZE_EXP, CATACOMBS_EXP, CRAFTED_MINIONS_TO_SLOTS, ENDERMAN_EXP, HOTM_EXP, LEVELING_CAPS,
4    LEVELING_EXP, PET_EXP, PET_RARITY_OFFSET, RUNECRAFTING_EXP, SOCIAL_EXP, SPIDER_EXP, WOLF_EXP,
5    ZOMBIE_EXP,
6};
7use crate::{
8    types::gamemode::Gamemode,
9    util::generic_json::{Property, Raw},
10};
11
12use serde::{Deserialize, Deserializer, Serialize};
13use serde_json::Value;
14use std::collections::{HashMap, HashSet};
15
16#[derive(Serialize, Deserialize, Debug)]
17pub struct SkyblockProfile {
18    pub profile_id: String,
19    pub members: Value,
20    pub community_upgrades: Option<SkyblockCommunityUpgrades>,
21    #[serde(default = "Default::default")]
22    pub selected: bool,
23    pub cute_name: Option<String>,
24    pub banking: Option<SkyblockBanking>,
25    #[serde(
26        deserialize_with = "deserialize_gamemode",
27        default = "Default::default"
28    )]
29    pub game_mode: Gamemode,
30}
31
32fn deserialize_gamemode<'de, D>(deserializer: D) -> Result<Gamemode, D::Error>
33where
34    D: Deserializer<'de>,
35{
36    let opt: Option<String> = Option::deserialize(deserializer)?;
37    Ok(match opt.unwrap_or_default().as_str() {
38        "ironman" => Gamemode::Ironman,
39        "island" => Gamemode::Stranded,
40        "bingo" => Gamemode::Bingo,
41        _ => Gamemode::Regular,
42    })
43}
44
45impl Raw for SkyblockProfile {
46    fn raw(&self) -> &Value {
47        &self.members
48    }
49}
50
51impl SkyblockProfile {
52    pub fn get_purse_coins(&self, uuid: &str) -> Option<f64> {
53        self.get_float_property(&format!("{uuid}.coin_purse"))
54    }
55
56    pub fn get_skill(&self, uuid: &str, skill_name: &str) -> Option<LevelingStruct> {
57        self.get_int_property(&format!(
58            "{}.experience_skill_{}",
59            uuid,
60            if skill_name == "social" {
61                "social2"
62            } else {
63                skill_name
64            }
65        ))
66        .map(|skill_exp| self.skill_exp_to_info(uuid, skill_name, skill_exp))
67    }
68
69    fn skill_exp_to_info(&self, uuid: &str, skill_name: &str, skill_exp: i64) -> LevelingStruct {
70        let leveling_table = match skill_name {
71            "catacombs" => *CATACOMBS_EXP,
72            "runecrafting" => *RUNECRAFTING_EXP,
73            "social" => *SOCIAL_EXP,
74            "hotm" => *HOTM_EXP,
75            _ => *LEVELING_EXP,
76        };
77        let max_level = self.get_max_level(uuid, skill_name);
78
79        if skill_exp == 0 {
80            return LevelingStruct {
81                name: skill_name.to_string(),
82                level: 0,
83                max_level,
84                total_exp: 0,
85                current_exp: 0,
86                exp_for_next: 0,
87                progress_to_next: 0.0,
88            };
89        }
90
91        let mut exp_total = 0;
92        let mut level = 0;
93        for i in 0..max_level {
94            let cur_exp_needed = leveling_table[i as usize];
95            exp_total += cur_exp_needed;
96
97            if exp_total > skill_exp {
98                exp_total -= cur_exp_needed;
99                break;
100            }
101
102            level = i + 1;
103        }
104
105        let exp_current = skill_exp - exp_total;
106        let exp_for_next = if level < max_level {
107            leveling_table[level as usize]
108        } else {
109            0
110        };
111
112        let progress = if exp_for_next > 0 {
113            (exp_current as f64 / exp_for_next as f64).clamp(0.0, 1.0)
114        } else {
115            0.0
116        };
117
118        LevelingStruct {
119            name: skill_name.to_string(),
120            level,
121            max_level,
122            total_exp: skill_exp,
123            current_exp: exp_current,
124            exp_for_next,
125            progress_to_next: progress,
126        }
127    }
128
129    pub fn get_max_level(&self, uuid: &str, name: &str) -> i64 {
130        LEVELING_CAPS.get(name).unwrap_or(&50)
131            + if name == "farming" {
132                self.get_farming_cap_upgrade(uuid)
133            } else {
134                0
135            }
136    }
137
138    pub fn get_farming_cap_upgrade(&self, uuid: &str) -> i64 {
139        self.get_int_property(&format!("{uuid}.jacob2.perks.farming_level_cap"))
140            .unwrap_or(0)
141    }
142
143    pub fn get_hotm(&self, uuid: &str) -> Option<LevelingStruct> {
144        self.get_int_property(&format!("{uuid}.mining_core.experience"))
145            .map(|exp| self.skill_exp_to_info(uuid, "hotm", exp))
146    }
147
148    fn slayer_exp_to_info(&self, uuid: &str, slayer_name: &str, slayer_exp: i64) -> LevelingStruct {
149        let leveling_table = match slayer_name {
150            "zombie" => *ZOMBIE_EXP,
151            "wolf" => *WOLF_EXP,
152            "spider" => *SPIDER_EXP,
153            "enderman" => *ENDERMAN_EXP,
154            _ => *BLAZE_EXP,
155        };
156        let max_level = self.get_max_level(uuid, slayer_name);
157
158        if slayer_exp == 0 {
159            return LevelingStruct {
160                name: slayer_name.to_string(),
161                level: 0,
162                max_level,
163                total_exp: 0,
164                current_exp: 0,
165                exp_for_next: 0,
166                progress_to_next: 0.0,
167            };
168        }
169
170        let mut level = 0;
171        for i in 0..max_level {
172            if leveling_table[i as usize] > slayer_exp {
173                break;
174            }
175
176            level = i + 1;
177        }
178
179        let exp_prev = if level > 0 {
180            leveling_table[(level - 1) as usize]
181        } else {
182            0
183        };
184        let exp_current = slayer_exp - exp_prev;
185        let exp_for_next = if level < max_level {
186            leveling_table[level as usize] - exp_prev
187        } else {
188            0
189        };
190
191        let progress = if exp_for_next > 0 {
192            (exp_current as f64 / exp_for_next as f64).clamp(0.0, 1.0)
193        } else {
194            0.0
195        };
196
197        LevelingStruct {
198            name: slayer_name.to_string(),
199            level,
200            max_level,
201            total_exp: slayer_exp,
202            current_exp: exp_current,
203            exp_for_next,
204            progress_to_next: progress,
205        }
206    }
207
208    pub fn get_slayer(&self, uuid: &str, slayer_name: &str) -> Option<LevelingStruct> {
209        self.get_int_property(&format!("{uuid}.slayer_bosses.{slayer_name}.xp"))
210            .map(|exp| self.slayer_exp_to_info(uuid, slayer_name, exp))
211    }
212
213    pub fn get_catacombs(&self, uuid: &str) -> Option<LevelingStruct> {
214        self.get_int_property(&format!(
215            "{uuid}.dungeons.dungeon_types.catacombs.experience"
216        ))
217        .map(|exp| self.skill_exp_to_info(uuid, "catacombs", exp))
218    }
219
220    pub fn get_dungeon_class(&self, uuid: &str, class_name: &str) -> Option<LevelingStruct> {
221        self.get_int_property(&format!(
222            "{uuid}.dungeons.player_classes.{class_name}.experience"
223        ))
224        .map(|exp| self.skill_exp_to_info(uuid, "catacombs", exp))
225    }
226
227    pub fn get_inventory(&self, uuid: &str) -> Option<Value> {
228        if let Some(data) = self.get_str_property(&format!("{uuid}.inv_contents.data")) {
229            parse_nbt(data)
230        } else {
231            None
232        }
233    }
234
235    pub fn get_personal_vault(&self, uuid: &str) -> Option<Value> {
236        if let Some(data) = self.get_str_property(&format!("{uuid}.personal_vault_contents.data")) {
237            parse_nbt(data)
238        } else {
239            None
240        }
241    }
242
243    pub fn get_talisman_bag(&self, uuid: &str) -> Option<Value> {
244        if let Some(data) = self.get_str_property(&format!("{uuid}.talisman_bag.data")) {
245            parse_nbt(data)
246        } else {
247            None
248        }
249    }
250
251    pub fn get_equippment(&self, uuid: &str) -> Option<Value> {
252        if let Some(data) = self.get_str_property(&format!("{uuid}.equippment_contents.data")) {
253            parse_nbt(data)
254        } else {
255            None
256        }
257    }
258
259    pub fn get_armor(&self, uuid: &str) -> Option<Value> {
260        if let Some(data) = self.get_str_property(&format!("{uuid}.inv_armor.data")) {
261            parse_nbt(data)
262        } else {
263            None
264        }
265    }
266
267    pub fn get_wardrobe(&self, uuid: &str) -> Option<Value> {
268        if let Some(data) = self.get_str_property(&format!("{uuid}.wardrobe_contents.data")) {
269            parse_nbt(data)
270        } else {
271            None
272        }
273    }
274
275    pub fn get_ender_chest(&self, uuid: &str) -> Option<Value> {
276        if let Some(data) = self.get_str_property(&format!("{uuid}.ender_chest_contents.data")) {
277            parse_nbt(data)
278        } else {
279            None
280        }
281    }
282
283    pub fn get_storage(&self, uuid: &str) -> Option<HashMap<&str, Value>> {
284        if let Some(data) = self.get_object_property(&format!("{uuid}.backpack_contents")) {
285            let mut storage = HashMap::new();
286            for ele in data {
287                if let Some(bp) = parse_nbt(
288                    ele.1
289                        .get("data")
290                        .and_then(serde_json::Value::as_str)
291                        .unwrap(),
292                ) {
293                    storage.insert(&**ele.0, bp);
294                }
295            }
296            Some(storage)
297        } else {
298            None
299        }
300    }
301
302    pub fn get_sacks(&self, uuid: &str) -> Option<HashMap<&str, i64>> {
303        if let Some(data) = self.get_object_property(&format!("{uuid}.sacks_counts")) {
304            let mut sacks = HashMap::new();
305            for ele in data {
306                sacks.insert(&**ele.0, ele.1.as_i64().unwrap_or(0));
307            }
308            Some(sacks)
309        } else {
310            None
311        }
312    }
313
314    pub fn get_pets(&self, uuid: &str) -> Option<Vec<PetStruct>> {
315        if let Some(pets) = self.get_array_property(&format!("{uuid}.pets")) {
316            let mut parsed_pets = Vec::new();
317            for pet in pets {
318                let rarity = pet.get_string_property("tier").unwrap();
319                parsed_pets.push(PetStruct {
320                    leveling: Self::pet_exp_to_info(
321                        pet.get_str_property("type").unwrap(),
322                        pet.get_int_property("exp").unwrap(),
323                        rarity.as_str(),
324                    ),
325                    rarity,
326                    skin: pet.get_string_property("skin"),
327                    held_item: pet.get_string_property("heldItem"),
328                });
329            }
330            Some(parsed_pets)
331        } else {
332            None
333        }
334    }
335
336    fn pet_exp_to_info(pet_name: &str, pet_exp: i64, pet_rarity: &str) -> LevelingStruct {
337        let leveling_table = *PET_EXP;
338        let max_level = 100; // TODO: golden dragon
339
340        if pet_exp == 0 {
341            return LevelingStruct {
342                name: pet_name.to_string(),
343                level: 0,
344                max_level,
345                total_exp: 0,
346                current_exp: 0,
347                exp_for_next: 0,
348                progress_to_next: 0.0,
349            };
350        }
351
352        let offset = *PET_RARITY_OFFSET.get(pet_rarity).unwrap();
353
354        let mut exp_total = 0;
355        let mut level = 1;
356        let mut is_maxed = true;
357        for i in offset..(leveling_table.len() as i64) {
358            let cur_exp_needed = leveling_table[i as usize];
359            exp_total += cur_exp_needed;
360
361            if exp_total > pet_exp {
362                exp_total -= cur_exp_needed;
363                is_maxed = false;
364                break;
365            }
366
367            level += 1;
368        }
369
370        if is_maxed {
371            level = 100;
372        }
373
374        let exp_current = pet_exp - exp_total;
375        let exp_for_next = if level < max_level {
376            leveling_table[(level + offset - 1) as usize]
377        } else {
378            0
379        };
380
381        let progress = if exp_for_next > 0 {
382            (exp_current as f64 / exp_for_next as f64).clamp(0.0, 1.0)
383        } else {
384            0.0
385        };
386
387        LevelingStruct {
388            name: pet_name.to_string(),
389            level,
390            max_level,
391            total_exp: pet_exp,
392            current_exp: exp_current,
393            exp_for_next,
394            progress_to_next: progress,
395        }
396    }
397
398    pub fn get_fairy_souls(&self, uuid: &str) -> i64 {
399        self.get_int_property(&format!("{uuid}.fairy_souls_collected"))
400            .unwrap_or(0)
401    }
402
403    pub fn get_minion_slots(&self) -> i64 {
404        let mut unique_minions = HashSet::new();
405
406        for member in self.members.as_object().unwrap().values() {
407            if let Some(minions_unwrap) = member.get_array_property("crafted_generators") {
408                for ele in minions_unwrap {
409                    unique_minions.insert(ele.as_str().unwrap());
410                }
411            }
412        }
413
414        let mut max = 0;
415        for i in 0..CRAFTED_MINIONS_TO_SLOTS.len() {
416            if &(unique_minions.len() as i64) < CRAFTED_MINIONS_TO_SLOTS.get(i).unwrap() {
417                break;
418            }
419
420            max = i;
421        }
422
423        (max as i64) + 5
424    }
425
426    pub fn get_pet_score(&self, uuid: &str) -> i64 {
427        let mut pets_map = HashMap::new();
428
429        if let Some(pets) = self.get_pets(uuid) {
430            for ele in pets {
431                let rarity = match ele.rarity.to_lowercase().as_str() {
432                    "common" => 1,
433                    "uncommon" => 2,
434                    "rare" => 3,
435                    "epic" => 4,
436                    "legendary" => 5,
437                    _ => 0,
438                };
439
440                if let Some(cur_rarity) = pets_map.get(&ele.name) {
441                    if cur_rarity < &rarity {
442                        pets_map.insert(ele.name.clone(), rarity);
443                    }
444                } else {
445                    pets_map.insert(ele.name.clone(), rarity);
446                }
447            }
448        }
449
450        pets_map.values().sum()
451    }
452}
453
454#[derive(Serialize, Deserialize, Debug)]
455pub struct SkyblockCommunityUpgrades {
456    pub currently_upgrading: Option<SkyblockCurrentCommunityUpgrade>,
457    pub upgrade_states: Vec<SkyblockCommunityUpgrade>,
458}
459
460#[derive(Serialize, Deserialize, Debug)]
461pub struct SkyblockCommunityUpgrade {
462    pub upgrade: String,
463    pub tier: i64,
464    pub started_ms: i64,
465    pub started_by: String,
466    pub claimed_ms: i64,
467    pub claimed_by: String,
468    pub fasttracked: bool,
469}
470
471#[derive(Serialize, Deserialize, Debug)]
472pub struct SkyblockCurrentCommunityUpgrade {
473    pub upgrade: String,
474    pub new_tier: i64,
475    pub start_ms: i64,
476    pub who_started: String,
477}
478
479#[derive(Serialize, Deserialize, Debug)]
480pub struct SkyblockBanking {
481    pub balance: f64,
482    pub transactions: Vec<SkyblockTransaction>,
483}
484
485#[derive(Serialize, Deserialize, Debug)]
486pub struct SkyblockTransaction {
487    pub amount: f64,
488    pub timestamp: i64,
489    pub action: String,
490    pub initiator_name: String,
491}
492
493#[derive(Debug)]
494pub struct LevelingStruct {
495    pub name: String,
496    pub level: i64,
497    pub max_level: i64,
498    pub total_exp: i64,
499    /// Exp only for the current level
500    pub current_exp: i64,
501    /// Total exp needed for the next level
502    pub exp_for_next: i64,
503    /// Progress to next level (0.0 to 1.0)
504    pub progress_to_next: f64,
505}
506
507impl LevelingStruct {
508    pub fn is_maxed(&self) -> bool {
509        self.level == self.max_level
510    }
511
512    pub fn get_progress_level(&self) -> f64 {
513        (self.current_exp as f64) + self.progress_to_next
514    }
515}
516
517#[derive(Debug)]
518pub struct PetStruct {
519    pub leveling: LevelingStruct,
520    pub rarity: String,
521    pub skin: Option<String>,
522    pub held_item: Option<String>,
523}
524
525impl std::ops::Deref for PetStruct {
526    type Target = LevelingStruct;
527    fn deref(&self) -> &Self::Target {
528        &self.leveling
529    }
530}