2020-09-17 23:02:14 +00:00
|
|
|
use core::ops::Not;
|
2021-01-09 18:10:12 +00:00
|
|
|
use std::{collections::HashMap, convert::TryFrom, mem, ops::Range};
|
2021-01-08 19:12:09 +00:00
|
|
|
|
2020-07-06 14:23:08 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-01-08 19:12:09 +00:00
|
|
|
use specs::{Component, DerefFlaggedStorage};
|
2020-07-06 05:56:02 +00:00
|
|
|
use specs_idvs::IdvStorage;
|
2021-01-08 19:12:09 +00:00
|
|
|
use tracing::{debug, trace, warn};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
comp::{
|
|
|
|
inventory::{
|
2021-02-23 20:29:27 +00:00
|
|
|
item::{ItemDef, MaterialStatManifest},
|
2021-01-08 19:12:09 +00:00
|
|
|
loadout::Loadout,
|
|
|
|
slot::{EquipSlot, Slot, SlotError},
|
|
|
|
},
|
|
|
|
slot::{InvSlotId, SlotId},
|
|
|
|
Item,
|
|
|
|
},
|
2021-02-16 01:05:54 +00:00
|
|
|
recipe::{Recipe, RecipeInput},
|
2021-01-08 19:12:09 +00:00
|
|
|
LoadoutBuilder,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub mod item;
|
|
|
|
pub mod loadout;
|
|
|
|
pub mod loadout_builder;
|
|
|
|
pub mod slot;
|
|
|
|
#[cfg(test)] mod test;
|
|
|
|
#[cfg(test)] mod test_helpers;
|
|
|
|
|
|
|
|
pub type InvSlot = Option<Item>;
|
|
|
|
const DEFAULT_INVENTORY_SLOTS: usize = 18;
|
2019-05-18 16:46:14 +00:00
|
|
|
|
2020-07-18 00:05:28 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
2019-05-18 16:46:14 +00:00
|
|
|
pub struct Inventory {
|
2021-01-08 19:12:09 +00:00
|
|
|
loadout: Loadout,
|
|
|
|
/// The "built-in" slots belonging to the inventory itself, all other slots
|
|
|
|
/// are provided by equipped items
|
|
|
|
slots: Vec<InvSlot>,
|
2019-05-18 16:46:14 +00:00
|
|
|
}
|
|
|
|
|
2019-10-31 04:47:17 +00:00
|
|
|
/// Errors which the methods on `Inventory` produce
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Error {
|
2020-02-01 20:39:39 +00:00
|
|
|
/// The inventory is full and items could not be added. The extra items have
|
|
|
|
/// been returned.
|
2019-10-31 04:47:17 +00:00
|
|
|
Full(Vec<Item>),
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Represents the Inventory of an entity. The inventory has 18 "built-in"
|
|
|
|
/// slots, with further slots being provided by items equipped in the Loadout
|
|
|
|
/// sub-struct. Inventory slots are indexed by `InvSlotId` which is
|
|
|
|
/// comprised of `loadout_idx` - the index of the loadout item that provides the
|
|
|
|
/// slot, 0 being the built-in inventory slots, and `slot_idx` - the index of
|
|
|
|
/// the slot within that loadout item.
|
|
|
|
///
|
|
|
|
/// Currently, it is not supported for inventories to contain items that have
|
|
|
|
/// items inside them. This is due to both game balance purposes, and the lack
|
|
|
|
/// of a UI to show such items. Because of this, any action that would result in
|
|
|
|
/// such an item being put into the inventory (item pickup, unequipping an item
|
|
|
|
/// that contains items etc) must first ensure items are unloaded from the item.
|
|
|
|
/// This is handled in `inventory\slot.rs`
|
2019-05-18 16:46:14 +00:00
|
|
|
impl Inventory {
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn new_empty() -> Inventory { Self::new_with_loadout(LoadoutBuilder::new().build()) }
|
|
|
|
|
|
|
|
pub fn new_with_loadout(loadout: Loadout) -> Inventory {
|
2020-09-17 23:02:14 +00:00
|
|
|
Inventory {
|
2021-01-08 19:12:09 +00:00
|
|
|
loadout,
|
|
|
|
slots: vec![None; DEFAULT_INVENTORY_SLOTS],
|
2020-09-17 23:02:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Total number of slots in in the inventory.
|
|
|
|
pub fn capacity(&self) -> usize { self.slots().count() }
|
2019-07-25 17:41:06 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// An iterator of all inventory slots
|
|
|
|
pub fn slots(&self) -> impl Iterator<Item = &InvSlot> {
|
|
|
|
self.slots
|
|
|
|
.iter()
|
|
|
|
.chain(self.loadout.inv_slots_with_id().map(|(_, slot)| slot))
|
|
|
|
}
|
2019-07-25 22:52:28 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// A mutable iterator of all inventory slots
|
|
|
|
fn slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
|
|
|
|
self.slots.iter_mut().chain(self.loadout.inv_slots_mut())
|
|
|
|
}
|
2020-09-17 23:02:14 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// An iterator of all inventory slots and their position
|
|
|
|
pub fn slots_with_id(&self) -> impl Iterator<Item = (InvSlotId, &InvSlot)> {
|
|
|
|
self.slots
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(i, slot)| ((InvSlotId::new(0, u16::try_from(i).unwrap())), slot))
|
|
|
|
.chain(
|
|
|
|
self.loadout
|
|
|
|
.inv_slots_with_id()
|
|
|
|
.map(|(loadout_slot_id, inv_slot)| (loadout_slot_id.into(), inv_slot)),
|
|
|
|
)
|
2020-03-23 23:23:21 +00:00
|
|
|
}
|
|
|
|
|
2020-03-19 13:30:50 +00:00
|
|
|
/// Adds a new item to the first fitting group of the inventory or starts a
|
|
|
|
/// new group. Returns the item again if no space was found.
|
|
|
|
pub fn push(&mut self, item: Item) -> Option<Item> {
|
2020-09-17 23:02:14 +00:00
|
|
|
if item.is_stackable() {
|
|
|
|
if let Some(slot_item) = self
|
2021-01-08 19:12:09 +00:00
|
|
|
.slots_mut()
|
2020-09-17 23:02:14 +00:00
|
|
|
.filter_map(Option::as_mut)
|
|
|
|
.find(|s| *s == &item)
|
|
|
|
{
|
|
|
|
return slot_item
|
|
|
|
.increase_amount(item.amount())
|
|
|
|
.err()
|
|
|
|
.and(Some(item));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No existing item to stack with or item not stackable, put the item in a new
|
|
|
|
// slot
|
2021-01-08 19:12:09 +00:00
|
|
|
self.insert(item)
|
2019-08-28 20:47:52 +00:00
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:39 +00:00
|
|
|
/// Add a series of items to inventory, returning any which do not fit as an
|
|
|
|
/// error.
|
2020-07-14 20:11:39 +00:00
|
|
|
pub fn push_all<I: Iterator<Item = Item>>(&mut self, items: I) -> Result<(), Error> {
|
2019-10-31 04:47:17 +00:00
|
|
|
// Vec doesn't allocate for zero elements so this should be cheap
|
|
|
|
let mut leftovers = Vec::new();
|
2020-07-14 20:11:39 +00:00
|
|
|
for item in items {
|
|
|
|
if let Some(item) = self.push(item) {
|
2019-10-31 04:47:17 +00:00
|
|
|
leftovers.push(item);
|
|
|
|
}
|
|
|
|
}
|
2020-06-08 18:37:41 +00:00
|
|
|
if !leftovers.is_empty() {
|
2019-10-31 04:47:17 +00:00
|
|
|
Err(Error::Full(leftovers))
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-27 16:48:42 +00:00
|
|
|
/// Add a series of items to an inventory without giving duplicates.
|
|
|
|
/// (n * m complexity)
|
2019-11-07 01:57:05 +00:00
|
|
|
///
|
2020-02-01 20:39:39 +00:00
|
|
|
/// Error if inventory cannot contain the items (is full), returning the
|
|
|
|
/// un-added items. This is a lazy inefficient implementation, as it
|
|
|
|
/// iterates over the inventory more times than necessary (n^2) and with
|
|
|
|
/// the proper structure wouldn't need to iterate at all, but because
|
|
|
|
/// this should be fairly cold code, clarity has been favored over
|
|
|
|
/// efficiency.
|
2019-10-31 14:26:25 +00:00
|
|
|
pub fn push_all_unique<I: Iterator<Item = Item>>(&mut self, mut items: I) -> Result<(), Error> {
|
2019-10-31 04:47:17 +00:00
|
|
|
let mut leftovers = Vec::new();
|
|
|
|
for item in &mut items {
|
|
|
|
if self.contains(&item).not() {
|
2019-10-31 14:26:25 +00:00
|
|
|
self.push(item).map(|overflow| leftovers.push(overflow));
|
2019-10-31 04:47:17 +00:00
|
|
|
} // else drop item if it was already in
|
|
|
|
}
|
2020-06-08 18:37:41 +00:00
|
|
|
if !leftovers.is_empty() {
|
2019-10-31 04:47:17 +00:00
|
|
|
Err(Error::Full(leftovers))
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:39 +00:00
|
|
|
/// Replaces an item in a specific slot of the inventory. Returns the old
|
|
|
|
/// item or the same item again if that slot was not found.
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn insert_at(&mut self, inv_slot_id: InvSlotId, item: Item) -> Result<Option<Item>, Item> {
|
|
|
|
match self.slot_mut(inv_slot_id) {
|
|
|
|
Some(slot) => Ok(core::mem::replace(slot, Some(item))),
|
2019-08-30 22:09:25 +00:00
|
|
|
None => Err(item),
|
2019-07-25 22:52:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-02 00:08:46 +00:00
|
|
|
/// Merge the stack of items at src into the stack at dst if the items are
|
|
|
|
/// compatible and stackable, and return whether anything was changed
|
|
|
|
pub fn merge_stack_into(&mut self, src: InvSlotId, dst: InvSlotId) -> bool {
|
|
|
|
let mut amount = None;
|
|
|
|
if let (Some(srcitem), Some(dstitem)) = (self.get(src), self.get(dst)) {
|
|
|
|
// The equality check ensures the items have the same definition, to avoid e.g.
|
|
|
|
// transmuting coins to diamonds, and the stackable check avoids creating a
|
|
|
|
// stack of swords
|
|
|
|
if srcitem == dstitem && srcitem.is_stackable() {
|
|
|
|
amount = Some(srcitem.amount());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(amount) = amount {
|
|
|
|
self.remove(src);
|
|
|
|
let dstitem = self
|
|
|
|
.get_mut(dst)
|
|
|
|
.expect("self.get(dst) was Some right above this");
|
|
|
|
dstitem
|
|
|
|
.increase_amount(amount)
|
|
|
|
.expect("already checked is_stackable");
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-18 02:41:45 +00:00
|
|
|
/// Checks if inserting item exists in given cell. Inserts an item if it
|
|
|
|
/// exists.
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn insert_or_stack_at(
|
|
|
|
&mut self,
|
|
|
|
inv_slot_id: InvSlotId,
|
|
|
|
item: Item,
|
|
|
|
) -> Result<Option<Item>, Item> {
|
2020-09-17 23:02:14 +00:00
|
|
|
if item.is_stackable() {
|
2021-01-08 19:12:09 +00:00
|
|
|
match self.slot_mut(inv_slot_id) {
|
2020-07-18 02:41:45 +00:00
|
|
|
Some(Some(slot_item)) => {
|
2020-09-17 23:02:14 +00:00
|
|
|
Ok(if slot_item == &item {
|
|
|
|
slot_item
|
|
|
|
.increase_amount(item.amount())
|
|
|
|
.err()
|
|
|
|
.and(Some(item))
|
2020-07-18 02:41:45 +00:00
|
|
|
} else {
|
2020-09-17 23:02:14 +00:00
|
|
|
let old_item = core::mem::replace(slot_item, item);
|
|
|
|
// No need to recount--we know the count is the same.
|
|
|
|
Some(old_item)
|
|
|
|
})
|
2020-07-18 02:41:45 +00:00
|
|
|
},
|
2021-01-08 19:12:09 +00:00
|
|
|
Some(None) => self.insert_at(inv_slot_id, item),
|
2020-07-18 02:41:45 +00:00
|
|
|
None => Err(item),
|
2020-09-17 23:02:14 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-01-08 19:12:09 +00:00
|
|
|
self.insert_at(inv_slot_id, item)
|
2020-07-18 02:41:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// 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 fn try_equip(&mut self, item: Item) -> Result<(), Item> { self.loadout.try_equip(item) }
|
|
|
|
|
|
|
|
pub fn populated_slots(&self) -> usize { self.slots().filter_map(|slot| slot.as_ref()).count() }
|
2019-09-25 22:53:43 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
fn free_slots(&self) -> usize { self.slots().filter(|slot| slot.is_none()).count() }
|
2019-10-31 04:47:17 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Check if an item is in this inventory.
|
2019-10-31 04:47:17 +00:00
|
|
|
pub fn contains(&self, item: &Item) -> bool {
|
2021-01-08 19:12:09 +00:00
|
|
|
self.slots().any(|slot| slot.as_ref() == Some(item))
|
2019-10-31 04:47:17 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 20:42:43 +00:00
|
|
|
/// Get content of a slot
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> {
|
|
|
|
self.slot(inv_slot_id).and_then(Option::as_ref)
|
|
|
|
}
|
|
|
|
|
2021-03-02 00:08:46 +00:00
|
|
|
/// Mutably get content of a slot
|
|
|
|
fn get_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut Item> {
|
|
|
|
self.slot_mut(inv_slot_id).and_then(Option::as_mut)
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Returns a reference to the item (if any) equipped in the given EquipSlot
|
|
|
|
pub fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
|
|
|
|
self.loadout.equipped(equip_slot)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn loadout_items_with_persistence_key(
|
|
|
|
&self,
|
|
|
|
) -> impl Iterator<Item = (&str, Option<&Item>)> {
|
|
|
|
self.loadout.items_with_persistence_key()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the range of inventory slot indexes that a particular equipped
|
|
|
|
/// item provides (used for UI highlighting of inventory slots when hovering
|
|
|
|
/// over a loadout item)
|
|
|
|
pub fn get_slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
|
|
|
|
// The slot range returned from `Loadout` must be offset by the number of slots
|
|
|
|
// that the inventory itself provides.
|
|
|
|
let offset = self.slots.len();
|
|
|
|
self.loadout
|
|
|
|
.slot_range_for_equip_slot(equip_slot)
|
|
|
|
.map(|loadout_range| (loadout_range.start + offset)..(loadout_range.end + offset))
|
2019-05-18 16:46:14 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 20:42:43 +00:00
|
|
|
/// Swap the items inside of two slots
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn swap_slots(&mut self, a: InvSlotId, b: InvSlotId) {
|
|
|
|
if self.slot(a).is_none() || self.slot(b).is_none() {
|
|
|
|
warn!("swap_slots called with non-existent inventory slot(s)");
|
|
|
|
return;
|
2019-07-25 22:52:28 +00:00
|
|
|
}
|
2021-01-08 19:12:09 +00:00
|
|
|
|
|
|
|
let slot_a = mem::take(self.slot_mut(a).unwrap());
|
|
|
|
let slot_b = mem::take(self.slot_mut(b).unwrap());
|
|
|
|
*self.slot_mut(a).unwrap() = slot_b;
|
|
|
|
*self.slot_mut(b).unwrap() = slot_a;
|
2019-07-25 22:52:28 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 20:42:43 +00:00
|
|
|
/// Remove an item from the slot
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
|
|
|
|
self.slot_mut(inv_slot_id).and_then(|item| item.take())
|
2019-05-18 16:46:14 +00:00
|
|
|
}
|
2020-03-19 13:30:50 +00:00
|
|
|
|
|
|
|
/// Remove just one item from the slot
|
2021-02-23 20:29:27 +00:00
|
|
|
pub fn take(&mut self, inv_slot_id: InvSlotId, msm: &MaterialStatManifest) -> Option<Item> {
|
2021-01-08 19:12:09 +00:00
|
|
|
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
|
2021-02-23 20:29:27 +00:00
|
|
|
let mut return_item = item.duplicate(msm);
|
2020-09-17 23:02:14 +00:00
|
|
|
|
|
|
|
if item.is_stackable() && item.amount() > 1 {
|
|
|
|
item.decrease_amount(1).ok()?;
|
|
|
|
return_item
|
|
|
|
.set_amount(1)
|
|
|
|
.expect("Items duplicated from a stackable item must be stackable.");
|
|
|
|
Some(return_item)
|
|
|
|
} else {
|
2021-01-08 19:12:09 +00:00
|
|
|
self.remove(inv_slot_id)
|
2020-03-19 13:30:50 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2020-07-14 20:11:39 +00:00
|
|
|
|
2021-03-02 00:08:46 +00:00
|
|
|
/// Takes half of the items from a slot in the inventory
|
|
|
|
pub fn take_half(
|
|
|
|
&mut self,
|
|
|
|
inv_slot_id: InvSlotId,
|
|
|
|
msm: &MaterialStatManifest,
|
|
|
|
) -> Option<Item> {
|
|
|
|
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
|
|
|
|
if item.is_stackable() && item.amount() > 1 {
|
|
|
|
let mut return_item = item.duplicate(msm);
|
|
|
|
let returning_amount = item.amount() / 2;
|
|
|
|
item.decrease_amount(returning_amount).ok()?;
|
|
|
|
return_item
|
|
|
|
.set_amount(returning_amount)
|
|
|
|
.expect("Items duplicated from a stackable item must be stackable.");
|
|
|
|
Some(return_item)
|
|
|
|
} else {
|
|
|
|
self.remove(inv_slot_id)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Takes all items from the inventory
|
|
|
|
pub fn drain(&mut self) -> impl Iterator<Item = Item> + '_ {
|
|
|
|
self.slots_mut()
|
|
|
|
.filter(|x| x.is_some())
|
|
|
|
.filter_map(mem::take)
|
|
|
|
}
|
|
|
|
|
2020-07-14 20:11:39 +00:00
|
|
|
/// Determine how many of a particular item there is in the inventory.
|
2020-09-26 15:20:46 +00:00
|
|
|
pub fn item_count(&self, item_def: &ItemDef) -> u64 {
|
2020-07-14 20:11:39 +00:00
|
|
|
self.slots()
|
|
|
|
.flatten()
|
2020-09-26 15:20:46 +00:00
|
|
|
.filter(|it| it.is_same_item_def(item_def))
|
2020-09-17 23:02:14 +00:00
|
|
|
.map(|it| u64::from(it.amount()))
|
2020-07-14 20:11:39 +00:00
|
|
|
.sum()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determine whether the inventory contains the ingredients for a recipe.
|
|
|
|
/// If it does, return a vector of numbers, where is number corresponds
|
|
|
|
/// to an inventory slot, along with the number of items that need
|
|
|
|
/// removing from it. It items are missing, return the missing items, and
|
|
|
|
/// how many are missing.
|
|
|
|
pub fn contains_ingredients<'a>(
|
|
|
|
&self,
|
|
|
|
recipe: &'a Recipe,
|
2021-02-16 01:05:54 +00:00
|
|
|
) -> Result<HashMap<InvSlotId, u32>, Vec<(&'a RecipeInput, u32)>> {
|
2021-01-08 19:12:09 +00:00
|
|
|
let mut slot_claims = HashMap::<InvSlotId, u32>::new();
|
2021-02-16 01:05:54 +00:00
|
|
|
let mut missing = Vec::<(&RecipeInput, u32)>::new();
|
2020-07-14 20:11:39 +00:00
|
|
|
|
|
|
|
for (input, mut needed) in recipe.inputs() {
|
|
|
|
let mut contains_any = false;
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
for (inv_slot_id, slot) in self.slots_with_id() {
|
2021-02-16 01:05:54 +00:00
|
|
|
if let Some(item) = slot
|
|
|
|
.as_ref()
|
|
|
|
.filter(|item| item.matches_recipe_input(&*input))
|
|
|
|
{
|
2021-01-08 19:12:09 +00:00
|
|
|
let claim = slot_claims.entry(inv_slot_id).or_insert(0);
|
|
|
|
let can_claim = (item.amount() - *claim).min(needed);
|
|
|
|
*claim += can_claim;
|
2020-07-14 20:11:39 +00:00
|
|
|
needed -= can_claim;
|
|
|
|
contains_any = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if needed > 0 || !contains_any {
|
|
|
|
missing.push((input, needed));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-12 14:10:12 +00:00
|
|
|
if missing.is_empty() {
|
2020-07-14 20:11:39 +00:00
|
|
|
Ok(slot_claims)
|
|
|
|
} else {
|
|
|
|
Err(missing)
|
|
|
|
}
|
|
|
|
}
|
2019-05-18 16:46:14 +00:00
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Adds a new item to the first empty slot of the inventory. Returns the
|
|
|
|
/// item again if no free slot was found.
|
|
|
|
fn insert(&mut self, item: Item) -> Option<Item> {
|
|
|
|
match self.slots_mut().find(|slot| slot.is_none()) {
|
|
|
|
Some(slot) => {
|
|
|
|
*slot = Some(item);
|
|
|
|
None
|
|
|
|
},
|
|
|
|
None => Some(item),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-19 20:20:27 +00:00
|
|
|
pub fn slot(&self, inv_slot_id: InvSlotId) -> Option<&InvSlot> {
|
2021-01-08 19:12:09 +00:00
|
|
|
match SlotId::from(inv_slot_id) {
|
|
|
|
SlotId::Inventory(slot_idx) => self.slots.get(slot_idx),
|
|
|
|
SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot(loadout_slot_id),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-18 22:22:15 +00:00
|
|
|
pub fn slot_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut InvSlot> {
|
2021-01-08 19:12:09 +00:00
|
|
|
match SlotId::from(inv_slot_id) {
|
|
|
|
SlotId::Inventory(slot_idx) => self.slots.get_mut(slot_idx),
|
|
|
|
SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot_mut(loadout_slot_id),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the number of free slots in the inventory ignoring any slots
|
|
|
|
/// granted by the item (if any) equipped in the provided EquipSlot.
|
|
|
|
pub fn free_slots_minus_equipped_item(&self, equip_slot: EquipSlot) -> usize {
|
|
|
|
if let Some(mut equip_slot_idx) = self.loadout.loadout_idx_for_equip_slot(equip_slot) {
|
|
|
|
// Offset due to index 0 representing built-in inventory slots
|
|
|
|
equip_slot_idx += 1;
|
|
|
|
|
|
|
|
self.slots_with_id()
|
|
|
|
.filter(|(inv_slot_id, slot)| {
|
|
|
|
inv_slot_id.loadout_idx() != equip_slot_idx && slot.is_none()
|
|
|
|
})
|
|
|
|
.count()
|
|
|
|
} else {
|
|
|
|
// TODO: return Option<usize> and evaluate to None here
|
|
|
|
warn!(
|
|
|
|
"Attempted to fetch loadout index for non-existent EquipSlot: {:?}",
|
|
|
|
equip_slot
|
|
|
|
);
|
|
|
|
0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn equipped_items(&self) -> impl Iterator<Item = &Item> { self.loadout.items() }
|
|
|
|
|
|
|
|
/// Replaces the loadout item (if any) in the given EquipSlot with the
|
|
|
|
/// provided item, returning the item that was previously in the slot.
|
|
|
|
pub fn replace_loadout_item(
|
|
|
|
&mut self,
|
|
|
|
equip_slot: EquipSlot,
|
|
|
|
replacement_item: Option<Item>,
|
|
|
|
) -> Option<Item> {
|
|
|
|
self.loadout.swap(equip_slot, replacement_item)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Equip an item from a slot in inventory. The currently equipped item will
|
|
|
|
/// go into inventory. If the item is going to mainhand, put mainhand in
|
|
|
|
/// offhand and place offhand into inventory.
|
|
|
|
#[must_use = "Returned items will be lost if not used"]
|
2021-01-09 18:10:12 +00:00
|
|
|
pub fn equip(&mut self, inv_slot: InvSlotId) -> Vec<Item> {
|
2021-01-08 19:12:09 +00:00
|
|
|
self.get(inv_slot)
|
2021-01-09 18:10:12 +00:00
|
|
|
.and_then(|item| self.loadout.get_slot_to_equip_into(item.kind()))
|
|
|
|
.map(|equip_slot| {
|
|
|
|
// Special case when equipping into main hand - swap with offhand first
|
|
|
|
if equip_slot == EquipSlot::Mainhand {
|
|
|
|
self.loadout
|
|
|
|
.swap_slots(EquipSlot::Mainhand, EquipSlot::Offhand);
|
|
|
|
}
|
2021-01-08 19:12:09 +00:00
|
|
|
|
2021-01-09 18:10:12 +00:00
|
|
|
self.swap_inventory_loadout(inv_slot, equip_slot)
|
|
|
|
})
|
|
|
|
.unwrap_or_else(Vec::new)
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines how many free inventory slots will be left after equipping an
|
|
|
|
/// item (because it could be swapped with an already equipped item that
|
|
|
|
/// provides more inventory slots than the item being equipped)
|
|
|
|
pub fn free_after_equip(&self, inv_slot: InvSlotId) -> i32 {
|
|
|
|
let (inv_slot_for_equipped, slots_from_equipped) = self
|
|
|
|
.get(inv_slot)
|
2021-01-09 18:10:12 +00:00
|
|
|
.and_then(|item| self.loadout.get_slot_to_equip_into(item.kind()))
|
2021-01-08 19:12:09 +00:00
|
|
|
.and_then(|equip_slot| self.equipped(equip_slot))
|
|
|
|
.map_or((1, 0), |item| (0, item.slots().len()));
|
|
|
|
|
|
|
|
let slots_from_inv = self
|
|
|
|
.get(inv_slot)
|
|
|
|
.map(|item| item.slots().len())
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
|
|
i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots")
|
|
|
|
- i32::try_from(slots_from_equipped)
|
|
|
|
.expect("Equipped item with more than i32::MAX slots")
|
|
|
|
+ i32::try_from(slots_from_inv).expect("Inventory item with more than i32::MAX slots")
|
|
|
|
- i32::try_from(self.populated_slots())
|
|
|
|
.expect("Inventory item with more than i32::MAX used slots")
|
|
|
|
+ inv_slot_for_equipped // If there is no item already in the equip slot we gain 1 slot
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handles picking up an item, unloading any items inside the item being
|
|
|
|
/// picked up and pushing them to the inventory to ensure that items
|
|
|
|
/// containing items aren't inserted into the inventory as this is not
|
|
|
|
/// currently supported.
|
|
|
|
pub fn pickup_item(&mut self, mut item: Item) -> Result<(), Item> {
|
2021-01-08 23:34:35 +00:00
|
|
|
if item.is_stackable() {
|
|
|
|
return self.push(item).map_or(Ok(()), Err);
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
if self.free_slots() < item.populated_slots() + 1 {
|
|
|
|
return Err(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unload any items contained within the item, and push those items and the item
|
|
|
|
// itself into the inventory. We already know that there are enough free slots
|
|
|
|
// so push will never give us an item back.
|
2021-01-09 18:10:12 +00:00
|
|
|
item.drain().for_each(|item| {
|
|
|
|
self.push(item).unwrap_none();
|
|
|
|
});
|
|
|
|
self.push(item).unwrap_none();
|
2021-01-08 19:12:09 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Unequip an item from slot and place into inventory. Will leave the item
|
|
|
|
/// equipped if inventory has no slots available.
|
|
|
|
#[must_use = "Returned items will be lost if not used"]
|
|
|
|
pub fn unequip(&mut self, equip_slot: EquipSlot) -> Result<Option<Vec<Item>>, SlotError> {
|
|
|
|
// Ensure there is enough space in the inventory to place the unequipped item
|
|
|
|
if self.free_slots_minus_equipped_item(equip_slot) == 0 {
|
|
|
|
return Err(SlotError::InventoryFull);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(self
|
|
|
|
.loadout
|
|
|
|
.swap(equip_slot, None)
|
|
|
|
.and_then(|mut unequipped_item| {
|
|
|
|
let unloaded_items: Vec<Item> = unequipped_item.drain().collect();
|
|
|
|
self.push(unequipped_item)
|
|
|
|
.expect_none("Failed to push item to inventory, precondition failed?");
|
|
|
|
|
|
|
|
// Unload any items that were inside the equipped item into the inventory, with
|
|
|
|
// any that don't fit to be to be dropped on the floor by the caller
|
|
|
|
match self.push_all(unloaded_items.into_iter()) {
|
|
|
|
Err(Error::Full(leftovers)) => Some(leftovers),
|
2021-01-09 18:10:12 +00:00
|
|
|
Ok(()) => None,
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines how many free inventory slots will be left after unequipping
|
|
|
|
/// an item
|
|
|
|
pub fn free_after_unequip(&self, equip_slot: EquipSlot) -> i32 {
|
|
|
|
let (inv_slot_for_unequipped, slots_from_equipped) = self
|
|
|
|
.equipped(equip_slot)
|
|
|
|
.map_or((0, 0), |item| (1, item.slots().len()));
|
|
|
|
|
|
|
|
i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots")
|
|
|
|
- i32::try_from(slots_from_equipped)
|
|
|
|
.expect("Equipped item with more than i32::MAX slots")
|
|
|
|
- i32::try_from(self.populated_slots())
|
|
|
|
.expect("Inventory item with more than i32::MAX used slots")
|
|
|
|
- inv_slot_for_unequipped // If there is an item being unequipped we lose 1 slot
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Swaps items from two slots, regardless of if either is inventory or
|
|
|
|
/// loadout.
|
|
|
|
#[must_use = "Returned items will be lost if not used"]
|
2021-01-09 18:10:12 +00:00
|
|
|
pub fn swap(&mut self, slot_a: Slot, slot_b: Slot) -> Vec<Item> {
|
2021-01-08 19:12:09 +00:00
|
|
|
match (slot_a, slot_b) {
|
|
|
|
(Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => {
|
|
|
|
self.swap_slots(slot_a, slot_b);
|
2021-01-09 18:10:12 +00:00
|
|
|
Vec::new()
|
2021-01-08 19:12:09 +00:00
|
|
|
},
|
|
|
|
(Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
|
|
|
|
| (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
|
|
|
|
self.swap_inventory_loadout(inv_slot, equip_slot)
|
|
|
|
},
|
|
|
|
(Slot::Equip(slot_a), Slot::Equip(slot_b)) => {
|
|
|
|
self.loadout.swap_slots(slot_a, slot_b);
|
2021-01-09 18:10:12 +00:00
|
|
|
Vec::new()
|
2021-01-08 19:12:09 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines how many free inventory slots will be left after swapping two
|
|
|
|
/// item slots
|
|
|
|
pub fn free_after_swap(&self, equip_slot: EquipSlot, inv_slot: InvSlotId) -> i32 {
|
|
|
|
let (inv_slot_for_equipped, slots_from_equipped) = self
|
|
|
|
.equipped(equip_slot)
|
|
|
|
.map_or((0, 0), |item| (1, item.slots().len()));
|
|
|
|
let (inv_slot_for_inv_item, slots_from_inv_item) = self
|
|
|
|
.get(inv_slot)
|
|
|
|
.map_or((0, 0), |item| (1, item.slots().len()));
|
|
|
|
|
|
|
|
// Return the number of inventory slots that will be free once this slot swap is
|
|
|
|
// performed
|
|
|
|
i32::try_from(self.capacity())
|
|
|
|
.expect("inventory with more than i32::MAX slots")
|
|
|
|
- i32::try_from(slots_from_equipped)
|
|
|
|
.expect("equipped item with more than i32::MAX slots")
|
|
|
|
+ i32::try_from(slots_from_inv_item)
|
|
|
|
.expect("inventory item with more than i32::MAX slots")
|
|
|
|
- i32::try_from(self.populated_slots())
|
|
|
|
.expect("inventory with more than i32::MAX used slots")
|
|
|
|
- inv_slot_for_equipped // +1 inventory slot required if an item was unequipped
|
|
|
|
+ inv_slot_for_inv_item // -1 inventory slot required if an item was equipped
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Swap item in an inventory slot with one in a loadout slot.
|
|
|
|
#[must_use = "Returned items will be lost if not used"]
|
|
|
|
pub fn swap_inventory_loadout(
|
|
|
|
&mut self,
|
|
|
|
inv_slot_id: InvSlotId,
|
|
|
|
equip_slot: EquipSlot,
|
2021-01-09 18:10:12 +00:00
|
|
|
) -> Vec<Item> {
|
2021-01-08 19:12:09 +00:00
|
|
|
if !self.can_swap(inv_slot_id, equip_slot) {
|
2021-01-09 18:10:12 +00:00
|
|
|
return Vec::new();
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Take the item from the inventory
|
|
|
|
let from_inv = self.remove(inv_slot_id);
|
|
|
|
|
|
|
|
// Swap the equipped item for the item from the inventory
|
|
|
|
let from_equip = self.loadout.swap(equip_slot, from_inv);
|
|
|
|
|
2021-01-09 18:10:12 +00:00
|
|
|
let unloaded_items = from_equip
|
|
|
|
.map(|mut from_equip| {
|
|
|
|
// Unload any items held inside the previously equipped item
|
|
|
|
let items: Vec<Item> = from_equip.drain().collect();
|
|
|
|
|
|
|
|
// Attempt to put the unequipped item in the same slot that the inventory item
|
|
|
|
// was in - if that slot no longer exists (because a large container was
|
|
|
|
// swapped for a smaller one) then push the item to the first free
|
|
|
|
// inventory slot instead.
|
|
|
|
if let Err(returned) = self.insert_at(inv_slot_id, from_equip) {
|
|
|
|
self.push(returned)
|
|
|
|
.expect_none("Unable to push to inventory, no slots (bug in can_swap()?)");
|
|
|
|
}
|
|
|
|
|
|
|
|
items
|
|
|
|
})
|
|
|
|
.unwrap_or_default();
|
2021-01-08 19:12:09 +00:00
|
|
|
|
|
|
|
// Attempt to put any items unloaded from the unequipped item into empty
|
|
|
|
// inventory slots and return any that don't fit to the caller where they
|
|
|
|
// will be dropped on the ground
|
2021-01-09 18:10:12 +00:00
|
|
|
match self.push_all(unloaded_items.into_iter()) {
|
|
|
|
Err(Error::Full(leftovers)) => leftovers,
|
|
|
|
Ok(()) => Vec::new(),
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines if an inventory and loadout slot can be swapped, taking into
|
|
|
|
/// account whether there will be free space in the inventory for the
|
|
|
|
/// loadout item once any slots that were provided by it have been
|
|
|
|
/// removed.
|
|
|
|
pub fn can_swap(&self, inv_slot_id: InvSlotId, equip_slot: EquipSlot) -> bool {
|
|
|
|
// Check if loadout slot can hold item
|
|
|
|
if !self
|
|
|
|
.get(inv_slot_id)
|
|
|
|
.map_or(true, |item| equip_slot.can_hold(&item.kind()))
|
|
|
|
{
|
|
|
|
trace!("can_swap = false, equip slot can't hold item");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're swapping an equipped item with an empty inventory slot, make
|
|
|
|
// sure that there will be enough space in the inventory after any
|
|
|
|
// slots granted by the item being unequipped have been removed.
|
|
|
|
if let Some(inv_slot) = self.slot(inv_slot_id) {
|
|
|
|
if inv_slot.is_none() && self.free_slots_minus_equipped_item(equip_slot) == 0 {
|
|
|
|
// No free inventory slots after slots provided by the equipped
|
|
|
|
//item are discounted
|
|
|
|
trace!("can_swap = false, no free slots minus item");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
debug!(
|
|
|
|
"can_swap = false, tried to swap into non-existent inventory slot: {:?}",
|
|
|
|
inv_slot_id
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
true
|
2019-07-25 14:02:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-18 16:46:14 +00:00
|
|
|
impl Component for Inventory {
|
2021-01-08 19:12:09 +00:00
|
|
|
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
2019-05-18 16:46:14 +00:00
|
|
|
}
|
2019-07-25 14:48:27 +00:00
|
|
|
|
2020-07-18 00:05:28 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
2020-03-04 10:09:48 +00:00
|
|
|
pub enum InventoryUpdateEvent {
|
|
|
|
Init,
|
|
|
|
Used,
|
2020-08-04 23:21:42 +00:00
|
|
|
Consumed(String),
|
2020-03-04 10:09:48 +00:00
|
|
|
Gave,
|
|
|
|
Given,
|
|
|
|
Swapped,
|
|
|
|
Dropped,
|
2020-06-26 18:40:28 +00:00
|
|
|
Collected(Item),
|
2020-03-11 10:30:59 +00:00
|
|
|
CollectFailed,
|
2020-03-04 10:09:48 +00:00
|
|
|
Possession,
|
|
|
|
Debug,
|
2020-07-14 20:11:39 +00:00
|
|
|
Craft,
|
2020-03-04 10:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for InventoryUpdateEvent {
|
|
|
|
fn default() -> Self { Self::Init }
|
|
|
|
}
|
|
|
|
|
2020-06-26 18:40:28 +00:00
|
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
2020-03-04 10:09:48 +00:00
|
|
|
pub struct InventoryUpdate {
|
|
|
|
event: InventoryUpdateEvent,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InventoryUpdate {
|
|
|
|
pub fn new(event: InventoryUpdateEvent) -> Self { Self { event } }
|
|
|
|
|
2020-06-26 18:40:28 +00:00
|
|
|
pub fn event(&self) -> InventoryUpdateEvent { self.event.clone() }
|
2020-03-04 10:09:48 +00:00
|
|
|
}
|
2019-07-25 14:48:27 +00:00
|
|
|
|
|
|
|
impl Component for InventoryUpdate {
|
2021-01-07 20:25:12 +00:00
|
|
|
type Storage = IdvStorage<Self>;
|
2019-07-25 14:48:27 +00:00
|
|
|
}
|