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)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct Inventory {
23 pub backpack: Vec<Option<Item>>,
24}
25
26#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
28pub struct BagPosition(pub(crate) usize);
29
30impl BagPosition {
31 #[must_use]
33 pub fn backpack_pos(&self) -> usize {
34 self.0
35 }
36 #[must_use]
40 pub fn inventory_pos(&self) -> (InventoryType, usize) {
41 let pos = self.0;
42 if pos <= 4 {
43 (InventoryType::MainInventory, pos)
44 } else {
45 (InventoryType::ExtendedInventory, pos - 5)
46 }
47 }
48}
49
50impl Inventory {
51 #[must_use]
56 pub fn as_split(&self) -> (&[Option<Item>], &[Option<Item>]) {
57 if self.backpack.len() < 5 {
58 return (&[], &[]);
59 }
60 self.backpack.split_at(5)
61 }
62
63 #[must_use]
64 pub fn as_split_mut(
69 &mut self,
70 ) -> (&mut [Option<Item>], &mut [Option<Item>]) {
71 if self.backpack.len() < 5 {
72 return (&mut [], &mut []);
73 }
74 self.backpack.split_at_mut(5)
75 }
76
77 #[must_use]
81 pub fn free_slot(&self) -> Option<BagPosition> {
82 for (pos, item) in self.iter() {
83 if item.is_none() {
84 return Some(pos);
85 }
86 }
87 None
88 }
89
90 #[must_use]
91 pub fn count_free_slots(&self) -> usize {
92 self.backpack.iter().filter(|slot| slot.is_none()).count()
93 }
94
95 pub fn iter(&self) -> impl Iterator<Item = (BagPosition, Option<&Item>)> {
97 self.backpack
98 .iter()
99 .enumerate()
100 .map(|(pos, item)| (BagPosition(pos), item.as_ref()))
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106#[allow(missing_docs)]
107pub enum PlayerItemPlace {
109 Equipment = 1,
110 MainInventory = 2,
111 ExtendedInventory = 5,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct ItemPosition {
116 pub place: ItemPlace,
117 pub position: usize,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct PlayerItemPosition {
122 pub place: PlayerItemPlace,
123 pub position: usize,
124}
125
126impl From<PlayerItemPosition> for ItemPosition {
127 fn from(value: PlayerItemPosition) -> Self {
128 Self {
129 place: value.place.item_position(),
130 position: value.position,
131 }
132 }
133}
134
135impl From<BagPosition> for ItemPosition {
136 fn from(value: BagPosition) -> Self {
137 let player: PlayerItemPosition = value.into();
138 player.into()
139 }
140}
141
142impl From<EquipmentPosition> for ItemPosition {
143 fn from(value: EquipmentPosition) -> Self {
144 let player: PlayerItemPosition = value.into();
145 player.into()
146 }
147}
148
149impl From<ShopPosition> for ItemPosition {
150 fn from(value: ShopPosition) -> Self {
151 Self {
152 place: value.typ.into(),
153 position: value.pos,
154 }
155 }
156}
157
158impl From<ShopType> for ItemPlace {
159 fn from(value: ShopType) -> Self {
160 match value {
161 ShopType::Weapon => ItemPlace::WeaponShop,
162 ShopType::Magic => ItemPlace::MageShop,
163 }
164 }
165}
166
167impl From<BagPosition> for PlayerItemPosition {
168 fn from(value: BagPosition) -> Self {
169 let p = value.inventory_pos();
170 Self {
171 place: p.0.player_item_position(),
172 position: p.1,
173 }
174 }
175}
176
177impl From<EquipmentPosition> for PlayerItemPosition {
178 fn from(value: EquipmentPosition) -> Self {
179 Self {
180 place: PlayerItemPlace::Equipment,
181 position: value.0,
182 }
183 }
184}
185
186impl PlayerItemPlace {
187 #[must_use]
190 pub fn item_position(&self) -> ItemPlace {
191 match self {
192 PlayerItemPlace::Equipment => ItemPlace::Equipment,
193 PlayerItemPlace::MainInventory => ItemPlace::MainInventory,
194 PlayerItemPlace::ExtendedInventory => ItemPlace::FortressChest,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201#[allow(missing_docs)]
202pub enum InventoryType {
204 MainInventory = 2,
205 ExtendedInventory = 5,
206}
207
208impl InventoryType {
209 #[must_use]
212 pub fn item_position(&self) -> ItemPlace {
213 match self {
214 InventoryType::MainInventory => ItemPlace::MainInventory,
215 InventoryType::ExtendedInventory => ItemPlace::FortressChest,
216 }
217 }
218 #[must_use]
221 pub fn player_item_position(&self) -> PlayerItemPlace {
222 match self {
223 InventoryType::MainInventory => PlayerItemPlace::MainInventory,
224 InventoryType::ExtendedInventory => {
225 PlayerItemPlace::ExtendedInventory
226 }
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233pub enum ItemPlace {
235 Equipment = 1,
237 MainInventory = 2,
239 WeaponShop = 3,
241 MageShop = 4,
243 FortressChest = 5,
245}
246
247#[derive(Debug, Default, Clone)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub struct Equipment(pub EnumMap<EquipmentSlot, Option<Item>>);
251
252#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
253pub struct EquipmentPosition(pub(crate) usize);
254
255impl EquipmentPosition {
256 #[must_use]
258 pub fn position(&self) -> usize {
259 self.0
260 }
261}
262
263impl Equipment {
264 pub fn iter(
266 &self,
267 ) -> impl Iterator<Item = (EquipmentPosition, Option<&Item>)> {
268 self.0
269 .as_slice()
270 .iter()
271 .enumerate()
272 .map(|(pos, item)| (EquipmentPosition(pos), item.as_ref()))
273 }
274
275 #[must_use]
276 pub fn has_enchantment(&self, enchantment: Enchantment) -> bool {
278 let item = self.0.get(enchantment.equipment_slot());
279 if let Some(item) = item {
280 return item.enchantment == Some(enchantment);
281 }
282 false
283 }
284
285 #[allow(clippy::indexing_slicing)]
287 pub(crate) fn parse(
288 data: &[i64],
289 server_time: ServerTime,
290 ) -> Result<Equipment, SFError> {
291 let mut res = Equipment::default();
292 if !data.len().is_multiple_of(ITEM_PARSE_LEN) {
293 return Err(SFError::ParsingError(
294 "Invalid Equipment",
295 format!("{data:?}"),
296 ));
297 }
298 for (chunk, slot) in
299 data.chunks_exact(ITEM_PARSE_LEN).zip(res.0.as_mut_slice())
300 {
301 *slot = Item::parse(chunk, server_time)?;
302 }
303 Ok(res)
304 }
305}
306
307pub(crate) const ITEM_PARSE_LEN: usize = 19;
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
311pub struct Item {
314 pub typ: ItemType,
316 pub price: u32,
318 pub mushroom_price: u32,
322 pub model_id: u16,
324 pub class: Option<Class>,
327 pub type_specific_val: u32,
331 pub attributes: EnumMap<AttributeType, u32>,
333 pub gem_slot: Option<GemSlot>,
335 pub rune: Option<Rune>,
337 pub enchantment: Option<Enchantment>,
339 pub color: u8,
342
343 pub upgrade_count: u8,
344 pub item_quality: u32,
345 pub is_washed: bool,
346}
347
348impl Item {
349 #[must_use]
352 pub fn equipment_ident(&self) -> Option<EquipmentIdent> {
353 Some(EquipmentIdent {
354 class: self.class,
355 typ: self.typ.equipment_slot()?,
356 model_id: self.model_id,
357 color: self.color,
358 })
359 }
360
361 #[must_use]
364 pub fn is_unique(&self) -> bool {
365 self.typ.is_unique()
366 }
367
368 #[must_use]
370 pub fn is_epic(&self) -> bool {
371 self.model_id >= 50
372 }
373
374 #[must_use]
376 pub fn is_legendary(&self) -> bool {
377 self.model_id >= 90
378 }
379
380 #[must_use]
382 pub fn armor(&self) -> u32 {
383 #[allow(clippy::enum_glob_use)]
384 use ItemType::*;
385 match self.typ {
386 Hat | BreastPlate | Gloves | FootWear | Amulet | Belt | Ring
387 | Talisman => self.type_specific_val,
388 _ => 0,
389 }
390 }
391
392 #[must_use]
394 pub fn is_enchantable(&self) -> bool {
395 self.typ.is_enchantable()
396 }
397
398 #[must_use]
403 pub fn can_be_equipped_by_companion(
404 &self,
405 class: impl Into<Class>,
406 ) -> bool {
407 !self.typ.is_shield() && self.can_be_equipped_by(class.into())
408 }
409
410 #[must_use]
418 pub fn can_be_equipped_by(&self, class: Class) -> bool {
419 self.typ.equipment_slot().is_some() && self.can_be_used_by(class)
420 }
421
422 #[must_use]
428 #[allow(clippy::enum_glob_use, clippy::match_same_arms)]
429 pub fn can_be_used_by(&self, class: Class) -> bool {
430 use Class::*;
431
432 let Some(class_requirement) = self.class else {
434 return true;
435 };
436
437 match (class, class_requirement) {
442 (Warrior, Warrior) => true,
444 (Berserker, Warrior) => !self.typ.is_shield(),
445 (Scout, Scout) => true,
447 (Mage | Necromancer, Mage) => true,
449 (Assassin, Warrior) => self.typ.is_weapon(),
451 (Assassin, Scout) => !self.typ.is_weapon(),
452 (Bard | Druid, Mage) => self.typ.is_weapon(),
454 (Bard | Druid, Scout) => !self.typ.is_weapon(),
455 (BattleMage, Warrior) => self.typ.is_weapon(),
457 (BattleMage, Mage) => !self.typ.is_weapon(),
458 (DemonHunter, Scout) => self.typ.is_weapon(),
460 (DemonHunter, Warrior) => {
461 !self.typ.is_weapon() && !self.typ.is_shield()
462 }
463 _ => false,
464 }
465 }
466
467 pub(crate) fn parse(
469 data: &[i64],
470 server_time: ServerTime,
471 ) -> Result<Option<Self>, SFError> {
472 let Some(typ) = ItemType::parse(data, server_time)? else {
473 return Ok(None);
474 };
475
476 let enchantment = data.cfpget(2, "item enchantment", |a| a)?;
477 let gem_slot_val = data.cimget(1, "gem slot val", |a| a)?;
478 let gem_pwr = data.cimget(16, "gem pwr", |a| a)?;
479
480 let gem_slot = GemSlot::parse(gem_slot_val, gem_pwr);
481
482 let class = if typ.is_class_item() {
483 data.cfpget(3, "item class", |x| (x & 0xFFFF) / 1000)?
484 } else {
485 None
486 };
487 let mut rune = None;
488 let mut attributes: EnumMap<AttributeType, u32> = EnumMap::default();
489 if typ.equipment_slot().is_some() {
490 for i in 0..3 {
491 let atr_typ = data.cget(i + 7, "item atr typ")?;
492 let Ok(atr_typ) = atr_typ.try_into() else {
493 warn!("Invalid attribute typ: {atr_typ}, {typ:?}");
494 continue;
495 };
496 let atr_val = data.cget(i + 10, "item atr val")?;
497 let Ok(atr_val): Result<u32, _> = atr_val.try_into() else {
498 warn!("Invalid attribute value: {atr_val}, {typ:?}");
499 continue;
500 };
501
502 match atr_typ {
503 0 => {}
504 1..=5 => {
505 let Some(atr_typ) = FromPrimitive::from_usize(atr_typ)
506 else {
507 continue;
508 };
509 *attributes.get_mut(atr_typ) = atr_val;
510 }
511 6 => {
512 attributes.as_mut_array().fill(atr_val);
513 }
514 21 => {
515 for atr in [
516 AttributeType::Strength,
517 AttributeType::Constitution,
518 AttributeType::Luck,
519 ] {
520 *attributes.get_mut(atr) = atr_val;
521 }
522 }
523 22 => {
524 for atr in [
525 AttributeType::Dexterity,
526 AttributeType::Constitution,
527 AttributeType::Luck,
528 ] {
529 *attributes.get_mut(atr) = atr_val;
530 }
531 }
532 23 => {
533 for atr in [
534 AttributeType::Intelligence,
535 AttributeType::Constitution,
536 AttributeType::Luck,
537 ] {
538 *attributes.get_mut(atr) = atr_val;
539 }
540 }
541 rune_typ => {
542 let Some(typ) = FromPrimitive::from_usize(rune_typ)
543 else {
544 warn!(
545 "Unhandled item val: {atr_typ} -> {atr_val} \
546 for {class:?} {typ:?}",
547 );
548 continue;
549 };
550 let Ok(value) = atr_val.try_into() else {
551 warn!("Rune value too big for a u8: {atr_val}");
552 continue;
553 };
554 rune = Some(Rune { typ, value });
555 }
556 }
557 }
558 }
559 let model_id: u16 =
560 data.cimget(3, "item model id", |x| (x & 0xFFFF) % 1000)?;
561
562 let color = match model_id {
563 ..=49 if typ != ItemType::Talisman => data
564 .get(5..=12)
565 .map(|a| a.iter().sum::<i64>())
566 .map(|a| (a % 5) + 1)
567 .and_then(|a| a.try_into().ok())
568 .unwrap_or(1),
569 _ => 1,
570 };
571
572 let item = Item {
573 typ,
574 model_id,
575 rune,
576 type_specific_val: data.csiget(5, "effect value", 0)?,
577 gem_slot,
578 enchantment,
579 class,
580 attributes,
581 color,
582 price: data.csiget(13, "item price", u32::MAX)?,
583 mushroom_price: data.csiget(14, "mushroom price", u32::MAX)?,
584 upgrade_count: data.csiget(15, "upgrade count", u8::MAX)?,
585 item_quality: data.csiget(17, "upgrade count", 0)?,
586 is_washed: data.csiget(18, "is washed", 0)? != 0,
587 };
588 Ok(Some(item))
589 }
590}
591
592#[derive(
593 Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash, Enum,
594)]
595#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
596pub enum Enchantment {
598 SwordOfVengeance = 11,
600 MariosBeard = 31,
602 ManyFeetBoots = 41,
604 ShadowOfTheCowboy = 51,
606 AdventurersArchaeologicalAura = 61,
608 ThirstyWanderer = 71,
610 UnholyAcquisitiveness = 81,
612 TheGraveRobbersPrayer = 91,
614 RobberBaronRitual = 101,
616}
617
618impl Enchantment {
619 #[must_use]
620 pub fn equipment_slot(&self) -> EquipmentSlot {
621 match self {
622 Enchantment::SwordOfVengeance => EquipmentSlot::Weapon,
623 Enchantment::MariosBeard => EquipmentSlot::BreastPlate,
624 Enchantment::ManyFeetBoots => EquipmentSlot::FootWear,
625 Enchantment::ShadowOfTheCowboy => EquipmentSlot::Gloves,
626 Enchantment::AdventurersArchaeologicalAura => EquipmentSlot::Hat,
627 Enchantment::ThirstyWanderer => EquipmentSlot::Belt,
628 Enchantment::UnholyAcquisitiveness => EquipmentSlot::Amulet,
629 Enchantment::TheGraveRobbersPrayer => EquipmentSlot::Ring,
630 Enchantment::RobberBaronRitual => EquipmentSlot::Talisman,
631 }
632 }
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
637pub struct Rune {
639 pub typ: RuneType,
641 pub value: u8,
644}
645
646#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash)]
647#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
648#[allow(missing_docs)]
649pub enum RuneType {
651 QuestGold = 31,
652 EpicChance,
653 ItemQuality,
654 QuestXP,
655 ExtraHitPoints,
656 FireResistance,
657 ColdResistence,
658 LightningResistance,
659 TotalResistence,
660 FireDamage,
661 ColdDamage,
662 LightningDamage,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Copy)]
666#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
667pub enum GemSlot {
669 Filled(Gem),
671 Empty,
673}
674
675impl GemSlot {
676 pub(crate) fn parse(slot_val: i64, gem_pwr: i64) -> Option<GemSlot> {
677 match slot_val {
678 0 => return None,
679 1 => return Some(GemSlot::Empty),
680 _ => {}
681 }
682
683 let Ok(value) = gem_pwr.try_into() else {
684 warn!("Invalid gem power {gem_pwr}");
685 return None;
686 };
687
688 match GemType::parse(slot_val, value) {
689 Some(typ) => Some(GemSlot::Filled(Gem { typ, value })),
690 None => Some(GemSlot::Empty),
691 }
692 }
693}
694#[derive(Debug, Clone, PartialEq, Eq, Copy)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696pub struct Potion {
698 pub typ: PotionType,
700 pub size: PotionSize,
702 pub expires: Option<DateTime<Local>>,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Copy)]
708#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
709#[allow(missing_docs)]
710pub enum ItemType {
714 Hat,
715 BreastPlate,
716 Gloves,
717 FootWear,
718 Weapon {
719 min_dmg: u32,
720 max_dmg: u32,
721 },
722 Amulet,
723 Belt,
724 Ring,
725 Talisman,
726 Shield {
727 block_chance: u32,
728 },
729 Shard {
730 piece: u32,
731 },
732 Potion(Potion),
733 Scrapbook,
734 DungeonKey {
735 id: u32,
736 shadow_key: bool,
737 },
738 Gem(Gem),
739 PetItem {
740 typ: PetItem,
741 },
742 QuickSandGlass,
743 HeartOfDarkness,
744 WheelOfFortune,
745 Mannequin,
746 Resource {
747 amount: u32,
748 typ: ResourceType,
749 },
750 ToiletKey,
751 Gral,
752 EpicItemBag,
753 Unknown(u8),
756}
757
758impl ItemType {
759 #[must_use]
761 pub const fn is_weapon(self) -> bool {
762 matches!(self, ItemType::Weapon { .. })
763 }
764
765 #[must_use]
767 pub const fn is_shield(self) -> bool {
768 matches!(self, ItemType::Shield { .. })
769 }
770
771 #[must_use]
773 pub fn is_class_item(&self) -> bool {
774 matches!(
775 self,
776 ItemType::Hat
777 | ItemType::Belt
778 | ItemType::Gloves
779 | ItemType::FootWear
780 | ItemType::Shield { .. }
781 | ItemType::Weapon { .. }
782 | ItemType::BreastPlate
783 )
784 }
785
786 #[must_use]
790 pub fn is_unique(&self) -> bool {
791 matches!(
792 self,
793 ItemType::Scrapbook
794 | ItemType::HeartOfDarkness
795 | ItemType::WheelOfFortune
796 | ItemType::Mannequin
797 | ItemType::ToiletKey
798 | ItemType::Gral
799 | ItemType::EpicItemBag
800 | ItemType::DungeonKey { .. }
801 )
802 }
803
804 #[must_use]
806 pub fn equipment_slot(&self) -> Option<EquipmentSlot> {
807 Some(match self {
808 ItemType::Hat => EquipmentSlot::Hat,
809 ItemType::BreastPlate => EquipmentSlot::BreastPlate,
810 ItemType::Gloves => EquipmentSlot::Gloves,
811 ItemType::FootWear => EquipmentSlot::FootWear,
812 ItemType::Weapon { .. } => EquipmentSlot::Weapon,
813 ItemType::Amulet => EquipmentSlot::Amulet,
814 ItemType::Belt => EquipmentSlot::Belt,
815 ItemType::Ring => EquipmentSlot::Ring,
816 ItemType::Talisman => EquipmentSlot::Talisman,
817 ItemType::Shield { .. } => EquipmentSlot::Shield,
818 _ => return None,
819 })
820 }
821
822 #[must_use]
824 pub fn is_enchantable(&self) -> bool {
825 self.equipment_slot()
826 .is_some_and(|e| e.enchantment().is_some())
827 }
828
829 pub(crate) fn parse_active_potions(
830 data: &[i64],
831 server_time: ServerTime,
832 ) -> [Option<Potion>; 3] {
833 if data.len() < 6 {
834 return Default::default();
835 }
836 #[allow(clippy::indexing_slicing)]
837 core::array::from_fn(move |i| {
838 Some(Potion {
839 typ: PotionType::parse(data[i])?,
840 size: PotionSize::parse(data[i])?,
841 expires: server_time
842 .convert_to_local(data[3 + i], "potion exp"),
843 })
844 })
845 }
846
847 pub(crate) fn parse(
848 data: &[i64],
849 _server_time: ServerTime,
850 ) -> Result<Option<Self>, SFError> {
851 let raw_typ: u8 = data.csimget(0, "item type", 255, |a| a & 0xFF)?;
852 let unknown_item = |name: &'static str| {
853 warn!("Could no parse item of type: {raw_typ}. {name} is faulty");
854 Ok(Some(ItemType::Unknown(raw_typ)))
855 };
856
857 let sub_ident = data.cget(3, "item sub type")?;
858
859 Ok(Some(match raw_typ {
860 0 => return Ok(None),
861 1 => ItemType::Weapon {
862 min_dmg: data.csiget(5, "weapon min dmg", 0)?,
863 max_dmg: data.csiget(6, "weapon min dmg", 0)?,
864 },
865 2 => ItemType::Shield {
866 block_chance: data.csiget(5, "shield block chance", 0)?,
867 },
868 3 => ItemType::BreastPlate,
869 4 => ItemType::FootWear,
870 5 => ItemType::Gloves,
871 6 => ItemType::Hat,
872 7 => ItemType::Belt,
873 8 => ItemType::Amulet,
874 9 => ItemType::Ring,
875 10 => ItemType::Talisman,
876 11 => {
877 let id = sub_ident & 0xFFFF;
878 let Ok(id) = id.try_into() else {
879 return unknown_item("unique sub ident");
880 };
881 match id {
882 1..=11 | 17 | 19 | 22 | 69 | 70 => ItemType::DungeonKey {
883 id,
884 shadow_key: false,
885 },
886 20 => ItemType::ToiletKey,
887 51..=64 | 67..=68 => ItemType::DungeonKey {
888 id,
889 shadow_key: true,
890 },
891 10000 => ItemType::EpicItemBag,
892 piece => ItemType::Shard { piece },
893 }
894 }
895 12 => {
896 let id = sub_ident & 0xFF;
897 if id > 16 {
898 let Some(typ) = FromPrimitive::from_i64(id) else {
899 return unknown_item("resource type");
900 };
901 ItemType::Resource {
902 amount: 0,
905 typ,
906 }
907 } else {
908 let Some(typ) = PotionType::parse(id) else {
909 return unknown_item("potion type");
910 };
911 let Some(size) = PotionSize::parse(id) else {
912 return unknown_item("potion size");
913 };
914 ItemType::Potion(Potion {
915 typ,
916 size,
917 expires: None,
919 })
925 }
926 }
927 13 => ItemType::Scrapbook,
928 15 => {
929 let gem_value = data.csiget(16, "gem pwr", 0)?;
930 let Some(typ) = GemType::parse(sub_ident, gem_value) else {
931 return unknown_item("gem type");
932 };
933 let gem = Gem {
934 typ,
935 value: gem_value,
936 };
937 ItemType::Gem(gem)
938 }
939 16 => {
940 let Some(typ) = PetItem::parse(sub_ident & 0xFFFF) else {
941 return unknown_item("pet item");
942 };
943 ItemType::PetItem { typ }
944 }
945 17 if (sub_ident & 0xFFFF) == 4 => ItemType::Gral,
946 17 => ItemType::QuickSandGlass,
947 18 => ItemType::HeartOfDarkness,
948 19 => ItemType::WheelOfFortune,
949 20 => ItemType::Mannequin,
950 _ => {
951 return unknown_item("main ident");
952 }
953 }))
954 }
955
956 #[must_use]
959 pub fn raw_id(&self) -> u8 {
960 match self {
961 ItemType::Weapon { .. } => 1,
962 ItemType::Shield { .. } => 2,
963 ItemType::BreastPlate => 3,
964 ItemType::FootWear => 4,
965 ItemType::Gloves => 5,
966 ItemType::Hat => 6,
967 ItemType::Belt => 7,
968 ItemType::Amulet => 8,
969 ItemType::Ring => 9,
970 ItemType::Talisman => 10,
971 ItemType::Shard { .. }
972 | ItemType::DungeonKey { .. }
973 | ItemType::ToiletKey
974 | ItemType::EpicItemBag => 11,
975 ItemType::Potion { .. } | ItemType::Resource { .. } => 12,
976 ItemType::Scrapbook => 13,
977 ItemType::Gem(_) => 15,
978 ItemType::PetItem { .. } => 16,
979 ItemType::QuickSandGlass | ItemType::Gral => 17,
980 ItemType::HeartOfDarkness => 18,
981 ItemType::WheelOfFortune => 19,
982 ItemType::Mannequin => 20,
983 ItemType::Unknown(u) => *u,
984 }
985 }
986}
987
988#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
989#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
990#[allow(missing_docs)]
991pub enum PotionType {
993 Strength,
994 Dexterity,
995 Intelligence,
996 Constitution,
997 Luck,
998 EternalLife,
999}
1000
1001impl From<AttributeType> for PotionType {
1002 fn from(value: AttributeType) -> Self {
1003 match value {
1004 AttributeType::Strength => PotionType::Strength,
1005 AttributeType::Dexterity => PotionType::Dexterity,
1006 AttributeType::Intelligence => PotionType::Intelligence,
1007 AttributeType::Constitution => PotionType::Constitution,
1008 AttributeType::Luck => PotionType::Luck,
1009 }
1010 }
1011}
1012
1013impl PotionType {
1014 pub(crate) fn parse(id: i64) -> Option<PotionType> {
1015 if id == 0 {
1016 return None;
1017 }
1018 if id == 16 {
1019 return Some(PotionType::EternalLife);
1020 }
1021 Some(match id % 5 {
1022 0 => PotionType::Luck,
1023 1 => PotionType::Strength,
1024 2 => PotionType::Dexterity,
1025 3 => PotionType::Intelligence,
1026 _ => PotionType::Constitution,
1027 })
1028 }
1029}
1030
1031#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1032#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1033#[allow(missing_docs)]
1034pub enum PotionSize {
1036 Small,
1037 Medium,
1038 Large,
1039}
1040
1041impl PartialOrd for PotionSize {
1042 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1043 self.effect().partial_cmp(&other.effect())
1044 }
1045}
1046
1047impl PotionSize {
1048 #[must_use]
1049 pub fn effect(&self) -> f64 {
1050 match self {
1051 PotionSize::Small => 0.1,
1052 PotionSize::Medium => 0.15,
1053 PotionSize::Large => 0.25,
1054 }
1055 }
1056
1057 pub(crate) fn parse(id: i64) -> Option<Self> {
1058 Some(match id {
1059 1..=5 => PotionSize::Small,
1060 6..=10 => PotionSize::Medium,
1061 11..=16 => PotionSize::Large,
1062 _ => return None,
1063 })
1064 }
1065}
1066
1067#[derive(Debug, Clone, PartialEq, Eq, Copy, FromPrimitive)]
1068#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1069#[allow(missing_docs)]
1070pub enum ResourceType {
1072 Wood = 17,
1073 Stone,
1074 Souls,
1075 Arcane,
1076 Metal,
1077}
1078
1079#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1080#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1081pub struct Gem {
1083 pub typ: GemType,
1085 pub value: u32,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1090#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1091#[allow(missing_docs)]
1092pub enum GemType {
1094 Strength,
1095 Dexterity,
1096 Intelligence,
1097 Constitution,
1098 Luck,
1099 All,
1100 Legendary,
1101}
1102
1103impl GemType {
1104 pub(crate) fn parse(id: i64, debug_value: u32) -> Option<GemType> {
1105 Some(match id {
1106 0 | 1 => return None,
1107 10..=40 => match id % 10 {
1108 0 => GemType::Strength,
1109 1 => GemType::Dexterity,
1110 2 => GemType::Intelligence,
1111 3 => GemType::Constitution,
1112 4 => GemType::Luck,
1113 5 => GemType::All,
1114 6 => GemType::Legendary,
1117 _ => {
1118 return None;
1119 }
1120 },
1121 _ => {
1122 warn!("Unknown gem: {id} - {debug_value}");
1123 return None;
1124 }
1125 })
1126 }
1127}
1128
1129#[derive(
1130 Debug, Copy, Clone, PartialEq, Eq, Hash, Enum, EnumIter, EnumCount,
1131)]
1132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1133#[allow(missing_docs)]
1134pub enum EquipmentSlot {
1136 Hat = 1,
1137 BreastPlate,
1138 Gloves,
1139 FootWear,
1140 Amulet,
1141 Belt,
1142 Ring,
1143 Talisman,
1144 Weapon,
1145 Shield,
1146}
1147
1148impl EquipmentSlot {
1149 #[must_use]
1152 pub fn raw_id(&self) -> u8 {
1153 match self {
1154 EquipmentSlot::Weapon => 1,
1155 EquipmentSlot::Shield => 2,
1156 EquipmentSlot::BreastPlate => 3,
1157 EquipmentSlot::FootWear => 4,
1158 EquipmentSlot::Gloves => 5,
1159 EquipmentSlot::Hat => 6,
1160 EquipmentSlot::Belt => 7,
1161 EquipmentSlot::Amulet => 8,
1162 EquipmentSlot::Ring => 9,
1163 EquipmentSlot::Talisman => 10,
1164 }
1165 }
1166
1167 #[must_use]
1170 pub const fn enchantment(&self) -> Option<Enchantment> {
1171 match self {
1172 EquipmentSlot::Hat => {
1173 Some(Enchantment::AdventurersArchaeologicalAura)
1174 }
1175 EquipmentSlot::BreastPlate => Some(Enchantment::MariosBeard),
1176 EquipmentSlot::Gloves => Some(Enchantment::ShadowOfTheCowboy),
1177 EquipmentSlot::FootWear => Some(Enchantment::ManyFeetBoots),
1178 EquipmentSlot::Amulet => Some(Enchantment::UnholyAcquisitiveness),
1179 EquipmentSlot::Belt => Some(Enchantment::ThirstyWanderer),
1180 EquipmentSlot::Ring => Some(Enchantment::TheGraveRobbersPrayer),
1181 EquipmentSlot::Talisman => Some(Enchantment::RobberBaronRitual),
1182 EquipmentSlot::Weapon => Some(Enchantment::SwordOfVengeance),
1183 EquipmentSlot::Shield => None,
1184 }
1185 }
1186}
1187
1188#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1190#[allow(missing_docs)]
1191pub enum PetItem {
1193 Egg(HabitatType),
1194 SpecialEgg(HabitatType),
1195 GoldenEgg,
1196 Nest,
1197 Fruit(HabitatType),
1198}
1199
1200impl PetItem {
1201 pub(crate) fn parse(val: i64) -> Option<Self> {
1202 Some(match val {
1203 1..=5 => PetItem::Egg(HabitatType::from_typ_id(val)?),
1204 11..=15 => PetItem::SpecialEgg(HabitatType::from_typ_id(val - 10)?),
1205 21 => PetItem::GoldenEgg,
1206 22 => PetItem::Nest,
1207 31..=35 => PetItem::Fruit(HabitatType::from_typ_id(val - 30)?),
1208 _ => return None,
1209 })
1210 }
1211}