game_features/
inventory.rs

1use crate::*;
2use std::fmt::Debug;
3use std::hash::Hash;
4
5/// The way the inventory size is handled.
6#[derive(new, Clone, Serialize, Deserialize, Debug)]
7pub enum InventorySizingMode {
8    /// The inventory uses a fixed size.
9    Fixed {
10        /// The size of the inventory.
11        size: usize,
12    },
13    /// The inventory grows and shrinks depending on the content.
14    /// Slot restrictions are ignored in this mode.
15    Dynamic {
16        /// The minimum size of the dynamic inventory.
17        min_size: usize,
18        /// The maximum size of the dynamic inventory.
19        max_size: usize,
20    },
21}
22
23/// The way items are removed from the inventory. Indicates if empty spots are left, and if not, how to fill them.
24#[derive(new, Clone, Serialize, Deserialize, Debug)]
25pub enum MoveToFrontMode {
26    /// Don't move items to the front when there is available space.
27    None,
28    /// Takes the last element and puts it where the removed one was.
29    TakeLast,
30    /// Moves all elements after the removed one.
31    Offset,
32}
33
34// for even more complex restrictions, like limit max weight -> wrap inventory in other struct and make
35// the checks there.
36
37// TODO Complete slot restriction integration
38// TODO Respect maximum stack size
39
40/// # Generics
41/// - K: Item Type
42/// - S: Type of inventory location
43/// - U: Custom item data
44#[derive(new, Clone, Serialize, Deserialize, Debug, Builder)]
45pub struct Inventory<K, S: SlotType, U: Default> {
46    /// The contents of the `Inventory`.
47    /// None values indicate empty but existing inventory slots.
48    pub content: Vec<Option<ItemInstance<K, U>>>,
49    /// Restricts what kind of item can go in different slots.
50    /// This is not compatible with `InventorySizingMode::Dynamic`.
51    ///
52    /// Maps to the inventory content using the index.
53    /// None values indicate that there are no restrictions for that slot.
54    #[builder(default)]
55    pub slot_restriction: Vec<Option<S>>,
56    /// Configures how item deletion is handled.
57    pub move_to_front: MoveToFrontMode,
58    /// Configures if the inventory resizes when item are inserted/removed or not.
59    pub sizing_mode: InventorySizingMode,
60}
61
62impl<
63        K: PartialEq + Clone + Debug + Hash + Eq,
64        S: SlotType,
65        U: Default + Clone + Debug + PartialEq,
66    > Inventory<K, S, U>
67{
68    /// Creates a new `Inventory` with a fixed slot count.
69    pub fn new_fixed(count: usize) -> Inventory<K, S, U> {
70        let mut content = Vec::with_capacity(count);
71        (0..count).for_each(|_| content.push(None));
72        let mut slot_restriction = Vec::with_capacity(count);
73        (0..count).for_each(|_| slot_restriction.push(None));
74        Inventory {
75            content,
76            slot_restriction,
77            move_to_front: MoveToFrontMode::None,
78            sizing_mode: InventorySizingMode::new_fixed(count),
79        }
80    }
81
82    /// Creates a new dynamically sized `Inventory`. A minimum of `minimum` slots are garanteed to
83    /// be present at all time. The quantity of slots will not go over `maximum`.
84    pub fn new_dynamic(minimum: usize, maximum: usize) -> Inventory<K, S, U> {
85        let mut content = Vec::with_capacity(minimum);
86        (0..minimum).for_each(|_| content.push(None));
87        Inventory {
88            content,
89            slot_restriction: vec![],
90            move_to_front: MoveToFrontMode::None,
91            sizing_mode: InventorySizingMode::new_dynamic(minimum, maximum),
92        }
93    }
94
95    /// Will attempt to decrease the durability of the item at the specified index.
96    /// If the item has no durability value (None) or a non zero durability, it will return this
97    /// value.
98    /// If the item has a durability of 0 when using it, it will break and
99    /// `ItemError::ItemDestroyed` will be returned.
100    pub fn use_item(&mut self, idx: usize) -> Result<Option<usize>, ItemError<K, U>> {
101        if let Some(Some(ii)) = self.content.get_mut(idx) {
102            if ii.durability.is_some() {
103                if ii.durability.unwrap() == 0 {
104                    //rm item
105                    Err(ItemError::ItemDestroyed(self.delete_stack(idx)?))
106                } else {
107                    *ii.durability.as_mut().unwrap() -= 1;
108                    Ok(Some(ii.durability.unwrap()))
109                }
110            } else {
111                Ok(None)
112            }
113        } else {
114            Err(ItemError::SlotEmpty)
115        }
116    }
117
118    /// Decreases the stack size by one and returns the current value.
119    /// Once the stack size hits zero, it will return `ItemError::StackConsumed`.
120    pub fn consume(&mut self, idx: usize) -> Result<usize, ItemError<K, U>> {
121        if let Some(Some(ii)) = self.content.get_mut(idx) {
122            ii.quantity -= 1;
123            if ii.quantity == 0 {
124                Err(ItemError::StackConsumed(self.delete_stack(idx)?))
125            } else {
126                Ok(ii.quantity)
127            }
128        } else {
129            Err(ItemError::SlotEmpty)
130        }
131    }
132
133    /// Looks if there is enough space to add another item stack.
134    pub fn has_space(&self) -> bool {
135        match self.sizing_mode {
136            InventorySizingMode::Fixed { size: _ } => self.content.iter().any(|o| o.is_none()),
137            InventorySizingMode::Dynamic {
138                min_size: _,
139                max_size,
140            } => self.content.len() != max_size,
141        }
142    }
143
144    // TODO transfer no target (ie transfer all)
145
146    /// Transfers a specified quantity of item from one slot of this inventory to a specified slot
147    /// of the provided target inventory.
148    /// with_overflow indicates if the item can be spread out in free slots in case that the target
149    /// slot does not have enough free space.
150    ///
151    /// Errors:
152    /// See `Transform::delete` and `Transform::insert_into`.
153    pub fn transfer<U2: Default>(
154        &mut self,
155        from_idx: usize,
156        target: &mut Inventory<K, S, U>,
157        to_idx: usize,
158        quantity: usize,
159        _with_overflow: bool,
160        item_defs: &ItemDefinitions<K, S, U2>,
161    ) -> Result<(), ItemError<K, U>> {
162        let mv = self.delete(from_idx, quantity)?;
163        target.insert_into(to_idx, mv, item_defs)?;
164        // TODO overflow control
165        // TODO stack maximum size
166        Ok(())
167    }
168
169    /// Transfers a whole stack from the specified slot into a specified slot of the provided
170    /// target directory.
171    /// with_overflow indicates if the item can be spread out in free slots in case that the target
172    /// slot does not have enough free space.
173    ///
174    /// Errors:
175    /// See `Transform::delete` and `Transform::insert_into`.
176    pub fn transfer_stack<U2: Default>(
177        &mut self,
178        from_idx: usize,
179        target: &mut Inventory<K, S, U>,
180        to_idx: usize,
181        with_overflow: bool,
182        item_defs: &ItemDefinitions<K, S, U2>,
183    ) -> Result<(), ItemError<K, U>> {
184        if let Some(Some(qty)) = self
185            .content
186            .get(from_idx)
187            .map(|i| i.as_ref().map(|i2| i2.quantity))
188        {
189            self.transfer(from_idx, target, to_idx, qty, with_overflow, item_defs)
190        } else {
191            Err(ItemError::SlotEmpty)
192        }
193    }
194
195    /// Moves a specified quantity of item from a slot to another.
196    /// with_overflow indicates if the item can be spread out in free slots in case that the target
197    /// slot does not have enough free space.
198    ///
199    /// Errors:
200    /// See `Inventory::delete` and `Inventory::insert_into`.
201    pub fn move_item<U2: Default>(
202        &mut self,
203        from_idx: usize,
204        to_idx: usize,
205        quantity: usize,
206        _with_overflow: bool,
207        item_defs: &ItemDefinitions<K, S, U2>,
208    ) -> Result<(), ItemError<K, U>> {
209        let mv = self.delete(from_idx, quantity)?;
210        self.insert_into(to_idx, mv, item_defs)?;
211        Ok(())
212    }
213
214    // TODO: swap item stacks
215
216    /// Moves a full stack of item from a slot to another.
217    /// with_overflow indicates if the item can be spread out in free slots in case that the target
218    /// slot does not have enough free space.
219    ///
220    /// Errors:
221    /// * SlotEmpty: Nothing is present in the specified slot.
222    pub fn move_stack<U2: Default>(
223        &mut self,
224        from_idx: usize,
225        to_idx: usize,
226        with_overflow: bool,
227        item_defs: &ItemDefinitions<K, S, U2>,
228    ) -> Result<(), ItemError<K, U>> {
229        if let Some(Some(qty)) = self
230            .content
231            .get(from_idx)
232            .map(|i| i.as_ref().map(|i2| i2.quantity))
233        {
234            self.move_item(from_idx, to_idx, qty, with_overflow, item_defs)
235        } else {
236            Err(ItemError::SlotEmpty)
237        }
238    }
239
240    /// Deletes a specified quantity of item from the specified slot.
241    ///
242    /// Errors:
243    /// * NotEnoughQuantity: Not enough items are present in the item stack.
244    /// * SlotEmpty: Nothing is present in the specified slot.
245    pub fn delete(
246        &mut self,
247        idx: usize,
248        quantity: usize,
249    ) -> Result<ItemInstance<K, U>, ItemError<K, U>> {
250        if let Some(Some(ii)) = self.content.get_mut(idx) {
251            if ii.quantity >= quantity {
252                ii.quantity -= quantity;
253                let mut ret = ItemInstance::new(ii.key.clone(), quantity);
254                ret.durability = ii.durability.clone();
255
256                if ii.quantity == 0 {
257                    self.remove_slot(idx);
258                }
259
260                Ok(ret)
261            } else {
262                Err(ItemError::NotEnoughQuantity)
263            }
264        } else {
265            Err(ItemError::SlotEmpty)
266        }
267    }
268
269    fn remove_slot(&mut self, idx: usize) -> Option<ItemInstance<K, U>> {
270        match self.move_to_front {
271            MoveToFrontMode::None => {
272                if let Some(s) = self.content.get_mut(idx) {
273                    let ret = s.clone();
274                    *s = None;
275                    ret
276                } else {
277                    None
278                }
279            }
280            MoveToFrontMode::TakeLast => {
281                let ret = self.content.swap_remove(idx);
282                self.content.push(None);
283                ret
284            }
285            MoveToFrontMode::Offset => {
286                let ret = self.content.remove(idx);
287                self.content.push(None);
288                ret
289            }
290        }
291    }
292
293    /// Deletes a full stack of item at the provided index and returns it.
294    ///
295    /// Errors:
296    /// See `Transform::delete`.
297    pub fn delete_stack(&mut self, idx: usize) -> Result<ItemInstance<K, U>, ItemError<K, U>> {
298        if let Some(Some(qty)) = self
299            .content
300            .get(idx)
301            .map(|i| i.as_ref().map(|i2| i2.quantity))
302        {
303            self.delete(idx, qty)
304        } else {
305            Err(ItemError::SlotEmpty)
306        }
307    }
308
309    /// Deletes items by matching the key until the deleted quantity reaches the specified
310    /// quantity.
311    ///
312    /// Errors:
313    /// * NotEnoughQuantity: Not enough items with the specified key are present in the inventory.
314    pub fn delete_key(
315        &mut self,
316        key: &K,
317        quantity: usize,
318    ) -> Result<ItemInstance<K, U>, ItemError<K, U>> {
319        if !self.has_quantity(key, quantity) {
320            return Err(ItemError::NotEnoughQuantity);
321        }
322        let mut remaining = quantity;
323        for idx in self
324            .content
325            .iter()
326            .enumerate()
327            .filter(|(_, ii)| ii.is_some() && ii.as_ref().unwrap().key == *key)
328            .map(|(idx, _)| idx)
329            .collect::<Vec<_>>()
330        {
331            let avail = self
332                .content
333                .get(idx)
334                .as_ref()
335                .unwrap()
336                .as_ref()
337                .unwrap()
338                .quantity;
339            let rm = if avail >= remaining { remaining } else { avail };
340            remaining -= rm;
341            self.delete(idx, rm)
342                .expect("Failed to delete from item stack during delete_key call. This is a bug.");
343            if remaining == 0 {
344                return Ok(ItemInstance::new(key.clone(), quantity));
345            }
346        }
347        unreachable!();
348    }
349
350    /// Checks if the total quantity of items of the specified key are present in the inventory.
351    pub fn has_quantity(&self, key: &K, quantity: usize) -> bool {
352        let sum: usize = self
353            .content
354            .iter()
355            .flatten()
356            .filter(|ii| ii.key == *key)
357            .map(|ii| ii.quantity)
358            .sum();
359        sum >= quantity
360    }
361
362    /// Checks if the inventory contains at least one `ItemInstance` of the specified key.
363    pub fn has(&self, key: &K) -> bool {
364        self.content
365            .iter()
366            .any(|ii| ii.is_some() && ii.as_ref().unwrap().key == *key)
367    }
368
369    /// Gets an immutable reference to the `ItemInstance` at the specified index.
370    pub fn get(&self, idx: usize) -> &Option<ItemInstance<K, U>> {
371        self.content.get(idx).unwrap_or(&None)
372    }
373
374    /// Gets a mutable reference to the `ItemInstance` at the specified index.
375    pub fn get_mut(&mut self, idx: usize) -> Option<&mut ItemInstance<K, U>> {
376        self.content
377            .get_mut(idx)
378            .map(|opt| opt.as_mut())
379            .unwrap_or(None)
380    }
381
382    /// Finds the item instances using the specified key. Returns an iterator of immutable
383    /// references.
384    pub fn get_key(&self, key: &K) -> impl Iterator<Item = &ItemInstance<K, U>> {
385        let key = key.clone();
386        self.content
387            .iter()
388            .flatten()
389            .filter(move |ii| ii.key == key)
390    }
391
392    /// Finds the item instances using the specified key. Returns an iterator of mutable
393    /// references.
394    pub fn get_key_mut(&mut self, key: &K) -> impl Iterator<Item = &mut ItemInstance<K, U>> {
395        let key = key.clone();
396        self.content
397            .iter_mut()
398            .flatten()
399            .filter(move |ii| ii.key == key)
400    }
401
402    /// Inserts the `ItemInstance` into the specified index.
403    ///
404    /// It will eventually attempt to merge stacks together, but this is not implemented yet.
405    ///
406    /// Errors:
407    /// * SlotOccupied: The slot is currently occupied by another item type.
408    pub fn insert_into<U2: Default>(
409        &mut self,
410        idx: usize,
411        item: ItemInstance<K, U>,
412        _item_defs: &ItemDefinitions<K, S, U2>,
413    ) -> Result<(), ItemError<K, U>> {
414        // TODO implement trying to insert whole `item` stack into current stack, otherwise give
415        // up.
416        let opt = self.content.get_mut(idx);
417        match opt {
418            Some(Some(_)) => Err(ItemError::SlotOccupied),
419            Some(None) => {
420                *opt.unwrap() = Some(item);
421                Ok(())
422            }
423            None => panic!("Out of bound inventory insertion at index {}", idx),
424        }
425    }
426
427    /// Inserts the `ItemInstance` at the first available inventory space.
428    /// If the inventory is dynamically size, it will attempt to create a slot and insert into it.
429    ///
430    /// It will eventually attempt to merge stacks together, but this is not implemented yet.
431    ///
432    /// Errors:
433    /// * InventoryFull: The inventory is full and no more space can be created.
434    pub fn insert<U2: Default>(
435        &mut self,
436        mut item: ItemInstance<K, U>,
437        item_defs: &ItemDefinitions<K, S, U2>,
438    ) -> Result<(), ItemError<K, U>> {
439        for inst in self.get_key_mut(&item.key) {
440            if item.quantity == 0 {
441                break;
442            }
443            inst.merge(&mut item, item_defs);
444        }
445        if item.quantity == 0 {
446            return Ok(());
447        }
448        // We have to insert into a new slot.
449        if let Some(slot) = self.first_empty_slot() {
450            self.insert_into(slot, item, item_defs).unwrap();
451            Ok(())
452        } else {
453            match self.sizing_mode {
454                InventorySizingMode::Fixed { size: _ } => Err(ItemError::InventoryFull),
455                InventorySizingMode::Dynamic {
456                    min_size: _,
457                    max_size: _,
458                } => {
459                    // Attempt to make room.
460                    if self.has_space() {
461                        self.content.push(None);
462                        self.insert_into(self.content.len() - 1, item, item_defs)
463                            .unwrap();
464                        Ok(())
465                    } else {
466                        Err(ItemError::InventoryFull)
467                    }
468                }
469            }
470        }
471    }
472
473    /// Returns the first empty slot if any is available.
474    pub fn first_empty_slot(&self) -> Option<usize> {
475        match self.move_to_front {
476            MoveToFrontMode::None => {
477                let ret = self
478                    .content
479                    .iter()
480                    .enumerate()
481                    .find(|t| t.1.is_none())
482                    .map(|t| t.0);
483                ret
484            }
485            MoveToFrontMode::TakeLast | MoveToFrontMode::Offset => {
486                let max = match self.sizing_mode {
487                    InventorySizingMode::Fixed { size } => size,
488                    InventorySizingMode::Dynamic {
489                        min_size: _,
490                        max_size,
491                    } => max_size,
492                };
493                if self.content.len() != max {
494                    Some(self.content.len())
495                } else {
496                    None
497                }
498            }
499        }
500    }
501
502    // TODO first insertable for key: &K
503
504    //pub fn first_empty_slot_filtered(&self,
505}
506
507/// The different errors that can happen when interacting with the `Inventory`.
508#[derive(Debug)]
509pub enum ItemError<K: PartialEq + Debug, U: Default> {
510    /// The stack doesn't fit completely inside of the slot.
511    StackOverflow(ItemInstance<K, U>),
512    /// The inventory is full and cannot be resized anymore.
513    InventoryFull,
514    /// The inventory cannot fit the specified items inside of itself.
515    /// It has no more empty slots, cannot be resized and no item can be stacked with others.
516    InventoryOverflow(Vec<ItemInstance<K, U>>),
517    /// The item was used and the durability is now 0.
518    ItemDestroyed(ItemInstance<K, U>),
519    /// The stack size was decreased and is now 0.
520    StackConsumed(ItemInstance<K, U>),
521    /// The slot already has something inside of it.
522    SlotOccupied,
523    /// The specified item cannot be inserted into this type of slot.
524    SlotRestricted,
525    /// The origin slot is locked. The item cannot be moved or inserted.
526    LockedOriginSlot,
527    /// The remote slot is locked. The item cannot be moved or inserted.
528    LockedRemoteSlot,
529    /// The slot at the specified index is empty or non-existant.
530    SlotEmpty,
531    /// There is not enough of the specified item to satisfy the query.
532    NotEnoughQuantity,
533}