veloren_common/comp/inventory/
loadout.rs

1use crate::{
2    comp::{
3        Item,
4        inventory::{
5            InvSlot,
6            item::{self, Hands, ItemDefinitionIdOwned, ItemKind, tool::Tool},
7            slot::{ArmorSlot, EquipSlot},
8        },
9    },
10    resources::Time,
11};
12use hashbrown::HashMap;
13use serde::{Deserialize, Serialize};
14use std::ops::Range;
15use tracing::warn;
16
17pub(super) const UNEQUIP_TRACKING_DURATION: f64 = 60.0;
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct Loadout {
21    slots: Vec<LoadoutSlot>,
22    // Includes time that item was unequipped at
23    #[serde(skip)]
24    // Tracks time unequipped at and number that have been unequipped (for things like dual
25    // wielding, rings, or other future cases)
26    pub(super) recently_unequipped_items: HashMap<ItemDefinitionIdOwned, (Time, u8)>,
27}
28
29/// NOTE: Please don't derive a PartialEq Instance for this; that's broken!
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct LoadoutSlot {
32    /// The EquipSlot that this slot represents
33    pub(super) equip_slot: EquipSlot,
34    /// The contents of the slot
35    slot: InvSlot,
36    /// The unique string that represents this loadout slot in the database (not
37    /// synced to clients)
38    #[serde(skip)]
39    persistence_key: String,
40}
41
42impl LoadoutSlot {
43    fn new(equip_slot: EquipSlot, persistence_key: String) -> LoadoutSlot {
44        LoadoutSlot {
45            equip_slot,
46            slot: None,
47            persistence_key,
48        }
49    }
50}
51
52pub(super) struct LoadoutSlotId {
53    // The index of the loadout item that provides this inventory slot.
54    pub loadout_idx: usize,
55    // The index of the slot within its container
56    pub slot_idx: usize,
57}
58
59pub enum LoadoutError {
60    InvalidPersistenceKey,
61    NoParentAtSlot,
62}
63
64impl Loadout {
65    pub(super) fn new_empty() -> Self {
66        Self {
67            slots: vec![
68                (EquipSlot::Lantern, "lantern".to_string()),
69                (EquipSlot::Glider, "glider".to_string()),
70                (
71                    EquipSlot::Armor(ArmorSlot::Shoulders),
72                    "shoulder".to_string(),
73                ),
74                (EquipSlot::Armor(ArmorSlot::Chest), "chest".to_string()),
75                (EquipSlot::Armor(ArmorSlot::Belt), "belt".to_string()),
76                (EquipSlot::Armor(ArmorSlot::Hands), "hand".to_string()),
77                (EquipSlot::Armor(ArmorSlot::Legs), "pants".to_string()),
78                (EquipSlot::Armor(ArmorSlot::Feet), "foot".to_string()),
79                (EquipSlot::Armor(ArmorSlot::Back), "back".to_string()),
80                (EquipSlot::Armor(ArmorSlot::Ring1), "ring1".to_string()),
81                (EquipSlot::Armor(ArmorSlot::Ring2), "ring2".to_string()),
82                (EquipSlot::Armor(ArmorSlot::Neck), "neck".to_string()),
83                (EquipSlot::Armor(ArmorSlot::Head), "head".to_string()),
84                (EquipSlot::Armor(ArmorSlot::Tabard), "tabard".to_string()),
85                (EquipSlot::Armor(ArmorSlot::Bag1), "bag1".to_string()),
86                (EquipSlot::Armor(ArmorSlot::Bag2), "bag2".to_string()),
87                (EquipSlot::Armor(ArmorSlot::Bag3), "bag3".to_string()),
88                (EquipSlot::Armor(ArmorSlot::Bag4), "bag4".to_string()),
89                (EquipSlot::ActiveMainhand, "active_mainhand".to_string()),
90                (EquipSlot::ActiveOffhand, "active_offhand".to_string()),
91                (EquipSlot::InactiveMainhand, "inactive_mainhand".to_string()),
92                (EquipSlot::InactiveOffhand, "inactive_offhand".to_string()),
93            ]
94            .into_iter()
95            .map(|(equip_slot, persistence_key)| LoadoutSlot::new(equip_slot, persistence_key))
96            .collect(),
97            recently_unequipped_items: HashMap::new(),
98        }
99    }
100
101    /// Replaces the item in the Loadout slot that corresponds to the given
102    /// EquipSlot and returns the previous item if any
103    pub(super) fn swap(
104        &mut self,
105        equip_slot: EquipSlot,
106        item: Option<Item>,
107        time: Time,
108    ) -> Option<Item> {
109        if let Some(item_def_id) = item.as_ref().map(|item| item.item_definition_id()) {
110            if let Some((_unequip_time, count)) =
111                self.recently_unequipped_items.get_mut(&item_def_id)
112            {
113                *count = count.saturating_sub(1);
114            }
115        }
116        self.cull_recently_unequipped_items(time);
117        let unequipped_item = self
118            .slots
119            .iter_mut()
120            .find(|x| x.equip_slot == equip_slot)
121            .and_then(|x| core::mem::replace(&mut x.slot, item));
122        if let Some(unequipped_item) = unequipped_item.as_ref() {
123            let entry = self
124                .recently_unequipped_items
125                .entry_ref(&unequipped_item.item_definition_id())
126                .or_insert((time, 0));
127            *entry = (time, entry.1.saturating_add(1));
128        }
129        unequipped_item
130    }
131
132    /// Returns a reference to the item (if any) equipped in the given EquipSlot
133    pub(super) fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
134        self.slot(equip_slot).and_then(|x| x.slot.as_ref())
135    }
136
137    fn slot(&self, equip_slot: EquipSlot) -> Option<&LoadoutSlot> {
138        self.slots
139            .iter()
140            .find(|loadout_slot| loadout_slot.equip_slot == equip_slot)
141    }
142
143    pub(super) fn loadout_idx_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<usize> {
144        self.slots
145            .iter()
146            .position(|loadout_slot| loadout_slot.equip_slot == equip_slot)
147    }
148
149    /// Returns all loadout items paired with their persistence key
150    pub(super) fn items_with_persistence_key(&self) -> impl Iterator<Item = (&str, Option<&Item>)> {
151        self.slots
152            .iter()
153            .map(|x| (x.persistence_key.as_str(), x.slot.as_ref()))
154    }
155
156    /// Sets a loadout item in the correct slot using its persistence key. Any
157    /// item that already exists in the slot is lost.
158    pub fn set_item_at_slot_using_persistence_key(
159        &mut self,
160        persistence_key: &str,
161        item: Item,
162    ) -> Result<(), LoadoutError> {
163        if let Some(slot) = self
164            .slots
165            .iter_mut()
166            .find(|x| x.persistence_key == persistence_key)
167        {
168            slot.slot = Some(item);
169            Ok(())
170        } else {
171            Err(LoadoutError::InvalidPersistenceKey)
172        }
173    }
174
175    pub fn get_mut_item_at_slot_using_persistence_key(
176        &mut self,
177        persistence_key: &str,
178    ) -> Result<&mut Item, LoadoutError> {
179        self.slots
180            .iter_mut()
181            .find(|loadout_slot| loadout_slot.persistence_key == persistence_key)
182            .map_or(Err(LoadoutError::InvalidPersistenceKey), |loadout_slot| {
183                loadout_slot
184                    .slot
185                    .as_mut()
186                    .ok_or(LoadoutError::NoParentAtSlot)
187            })
188    }
189
190    /// Swaps the contents of two loadout slots
191    pub(super) fn swap_slots(
192        &mut self,
193        equip_slot_a: EquipSlot,
194        equip_slot_b: EquipSlot,
195        time: Time,
196    ) {
197        if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() {
198            // Currently all loadouts contain slots for all EquipSlots so this can never
199            // happen, but if loadouts with alternate slot combinations are
200            // introduced then it could.
201            warn!("Cannot swap slots for non-existent equip slot");
202            return;
203        }
204
205        let item_a = self.swap(equip_slot_a, None, time);
206        let item_b = self.swap(equip_slot_b, item_a, time);
207        assert_eq!(self.swap(equip_slot_a, item_b, time), None);
208
209        // Check if items are valid in their new positions
210        if !self.slot_can_hold(
211            equip_slot_a,
212            self.equipped(equip_slot_a).map(|x| x.kind()).as_deref(),
213        ) || !self.slot_can_hold(
214            equip_slot_b,
215            self.equipped(equip_slot_b).map(|x| x.kind()).as_deref(),
216        ) {
217            // If not, revert the swap
218            let item_a = self.swap(equip_slot_a, None, time);
219            let item_b = self.swap(equip_slot_b, item_a, time);
220            assert_eq!(self.swap(equip_slot_a, item_b, time), None);
221        }
222    }
223
224    /// Gets a slot that an item of a particular `ItemKind` can be equipped
225    /// into. A slot compatible with the item will be returned, prioritizing
226    /// empty slots, then slots with differing items, and returning the first
227    /// slot if no other is availible.
228    pub(super) fn get_slot_to_equip_into(&self, item: &Item) -> Option<EquipSlot> {
229        let mut suitable_slots = self
230            .slots
231            .iter()
232            .filter(|s| self.slot_can_hold(s.equip_slot, Some(&*item.kind())));
233
234        let first_suitable = suitable_slots.next();
235
236        let mut differing_suitable_slots =
237            first_suitable
238                .into_iter()
239                .chain(suitable_slots)
240                .filter(|loadout_slot| {
241                    loadout_slot.slot.as_ref().is_none_or(|equipped_item| {
242                        equipped_item.item_definition_id() != item.item_definition_id()
243                            || equipped_item.durability_lost() != item.durability_lost()
244                    })
245                });
246
247        let first_differing = differing_suitable_slots.next();
248
249        first_differing
250            .into_iter()
251            .chain(differing_suitable_slots)
252            .find(|loadout_slot| loadout_slot.slot.is_none())
253            .map(|x| x.equip_slot)
254            .or_else(|| first_differing.map(|x| x.equip_slot))
255            .or_else(|| first_suitable.map(|x| x.equip_slot))
256    }
257
258    /// Returns all items currently equipped that an item of the given ItemKind
259    /// could replace
260    pub(super) fn equipped_items_replaceable_by<'a>(
261        &'a self,
262        item_kind: &'a ItemKind,
263    ) -> impl Iterator<Item = &'a Item> {
264        self.slots
265            .iter()
266            .filter(move |s| self.slot_can_hold(s.equip_slot, Some(item_kind)))
267            .filter_map(|s| s.slot.as_ref())
268    }
269
270    /// Returns the `InvSlot` for a given `LoadoutSlotId`
271    pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> {
272        self.slots
273            .get(loadout_slot_id.loadout_idx)
274            .and_then(|loadout_slot| loadout_slot.slot.as_ref())
275            .and_then(|item| item.slot(loadout_slot_id.slot_idx))
276    }
277
278    /// Returns the `InvSlot` for a given `LoadoutSlotId`
279    pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> {
280        self.slots
281            .get_mut(loadout_slot_id.loadout_idx)
282            .and_then(|loadout_slot| loadout_slot.slot.as_mut())
283            .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx))
284    }
285
286    /// Returns all inventory slots provided by equipped loadout items, along
287    /// with their `LoadoutSlotId`
288    pub(super) fn inv_slots_with_id(&self) -> impl Iterator<Item = (LoadoutSlotId, &InvSlot)> {
289        self.slots
290            .iter()
291            .enumerate()
292            .filter_map(|(i, loadout_slot)| {
293                loadout_slot.slot.as_ref().map(|item| (i, item.slots()))
294            })
295            .flat_map(|(loadout_slot_index, loadout_slots)| {
296                loadout_slots
297                    .iter()
298                    .enumerate()
299                    .map(move |(item_slot_index, inv_slot)| {
300                        (
301                            LoadoutSlotId {
302                                loadout_idx: loadout_slot_index,
303                                slot_idx: item_slot_index,
304                            },
305                            inv_slot,
306                        )
307                    })
308            })
309    }
310
311    /// Returns all inventory slots provided by equipped loadout items
312    pub(super) fn inv_slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
313        self.slots.iter_mut()
314            .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut()))  // Discard loadout items that have no slots of their own
315            .flat_map(|loadout_slots| loadout_slots.iter_mut()) //Collapse iter of Vec<InvSlot> to iter of InvSlot
316    }
317
318    pub(super) fn inv_slots_mut_with_mutable_recently_unequipped_items(
319        &mut self,
320    ) -> (
321        impl Iterator<Item = &mut InvSlot>,
322        &mut HashMap<ItemDefinitionIdOwned, (Time, u8)>,
323    ) {
324        let slots_mut = self.slots.iter_mut()
325            .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut()))  // Discard loadout items that have no slots of their own
326            .flat_map(|loadout_slots| loadout_slots.iter_mut()); //Collapse iter of Vec<InvSlot> to iter of InvSlot
327        (slots_mut, &mut self.recently_unequipped_items)
328    }
329
330    /// Gets the range of loadout-provided inventory slot indexes that are
331    /// provided by the item in the given `EquipSlot`
332    pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
333        self.slots
334            .iter()
335            .map(|loadout_slot| {
336                (
337                    loadout_slot.equip_slot,
338                    loadout_slot
339                        .slot
340                        .as_ref()
341                        .map_or(0, |item| item.slots().len()),
342                )
343            })
344            .scan(0, |acc_len, (equip_slot, len)| {
345                let res = Some((equip_slot, len, *acc_len));
346                *acc_len += len;
347                res
348            })
349            .find(|(e, len, _)| *e == equip_slot && len > &0)
350            .map(|(_, slot_len, start)| start..start + slot_len)
351    }
352
353    /// Attempts to equip the item into a compatible, unpopulated loadout slot.
354    /// If no slot is available the item is returned.
355    #[must_use = "Returned item will be lost if not used"]
356    pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> {
357        let loadout_slot = self
358            .slots
359            .iter()
360            .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, Some(&*item.kind())))
361            .map(|s| s.equip_slot);
362        if let Some(slot) = self
363            .slots
364            .iter_mut()
365            .find(|s| Some(s.equip_slot) == loadout_slot)
366        {
367            slot.slot = Some(item);
368            Ok(())
369        } else {
370            Err(item)
371        }
372    }
373
374    pub(super) fn items(&self) -> impl Iterator<Item = &Item> {
375        self.slots.iter().filter_map(|x| x.slot.as_ref())
376    }
377
378    pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
379        self.slots
380            .iter()
381            .filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
382    }
383
384    /// Checks that a slot can hold a given item
385    pub(super) fn slot_can_hold(
386        &self,
387        equip_slot: EquipSlot,
388        item_kind: Option<&ItemKind>,
389    ) -> bool {
390        // Disallow equipping incompatible weapon pairs (i.e a two-handed weapon and a
391        // one-handed weapon)
392        if !(match equip_slot {
393            EquipSlot::ActiveMainhand => Loadout::is_valid_weapon_pair(
394                item_kind,
395                self.equipped(EquipSlot::ActiveOffhand)
396                    .map(|x| x.kind())
397                    .as_deref(),
398            ),
399            EquipSlot::ActiveOffhand => Loadout::is_valid_weapon_pair(
400                self.equipped(EquipSlot::ActiveMainhand)
401                    .map(|x| x.kind())
402                    .as_deref(),
403                item_kind,
404            ),
405            EquipSlot::InactiveMainhand => Loadout::is_valid_weapon_pair(
406                item_kind,
407                self.equipped(EquipSlot::InactiveOffhand)
408                    .map(|x| x.kind())
409                    .as_deref(),
410            ),
411            EquipSlot::InactiveOffhand => Loadout::is_valid_weapon_pair(
412                self.equipped(EquipSlot::InactiveMainhand)
413                    .map(|x| x.kind())
414                    .as_deref(),
415                item_kind,
416            ),
417            _ => true,
418        }) {
419            return false;
420        }
421
422        item_kind.is_none_or(|item| equip_slot.can_hold(item))
423    }
424
425    #[rustfmt::skip]
426    fn is_valid_weapon_pair(main_hand: Option<&ItemKind>, off_hand: Option<&ItemKind>) -> bool {
427        matches!((main_hand, off_hand),
428            (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), None) |
429            (Some(ItemKind::Tool(Tool { hands: Hands::Two, .. })), None) |
430            (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), Some(ItemKind::Tool(Tool { hands: Hands::One, .. }))) |
431            (None, None))
432    }
433
434    pub(super) fn swap_equipped_weapons(&mut self, time: Time) {
435        // Checks if a given slot can hold an item right now, defaults to true if
436        // nothing is equipped in slot
437        let valid_slot = |equip_slot| {
438            self.equipped(equip_slot)
439                .is_none_or(|i| self.slot_can_hold(equip_slot, Some(&*i.kind())))
440        };
441
442        // If every weapon is currently in a valid slot, after this change they will
443        // still be in a valid slot. This is because active mainhand and
444        // inactive mainhand, and active offhand and inactive offhand have the same
445        // requirements on what can be equipped.
446        if valid_slot(EquipSlot::ActiveMainhand)
447            && valid_slot(EquipSlot::ActiveOffhand)
448            && valid_slot(EquipSlot::InactiveMainhand)
449            && valid_slot(EquipSlot::InactiveOffhand)
450        {
451            // Get weapons from each slot
452            let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None, time);
453            let active_offhand = self.swap(EquipSlot::ActiveOffhand, None, time);
454            let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None, time);
455            let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None, time);
456            // Equip weapons into new slots
457            assert!(
458                self.swap(EquipSlot::ActiveMainhand, inactive_mainhand, time)
459                    .is_none()
460            );
461            assert!(
462                self.swap(EquipSlot::ActiveOffhand, inactive_offhand, time)
463                    .is_none()
464            );
465            assert!(
466                self.swap(EquipSlot::InactiveMainhand, active_mainhand, time)
467                    .is_none()
468            );
469            assert!(
470                self.swap(EquipSlot::InactiveOffhand, active_offhand, time)
471                    .is_none()
472            );
473        }
474    }
475
476    /// Update internal computed state of all top level items in this loadout.
477    /// Used only when loading in persistence code.
478    pub fn persistence_update_all_item_states(
479        &mut self,
480        ability_map: &item::tool::AbilityMap,
481        msm: &item::MaterialStatManifest,
482    ) {
483        self.slots.iter_mut().for_each(|slot| {
484            if let Some(item) = &mut slot.slot {
485                item.update_item_state(ability_map, msm);
486            }
487        });
488    }
489
490    /// Increments durability by 1 of all valid items
491    pub(super) fn damage_items(
492        &mut self,
493        ability_map: &item::tool::AbilityMap,
494        msm: &item::MaterialStatManifest,
495    ) {
496        self.slots
497            .iter_mut()
498            .filter_map(|slot| slot.slot.as_mut())
499            .filter(|item| item.has_durability())
500            .for_each(|item| item.increment_damage(ability_map, msm));
501    }
502
503    /// Resets durability of item in specified slot
504    pub(super) fn repair_item_at_slot(
505        &mut self,
506        equip_slot: EquipSlot,
507        ability_map: &item::tool::AbilityMap,
508        msm: &item::MaterialStatManifest,
509    ) {
510        if let Some(item) = self
511            .slots
512            .iter_mut()
513            .find(|slot| slot.equip_slot == equip_slot)
514            .and_then(|slot| slot.slot.as_mut())
515        {
516            item.reset_durability(ability_map, msm);
517        }
518    }
519
520    pub(super) fn cull_recently_unequipped_items(&mut self, time: Time) {
521        self.recently_unequipped_items
522            .retain(|_def, (unequip_time, count)| {
523                // If somehow time went backwards or faulty unequip time supplied, set unequip
524                // time to minimum of current time and unequip time
525                if time.0 < unequip_time.0 {
526                    *unequip_time = time;
527                }
528
529                (time.0 - unequip_time.0 < UNEQUIP_TRACKING_DURATION) && *count > 0
530            });
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use crate::{
537        comp::{
538            Item,
539            inventory::{
540                loadout::Loadout,
541                slot::{ArmorSlot, EquipSlot},
542                test_helpers::get_test_bag,
543            },
544        },
545        resources::Time,
546    };
547
548    #[test]
549    fn test_slot_range_for_equip_slot() {
550        let mut loadout = Loadout::new_empty();
551
552        let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
553        let bag = get_test_bag(18);
554        loadout.swap(bag1_slot, Some(bag), Time(0.0));
555
556        let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap();
557
558        assert_eq!(0..18, result);
559    }
560
561    #[test]
562    fn test_slot_range_for_equip_slot_no_item() {
563        let loadout = Loadout::new_empty();
564        let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1));
565
566        assert_eq!(None, result);
567    }
568
569    #[test]
570    fn test_slot_range_for_equip_slot_item_without_slots() {
571        let mut loadout = Loadout::new_empty();
572
573        let feet_slot = EquipSlot::Armor(ArmorSlot::Feet);
574        let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
575        loadout.swap(feet_slot, Some(boots), Time(0.0));
576        let result = loadout.slot_range_for_equip_slot(feet_slot);
577
578        assert_eq!(None, result);
579    }
580
581    #[test]
582    fn test_get_slot_to_equip_into_second_bag_slot_free() {
583        let mut loadout = Loadout::new_empty();
584
585        loadout.swap(
586            EquipSlot::Armor(ArmorSlot::Bag1),
587            Some(get_test_bag(1)),
588            Time(0.0),
589        );
590
591        let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
592
593        assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result);
594    }
595
596    #[test]
597    fn test_get_slot_to_equip_into_no_bag_slots_free() {
598        let mut loadout = Loadout::new_empty();
599
600        loadout.swap(
601            EquipSlot::Armor(ArmorSlot::Bag1),
602            Some(get_test_bag(1)),
603            Time(0.0),
604        );
605        loadout.swap(
606            EquipSlot::Armor(ArmorSlot::Bag2),
607            Some(get_test_bag(1)),
608            Time(0.0),
609        );
610        loadout.swap(
611            EquipSlot::Armor(ArmorSlot::Bag3),
612            Some(get_test_bag(1)),
613            Time(0.0),
614        );
615        loadout.swap(
616            EquipSlot::Armor(ArmorSlot::Bag4),
617            Some(get_test_bag(1)),
618            Time(0.0),
619        );
620
621        let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
622
623        assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result);
624    }
625}