1use std::num::NonZeroU8;
2
3use chrono::{DateTime, Local};
4use enum_map::Enum;
5use log::error;
6use num_derive::FromPrimitive;
7use strum::EnumIter;
8
9use super::*;
10use crate::{PlayerId, gamestate::items::*, misc::*};
11
12#[derive(Debug, Default, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct HellevatorEvent {
17 pub start: Option<DateTime<Local>>,
19 pub end: Option<DateTime<Local>>,
21 pub collect_time_end: Option<DateTime<Local>>,
24 pub(crate) active: Option<Hellevator>,
27}
28
29#[derive(Debug)]
30pub enum HellevatorStatus<'a> {
31 NotEntered,
34 NotAvailable,
36 RewardClaimable,
38 Active(&'a Hellevator),
40}
41
42impl HellevatorEvent {
43 #[must_use]
46 pub fn is_event_ongoing(&self) -> bool {
47 let now = Local::now();
48 matches!((self.start, self.end), (Some(start), Some(end)) if end > now && start < now)
49 }
50
51 #[must_use]
55 pub fn status(&self) -> HellevatorStatus<'_> {
56 match self.active.as_ref() {
57 None => HellevatorStatus::NotAvailable,
58 Some(h) if !self.is_event_ongoing() => {
59 if let Some(cend) = self.collect_time_end
60 && !h.has_final_reward
61 && Local::now() < cend
62 {
63 return HellevatorStatus::RewardClaimable;
64 }
65 HellevatorStatus::NotAvailable
66 }
67 Some(h) if h.current_floor == 0 => HellevatorStatus::NotEntered,
68 Some(h) => HellevatorStatus::Active(h),
69 }
70 }
71}
72
73#[derive(Debug, Default, Clone)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct Hellevator {
76 pub key_cards: u32,
77 pub current_floor: u32,
78 pub points: u32,
79 pub has_final_reward: bool,
80
81 pub guild_points_today: u32,
82 pub guild_rank: u32,
83 pub guild_raid_floors: Vec<HellevatorRaidFloor>,
84
85 pub guild_raid_signup_start: DateTime<Local>,
86 pub guild_raid_start: DateTime<Local>,
87 pub monster_rewards: Vec<HellevatorMonsterReward>,
88
89 pub own_best_floor: u32,
90 pub shop_items: [HellevatorShopTreat; 3],
91
92 pub current_treat: Option<HellevatorShopTreat>,
93
94 pub next_card_generated: Option<DateTime<Local>>,
95 pub next_reset: Option<DateTime<Local>>,
96 pub start_contrib_date: Option<DateTime<Local>>,
97
98 pub rewards_yesterday: Option<HellevatorDailyReward>,
99 pub rewards_today: Option<HellevatorDailyReward>,
100 pub rewards_next: Option<HellevatorDailyReward>,
101
102 pub daily_treat_bonus: Option<HellevatorTreatBonus>,
103
104 pub current_monster: Option<HellevatorMonster>,
105
106 pub earned_today: u32,
107 pub earned_yesterday: u32,
108
109 pub(crate) brackets: Vec<u32>,
110}
111
112#[derive(Debug, Default, Clone)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub struct HellevatorTreatBonus {
115 pub typ: HellevatorTreatBonusType,
116 pub amount: u32,
117}
118
119#[derive(Debug, Default, Clone)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub struct HellevatorMonster {
122 pub id: i64,
123 pub level: u32,
124 pub typ: HellevatorMonsterElement,
125}
126
127#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash, FromPrimitive)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129pub enum HellevatorMonsterElement {
130 Fire = 1,
131 Cold = 2,
132 Lightning = 3,
133 #[default]
134 Unknown = 240,
135}
136
137impl HellevatorMonster {
138 pub(crate) fn parse(data: &[i64]) -> Result<Self, SFError> {
139 Ok(HellevatorMonster {
140 id: data.cget(0, "h monster id")?,
141 level: data.csiget(1, "h monster level", 0)?,
142 typ: data.cfpget(2, "h monster typ", |a| a)?.unwrap_or_default(),
143 })
144 }
145}
146
147impl HellevatorTreatBonus {
148 pub(crate) fn parse(data: &[i64]) -> Result<Self, SFError> {
149 Ok(HellevatorTreatBonus {
150 typ: data
151 .cfpget(0, "hellevator treat bonus", |a| a)?
152 .unwrap_or_default(),
153 amount: data.csiget(1, "hellevator treat bonus a", 0)?,
154 })
155 }
156}
157
158#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash, FromPrimitive)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160pub enum HellevatorTreatBonusType {
161 ExtraDamage = 14,
162 #[default]
163 Unknown = 240,
164}
165
166#[derive(Debug, Default, Clone)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168pub struct HellevatorMonsterReward {
169 pub typ: HellevatorMonsterRewardTyp,
170 pub amount: u64,
171}
172
173#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175pub enum HellevatorMonsterRewardTyp {
176 Points,
177 Tickets,
178 Mushrooms,
179 Silver,
180 LuckyCoin,
181 Wood,
182 Stone,
183 Arcane,
184 Metal,
185 Souls,
186 Fruit(HabitatType),
187
188 #[default]
189 Unknown,
190}
191
192impl HellevatorMonsterRewardTyp {
193 pub(crate) fn parse(data: i64) -> HellevatorMonsterRewardTyp {
194 match data {
195 1 => HellevatorMonsterRewardTyp::Points,
196 2 => HellevatorMonsterRewardTyp::Tickets,
197 3 => HellevatorMonsterRewardTyp::Mushrooms,
198 4 => HellevatorMonsterRewardTyp::Silver,
199 5 => HellevatorMonsterRewardTyp::LuckyCoin,
200 6 => HellevatorMonsterRewardTyp::Wood,
201 7 => HellevatorMonsterRewardTyp::Stone,
202 8 => HellevatorMonsterRewardTyp::Arcane,
203 9 => HellevatorMonsterRewardTyp::Metal,
204 10 => HellevatorMonsterRewardTyp::Souls,
205 11 => HellevatorMonsterRewardTyp::Fruit(HabitatType::Shadow),
206 12 => HellevatorMonsterRewardTyp::Fruit(HabitatType::Light),
207 13 => HellevatorMonsterRewardTyp::Fruit(HabitatType::Earth),
208 14 => HellevatorMonsterRewardTyp::Fruit(HabitatType::Fire),
209 15 => HellevatorMonsterRewardTyp::Fruit(HabitatType::Water),
210
211 _ => HellevatorMonsterRewardTyp::Unknown,
212 }
213 }
214}
215
216#[derive(Debug, Default, Clone)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218pub struct HellevatorRaidFloor {
219 pub(crate) today: i64,
220 pub(crate) yesterday: i64,
221
222 pub point_reward: u32,
223 pub silver_reward: u64,
224
225 pub today_assigned: Vec<String>,
226 pub yesterday_assigned: Vec<String>,
227}
228
229#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash, FromPrimitive)]
230#[non_exhaustive]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232pub enum HellevatorTreatType {
233 ChocolateChilliPepper = 1,
234 PeppermintChocolate = 2,
235 Electroshock = 3,
236 ChillIceCream = 4,
237 CracklingChewingGum = 5,
238 PeppermintChewingGum = 6,
239 BeerBiscuit = 7,
240 GingerBreadHeart = 8,
241 FortuneCookie = 9,
242 CannedSpinach = 10,
243 StoneBiscuit = 11,
244 OrganicGranolaBar = 12,
245 ChocolateGoldCoin = 13,
246 #[default]
247 Unknown = 230,
248}
249
250#[derive(Debug, Default, Clone)]
251#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
252pub struct HellevatorShopTreat {
253 pub is_special: bool,
254 pub typ: HellevatorTreatType,
255 pub price: u32,
256 pub duration: u32,
257 pub effect_strength: u32,
258}
259
260#[derive(Debug, Clone, Default, PartialEq, Eq)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262pub struct HellevatorDailyReward {
263 pub(crate) start_level: u16,
265 pub(crate) end_level: u16,
266
267 pub gold_chests: u16,
268 pub silver: u64,
269
270 pub fortress_chests: u16,
271 pub wood: u64,
272 pub stone: u64,
273
274 pub blacksmith_chests: u16,
275 pub arcane: u64,
276 pub metal: u64,
277}
278
279impl HellevatorDailyReward {
280 #[must_use]
282 pub fn claimable(&self) -> bool {
283 self.gold_chests > 0
284 || self.fortress_chests > 0
285 || self.blacksmith_chests > 0
286 }
287
288 pub(crate) fn parse(data: &[i64]) -> Option<HellevatorDailyReward> {
289 if data.len() < 10 {
290 return None;
291 }
292
293 Some(HellevatorDailyReward {
294 start_level: data.csiget(0, "start level", 0).unwrap_or(0),
295 end_level: data.csiget(1, "end level", 0).unwrap_or(0),
296 gold_chests: data.csiget(2, "gold chests", 0).unwrap_or(0),
297 silver: data.csiget(5, "silver reward", 0).unwrap_or(0),
298 fortress_chests: data.csiget(3, "ft chests", 0).unwrap_or(0),
299 wood: data.csiget(6, "wood reward", 0).unwrap_or(0),
300 stone: data.csiget(7, "stone reward", 0).unwrap_or(0),
301 blacksmith_chests: data.csiget(4, "bs chests", 0).unwrap_or(0),
302 arcane: data.csiget(8, "arcane reward", 0).unwrap_or(0),
303 metal: data.csiget(9, "metal reward", 0).unwrap_or(0),
304 })
305 }
306}
307
308impl Hellevator {
309 #[must_use]
313 pub fn rank_to_rewards_rank(&self, rank: u32) -> Option<u32> {
314 let mut rank_limit = 0;
315 let mut bracket = 0;
316 for bracket_len in &self.brackets {
317 bracket += 1;
318 rank_limit += *bracket_len;
319 if rank <= rank_limit {
320 return Some(bracket);
321 }
322 }
323 None
324 }
325
326 pub(crate) fn update(
327 &mut self,
328 data: &[i64],
329 server_time: ServerTime,
330 ) -> Result<(), SFError> {
331 self.key_cards = data.csiget(0, "h key cards", 0)?;
332 self.next_card_generated = data.cstget(1, "next card", server_time)?;
333 self.next_reset = data.cstget(2, "h next reset", server_time)?;
334 self.current_floor = data.csiget(3, "h current floor", 0)?;
335 self.points = data.csiget(4, "h points", 0)?;
336 self.start_contrib_date =
337 data.cstget(5, "start contrib", server_time)?;
338 self.has_final_reward = data.cget(6, "hellevator final")? == 1;
339 self.own_best_floor = data.csiget(7, "hellevator best rank", 0)?;
340
341 for (pos, shop_item) in self.shop_items.iter_mut().enumerate() {
342 let start = data.skip(8 + pos, "shop item start")?;
343 shop_item.typ = start
344 .cfpget(0, "hellevator shop treat", |a| a)?
345 .unwrap_or_default();
346 shop_item.is_special =
348 start.cget(3, "hellevator shop special")? > 0;
349 shop_item.price =
350 start.csiget(6, "hellevator shop price", u32::MAX)?;
351 shop_item.duration =
352 start.csiget(9, "hellevator shop duration", 0)?;
353 shop_item.effect_strength =
354 start.csiget(12, "hellevator effect str", 0)?;
355 }
356
357 let c_typ = data.cget(23, "current ctyp")?;
358 self.current_treat = if c_typ > 0 {
359 Some(HellevatorShopTreat {
360 typ: FromPrimitive::from_i64(c_typ).unwrap_or_default(),
361 is_special: data.cget(24, "current item special")? > 0,
362 price: 0,
363 duration: data.csiget(25, "current item remaining", 0)?,
364 effect_strength: data.csiget(26, "current item effect", 0)?,
365 })
366 } else {
367 None
368 };
369
370 self.earned_today = data.csiget(27, "points earned today", 0)?;
371 self.earned_yesterday = data.csiget(29, "points earned yd", 0)?;
373 Ok(())
376 }
377}
378
379#[derive(Debug, Default, Clone)]
380#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
381pub struct Witch {
382 pub required_item: Option<EquipmentSlot>,
384 pub cauldron_bubbling: bool,
386 pub progress: u32,
388 pub enchantment_price: u64,
390 pub enchantments: EnumMap<Enchantment, Option<EnchantmentIdent>>,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
399pub struct EnchantmentIdent(pub(crate) NonZeroU8);
400
401impl Witch {
402 pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
403 self.enchantment_price = data.csiget(35, "witch price", u64::MAX)?;
404 self.required_item = None;
405 if data.cget(33, "w needs more")? == 0 {
406 let raw_required = data.cget(34, "w required")?;
407 for slot in EquipmentSlot::iter() {
408 let id = i64::from(slot.raw_id());
409 if id == raw_required {
410 self.required_item = Some(slot);
411 break;
412 }
413 }
414 }
415 if self.required_item.is_none() {
416 self.cauldron_bubbling = true;
417 } else {
418 let current: i32 = data.ciget(2, "witch current")?;
421 let target: i32 = data.ciget(3, "witch target")?;
422 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
423 if current < 0 || target <= 0 {
424 self.progress = 100;
425 } else {
426 let current = f64::from(current);
427 let target = f64::from(target);
428 self.progress = ((current / target) * 100.0) as u32;
429 }
430 }
431
432 let e_count: u8 = data.ciget(4, "enchant count")?;
433 for i in 0..e_count {
434 let iid = data.cget(6 + 3 * i as usize, "iid")? - 1;
435 let key = match iid {
436 0 => continue,
437 10 => Enchantment::SwordOfVengeance,
438 30 => Enchantment::MariosBeard,
439 40 => Enchantment::ManyFeetBoots,
440 50 => Enchantment::ShadowOfTheCowboy,
441 60 => Enchantment::AdventurersArchaeologicalAura,
442 70 => Enchantment::ThirstyWanderer,
443 80 => Enchantment::UnholyAcquisitiveness,
444 90 => Enchantment::TheGraveRobbersPrayer,
445 100 => Enchantment::RobberBaronRitual,
446 x => {
447 warn!("Unknown witch enchant itemtype: {x}");
448 continue;
449 }
450 };
451 if let Some(val) = NonZeroU8::new(i + 1) {
452 *self.enchantments.get_mut(key) = Some(EnchantmentIdent(val));
453 }
454 }
455 Ok(())
456 }
457}
458
459#[derive(Debug, Clone, Default)]
460#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
461pub struct Blacksmith {
462 pub metal: u64,
463 pub arcane: u64,
464 pub dismantle_left: u8,
465 pub last_dismantled: Option<DateTime<Local>>,
467}
468
469const PETS_PER_HABITAT: usize = 20;
470
471#[derive(Debug, Default, Clone)]
472#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
473pub struct Pets {
474 pub total_collected: u16,
476 pub rank: u32,
478 pub honor: u32,
480 pub max_pet_level: u16,
481 pub opponent: PetOpponent,
483 pub habitats: EnumMap<HabitatType, Habitat>,
485 pub next_free_exploration: Option<DateTime<Local>>,
488 pub atr_bonus: EnumMap<AttributeType, u32>,
490}
491
492#[cfg(feature = "simulation")]
494static PET_BASE_STAT_ARRAY: [u32; 20] = [
495 10, 11, 12, 13, 14, 16, 18, 20, 25, 30, 35, 40, 50, 60, 70, 80, 100, 130,
496 160, 160,
497];
498
499#[cfg(feature = "simulation")]
501#[rustfmt::skip]
502static PET_CLASS_LOOKUP: EnumMap<HabitatType, [Class; 20]> =
503 EnumMap::from_array([
504 [
506 Class::Scout, Class::Warrior, Class::Warrior, Class::Mage,
507 Class::Mage, Class::Mage, Class::Scout, Class::Scout,
508 Class::Scout, Class::Warrior, Class::Mage, Class::Mage,
509 Class::Scout, Class::Scout, Class::Warrior, Class::Warrior,
510 Class::Mage, Class::Warrior, Class::Warrior, Class::Scout,
511 ],
512 [
514 Class::Warrior, Class::Warrior, Class::Mage, Class::Mage,
515 Class::Scout, Class::Scout, Class::Mage, Class::Warrior,
516 Class::Warrior, Class::Mage, Class::Mage, Class::Scout,
517 Class::Scout, Class::Mage, Class::Mage, Class::Warrior,
518 Class::Warrior, Class::Warrior, Class::Mage, Class::Scout,
519 ],
520 [
522 Class::Warrior, Class::Warrior, Class::Scout, Class::Scout,
523 Class::Warrior, Class::Scout, Class::Mage, Class::Mage,
524 Class::Warrior, Class::Warrior, Class::Scout, Class::Warrior,
525 Class::Scout, Class::Scout, Class::Mage, Class::Mage,
526 Class::Mage, Class::Warrior, Class::Warrior, Class::Warrior,
527 ],
528 [
530 Class::Scout, Class::Scout, Class::Warrior, Class::Mage,
531 Class::Mage, Class::Scout, Class::Scout, Class::Mage,
532 Class::Warrior, Class::Mage, Class::Mage, Class::Scout,
533 Class::Scout, Class::Scout, Class::Scout, Class::Scout,
534 Class::Mage, Class::Warrior, Class::Mage, Class::Warrior,
535 ],
536 [ Class::Mage, Class::Warrior, Class::Warrior, Class::Warrior,
538 Class::Warrior, Class::Scout, Class::Warrior, Class::Scout,
539 Class::Scout, Class::Warrior, Class::Mage, Class::Mage,
540 Class::Mage, Class::Warrior, Class::Mage, Class::Mage,
541 Class::Warrior, Class::Mage, Class::Warrior, Class::Scout,
542 ],
543 ]);
544
545impl Pets {
546 #[cfg(feature = "simulation")]
548 pub fn get_exploration_enemy(
549 &self,
550 habitat: HabitatType,
551 ) -> Option<crate::simulate::Monster> {
552 let h = &self.habitats[habitat];
553 let stage = match h.exploration {
554 HabitatExploration::Finished => return None,
555 HabitatExploration::Exploring { fights_won, .. } => fights_won,
556 };
557 crate::simulate::constants::PET_MONSTER
558 .get(&habitat)
559 .and_then(|a| a.get((stage) as usize))
560 .cloned()
561 }
562
563 #[cfg(feature = "simulation")]
567 #[must_use]
568 pub fn pet_to_fighter(
569 &self,
570 pet: &Pet,
571 gladiator: u32,
572 ) -> crate::simulate::Fighter {
573 let habitat_pets = &self.habitats[pet.element].pets;
574 let pack_bonus = habitat_pets
575 .iter()
576 .map(|a| match a.level {
577 0 => 0.0,
578 _ => 0.05,
579 })
580 .sum::<f64>();
581
582 let level_bonus = habitat_pets
583 .iter()
584 .map(|p| match p.level {
585 ..100 => 0.0,
586 100..150 => 0.05,
587 150..200 => 0.75,
588 200.. => 0.1,
589 })
590 .sum::<f64>();
591
592 let habitat_idx = habitat_pets
593 .iter()
594 .position(|a| a.id == pet.id)
595 .unwrap_or(0);
596
597 let base_stat =
598 PET_BASE_STAT_ARRAY.get(habitat_idx).copied().unwrap_or(0);
599 let high_stat = (f64::from(base_stat * (u32::from(pet.level) + 1))
600 * (1.0 + pack_bonus + level_bonus))
601 .floor();
602 let low_stat = (0.5 * high_stat).round();
603 let luck = (0.75 * high_stat).round();
604 let con = high_stat;
605
606 let class = *PET_CLASS_LOOKUP[pet.element]
607 .get(habitat_idx)
608 .unwrap_or(&Class::Warrior);
609
610 let (str, dex, int) = match class {
611 Class::Warrior => (high_stat, low_stat, low_stat),
612 Class::Mage => (low_stat, low_stat, high_stat),
613 _ => (low_stat, high_stat, low_stat),
614 };
615
616 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
617 let pet_fighter = crate::simulate::UpgradeableFighter {
618 name: format!(
619 "{:?} pet #{} ({}) ",
620 pet.element,
621 pet.id,
622 habitat_idx + 1
623 )
624 .into(),
625 class,
626 level: pet.level,
627 attribute_basis: EnumMap::from_array([
628 str as u32,
629 dex as u32,
630 int as u32,
631 con as u32,
632 luck as u32,
633 ]),
634 is_companion: false,
635 pet_attribute_bonus_perc: EnumMap::default(),
636 equipment: Equipment::default(),
637 active_potions: Default::default(),
638 portal_hp_bonus: 0,
639 portal_dmg_bonus: 0,
640 gladiator,
641 };
642 (&pet_fighter).into()
643 }
644}
645
646#[derive(Debug, Default, Clone)]
647#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
648pub struct Habitat {
649 pub exploration: HabitatExploration,
651 pub fruits: u16,
653 pub battled_opponent: bool,
656 pub pets: [Pet; PETS_PER_HABITAT],
658}
659
660#[derive(Debug, Default, Clone)]
662#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
663pub enum HabitatExploration {
664 #[default]
665 Finished,
668 Exploring {
670 fights_won: u32,
673 next_fight_lvl: u16,
675 },
676}
677
678#[derive(Debug, Default, Clone)]
679#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
680pub struct PetOpponent {
681 pub id: PlayerId,
682 pub pet_count: u32,
683 pub level_total: u32,
684 pub next_free_battle: Option<DateTime<Local>>,
686 pub reroll_date: Option<DateTime<Local>>,
688 pub habitat: Option<HabitatType>,
689}
690
691impl Pets {
692 pub(crate) fn update(
693 &mut self,
694 data: &[i64],
695 server_time: ServerTime,
696 ) -> Result<(), SFError> {
697 let mut pet_id = 0;
698 for (element_idx, element) in [
699 HabitatType::Shadow,
700 HabitatType::Light,
701 HabitatType::Earth,
702 HabitatType::Fire,
703 HabitatType::Water,
704 ]
705 .into_iter()
706 .enumerate()
707 {
708 let info = self.habitats.get_mut(element);
709 let explored = data.csiget(210 + element_idx, "pet exp", 20)?;
710 info.exploration = if explored == 20 {
711 HabitatExploration::Finished
712 } else {
713 let next_lvl =
714 data.csiget(238 + element_idx, "next exp pet lvl", 1_000)?;
715 HabitatExploration::Exploring {
716 fights_won: explored,
717 next_fight_lvl: next_lvl,
718 }
719 };
720 for (pet_pos, pet) in info.pets.iter_mut().enumerate() {
721 pet_id += 1;
722 pet.id = pet_id;
723 pet.level =
724 data.csiget((pet_id + 1) as usize, "pet level", 0)?;
725 pet.fruits_today =
726 data.csiget((pet_id + 109) as usize, "pet fruits td", 0)?;
727 pet.element = element;
728 pet.can_be_found =
729 pet.level == 0 && explored as usize >= pet_pos;
730 }
731 info.battled_opponent =
732 1 == data.cget(223 + element_idx, "element ff")?;
733 }
734
735 self.total_collected = data.csiget(103, "total pets", 0)?;
736 self.opponent.id = data.csiget(231, "pet opponent id", 0)?;
737 self.opponent.next_free_battle =
738 data.cstget(232, "next free pet fight", server_time)?;
739 self.rank = data.csiget(233, "pet rank", 0)?;
740 self.honor = data.csiget(234, "pet honor", 0)?;
741
742 self.opponent.pet_count = data.csiget(235, "pet enemy count", 0)?;
743 self.opponent.level_total =
744 data.csiget(236, "pet enemy lvl total", 0)?;
745 self.opponent.reroll_date =
746 data.cstget(237, "pet enemy reroll date", server_time)?;
747
748 update_enum_map(&mut self.atr_bonus, data.skip(250, "pet atr boni")?);
749 Ok(())
750 }
751
752 pub(crate) fn update_pet_stat(&mut self, data: &[i64]) {
753 match PetStats::parse(data) {
754 Ok(ps) => {
755 let idx = ps.id;
756 if let Some(pet) =
757 self.habitats.get_mut(ps.element).pets.get_mut(idx % 20)
758 {
759 pet.stats = Some(ps);
760 }
761 }
762 Err(e) => {
763 error!("Could not parse pet stats: {e}");
764 }
765 }
766 }
767}
768
769#[derive(Debug, Default, Clone)]
770#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
771pub struct Pet {
772 pub id: u32,
774 pub level: u16,
775 pub fruits_today: u16,
777 pub element: HabitatType,
778 pub stats: Option<PetStats>,
780 pub can_be_found: bool,
782}
783
784#[derive(Debug, Default, Clone)]
785#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
786pub struct PetStats {
787 pub id: usize,
788 pub level: u16,
789 pub armor: u16,
790 pub class: Class,
791 pub attributes: EnumMap<AttributeType, u32>,
792 pub bonus_attributes: EnumMap<AttributeType, u32>,
793 pub min_damage: u16,
794 pub max_damage: u16,
795 pub element: HabitatType,
796}
797
798#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Enum, EnumIter, Hash)]
799#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
800pub enum HabitatType {
801 #[default]
802 Shadow = 0,
803 Light = 1,
804 Earth = 2,
805 Fire = 3,
806 Water = 4,
807}
808
809impl From<HabitatType> for AttributeType {
810 fn from(value: HabitatType) -> Self {
811 match value {
812 HabitatType::Water => AttributeType::Strength,
813 HabitatType::Light => AttributeType::Dexterity,
814 HabitatType::Earth => AttributeType::Intelligence,
815 HabitatType::Shadow => AttributeType::Constitution,
816 HabitatType::Fire => AttributeType::Luck,
817 }
818 }
819}
820
821impl HabitatType {
822 pub(crate) fn from_pet_id(id: i64) -> Option<Self> {
823 Some(match id {
824 1..=20 => HabitatType::Shadow,
825 21..=40 => HabitatType::Light,
826 41..=60 => HabitatType::Earth,
827 61..=80 => HabitatType::Fire,
828 81..=100 => HabitatType::Water,
829 _ => return None,
830 })
831 }
832
833 pub(crate) fn from_typ_id(id: i64) -> Option<Self> {
834 Some(match id {
835 1 => HabitatType::Shadow,
836 2 => HabitatType::Light,
837 3 => HabitatType::Earth,
838 4 => HabitatType::Fire,
839 5 => HabitatType::Water,
840 _ => return None,
841 })
842 }
843}
844
845impl PetStats {
846 pub(crate) fn parse(data: &[i64]) -> Result<Self, SFError> {
847 let pet_id: u32 = data.csiget(0, "pet index", 0)?;
848 let mut s = Self {
849 id: pet_id as usize,
850 level: data.csiget(1, "pet lvl", 0)?,
851 armor: data.csiget(2, "pet armor", 0)?,
852 class: data.cfpuget(3, "pet class", |a| a)?,
853 min_damage: data.csiget(14, "min damage", 0)?,
854 max_damage: data.csiget(15, "max damage", 0)?,
855
856 element: match data.cget(16, "pet element")? {
857 0 => HabitatType::from_pet_id(i64::from(pet_id)).ok_or_else(
858 || SFError::ParsingError("det pet typ", pet_id.to_string()),
859 )?,
860 x => HabitatType::from_typ_id(x).ok_or_else(|| {
861 SFError::ParsingError("det pet typ", x.to_string())
862 })?,
863 },
864 ..Default::default()
865 };
866 update_enum_map(&mut s.attributes, data.skip(4, "pet attrs")?);
867 update_enum_map(&mut s.bonus_attributes, data.skip(9, "pet bonus")?);
868 Ok(s)
869 }
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq)]
873#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
874pub struct Unlockable {
875 pub main_ident: i64,
877 pub sub_ident: i64,
879}
880
881impl Unlockable {
882 pub(crate) fn parse(data: &[i64]) -> Result<Vec<Unlockable>, SFError> {
883 data.chunks_exact(2)
884 .filter(|chunk| chunk.first().copied().unwrap_or_default() != 0)
885 .map(|chunk| {
886 Ok(Unlockable {
887 main_ident: chunk.cget(0, "unlockable ident")?,
888 sub_ident: chunk.cget(1, "unlockable sub ident")?,
889 })
890 })
891 .collect()
892 }
893}
894
895#[derive(Debug, Default, Clone)]
897#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
898pub struct Achievements(pub Vec<Achievement>);
899
900impl Achievements {
901 pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
902 self.0.clear();
903 let total_count = data.len() / 2;
904 if !data.len().is_multiple_of(2) {
905 warn!("achievement data has the wrong length: {}", data.len());
906 return Ok(());
907 }
908
909 for i in 0..total_count {
910 self.0.push(Achievement {
911 achieved: data.cget(i, "achievement achieved")? == 1,
912 progress: data.cget(i + total_count, "achievement achieved")?,
913 });
914 }
915 Ok(())
916 }
917
918 #[must_use]
920 pub fn owned(&self) -> u32 {
921 self.0.iter().map(|a| u32::from(a.achieved)).sum()
922 }
923}
924
925#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
927#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
928pub struct Achievement {
929 pub achieved: bool,
931 pub progress: i64,
933}
934
935#[derive(Debug, Clone)]
937#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
938pub struct ScrapBook {
939 pub items: HashSet<EquipmentIdent>,
943 pub monster: HashSet<u16>,
947}
948
949impl ScrapBook {
950 pub(crate) fn parse(val: &str) -> Option<ScrapBook> {
953 let text = base64::Engine::decode(
954 &base64::engine::general_purpose::URL_SAFE,
955 val,
956 )
957 .ok()?;
958 if text.iter().all(|a| *a == 0) {
959 return None;
960 }
961
962 let mut index = 0;
963 let mut items = HashSet::new();
964 let mut monster = HashSet::new();
965
966 for byte in text {
967 for bit_pos in (0..=7).rev() {
968 index += 1;
969 let is_owned = ((byte >> bit_pos) & 1) == 1;
970 if !is_owned {
971 continue;
972 }
973 if index < 801 {
974 monster.insert(index.try_into().unwrap_or_default());
976 } else if let Some(ident) = parse_scrapbook_item(index) {
977 if !items.insert(ident) {
979 error!(
980 "Two scrapbook positions parsed to the same \
981 ident: {index}"
982 );
983 }
984 } else {
985 error!("Owned, but not parsed: {index}");
986 }
987 }
988 }
989 Some(ScrapBook { items, monster })
990 }
991}
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
995#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
996pub struct EquipmentIdent {
997 pub class: Option<Class>,
999 pub typ: EquipmentSlot,
1001 pub model_id: u16,
1003 pub color: u8,
1005}
1006
1007#[allow(clippy::to_string_trait_impl)]
1008impl ToString for EquipmentIdent {
1009 fn to_string(&self) -> String {
1010 let item_typ = self.typ.raw_id();
1011 let model_id = self.model_id;
1012 let color = self.color;
1013
1014 if let Some(class) = self.class {
1015 let ci = class as u8 + 1;
1016 format!("itm{item_typ}_{model_id}_{color}_{ci}")
1017 } else {
1018 format!("itm{item_typ}_{model_id}_{color}")
1019 }
1020 }
1021}
1022
1023#[allow(clippy::enum_glob_use)]
1024fn parse_scrapbook_item(item_idx: i64) -> Option<EquipmentIdent> {
1025 use Class::*;
1026 use EquipmentSlot::*;
1027 let slots: [(_, _, _, &[_]); 44] = [
1028 (801..1011, Amulet, None, &[]),
1029 (1011..1051, Amulet, None, &[]),
1030 (1051..1211, Ring, None, &[]),
1031 (1211..1251, Ring, None, &[]),
1032 (1251..1325, Talisman, None, &[]),
1033 (1325..1365, Talisman, None, &[]),
1034 (1365..1665, Weapon, Some(Warrior), &[]),
1035 (1665..1705, Weapon, Some(Warrior), &[]),
1036 (1705..1805, Shield, Some(Warrior), &[]),
1037 (1805..1845, Shield, Some(Warrior), &[]),
1038 (1845..1945, BreastPlate, Some(Warrior), &[]),
1039 (1945..1985, BreastPlate, Some(Warrior), &[1954, 1955]),
1040 (1985..2085, FootWear, Some(Warrior), &[]),
1041 (2085..2125, FootWear, Some(Warrior), &[2094, 2095]),
1042 (2125..2225, Gloves, Some(Warrior), &[]),
1043 (2225..2265, Gloves, Some(Warrior), &[2234, 2235]),
1044 (2265..2365, Hat, Some(Warrior), &[]),
1045 (2365..2405, Hat, Some(Warrior), &[2374, 2375]),
1046 (2405..2505, Belt, Some(Warrior), &[]),
1047 (2505..2545, Belt, Some(Warrior), &[2514, 2515]),
1048 (2545..2645, Weapon, Some(Mage), &[]),
1049 (2645..2685, Weapon, Some(Mage), &[]),
1050 (2685..2785, BreastPlate, Some(Mage), &[]),
1051 (2785..2825, BreastPlate, Some(Mage), &[2794, 2795]),
1052 (2825..2925, FootWear, Some(Mage), &[]),
1053 (2925..2965, FootWear, Some(Mage), &[2934, 2935]),
1054 (2965..3065, Gloves, Some(Mage), &[]),
1055 (3065..3105, Gloves, Some(Mage), &[3074, 3075]),
1056 (3105..3205, Hat, Some(Mage), &[]),
1057 (3205..3245, Hat, Some(Mage), &[3214, 3215]),
1058 (3245..3345, Belt, Some(Mage), &[]),
1059 (3345..3385, Belt, Some(Mage), &[3354, 3355]),
1060 (3385..3485, Weapon, Some(Scout), &[]),
1061 (3485..3525, Weapon, Some(Scout), &[]),
1062 (3525..3625, BreastPlate, Some(Scout), &[]),
1063 (3625..3665, BreastPlate, Some(Scout), &[3634, 3635]),
1064 (3665..3765, FootWear, Some(Scout), &[]),
1065 (3765..3805, FootWear, Some(Scout), &[3774, 3775]),
1066 (3805..3905, Gloves, Some(Scout), &[]),
1067 (3905..3945, Gloves, Some(Scout), &[3914, 3915]),
1068 (3945..4045, Hat, Some(Scout), &[]),
1069 (4045..4085, Hat, Some(Scout), &[4054, 4055]),
1070 (4085..4185, Belt, Some(Scout), &[]),
1071 (4185..4225, Belt, Some(Scout), &[4194, 4195]),
1072 ];
1073
1074 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1075 for (pos, (range, typ, class, ignore)) in slots.into_iter().enumerate() {
1076 if !range.contains(&item_idx) {
1077 continue;
1078 }
1079 if ignore.contains(&item_idx) {
1080 return None;
1081 }
1082
1083 let is_epic = pos % 2 == 1;
1084 let relative_pos = item_idx - range.start + 1;
1085
1086 let color = match relative_pos % 10 {
1087 _ if typ == Talisman || is_epic => 1,
1088 0 => 5,
1089 1..=5 => relative_pos % 10,
1090 _ => relative_pos % 10 - 5,
1091 } as u8;
1092
1093 let model_id = match () {
1094 () if is_epic => relative_pos + 49,
1095 () if typ == Talisman => relative_pos,
1096 () if relative_pos % 5 != 0 => relative_pos / 5 + 1,
1097 () => relative_pos / 5,
1098 } as u16;
1099
1100 return Some(EquipmentIdent {
1101 class,
1102 typ,
1103 model_id,
1104 color,
1105 });
1106 }
1107 None
1108}