1use std::cmp::Ordering;
2
3use chrono::{DateTime, Local};
4use enum_map::{Enum, EnumMap};
5use log::warn;
6use num_derive::FromPrimitive;
7use num_traits::FromPrimitive;
8use strum::{EnumCount, EnumIter};
9
10use super::{
11 CFPGet, Class, EnumMapGet, HabitatType, SFError, ServerTime,
12 unlockables::EquipmentIdent,
13};
14use crate::{
15 command::{AttributeType, ShopType},
16 gamestate::{CCGet, CGet, ShopPosition},
17};
18
19#[derive(Debug, Default, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct Inventory {
23 pub backpack: Vec<Option<Item>>,
24}
25
26#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct BagPosition(pub(crate) usize);
30
31impl BagPosition {
32 #[must_use]
34 pub fn backpack_pos(&self) -> usize {
35 self.0
36 }
37 #[must_use]
41 pub fn inventory_pos(&self) -> (InventoryType, usize) {
42 let pos = self.0;
43 if pos <= 4 {
44 (InventoryType::MainInventory, pos)
45 } else {
46 (InventoryType::ExtendedInventory, pos - 5)
47 }
48 }
49}
50
51impl Inventory {
52 #[must_use]
57 pub fn as_split(&self) -> (&[Option<Item>], &[Option<Item>]) {
58 if self.backpack.len() < 5 {
59 return (&[], &[]);
60 }
61 self.backpack.split_at(5)
62 }
63
64 #[must_use]
69 pub fn as_split_mut(
70 &mut self,
71 ) -> (&mut [Option<Item>], &mut [Option<Item>]) {
72 if self.backpack.len() < 5 {
73 return (&mut [], &mut []);
74 }
75 self.backpack.split_at_mut(5)
76 }
77
78 #[must_use]
82 pub fn free_slot(&self) -> Option<BagPosition> {
83 for (pos, item) in self.iter() {
84 if item.is_none() {
85 return Some(pos);
86 }
87 }
88 None
89 }
90
91 #[must_use]
92 pub fn count_free_slots(&self) -> usize {
93 self.backpack.iter().filter(|slot| slot.is_none()).count()
94 }
95
96 pub fn iter(&self) -> impl Iterator<Item = (BagPosition, Option<&Item>)> {
98 self.backpack
99 .iter()
100 .enumerate()
101 .map(|(pos, item)| (BagPosition(pos), item.as_ref()))
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108#[allow(missing_docs)]
109pub enum PlayerItemPlace {
110 Equipment = 1,
111 MainInventory = 2,
112 ExtendedInventory = 5,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct ItemPosition {
118 pub place: ItemPlace,
119 pub position: usize,
120}
121
122impl std::fmt::Display for ItemPosition {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{}/{}", self.place as usize, self.position + 1)
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct PlayerItemPosition {
131 pub place: PlayerItemPlace,
132 pub position: usize,
133}
134
135impl std::fmt::Display for PlayerItemPosition {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 write!(f, "{}/{}", self.place as usize, self.position + 1)
138 }
139}
140
141impl From<PlayerItemPosition> for ItemPosition {
142 fn from(value: PlayerItemPosition) -> Self {
143 Self {
144 place: value.place.item_position(),
145 position: value.position,
146 }
147 }
148}
149
150impl From<BagPosition> for ItemPosition {
151 fn from(value: BagPosition) -> Self {
152 let player: PlayerItemPosition = value.into();
153 player.into()
154 }
155}
156
157impl From<EquipmentSlot> for ItemPosition {
158 fn from(value: EquipmentSlot) -> Self {
159 let player: PlayerItemPosition = value.into();
160 player.into()
161 }
162}
163
164impl From<ShopPosition> for ItemPosition {
165 fn from(value: ShopPosition) -> Self {
166 Self {
167 place: value.typ.into(),
168 position: value.pos,
169 }
170 }
171}
172
173impl From<ShopType> for ItemPlace {
174 fn from(value: ShopType) -> Self {
175 match value {
176 ShopType::Weapon => ItemPlace::WeaponShop,
177 ShopType::Magic => ItemPlace::MageShop,
178 }
179 }
180}
181
182impl From<BagPosition> for PlayerItemPosition {
183 fn from(value: BagPosition) -> Self {
184 let p = value.inventory_pos();
185 Self {
186 place: p.0.player_item_position(),
187 position: p.1,
188 }
189 }
190}
191
192impl From<EquipmentSlot> for PlayerItemPosition {
193 fn from(value: EquipmentSlot) -> Self {
194 Self {
195 place: PlayerItemPlace::Equipment,
196 position: value as usize - 1,
197 }
198 }
199}
200
201impl PlayerItemPlace {
202 #[must_use]
205 pub fn item_position(&self) -> ItemPlace {
206 match self {
207 PlayerItemPlace::Equipment => ItemPlace::Equipment,
208 PlayerItemPlace::MainInventory => ItemPlace::MainInventory,
209 PlayerItemPlace::ExtendedInventory => ItemPlace::FortressChest,
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[allow(missing_docs)]
218pub enum InventoryType {
219 MainInventory = 2,
220 ExtendedInventory = 5,
221}
222
223impl InventoryType {
224 #[must_use]
227 pub fn item_position(&self) -> ItemPlace {
228 match self {
229 InventoryType::MainInventory => ItemPlace::MainInventory,
230 InventoryType::ExtendedInventory => ItemPlace::FortressChest,
231 }
232 }
233 #[must_use]
236 pub fn player_item_position(&self) -> PlayerItemPlace {
237 match self {
238 InventoryType::MainInventory => PlayerItemPlace::MainInventory,
239 InventoryType::ExtendedInventory => {
240 PlayerItemPlace::ExtendedInventory
241 }
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub enum ItemPlace {
250 Equipment = 1,
252 MainInventory = 2,
254 WeaponShop = 3,
256 MageShop = 4,
258 FortressChest = 5,
260}
261
262#[derive(Debug, Default, Clone)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
265pub struct Equipment(pub EnumMap<EquipmentSlot, Option<Item>>);
266
267impl Equipment {
268 #[must_use]
270 pub fn has_enchantment(&self, enchantment: Enchantment) -> bool {
271 let item = self.0.get(enchantment.equipment_slot());
272 if let Some(item) = item {
273 return item.enchantment == Some(enchantment);
274 }
275 false
276 }
277
278 #[allow(clippy::indexing_slicing)]
280 pub(crate) fn parse(
281 data: &[i64],
282 server_time: ServerTime,
283 ) -> Result<Equipment, SFError> {
284 let mut res = Equipment::default();
285 if !data.len().is_multiple_of(ITEM_PARSE_LEN) {
286 return Err(SFError::ParsingError(
287 "Invalid Equipment",
288 format!("{data:?}"),
289 ));
290 }
291 for (chunk, slot) in
292 data.chunks_exact(ITEM_PARSE_LEN).zip(res.0.as_mut_slice())
293 {
294 *slot = Item::parse(chunk, server_time)?;
295 }
296 Ok(res)
297 }
298}
299
300pub(crate) const ITEM_PARSE_LEN: usize = 19;
301
302#[derive(Debug, Clone, PartialEq, Eq)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306pub struct Item {
307 pub typ: ItemType,
309 pub price: u32,
311 pub mushroom_price: u32,
315 pub full_model_id: u32,
319 pub model_id: u16,
321 pub class: Option<Class>,
324 pub type_specific_val: u32,
328 pub attributes: EnumMap<AttributeType, u32>,
330 pub gem_slot: Option<GemSlot>,
332 pub rune: Option<Rune>,
334 pub enchantment: Option<Enchantment>,
336 pub color: u8,
339 pub upgrade_count: u8,
341 pub item_quality: u32,
343 pub is_washed: bool,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq)]
348#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
349pub struct ItemCommandIdent {
350 typ: u8,
351 full_model_id: u32,
352 price: u32,
353 mush_price: u32,
354}
355
356impl std::fmt::Display for ItemCommandIdent {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(
359 f,
360 "{}/{}/{}/{}",
361 self.typ, self.full_model_id, self.price, self.mush_price
362 )
363 }
364}
365
366#[derive(Debug, Clone, Copy)]
367pub struct BlacksmithPayment {
368 pub metal: u64,
369 pub arcane: u64,
370}
371
372impl Item {
373 #[must_use]
380 pub fn dismantle_reward(&self) -> BlacksmithPayment {
381 let mut attribute_val =
382 f64::from(*self.attributes.values().max().unwrap_or(&0));
383 let item_stats = self.attributes.values().filter(|a| **a > 0).count();
384 let is_scout_or_mage_weapon = self
385 .class
386 .is_some_and(|a| a == Class::Scout || a == Class::Mage)
387 && self.typ.is_weapon();
388
389 if self.price != 0 {
390 for _ in 0..self.upgrade_count {
391 attribute_val = (attribute_val / 1.04).round();
392 }
393 }
394
395 if item_stats >= 4 {
396 attribute_val *= 1.2;
397 }
398 if is_scout_or_mage_weapon {
399 attribute_val /= 2.0;
400 }
401 if (item_stats == 1) && attribute_val > 66.0 {
403 attribute_val = attribute_val.round() * 0.75;
404 }
405
406 attribute_val = attribute_val.round().powf(1.2).floor();
407
408 let (min_dmg, max_dmg) = match self.typ {
409 ItemType::Weapon { min_dmg, max_dmg } => (min_dmg, max_dmg),
410 _ => (0, 0),
411 };
412
413 let price = (u32::from(self.typ.raw_id()) * 37)
414 + (self.full_model_id * 83)
415 + (min_dmg * 1731)
416 + (max_dmg * 162);
417
418 let (metal_price, arcane_price) = match item_stats {
419 1 => (75 + (price % 26), price % 2),
420 2 => (50 + (price % 31), 5 + (price % 6)),
421 _ => (25 + (price % 26), 50 + (price % 51)),
423 };
424
425 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
426 let calc_result = |rng: u32| {
427 ((attribute_val * f64::from(rng)) / 100.0).floor() as u64
428 };
429 let mut metal_result = calc_result(metal_price);
430 let mut arcane_result = calc_result(arcane_price);
431
432 if is_scout_or_mage_weapon {
433 metal_result *= 2;
434 arcane_result *= 2;
435 }
436 BlacksmithPayment {
437 metal: metal_result * 2,
438 arcane: arcane_result * 2,
439 }
440 }
441
442 #[must_use]
450 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
451 pub fn upgrade_costs(&self) -> Option<BlacksmithPayment> {
452 if self.upgrade_count >= 20 || self.equipment_ident().is_none() {
453 return None;
454 }
455
456 let item_stats = self.attributes.values().filter(|a| **a > 0).count();
457 let is_scout_or_mage_weapon = self
458 .class
459 .is_some_and(|a| a == Class::Scout || a == Class::Mage)
460 && self.typ.is_weapon();
461
462 let mut price =
464 f64::from(*self.attributes.values().max().unwrap_or(&0));
465
466 if item_stats >= 4 {
468 price *= 1.2;
469 }
470
471 if is_scout_or_mage_weapon {
472 price /= 2.0;
473 }
474
475 if item_stats == 1 && price > 66.0 {
477 price = (price * 0.75).ceil();
478 }
479
480 price = price.round().powf(1.2).floor();
481
482 let mut metal_price = 50;
483 let mut arcane_price = match item_stats {
484 1 => 25,
485 2 => 50,
486 _ => 75,
488 };
489
490 let i = i64::from(self.upgrade_count);
491 match i {
492 0 => {
493 metal_price *= 3;
494 arcane_price = 0;
495 }
496 1 => {
497 metal_price *= 4;
498 arcane_price = 1;
499 }
500 2..=7 => {
501 metal_price *= i + 3;
502 arcane_price *= i - 1;
503 }
504 8 => {
505 metal_price *= 12;
506 arcane_price *= 8;
507 }
508 9 => {
509 metal_price *= 15;
510 arcane_price *= 10;
511 }
512 _ => {
513 metal_price *= i + 6;
514 arcane_price *= 10 + 2 * (i - 9);
515 }
516 }
517
518 metal_price = ((price * (metal_price as f64)) / 100.0).floor() as i64;
519 arcane_price = ((price * (arcane_price as f64)) / 100.0).floor() as i64;
520
521 if is_scout_or_mage_weapon {
522 metal_price *= 2;
523 arcane_price *= 2;
524 }
525
526 Some(BlacksmithPayment {
527 metal: metal_price.try_into().unwrap_or(0),
528 arcane: arcane_price.try_into().unwrap_or(0),
529 })
530 }
531
532 #[must_use]
535 pub fn equipment_ident(&self) -> Option<EquipmentIdent> {
536 Some(EquipmentIdent {
537 class: self.class,
538 typ: self.typ.equipment_slot()?,
539 model_id: self.model_id,
540 color: self.color,
541 })
542 }
543
544 #[must_use]
548 pub fn command_ident(&self) -> ItemCommandIdent {
549 ItemCommandIdent {
550 typ: self.typ.raw_id(),
551 full_model_id: self.full_model_id,
552 price: self.price,
553 mush_price: self.mushroom_price,
554 }
555 }
556
557 #[must_use]
560 pub fn is_unique(&self) -> bool {
561 self.typ.is_unique()
562 }
563
564 #[must_use]
566 pub fn is_epic(&self) -> bool {
567 self.model_id >= 50
568 }
569
570 #[must_use]
572 pub fn is_legendary(&self) -> bool {
573 self.model_id >= 90
574 }
575
576 #[must_use]
578 pub fn armor(&self) -> u32 {
579 #[allow(clippy::enum_glob_use)]
580 use ItemType::*;
581 match self.typ {
582 Hat | BreastPlate | Gloves | FootWear | Amulet | Belt | Ring
583 | Talisman => self.type_specific_val,
584 _ => 0,
585 }
586 }
587
588 #[must_use]
590 pub fn is_enchantable(&self) -> bool {
591 self.typ.is_enchantable()
592 }
593
594 #[must_use]
599 pub fn can_be_equipped_by_companion(
600 &self,
601 class: impl Into<Class>,
602 ) -> bool {
603 !self.typ.is_shield() && self.can_be_equipped_by(class.into())
604 }
605
606 #[must_use]
614 pub fn can_be_equipped_by(&self, class: Class) -> bool {
615 self.typ.equipment_slot().is_some() && self.can_be_used_by(class)
616 }
617
618 #[must_use]
624 #[allow(clippy::enum_glob_use, clippy::match_same_arms)]
625 pub fn can_be_used_by(&self, class: Class) -> bool {
626 use Class::*;
627
628 let Some(class_requirement) = self.class else {
630 return true;
631 };
632
633 match class {
634 Warrior | Paladin => class_requirement == Warrior,
635 Berserker => class_requirement == Warrior && !self.typ.is_shield(),
636 Scout => class_requirement == Scout,
637 Mage | Necromancer => class_requirement == Mage,
638 Assassin => match class_requirement {
639 Warrior => self.typ.is_weapon(),
640 Scout => !self.typ.is_weapon(),
641 _ => false,
642 },
643 Bard | Druid => match class_requirement {
644 Mage => self.typ.is_weapon(),
645 Scout => !self.typ.is_weapon(),
646 _ => false,
647 },
648 BattleMage | PlagueDoctor => match class_requirement {
649 Warrior => self.typ.is_weapon(),
650 Mage => !self.typ.is_weapon(),
651 _ => false,
652 },
653 DemonHunter => match class_requirement {
654 Scout => self.typ.is_weapon(),
655 Warrior => !self.typ.is_weapon() && !self.typ.is_shield(),
656 _ => false,
657 },
658 }
659 }
660
661 pub(crate) fn parse(
663 data: &[i64],
664 server_time: ServerTime,
665 ) -> Result<Option<Self>, SFError> {
666 let Some(typ) = ItemType::parse(data, server_time)? else {
667 return Ok(None);
668 };
669
670 let enchantment = data.cfpget(2, "item enchantment", |a| a)?;
671 let gem_slot_val = data.cimget(1, "gem slot val", |a| a)?;
672 let gem_pwr = data.cimget(16, "gem pwr", |a| a)?;
673
674 let gem_slot = GemSlot::parse(gem_slot_val, gem_pwr);
675
676 let class = if typ.is_class_item() {
677 data.cfpget(3, "item class", |x| (x & 0xFFFF) / 1000)?
678 } else {
679 None
680 };
681 let mut rune = None;
682 let mut attributes: EnumMap<AttributeType, u32> = EnumMap::default();
683 let price = data.csiget(13, "item price", u32::MAX)?;
684
685 if typ.equipment_slot().is_some() {
686 for i in 0..3 {
687 let atr_typ = data.cget(i + 7, "item atr typ")?;
688 let Ok(atr_typ) = atr_typ.try_into() else {
689 warn!("Invalid attribute typ: {atr_typ}, {typ:?}");
690 continue;
691 };
692 let atr_val = data.cget(i + 10, "item atr val")?;
693 let Ok(atr_val): Result<u32, _> = atr_val.try_into() else {
694 warn!("Invalid attribute value: {atr_val}, {typ:?}");
695 continue;
696 };
697 match atr_typ {
698 0 => {}
699 1..=5 => {
700 let Some(atr_typ) = FromPrimitive::from_usize(atr_typ)
701 else {
702 continue;
703 };
704 *attributes.get_mut(atr_typ) += atr_val;
705 }
706 6 => {
707 for atr in attributes.values_mut() {
708 *atr += atr_val;
709 }
710 }
711 21 => {
712 for atr in [
713 AttributeType::Strength,
714 AttributeType::Constitution,
715 AttributeType::Luck,
716 ] {
717 *attributes.get_mut(atr) += atr_val;
718 }
719 }
720 22 => {
721 for atr in [
722 AttributeType::Dexterity,
723 AttributeType::Constitution,
724 AttributeType::Luck,
725 ] {
726 *attributes.get_mut(atr) += atr_val;
727 }
728 }
729 23 => {
730 for atr in [
731 AttributeType::Intelligence,
732 AttributeType::Constitution,
733 AttributeType::Luck,
734 ] {
735 *attributes.get_mut(atr) += atr_val;
736 }
737 }
738 rune_typ => {
739 let Some(typ) = FromPrimitive::from_usize(rune_typ)
740 else {
741 warn!(
742 "Unhandled item val: {atr_typ} -> {atr_val} \
743 for {class:?} {typ:?}",
744 );
745 continue;
746 };
747 let Ok(value) = atr_val.try_into() else {
748 warn!("Rune value too big for a u8: {atr_val}");
749 continue;
750 };
751 rune = Some(Rune { typ, value });
752 }
753 }
754 }
755 }
756 let model_id: u16 =
757 data.cimget(3, "item model id", |x| (x & 0xFFFF) % 1000)?;
758
759 let color = match model_id {
760 ..=49 if typ != ItemType::Talisman => data
761 .get(5..=12)
762 .map(|a| a.iter().sum::<i64>())
763 .map(|a| (a % 5) + 1)
764 .and_then(|a| a.try_into().ok())
765 .unwrap_or(1),
766 _ => 1,
767 };
768
769 let item = Item {
770 typ,
771 model_id,
772 rune,
773 type_specific_val: data.csiget(5, "effect value", 0)?,
774 gem_slot,
775 enchantment,
776 class,
777 attributes,
778 color,
779 price,
780 mushroom_price: data.csiget(14, "mushroom price", u32::MAX)?,
781 upgrade_count: data.csiget(15, "upgrade count", u8::MAX)?,
782 item_quality: data.csiget(17, "item quality", 0)?,
783 is_washed: data.csiget(18, "is washed", 0)? != 0,
784 full_model_id: data.csiget(3, "raw model id", 0)?,
785 };
786 Ok(Some(item))
787 }
788}
789
790#[derive(
792 Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash, Enum,
793)]
794#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
795pub enum Enchantment {
796 SwordOfVengeance = 11,
798 MariosBeard = 31,
800 ManyFeetBoots = 41,
802 ShadowOfTheCowboy = 51,
804 AdventurersArchaeologicalAura = 61,
806 ThirstyWanderer = 71,
808 UnholyAcquisitiveness = 81,
810 TheGraveRobbersPrayer = 91,
812 RobberBaronRitual = 101,
814}
815
816impl Enchantment {
817 #[must_use]
818 pub fn equipment_slot(&self) -> EquipmentSlot {
819 match self {
820 Enchantment::SwordOfVengeance => EquipmentSlot::Weapon,
821 Enchantment::MariosBeard => EquipmentSlot::BreastPlate,
822 Enchantment::ManyFeetBoots => EquipmentSlot::FootWear,
823 Enchantment::ShadowOfTheCowboy => EquipmentSlot::Gloves,
824 Enchantment::AdventurersArchaeologicalAura => EquipmentSlot::Hat,
825 Enchantment::ThirstyWanderer => EquipmentSlot::Belt,
826 Enchantment::UnholyAcquisitiveness => EquipmentSlot::Amulet,
827 Enchantment::TheGraveRobbersPrayer => EquipmentSlot::Ring,
828 Enchantment::RobberBaronRitual => EquipmentSlot::Talisman,
829 }
830 }
831}
832
833#[derive(Debug, Clone, Copy, PartialEq, Eq)]
835#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
836pub struct Rune {
837 pub typ: RuneType,
839 pub value: u8,
842}
843
844#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash)]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846#[allow(missing_docs)]
847pub enum RuneType {
849 QuestGold = 31,
850 EpicChance,
851 ItemQuality,
852 QuestXP,
853 ExtraHitPoints,
854 FireResistance,
855 ColdResistence,
856 LightningResistance,
857 TotalResistence,
858 FireDamage,
859 ColdDamage,
860 LightningDamage,
861}
862
863#[derive(Debug, Clone, PartialEq, Eq, Copy)]
865#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
866pub enum GemSlot {
867 Filled(Gem),
869 Empty,
871}
872
873impl GemSlot {
874 pub(crate) fn parse(slot_val: i64, gem_pwr: i64) -> Option<GemSlot> {
875 match slot_val {
876 0 => return None,
877 1 => return Some(GemSlot::Empty),
878 _ => {}
879 }
880
881 let Ok(value) = gem_pwr.try_into() else {
882 warn!("Invalid gem power {gem_pwr}");
883 return None;
884 };
885
886 match GemType::parse(slot_val, value) {
887 Some(typ) => Some(GemSlot::Filled(Gem { typ, value })),
888 None => Some(GemSlot::Empty),
889 }
890 }
891}
892
893#[derive(Debug, Clone, PartialEq, Eq, Copy)]
895#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
896pub struct Potion {
897 pub typ: PotionType,
899 pub size: PotionSize,
901 pub expires: Option<DateTime<Local>>,
904}
905
906#[derive(Debug, Clone, PartialEq, Eq, Copy)]
910#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
911#[allow(missing_docs)]
912pub enum ItemType {
913 Hat,
914 BreastPlate,
915 Gloves,
916 FootWear,
917 Weapon {
918 min_dmg: u32,
919 max_dmg: u32,
920 },
921 Amulet,
922 Belt,
923 Ring,
924 Talisman,
925 Shield {
926 block_chance: u32,
927 },
928 Shard {
929 piece: u32,
930 },
931 Potion(Potion),
932 Scrapbook,
933 DungeonKey {
934 id: u32,
935 shadow_key: bool,
936 },
937 Gem(Gem),
938 PetItem {
939 typ: PetItem,
940 },
941 QuickSandGlass,
942 HeartOfDarkness,
943 WheelOfFortune,
944 Mannequin,
945 Resource {
946 amount: u32,
947 typ: ResourceType,
948 },
949 ToiletKey,
950 Gral,
951 EpicItemBag,
952 Unknown(u8),
955}
956
957impl ItemType {
958 #[must_use]
960 pub const fn is_weapon(self) -> bool {
961 matches!(self, ItemType::Weapon { .. })
962 }
963
964 #[must_use]
966 pub const fn is_shield(self) -> bool {
967 matches!(self, ItemType::Shield { .. })
968 }
969
970 #[must_use]
972 pub fn is_class_item(&self) -> bool {
973 matches!(
974 self,
975 ItemType::Hat
976 | ItemType::Belt
977 | ItemType::Gloves
978 | ItemType::FootWear
979 | ItemType::Shield { .. }
980 | ItemType::Weapon { .. }
981 | ItemType::BreastPlate
982 )
983 }
984
985 #[must_use]
989 pub fn is_unique(&self) -> bool {
990 matches!(
991 self,
992 ItemType::Scrapbook
993 | ItemType::HeartOfDarkness
994 | ItemType::WheelOfFortune
995 | ItemType::Mannequin
996 | ItemType::ToiletKey
997 | ItemType::Gral
998 | ItemType::EpicItemBag
999 | ItemType::DungeonKey { .. }
1000 )
1001 }
1002
1003 #[must_use]
1005 pub fn equipment_slot(&self) -> Option<EquipmentSlot> {
1006 Some(match self {
1007 ItemType::Hat => EquipmentSlot::Hat,
1008 ItemType::BreastPlate => EquipmentSlot::BreastPlate,
1009 ItemType::Gloves => EquipmentSlot::Gloves,
1010 ItemType::FootWear => EquipmentSlot::FootWear,
1011 ItemType::Weapon { .. } => EquipmentSlot::Weapon,
1012 ItemType::Amulet => EquipmentSlot::Amulet,
1013 ItemType::Belt => EquipmentSlot::Belt,
1014 ItemType::Ring => EquipmentSlot::Ring,
1015 ItemType::Talisman => EquipmentSlot::Talisman,
1016 ItemType::Shield { .. } => EquipmentSlot::Shield,
1017 _ => return None,
1018 })
1019 }
1020
1021 #[must_use]
1023 pub fn is_enchantable(&self) -> bool {
1024 self.equipment_slot()
1025 .is_some_and(|e| e.enchantment().is_some())
1026 }
1027
1028 pub(crate) fn parse(
1029 data: &[i64],
1030 _server_time: ServerTime,
1031 ) -> Result<Option<Self>, SFError> {
1032 let raw_typ: u8 = data.csimget(0, "item type", 255, |a| a & 0xFF)?;
1033 let unknown_item = |name: &'static str| {
1034 warn!("Could no parse item of type: {raw_typ}. {name} is faulty");
1035 Ok(Some(ItemType::Unknown(raw_typ)))
1036 };
1037
1038 let sub_ident = data.cget(3, "item sub type")?;
1039
1040 Ok(Some(match raw_typ {
1041 0 => return Ok(None),
1042 1 => ItemType::Weapon {
1043 min_dmg: data.csiget(5, "weapon min dmg", 0)?,
1044 max_dmg: data.csiget(6, "weapon min dmg", 0)?,
1045 },
1046 2 => ItemType::Shield {
1047 block_chance: data.csiget(5, "shield block chance", 0)?,
1048 },
1049 3 => ItemType::BreastPlate,
1050 4 => ItemType::FootWear,
1051 5 => ItemType::Gloves,
1052 6 => ItemType::Hat,
1053 7 => ItemType::Belt,
1054 8 => ItemType::Amulet,
1055 9 => ItemType::Ring,
1056 10 => ItemType::Talisman,
1057 11 => {
1058 let id = sub_ident & 0xFFFF;
1059 let Ok(id) = id.try_into() else {
1060 return unknown_item("unique sub ident");
1061 };
1062 match id {
1063 1..=11 | 17 | 19 | 22 | 69 | 70 => ItemType::DungeonKey {
1064 id,
1065 shadow_key: false,
1066 },
1067 20 => ItemType::ToiletKey,
1068 51..=64 | 67..=68 => ItemType::DungeonKey {
1069 id,
1070 shadow_key: true,
1071 },
1072 10000 => ItemType::EpicItemBag,
1073 piece => ItemType::Shard { piece },
1074 }
1075 }
1076 12 => {
1077 let id = sub_ident & 0xFF;
1078 if id > 16 {
1079 let Some(typ) = FromPrimitive::from_i64(id) else {
1080 return unknown_item("resource type");
1081 };
1082 ItemType::Resource {
1083 amount: 0,
1086 typ,
1087 }
1088 } else {
1089 let Some(typ) = PotionType::parse(id) else {
1090 return unknown_item("potion type");
1091 };
1092 let Some(size) = PotionSize::parse(id) else {
1093 return unknown_item("potion size");
1094 };
1095 ItemType::Potion(Potion {
1096 typ,
1097 size,
1098 expires: None,
1100 })
1106 }
1107 }
1108 13 => ItemType::Scrapbook,
1109 15 => {
1110 let gem_value = data.csiget(16, "gem pwr", 0)?;
1111 let Some(typ) = GemType::parse(sub_ident, gem_value) else {
1112 return unknown_item("gem type");
1113 };
1114 let gem = Gem {
1115 typ,
1116 value: gem_value,
1117 };
1118 ItemType::Gem(gem)
1119 }
1120 16 => {
1121 let Some(typ) = PetItem::parse(sub_ident & 0xFFFF) else {
1122 return unknown_item("pet item");
1123 };
1124 ItemType::PetItem { typ }
1125 }
1126 17 if (sub_ident & 0xFFFF) == 4 => ItemType::Gral,
1127 17 => ItemType::QuickSandGlass,
1128 18 => ItemType::HeartOfDarkness,
1129 19 => ItemType::WheelOfFortune,
1130 20 => ItemType::Mannequin,
1131 _ => {
1132 return unknown_item("main ident");
1133 }
1134 }))
1135 }
1136
1137 #[must_use]
1140 pub fn raw_id(&self) -> u8 {
1141 match self {
1142 ItemType::Weapon { .. } => 1,
1143 ItemType::Shield { .. } => 2,
1144 ItemType::BreastPlate => 3,
1145 ItemType::FootWear => 4,
1146 ItemType::Gloves => 5,
1147 ItemType::Hat => 6,
1148 ItemType::Belt => 7,
1149 ItemType::Amulet => 8,
1150 ItemType::Ring => 9,
1151 ItemType::Talisman => 10,
1152 ItemType::Shard { .. }
1153 | ItemType::DungeonKey { .. }
1154 | ItemType::ToiletKey
1155 | ItemType::EpicItemBag => 11,
1156 ItemType::Potion { .. } | ItemType::Resource { .. } => 12,
1157 ItemType::Scrapbook => 13,
1158 ItemType::Gem(_) => 15,
1159 ItemType::PetItem { .. } => 16,
1160 ItemType::QuickSandGlass | ItemType::Gral => 17,
1161 ItemType::HeartOfDarkness => 18,
1162 ItemType::WheelOfFortune => 19,
1163 ItemType::Mannequin => 20,
1164 ItemType::Unknown(u) => *u,
1165 }
1166 }
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1172#[allow(missing_docs)]
1173pub enum PotionType {
1174 Strength,
1175 Dexterity,
1176 Intelligence,
1177 Constitution,
1178 Luck,
1179 EternalLife,
1180}
1181
1182impl From<AttributeType> for PotionType {
1183 fn from(value: AttributeType) -> Self {
1184 match value {
1185 AttributeType::Strength => PotionType::Strength,
1186 AttributeType::Dexterity => PotionType::Dexterity,
1187 AttributeType::Intelligence => PotionType::Intelligence,
1188 AttributeType::Constitution => PotionType::Constitution,
1189 AttributeType::Luck => PotionType::Luck,
1190 }
1191 }
1192}
1193
1194impl PotionType {
1195 pub(crate) fn parse(id: i64) -> Option<PotionType> {
1196 if id == 0 {
1197 return None;
1198 }
1199 if id == 16 {
1200 return Some(PotionType::EternalLife);
1201 }
1202 Some(match id % 5 {
1203 0 => PotionType::Luck,
1204 1 => PotionType::Strength,
1205 2 => PotionType::Dexterity,
1206 3 => PotionType::Intelligence,
1207 _ => PotionType::Constitution,
1208 })
1209 }
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1215#[allow(missing_docs)]
1216pub enum PotionSize {
1217 Small,
1218 Medium,
1219 Large,
1220}
1221
1222impl PartialOrd for PotionSize {
1223 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1224 self.effect().partial_cmp(&other.effect())
1225 }
1226}
1227
1228impl PotionSize {
1229 #[must_use]
1230 pub fn effect(&self) -> f64 {
1231 match self {
1232 PotionSize::Small => 0.1,
1233 PotionSize::Medium => 0.15,
1234 PotionSize::Large => 0.25,
1235 }
1236 }
1237
1238 pub(crate) fn parse(id: i64) -> Option<Self> {
1239 Some(match id {
1240 1..=5 => PotionSize::Small,
1241 6..=10 => PotionSize::Medium,
1242 11..=16 => PotionSize::Large,
1243 _ => return None,
1244 })
1245 }
1246}
1247
1248#[derive(Debug, Clone, PartialEq, Eq, Copy, FromPrimitive)]
1250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1251#[allow(missing_docs)]
1252pub enum ResourceType {
1253 Wood = 17,
1254 Stone,
1255 Souls,
1256 Arcane,
1257 Metal,
1258}
1259
1260#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1263pub struct Gem {
1264 pub typ: GemType,
1266 pub value: u32,
1268}
1269
1270#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1272#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1273#[allow(missing_docs)]
1274pub enum GemType {
1275 Strength,
1276 Dexterity,
1277 Intelligence,
1278 Constitution,
1279 Luck,
1280 All,
1281 Legendary,
1282}
1283
1284impl GemType {
1285 pub(crate) fn parse(id: i64, debug_value: u32) -> Option<GemType> {
1286 Some(match id {
1287 0 | 1 => return None,
1288 10..=40 => match id % 10 {
1289 0 => GemType::Strength,
1290 1 => GemType::Dexterity,
1291 2 => GemType::Intelligence,
1292 3 => GemType::Constitution,
1293 4 => GemType::Luck,
1294 5 => GemType::All,
1295 6 => GemType::Legendary,
1298 _ => {
1299 return None;
1300 }
1301 },
1302 _ => {
1303 warn!("Unknown gem: {id} - {debug_value}");
1304 return None;
1305 }
1306 })
1307 }
1308}
1309
1310#[derive(
1312 Debug, Copy, Clone, PartialEq, Eq, Hash, Enum, EnumIter, EnumCount,
1313)]
1314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1315#[allow(missing_docs)]
1316pub enum EquipmentSlot {
1317 Hat = 1,
1318 BreastPlate,
1319 Gloves,
1320 FootWear,
1321 Amulet,
1322 Belt,
1323 Ring,
1324 Talisman,
1325 Weapon,
1326 Shield,
1327}
1328
1329impl EquipmentSlot {
1330 #[must_use]
1333 pub fn raw_id(&self) -> u8 {
1334 match self {
1335 EquipmentSlot::Weapon => 1,
1336 EquipmentSlot::Shield => 2,
1337 EquipmentSlot::BreastPlate => 3,
1338 EquipmentSlot::FootWear => 4,
1339 EquipmentSlot::Gloves => 5,
1340 EquipmentSlot::Hat => 6,
1341 EquipmentSlot::Belt => 7,
1342 EquipmentSlot::Amulet => 8,
1343 EquipmentSlot::Ring => 9,
1344 EquipmentSlot::Talisman => 10,
1345 }
1346 }
1347
1348 #[must_use]
1351 pub const fn enchantment(&self) -> Option<Enchantment> {
1352 match self {
1353 EquipmentSlot::Hat => {
1354 Some(Enchantment::AdventurersArchaeologicalAura)
1355 }
1356 EquipmentSlot::BreastPlate => Some(Enchantment::MariosBeard),
1357 EquipmentSlot::Gloves => Some(Enchantment::ShadowOfTheCowboy),
1358 EquipmentSlot::FootWear => Some(Enchantment::ManyFeetBoots),
1359 EquipmentSlot::Amulet => Some(Enchantment::UnholyAcquisitiveness),
1360 EquipmentSlot::Belt => Some(Enchantment::ThirstyWanderer),
1361 EquipmentSlot::Ring => Some(Enchantment::TheGraveRobbersPrayer),
1362 EquipmentSlot::Talisman => Some(Enchantment::RobberBaronRitual),
1363 EquipmentSlot::Weapon => Some(Enchantment::SwordOfVengeance),
1364 EquipmentSlot::Shield => None,
1365 }
1366 }
1367}
1368
1369#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1371#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1372#[allow(missing_docs)]
1373pub enum PetItem {
1374 Egg(HabitatType),
1375 SpecialEgg(HabitatType),
1376 GoldenEgg,
1377 Nest,
1378 Fruit(HabitatType),
1379}
1380
1381impl PetItem {
1382 pub(crate) fn parse(val: i64) -> Option<Self> {
1383 Some(match val {
1384 1..=5 => PetItem::Egg(HabitatType::from_typ_id(val)?),
1385 11..=15 => PetItem::SpecialEgg(HabitatType::from_typ_id(val - 10)?),
1386 21 => PetItem::GoldenEgg,
1387 22 => PetItem::Nest,
1388 31..=35 => PetItem::Fruit(HabitatType::from_typ_id(val - 30)?),
1389 _ => return None,
1390 })
1391 }
1392}
1393
1394pub(crate) fn parse_active_potions(
1395 data: &[i64],
1396 server_time: ServerTime,
1397) -> [Option<Potion>; 3] {
1398 if data.len() < 10 {
1399 return Default::default();
1400 }
1401 #[allow(clippy::indexing_slicing)]
1402 core::array::from_fn(move |i| {
1403 Some(Potion {
1404 typ: PotionType::parse(data[i + 1])?,
1405 size: PotionSize::parse(data[i + 1])?,
1406 expires: server_time.convert_to_local(data[4 + i], "potion exp"),
1407 })
1409 })
1410}