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}