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; 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 pub current_exp: i64,
501 pub exp_for_next: i64,
503 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}