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)]
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]
68 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)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107#[allow(missing_docs)]
108pub 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)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
202#[allow(missing_docs)]
203pub 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)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub enum ItemPlace {
235 Equipment = 1,
237 MainInventory = 2,
239 WeaponShop = 3,
241 MageShop = 4,
243 FortressChest = 5,
245}
246
247#[derive(Debug, Default, Clone)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250pub 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]
277 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)]
312#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
313pub 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(
594 Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash, Enum,
595)]
596#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
597pub 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)]
637#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
638pub 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)]
667#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
668pub 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
695#[derive(Debug, Clone, PartialEq, Eq, Copy)]
697#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
698pub struct Potion {
699 pub typ: PotionType,
701 pub size: PotionSize,
703 pub expires: Option<DateTime<Local>>,
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Copy)]
712#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
713#[allow(missing_docs)]
714pub enum ItemType {
715 Hat,
716 BreastPlate,
717 Gloves,
718 FootWear,
719 Weapon {
720 min_dmg: u32,
721 max_dmg: u32,
722 },
723 Amulet,
724 Belt,
725 Ring,
726 Talisman,
727 Shield {
728 block_chance: u32,
729 },
730 Shard {
731 piece: u32,
732 },
733 Potion(Potion),
734 Scrapbook,
735 DungeonKey {
736 id: u32,
737 shadow_key: bool,
738 },
739 Gem(Gem),
740 PetItem {
741 typ: PetItem,
742 },
743 QuickSandGlass,
744 HeartOfDarkness,
745 WheelOfFortune,
746 Mannequin,
747 Resource {
748 amount: u32,
749 typ: ResourceType,
750 },
751 ToiletKey,
752 Gral,
753 EpicItemBag,
754 Unknown(u8),
757}
758
759impl ItemType {
760 #[must_use]
762 pub const fn is_weapon(self) -> bool {
763 matches!(self, ItemType::Weapon { .. })
764 }
765
766 #[must_use]
768 pub const fn is_shield(self) -> bool {
769 matches!(self, ItemType::Shield { .. })
770 }
771
772 #[must_use]
774 pub fn is_class_item(&self) -> bool {
775 matches!(
776 self,
777 ItemType::Hat
778 | ItemType::Belt
779 | ItemType::Gloves
780 | ItemType::FootWear
781 | ItemType::Shield { .. }
782 | ItemType::Weapon { .. }
783 | ItemType::BreastPlate
784 )
785 }
786
787 #[must_use]
791 pub fn is_unique(&self) -> bool {
792 matches!(
793 self,
794 ItemType::Scrapbook
795 | ItemType::HeartOfDarkness
796 | ItemType::WheelOfFortune
797 | ItemType::Mannequin
798 | ItemType::ToiletKey
799 | ItemType::Gral
800 | ItemType::EpicItemBag
801 | ItemType::DungeonKey { .. }
802 )
803 }
804
805 #[must_use]
807 pub fn equipment_slot(&self) -> Option<EquipmentSlot> {
808 Some(match self {
809 ItemType::Hat => EquipmentSlot::Hat,
810 ItemType::BreastPlate => EquipmentSlot::BreastPlate,
811 ItemType::Gloves => EquipmentSlot::Gloves,
812 ItemType::FootWear => EquipmentSlot::FootWear,
813 ItemType::Weapon { .. } => EquipmentSlot::Weapon,
814 ItemType::Amulet => EquipmentSlot::Amulet,
815 ItemType::Belt => EquipmentSlot::Belt,
816 ItemType::Ring => EquipmentSlot::Ring,
817 ItemType::Talisman => EquipmentSlot::Talisman,
818 ItemType::Shield { .. } => EquipmentSlot::Shield,
819 _ => return None,
820 })
821 }
822
823 #[must_use]
825 pub fn is_enchantable(&self) -> bool {
826 self.equipment_slot()
827 .is_some_and(|e| e.enchantment().is_some())
828 }
829
830 pub(crate) fn parse_active_potions(
831 data: &[i64],
832 server_time: ServerTime,
833 ) -> [Option<Potion>; 3] {
834 if data.len() < 6 {
835 return Default::default();
836 }
837 #[allow(clippy::indexing_slicing)]
838 core::array::from_fn(move |i| {
839 Some(Potion {
840 typ: PotionType::parse(data[i])?,
841 size: PotionSize::parse(data[i])?,
842 expires: server_time
843 .convert_to_local(data[3 + i], "potion exp"),
844 })
845 })
846 }
847
848 pub(crate) fn parse(
849 data: &[i64],
850 _server_time: ServerTime,
851 ) -> Result<Option<Self>, SFError> {
852 let raw_typ: u8 = data.csimget(0, "item type", 255, |a| a & 0xFF)?;
853 let unknown_item = |name: &'static str| {
854 warn!("Could no parse item of type: {raw_typ}. {name} is faulty");
855 Ok(Some(ItemType::Unknown(raw_typ)))
856 };
857
858 let sub_ident = data.cget(3, "item sub type")?;
859
860 Ok(Some(match raw_typ {
861 0 => return Ok(None),
862 1 => ItemType::Weapon {
863 min_dmg: data.csiget(5, "weapon min dmg", 0)?,
864 max_dmg: data.csiget(6, "weapon min dmg", 0)?,
865 },
866 2 => ItemType::Shield {
867 block_chance: data.csiget(5, "shield block chance", 0)?,
868 },
869 3 => ItemType::BreastPlate,
870 4 => ItemType::FootWear,
871 5 => ItemType::Gloves,
872 6 => ItemType::Hat,
873 7 => ItemType::Belt,
874 8 => ItemType::Amulet,
875 9 => ItemType::Ring,
876 10 => ItemType::Talisman,
877 11 => {
878 let id = sub_ident & 0xFFFF;
879 let Ok(id) = id.try_into() else {
880 return unknown_item("unique sub ident");
881 };
882 match id {
883 1..=11 | 17 | 19 | 22 | 69 | 70 => ItemType::DungeonKey {
884 id,
885 shadow_key: false,
886 },
887 20 => ItemType::ToiletKey,
888 51..=64 | 67..=68 => ItemType::DungeonKey {
889 id,
890 shadow_key: true,
891 },
892 10000 => ItemType::EpicItemBag,
893 piece => ItemType::Shard { piece },
894 }
895 }
896 12 => {
897 let id = sub_ident & 0xFF;
898 if id > 16 {
899 let Some(typ) = FromPrimitive::from_i64(id) else {
900 return unknown_item("resource type");
901 };
902 ItemType::Resource {
903 amount: 0,
906 typ,
907 }
908 } else {
909 let Some(typ) = PotionType::parse(id) else {
910 return unknown_item("potion type");
911 };
912 let Some(size) = PotionSize::parse(id) else {
913 return unknown_item("potion size");
914 };
915 ItemType::Potion(Potion {
916 typ,
917 size,
918 expires: None,
920 })
926 }
927 }
928 13 => ItemType::Scrapbook,
929 15 => {
930 let gem_value = data.csiget(16, "gem pwr", 0)?;
931 let Some(typ) = GemType::parse(sub_ident, gem_value) else {
932 return unknown_item("gem type");
933 };
934 let gem = Gem {
935 typ,
936 value: gem_value,
937 };
938 ItemType::Gem(gem)
939 }
940 16 => {
941 let Some(typ) = PetItem::parse(sub_ident & 0xFFFF) else {
942 return unknown_item("pet item");
943 };
944 ItemType::PetItem { typ }
945 }
946 17 if (sub_ident & 0xFFFF) == 4 => ItemType::Gral,
947 17 => ItemType::QuickSandGlass,
948 18 => ItemType::HeartOfDarkness,
949 19 => ItemType::WheelOfFortune,
950 20 => ItemType::Mannequin,
951 _ => {
952 return unknown_item("main ident");
953 }
954 }))
955 }
956
957 #[must_use]
960 pub fn raw_id(&self) -> u8 {
961 match self {
962 ItemType::Weapon { .. } => 1,
963 ItemType::Shield { .. } => 2,
964 ItemType::BreastPlate => 3,
965 ItemType::FootWear => 4,
966 ItemType::Gloves => 5,
967 ItemType::Hat => 6,
968 ItemType::Belt => 7,
969 ItemType::Amulet => 8,
970 ItemType::Ring => 9,
971 ItemType::Talisman => 10,
972 ItemType::Shard { .. }
973 | ItemType::DungeonKey { .. }
974 | ItemType::ToiletKey
975 | ItemType::EpicItemBag => 11,
976 ItemType::Potion { .. } | ItemType::Resource { .. } => 12,
977 ItemType::Scrapbook => 13,
978 ItemType::Gem(_) => 15,
979 ItemType::PetItem { .. } => 16,
980 ItemType::QuickSandGlass | ItemType::Gral => 17,
981 ItemType::HeartOfDarkness => 18,
982 ItemType::WheelOfFortune => 19,
983 ItemType::Mannequin => 20,
984 ItemType::Unknown(u) => *u,
985 }
986 }
987}
988
989#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
991#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
992#[allow(missing_docs)]
993pub enum PotionType {
994 Strength,
995 Dexterity,
996 Intelligence,
997 Constitution,
998 Luck,
999 EternalLife,
1000}
1001
1002impl From<AttributeType> for PotionType {
1003 fn from(value: AttributeType) -> Self {
1004 match value {
1005 AttributeType::Strength => PotionType::Strength,
1006 AttributeType::Dexterity => PotionType::Dexterity,
1007 AttributeType::Intelligence => PotionType::Intelligence,
1008 AttributeType::Constitution => PotionType::Constitution,
1009 AttributeType::Luck => PotionType::Luck,
1010 }
1011 }
1012}
1013
1014impl PotionType {
1015 pub(crate) fn parse(id: i64) -> Option<PotionType> {
1016 if id == 0 {
1017 return None;
1018 }
1019 if id == 16 {
1020 return Some(PotionType::EternalLife);
1021 }
1022 Some(match id % 5 {
1023 0 => PotionType::Luck,
1024 1 => PotionType::Strength,
1025 2 => PotionType::Dexterity,
1026 3 => PotionType::Intelligence,
1027 _ => PotionType::Constitution,
1028 })
1029 }
1030}
1031
1032#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1034#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1035#[allow(missing_docs)]
1036pub enum PotionSize {
1037 Small,
1038 Medium,
1039 Large,
1040}
1041
1042impl PartialOrd for PotionSize {
1043 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1044 self.effect().partial_cmp(&other.effect())
1045 }
1046}
1047
1048impl PotionSize {
1049 #[must_use]
1050 pub fn effect(&self) -> f64 {
1051 match self {
1052 PotionSize::Small => 0.1,
1053 PotionSize::Medium => 0.15,
1054 PotionSize::Large => 0.25,
1055 }
1056 }
1057
1058 pub(crate) fn parse(id: i64) -> Option<Self> {
1059 Some(match id {
1060 1..=5 => PotionSize::Small,
1061 6..=10 => PotionSize::Medium,
1062 11..=16 => PotionSize::Large,
1063 _ => return None,
1064 })
1065 }
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Eq, Copy, FromPrimitive)]
1070#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1071#[allow(missing_docs)]
1072pub enum ResourceType {
1073 Wood = 17,
1074 Stone,
1075 Souls,
1076 Arcane,
1077 Metal,
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1082#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1083pub struct Gem {
1084 pub typ: GemType,
1086 pub value: u32,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1092#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1093#[allow(missing_docs)]
1094pub enum GemType {
1095 Strength,
1096 Dexterity,
1097 Intelligence,
1098 Constitution,
1099 Luck,
1100 All,
1101 Legendary,
1102}
1103
1104impl GemType {
1105 pub(crate) fn parse(id: i64, debug_value: u32) -> Option<GemType> {
1106 Some(match id {
1107 0 | 1 => return None,
1108 10..=40 => match id % 10 {
1109 0 => GemType::Strength,
1110 1 => GemType::Dexterity,
1111 2 => GemType::Intelligence,
1112 3 => GemType::Constitution,
1113 4 => GemType::Luck,
1114 5 => GemType::All,
1115 6 => GemType::Legendary,
1118 _ => {
1119 return None;
1120 }
1121 },
1122 _ => {
1123 warn!("Unknown gem: {id} - {debug_value}");
1124 return None;
1125 }
1126 })
1127 }
1128}
1129
1130#[derive(
1132 Debug, Copy, Clone, PartialEq, Eq, Hash, Enum, EnumIter, EnumCount,
1133)]
1134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1135#[allow(missing_docs)]
1136pub enum EquipmentSlot {
1137 Hat = 1,
1138 BreastPlate,
1139 Gloves,
1140 FootWear,
1141 Amulet,
1142 Belt,
1143 Ring,
1144 Talisman,
1145 Weapon,
1146 Shield,
1147}
1148
1149impl EquipmentSlot {
1150 #[must_use]
1153 pub fn raw_id(&self) -> u8 {
1154 match self {
1155 EquipmentSlot::Weapon => 1,
1156 EquipmentSlot::Shield => 2,
1157 EquipmentSlot::BreastPlate => 3,
1158 EquipmentSlot::FootWear => 4,
1159 EquipmentSlot::Gloves => 5,
1160 EquipmentSlot::Hat => 6,
1161 EquipmentSlot::Belt => 7,
1162 EquipmentSlot::Amulet => 8,
1163 EquipmentSlot::Ring => 9,
1164 EquipmentSlot::Talisman => 10,
1165 }
1166 }
1167
1168 #[must_use]
1171 pub const fn enchantment(&self) -> Option<Enchantment> {
1172 match self {
1173 EquipmentSlot::Hat => {
1174 Some(Enchantment::AdventurersArchaeologicalAura)
1175 }
1176 EquipmentSlot::BreastPlate => Some(Enchantment::MariosBeard),
1177 EquipmentSlot::Gloves => Some(Enchantment::ShadowOfTheCowboy),
1178 EquipmentSlot::FootWear => Some(Enchantment::ManyFeetBoots),
1179 EquipmentSlot::Amulet => Some(Enchantment::UnholyAcquisitiveness),
1180 EquipmentSlot::Belt => Some(Enchantment::ThirstyWanderer),
1181 EquipmentSlot::Ring => Some(Enchantment::TheGraveRobbersPrayer),
1182 EquipmentSlot::Talisman => Some(Enchantment::RobberBaronRitual),
1183 EquipmentSlot::Weapon => Some(Enchantment::SwordOfVengeance),
1184 EquipmentSlot::Shield => None,
1185 }
1186 }
1187}
1188
1189#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1192#[allow(missing_docs)]
1193pub enum PetItem {
1194 Egg(HabitatType),
1195 SpecialEgg(HabitatType),
1196 GoldenEgg,
1197 Nest,
1198 Fruit(HabitatType),
1199}
1200
1201impl PetItem {
1202 pub(crate) fn parse(val: i64) -> Option<Self> {
1203 Some(match val {
1204 1..=5 => PetItem::Egg(HabitatType::from_typ_id(val)?),
1205 11..=15 => PetItem::SpecialEgg(HabitatType::from_typ_id(val - 10)?),
1206 21 => PetItem::GoldenEgg,
1207 22 => PetItem::Nest,
1208 31..=35 => PetItem::Fruit(HabitatType::from_typ_id(val - 30)?),
1209 _ => return None,
1210 })
1211 }
1212}