use crate::comp::{ inventory::{ item::{tool, ItemKind}, slot::{ArmorSlot, EquipSlot}, InvSlot, }, Item, }; use serde::{Deserialize, Serialize}; use std::ops::Range; use tracing::warn; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Loadout { slots: Vec, } /// NOTE: Please don't derive a PartialEq Instance for this; that's broken! #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LoadoutSlot { /// The EquipSlot that this slot represents pub(super) equip_slot: EquipSlot, /// The contents of the slot slot: InvSlot, /// The unique string that represents this loadout slot in the database (not /// synced to clients) #[serde(skip)] persistence_key: String, } impl LoadoutSlot { fn new(equip_slot: EquipSlot, persistence_key: String) -> LoadoutSlot { LoadoutSlot { equip_slot, slot: None, persistence_key, } } } pub(super) struct LoadoutSlotId { // The index of the loadout item that provides this inventory slot. pub loadout_idx: usize, // The index of the slot within its container pub slot_idx: usize, } pub enum LoadoutError { InvalidPersistenceKey, NoParentAtSlot, } impl Loadout { pub(super) fn new_empty() -> Self { Self { slots: vec![ (EquipSlot::Lantern, "lantern".to_string()), (EquipSlot::Glider, "glider".to_string()), ( EquipSlot::Armor(ArmorSlot::Shoulders), "shoulder".to_string(), ), (EquipSlot::Armor(ArmorSlot::Chest), "chest".to_string()), (EquipSlot::Armor(ArmorSlot::Belt), "belt".to_string()), (EquipSlot::Armor(ArmorSlot::Hands), "hand".to_string()), (EquipSlot::Armor(ArmorSlot::Legs), "pants".to_string()), (EquipSlot::Armor(ArmorSlot::Feet), "foot".to_string()), (EquipSlot::Armor(ArmorSlot::Back), "back".to_string()), (EquipSlot::Armor(ArmorSlot::Ring1), "ring1".to_string()), (EquipSlot::Armor(ArmorSlot::Ring2), "ring2".to_string()), (EquipSlot::Armor(ArmorSlot::Neck), "neck".to_string()), (EquipSlot::Armor(ArmorSlot::Head), "head".to_string()), (EquipSlot::Armor(ArmorSlot::Tabard), "tabard".to_string()), (EquipSlot::Armor(ArmorSlot::Bag1), "bag1".to_string()), (EquipSlot::Armor(ArmorSlot::Bag2), "bag2".to_string()), (EquipSlot::Armor(ArmorSlot::Bag3), "bag3".to_string()), (EquipSlot::Armor(ArmorSlot::Bag4), "bag4".to_string()), (EquipSlot::ActiveMainhand, "active_mainhand".to_string()), (EquipSlot::ActiveOffhand, "active_offhand".to_string()), (EquipSlot::InactiveMainhand, "inactive_mainhand".to_string()), (EquipSlot::InactiveOffhand, "inactive_offhand".to_string()), ] .into_iter() .map(|(equip_slot, persistence_key)| LoadoutSlot::new(equip_slot, persistence_key)) .collect(), } } /// Replaces the item in the Loadout slot that corresponds to the given /// EquipSlot and returns the previous item if any pub(super) fn swap(&mut self, equip_slot: EquipSlot, item: Option) -> Option { self.slots .iter_mut() .find(|x| x.equip_slot == equip_slot) .and_then(|x| core::mem::replace(&mut x.slot, item)) } /// Returns a reference to the item (if any) equipped in the given EquipSlot pub(super) fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> { self.slot(equip_slot).and_then(|x| x.slot.as_ref()) } fn slot(&self, equip_slot: EquipSlot) -> Option<&LoadoutSlot> { self.slots .iter() .find(|loadout_slot| loadout_slot.equip_slot == equip_slot) } pub(super) fn loadout_idx_for_equip_slot(&self, equip_slot: EquipSlot) -> Option { self.slots .iter() .position(|loadout_slot| loadout_slot.equip_slot == equip_slot) } /// Returns all loadout items paired with their persistence key pub(super) fn items_with_persistence_key(&self) -> impl Iterator)> { self.slots .iter() .map(|x| (x.persistence_key.as_str(), x.slot.as_ref())) } /// Sets a loadout item in the correct slot using its persistence key. Any /// item that already exists in the slot is lost. pub fn set_item_at_slot_using_persistence_key( &mut self, persistence_key: &str, item: Item, ) -> Result<(), LoadoutError> { if let Some(slot) = self .slots .iter_mut() .find(|x| x.persistence_key == persistence_key) { slot.slot = Some(item); Ok(()) } else { Err(LoadoutError::InvalidPersistenceKey) } } pub fn update_item_at_slot_using_persistence_key( &mut self, persistence_key: &str, f: F, ) -> Result<(), LoadoutError> { self.slots .iter_mut() .find(|loadout_slot| loadout_slot.persistence_key == persistence_key) .map_or(Err(LoadoutError::InvalidPersistenceKey), |loadout_slot| { loadout_slot .slot .as_mut() .map_or(Err(LoadoutError::NoParentAtSlot), |item| { f(item); Ok(()) }) }) } /// Swaps the contents of two loadout slots pub(super) fn swap_slots(&mut self, equip_slot_a: EquipSlot, equip_slot_b: EquipSlot) { if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() { // Currently all loadouts contain slots for all EquipSlots so this can never // happen, but if loadouts with alternate slot combinations are // introduced then it could. warn!("Cannot swap slots for non-existent equip slot"); return; } enum MainhandHand { MainhandA, MainhandB, } let hands_swapping = match (equip_slot_a, equip_slot_b) { (EquipSlot::ActiveMainhand, EquipSlot::ActiveOffhand) => Some(MainhandHand::MainhandA), (EquipSlot::ActiveOffhand, EquipSlot::ActiveMainhand) => Some(MainhandHand::MainhandB), (EquipSlot::InactiveMainhand, EquipSlot::InactiveOffhand) => { Some(MainhandHand::MainhandA) }, (EquipSlot::InactiveOffhand, EquipSlot::InactiveMainhand) => { Some(MainhandHand::MainhandB) }, _ => None, }; let item_a = self.swap(equip_slot_a, None); let item_b = self.swap(equip_slot_b, None); if let Some(hands_swapping) = hands_swapping { match hands_swapping { MainhandHand::MainhandA => { if item_b .as_ref() .map_or(true, |i| self.slot_can_hold(equip_slot_a, &i.kind())) && item_a .as_ref() .map_or(true, |i| equip_slot_b.can_hold(&i.kind())) { // Checks that item b (from offhand) can go into equip slot a (mainhand // slot) and that item a (from mainhand) is a valid item to insert into // equip slot b (offhand slot) Swap self.swap(equip_slot_a, item_b).unwrap_none(); self.swap(equip_slot_b, item_a).unwrap_none(); } else { // Otherwise put the items back self.swap(equip_slot_a, item_a).unwrap_none(); self.swap(equip_slot_b, item_b).unwrap_none(); } }, MainhandHand::MainhandB => { if item_a .as_ref() .map_or(true, |i| self.slot_can_hold(equip_slot_b, &i.kind())) && item_b .as_ref() .map_or(true, |i| equip_slot_b.can_hold(&i.kind())) { // Checks that item a (from offhand) can go into equip slot b (mainhand // slot) and that item b (from mainhand) is a valid item to insert into // equip slot a (offhand slot) Swap self.swap(equip_slot_b, item_a).unwrap_none(); self.swap(equip_slot_a, item_b).unwrap_none(); } else { // Otherwise put the items back self.swap(equip_slot_a, item_a).unwrap_none(); self.swap(equip_slot_b, item_b).unwrap_none(); } }, } } else { // Check if items can go in the other slots if item_a .as_ref() .map_or(true, |i| self.slot_can_hold(equip_slot_b, &i.kind())) && item_b .as_ref() .map_or(true, |i| self.slot_can_hold(equip_slot_a, &i.kind())) { // Swap self.swap(equip_slot_b, item_a).unwrap_none(); self.swap(equip_slot_a, item_b).unwrap_none(); } else { // Otherwise put the items back self.swap(equip_slot_a, item_a).unwrap_none(); self.swap(equip_slot_b, item_b).unwrap_none(); } } } /// Gets a slot that an item of a particular `ItemKind` can be equipped /// into. The first empty slot compatible with the item will be /// returned, or if there are no free slots then the first occupied slot /// will be returned. The bool part of the tuple indicates whether an item /// is already equipped in the slot. pub(super) fn get_slot_to_equip_into(&self, item_kind: &ItemKind) -> Option { let mut suitable_slots = self .slots .iter() .filter(|s| self.slot_can_hold(s.equip_slot, item_kind)); let first = suitable_slots.next(); first .into_iter() .chain(suitable_slots) .find(|loadout_slot| loadout_slot.slot.is_none()) .map(|x| x.equip_slot) .or_else(|| first.map(|x| x.equip_slot)) } /// Returns all items currently equipped that an item of the given ItemKind /// could replace pub(super) fn equipped_items_of_kind( &self, item_kind: ItemKind, ) -> impl Iterator { self.slots .iter() .filter(move |s| self.slot_can_hold(s.equip_slot, &item_kind)) .filter_map(|s| s.slot.as_ref()) } /// Returns the `InvSlot` for a given `LoadoutSlotId` pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> { self.slots .get(loadout_slot_id.loadout_idx) .and_then(|loadout_slot| loadout_slot.slot.as_ref()) .and_then(|item| item.slot(loadout_slot_id.slot_idx)) } /// Returns the `InvSlot` for a given `LoadoutSlotId` pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> { self.slots .get_mut(loadout_slot_id.loadout_idx) .and_then(|loadout_slot| loadout_slot.slot.as_mut()) .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx)) } /// Returns all inventory slots provided by equipped loadout items, along /// with their `LoadoutSlotId` pub(super) fn inv_slots_with_id(&self) -> impl Iterator { self.slots .iter() .enumerate() .filter_map(|(i, loadout_slot)| { loadout_slot.slot.as_ref().map(|item| (i, item.slots())) }) .flat_map(|(loadout_slot_index, loadout_slots)| { loadout_slots .iter() .enumerate() .map(move |(item_slot_index, inv_slot)| { ( LoadoutSlotId { loadout_idx: loadout_slot_index, slot_idx: item_slot_index, }, inv_slot, ) }) }) } /// Returns all inventory slots provided by equipped loadout items pub(super) fn inv_slots_mut(&mut self) -> impl Iterator { self.slots.iter_mut() .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) // Discard loadout items that have no slots of their own .flat_map(|loadout_slots| loadout_slots.iter_mut()) //Collapse iter of Vec to iter of InvSlot } /// Gets the range of loadout-provided inventory slot indexes that are /// provided by the item in the given `EquipSlot` pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option> { self.slots .iter() .map(|loadout_slot| { ( loadout_slot.equip_slot, loadout_slot .slot .as_ref() .map_or(0, |item| item.slots().len()), ) }) .scan(0, |acc_len, (equip_slot, len)| { let res = Some((equip_slot, len, *acc_len)); *acc_len += len; res }) .find(|(e, len, _)| *e == equip_slot && len > &0) .map(|(_, slot_len, start)| start..start + slot_len) } /// Attempts to equip the item into a compatible, unpopulated loadout slot. /// If no slot is available the item is returned. #[must_use = "Returned item will be lost if not used"] pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> { /* if let Some(loadout_slot) = self .slots .iter_mut() .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, item.kind())) { loadout_slot.slot = Some(item); Ok(()) } else { Err(item) } */ // TODO: Get XVar to see if there better way to handle mutability issues let loadout_slot = self .slots .iter() .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, item.kind())) .map(|s| s.equip_slot); if let Some(slot) = self .slots .iter_mut() .find(|s| Some(s.equip_slot) == loadout_slot) { slot.slot = Some(item); Ok(()) } else { Err(item) } } pub(super) fn items(&self) -> impl Iterator { self.slots.iter().filter_map(|x| x.slot.as_ref()) } /// Checks that a slot can hold a given item pub(super) fn slot_can_hold(&self, equip_slot: EquipSlot, item_kind: &ItemKind) -> bool { // Checks if item can be equipped in a mainhand slot let mainhand_check = |offhand_slot| { // Allows item to be equipped if itemkind is a tool and... matches!(item_kind, ItemKind::Tool(mainhand) if { if let Some(ItemKind::Tool(offhand)) = self.equipped(offhand_slot).map(|i| i.kind()) { // if offhand is 1 handed, only if mainhand is also 1 handed matches!(offhand.hands, tool::Hands::One) && matches!(mainhand.hands, tool::Hands::One) } else { // else there is no tool equipped in offhand, so only if slot can normally hold this item equip_slot.can_hold(item_kind) } }) }; // Checks if item can be equipped in an offhand slot let offhand_check = |mainhand_slot| { // Allows item to be equipped if itemkind is a tool and... matches!(item_kind, ItemKind::Tool(offhand) if { // if offhand weapon is 1 handed... matches!(offhand.hands, tool::Hands::One) // and if mainhand has a 1 handed weapon... && matches!(self.equipped(mainhand_slot).map(|i| i.kind()), Some(ItemKind::Tool(mainhand)) if matches!(mainhand.hands, tool::Hands::One)) }) }; match equip_slot { EquipSlot::ActiveMainhand => mainhand_check(EquipSlot::ActiveOffhand), EquipSlot::ActiveOffhand => offhand_check(EquipSlot::ActiveMainhand), EquipSlot::InactiveMainhand => mainhand_check(EquipSlot::InactiveOffhand), EquipSlot::InactiveOffhand => offhand_check(EquipSlot::InactiveMainhand), _ => equip_slot.can_hold(item_kind), } } pub(super) fn swap_equipped_weapons(&mut self) { // Checks if a given slot can hold an item right now, defaults to true if // nothing is equipped in slot let valid_slot = |equip_slot| { self.equipped(equip_slot) .map_or(true, |i| self.slot_can_hold(equip_slot, i.kind())) }; if valid_slot(EquipSlot::ActiveMainhand) && valid_slot(EquipSlot::ActiveOffhand) && valid_slot(EquipSlot::InactiveMainhand) && valid_slot(EquipSlot::InactiveOffhand) { // Get weapons from each slot let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None); let active_offhand = self.swap(EquipSlot::ActiveOffhand, None); let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None); let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None); // Equip weapons into new slots self.swap(EquipSlot::ActiveMainhand, inactive_mainhand) .unwrap_none(); self.swap(EquipSlot::ActiveOffhand, inactive_offhand) .unwrap_none(); self.swap(EquipSlot::InactiveMainhand, active_mainhand) .unwrap_none(); self.swap(EquipSlot::InactiveOffhand, active_offhand) .unwrap_none(); } } } #[cfg(test)] mod tests { use crate::comp::{ inventory::{ item::{ armor::{Armor, ArmorKind, Protection}, ItemKind, }, loadout::Loadout, slot::{ArmorSlot, EquipSlot}, test_helpers::get_test_bag, }, Item, }; #[test] fn test_slot_range_for_equip_slot() { let mut loadout = Loadout::new_empty(); let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1); let bag = get_test_bag(18); loadout.swap(bag1_slot, Some(bag)); let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap(); assert_eq!(0..18, result); } #[test] fn test_slot_range_for_equip_slot_no_item() { let loadout = Loadout::new_empty(); let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1)); assert_eq!(None, result); } #[test] fn test_slot_range_for_equip_slot_item_without_slots() { let mut loadout = Loadout::new_empty(); let feet_slot = EquipSlot::Armor(ArmorSlot::Feet); let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); loadout.swap(feet_slot, Some(boots)); let result = loadout.slot_range_for_equip_slot(feet_slot); assert_eq!(None, result); } #[test] fn test_get_slot_to_equip_into_second_bag_slot_free() { let mut loadout = Loadout::new_empty(); loadout.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(1))); let result = loadout .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor( ArmorKind::Bag("test".to_string()), Protection::Normal(0.0), Protection::Normal(0.0), ))) .unwrap(); assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result); } #[test] fn test_get_slot_to_equip_into_no_bag_slots_free() { let mut loadout = Loadout::new_empty(); loadout.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(1))); loadout.swap(EquipSlot::Armor(ArmorSlot::Bag2), Some(get_test_bag(1))); loadout.swap(EquipSlot::Armor(ArmorSlot::Bag3), Some(get_test_bag(1))); loadout.swap(EquipSlot::Armor(ArmorSlot::Bag4), Some(get_test_bag(1))); let result = loadout .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor( ArmorKind::Bag("test".to_string()), Protection::Normal(0.0), Protection::Normal(0.0), ))) .unwrap(); assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result); } }