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