veloren/common/src/comp/inventory/mod.rs

821 lines
32 KiB
Rust
Raw Normal View History

use core::ops::Not;
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
2021-04-17 16:24:33 +00:00
use std::{collections::HashMap, convert::TryFrom, mem, ops::Range};
use tracing::{debug, trace, warn};
use crate::{
comp::{
inventory::{
2021-04-29 23:34:14 +00:00
item::{tool::AbilityMap, ItemDef, ItemKind, MaterialStatManifest, TagExampleInfo},
loadout::Loadout,
slot::{EquipSlot, Slot, SlotError},
},
slot::{InvSlotId, SlotId},
Item,
},
recipe::{Recipe, RecipeInput},
LoadoutBuilder,
};
pub mod item;
pub mod loadout;
pub mod loadout_builder;
pub mod slot;
#[cfg(test)] mod test;
#[cfg(test)] mod test_helpers;
Implement /price_list (work in progress), stub for /buy and /sell remove outdated economic simulation code remove old values, document add natural resources to economy Remove NaturalResources from Place (now in Economy) find closest site to each chunk implement natural resources (the distance scale is wrong) cargo fmt working distance calculation this collection of natural resources seem to make sense, too much Wheat though use natural resources and controlled area to replenish goods increase the amount of chunks controlled by one guard to 50 add new professions and goods to the list implement multiple products per worker remove the old code and rename the new code to the previous name correctly determine which goods guards will give you access to correctly estimate the amount of natural resources controlled adapt to new server API instrument tooltips Now I just need to figure out how to store a (reference to) a closure closures for tooltip content generation pass site/cave id to the client Add economic information to the client structure (not yet exchanged with the server) Send SiteId to the client, prepare messages for economy request Make client::sites a HashMap Specialize the Crafter into Brewer,Bladesmith and Blacksmith working server request for economic info from within tooltip fully operational economic tooltips I need to fix the id clash between caves and towns though fix overlapping ids between caves and sites display stock amount correctly handle invalid (cave) ids in the request some initial balancing, turn off most info logging less intrusive way of implementing the dynamic tool tips in map further tooltip cleanup further cleanup, dynamic tooltip not fully working as intended correctly working tooltip visibility logic cleanup, display labor value separate economy info request in a separate translation unit display values as well nicer display format for economy add last_exports and coin to the new economy do not allocate natural resources to Dungeons (making town so much larger) balancing attempt print town size statistics cargo fmt (dead code) resource tweaks, csv debugging output a more interesting town (and then all sites) fix the labor value logic (now we have meaningful prices) load professions from ron (WIP) use assets manager in economy loading professions works use professions from ron file fix Labor debug logic count chunks per type separately (preparing for better resource control) better structured resource data traders, more professions (WIP) fix exception when starting the simulation fix replenish function TODO: - use area_ratio for resource usage (chunks should be added to stock, ratio on usage?) - fix trading documentation clean up fix merge artifact Revise trader mechanic start Coin with a reasonable default remove the outdated economy code preserve documentation from removed old structure output neighboring sites (preparation) pass list of neighbors to economy add trade structures trading stub Description of purpose by zesterer on Discord remember prices (needed for planning) avoid growing the order vector unboundedly into_iter doesn't clear the Vec, so clear it manually use drain to process Vecs, avoid clone fix the test server implement a test stub (I need to get it faster than 30 seconds to be more useful) enable info output in test debug missing and extra goods use the same logging extension as water, activate feature update dependencies determine good prices, good exchange goods a working set of revisions a cozy world which economy tests in 2s first order planning version fun with package version buy according to value/priority, not missing amount introduce min price constant, fix order priority in depth debugging with a correct sign the trading plans start to make sense move the trade planning to a separate function rename new function reorganize code into subroutines (much cleaner) each trading step now has its own function cut down the number of debugging output introduce RoadSecurity and Transportation transport capacity bookkeeping only plan to pay with valuable goods, you can no longer stockpile unused options (which interestingly shows a huge impact, to be investigated) Coin is now listed as a payment (although not used) proper transportation estimation (although 0) remove more left overs uncovered by viewing the code in a merge request use the old default values, handle non-pileable stocks directly before increasing it (as economy is based on last year's products) don't order the missing good multiple times also it uses coin to buy things! fix warnings and use the transportation from stock again cargo fmt prepare evaluation of trade don't count transportation multiple times fix merge artifact operational trade planning trade itself is still misleading make clippy happy clean up correct labor ratio of merchants (no need to multiply with amount produced) incomplete merchant labor_value computation correct last commit make economy of scale more explicit make clippy happy (and code cleaner) more merchant tweaks (more pop=better) beginning of real trading code revert the update of dependencies remove stale comments/unused code trading implementation complete (but untested) something is still strange ... fix sign in trading another sign fix some bugfixes and plenty of debugging code another bug fixed, more to go fix another invariant (rounding will lead to very small negative value) introduce Terrain and Territory fix merge mistakes
2021-03-14 03:18:32 +00:00
pub mod trade_pricing;
pub type InvSlot = Option<Item>;
const DEFAULT_INVENTORY_SLOTS: usize = 18;
/// NOTE: Do not add a PartialEq instance for Inventory; that's broken!
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Inventory {
2021-04-17 16:24:33 +00:00
next_sort_order: InventorySortOrder,
loadout: Loadout,
/// The "built-in" slots belonging to the inventory itself, all other slots
/// are provided by equipped items
slots: Vec<InvSlot>,
}
/// Errors which the methods on `Inventory` produce
#[derive(Debug)]
pub enum Error {
/// The inventory is full and items could not be added. The extra items have
/// been returned.
Full(Vec<Item>),
}
2021-04-17 16:24:33 +00:00
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum InventorySortOrder {
Name,
Quality,
Tag,
}
impl InventorySortOrder {
fn next(&self) -> InventorySortOrder {
match self {
InventorySortOrder::Name => InventorySortOrder::Quality,
InventorySortOrder::Quality => InventorySortOrder::Tag,
InventorySortOrder::Tag => InventorySortOrder::Name,
}
}
}
/// 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`
impl Inventory {
pub fn new_empty() -> Inventory { Self::new_with_loadout(LoadoutBuilder::new().build()) }
pub fn new_with_loadout(loadout: Loadout) -> Inventory {
Inventory {
2021-04-17 16:24:33 +00:00
next_sort_order: InventorySortOrder::Name,
loadout,
slots: vec![None; DEFAULT_INVENTORY_SLOTS],
}
}
/// Total number of slots in in the inventory.
pub fn capacity(&self) -> usize { self.slots().count() }
/// 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
/// 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())
}
/// 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
}
2021-04-17 16:24:33 +00:00
/// Sorts the inventory using the next sort order
pub fn sort(&mut self) {
let sort_order = self.next_sort_order;
let mut items: Vec<Item> = self.slots_mut().filter_map(|x| mem::take(x)).collect();
items.sort_by(|a, b| match sort_order {
InventorySortOrder::Name => Ord::cmp(a.name(), b.name()),
// Quality is sorted in reverse since we want high quality items first
InventorySortOrder::Quality => Ord::cmp(&b.quality(), &a.quality()),
InventorySortOrder::Tag => Ord::cmp(
a.tags.first().map_or("", |tag| tag.name()),
b.tags.first().map_or("", |tag| tag.name()),
),
});
self.push_all(items.into_iter()).expect(
"It is impossible for there to be insufficient inventory space when sorting the \
inventory",
);
self.next_sort_order = self.next_sort_order.next();
}
/// Returns the sort order that will be used when Inventory::sort() is next
/// called
pub fn next_sort_order(&self) -> InventorySortOrder { self.next_sort_order }
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 in an error if no space was found, otherwise
/// returns the found slot.
pub fn push(&mut self, item: Item) -> Result<(), Item> {
// First, check to make sure there's enough room for all instances of the
// item (note that if we find any empty slots, we can guarantee this by
// just filling up the whole slot, but to be nice we won't use it if we
// can find enough space in any combination of existing slots, and
// that's what we check in the `is_stackable` case).
if item.is_stackable()
&& self
.slots()
.filter_map(Option::as_ref)
.filter(|s| *s == &item)
.try_fold(item.amount(), |remaining, current| {
remaining
.checked_sub(current.max_amount() - current.amount())
.filter(|&remaining| remaining > 0)
})
.is_none()
{
// We either exactly matched or had more than enough space for inserting the
// item into existing slots, so go do that!
assert!(
self.slots_mut()
.filter_map(Option::as_mut)
.filter(|s| *s == &item)
.try_fold(item.amount(), |remaining, current| {
// NOTE: Invariant that current.amount <= current.max_amount(), so the
// subtraction is safe.
let new_remaining = remaining
.checked_sub(current.max_amount() - current.amount())
.filter(|&remaining| remaining > 0);
if new_remaining.is_some() {
// Not enough capacity left to hold all the remaining items, so we fill
// it as much as we can.
current
.set_amount(current.max_amount())
.expect("max_amount() is always a valid amount.");
} else {
// Enough capacity to hold all the remaining items.
current
.increase_amount(remaining)
.expect("Already checked that there is enough room.");
}
new_remaining
})
.is_none()
);
Ok(())
} else {
// No existing item to stack with or item not stackable, put the item in a new
// slot
self.insert(item)
}
}
/// 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> {
// 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 Err(item) = self.push(item) {
leftovers.push(item);
}
}
if !leftovers.is_empty() {
Err(Error::Full(leftovers))
} else {
Ok(())
}
}
/// Add a series of items to an inventory without giving duplicates.
/// (n * m complexity)
2019-11-07 01:57:05 +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> {
let mut leftovers = Vec::new();
for item in &mut items {
if self.contains(&item).not() {
if let Err(overflow) = self.push(item) {
leftovers.push(overflow);
}
} // else drop item if it was already in
}
if !leftovers.is_empty() {
Err(Error::Full(leftovers))
} else {
Ok(())
}
}
/// 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.
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))),
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 {
let dstitem = self
.get_mut(dst)
.expect("self.get(dst) was Some right above this");
dstitem
.increase_amount(amount)
.map(|_| {
// Suceeded in adding the item, so remove it from `src`.
self.remove(src).expect("Already verified that src was populated.");
})
// Can fail if we exceed `max_amount`
.is_ok()
2021-03-02 00:08:46 +00:00
} else {
false
}
}
/// Checks if inserting item exists in given cell. Inserts an item if it
/// exists.
pub fn insert_or_stack_at(
&mut self,
inv_slot_id: InvSlotId,
item: Item,
) -> Result<Option<Item>, Item> {
if item.is_stackable() {
match self.slot_mut(inv_slot_id) {
Some(Some(slot_item)) => {
Ok(if slot_item == &item {
slot_item
.increase_amount(item.amount())
.err()
.and(Some(item))
} else {
let old_item = core::mem::replace(slot_item, item);
// No need to recount--we know the count is the same.
Some(old_item)
})
},
Some(None) => self.insert_at(inv_slot_id, item),
None => Err(item),
}
} else {
self.insert_at(inv_slot_id, item)
}
}
/// 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
fn free_slots(&self) -> usize { self.slots().filter(|slot| slot.is_none()).count() }
/// Check if an item is in this inventory.
pub fn contains(&self, item: &Item) -> bool {
self.slots().any(|slot| slot.as_ref() == Some(item))
}
2019-08-30 20:42:43 +00:00
/// Get content of a slot
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)
}
/// 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-08-30 20:42:43 +00:00
/// Swap the items inside of two slots
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
}
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
pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
self.slot_mut(inv_slot_id).and_then(|item| item.take())
}
2020-03-19 13:30:50 +00:00
/// Remove just one item from the slot
2021-04-29 23:34:14 +00:00
pub fn take(
&mut self,
inv_slot_id: InvSlotId,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Option<Item> {
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
2021-04-29 23:34:14 +00:00
let mut return_item = item.duplicate(ability_map, msm);
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 {
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,
2021-04-29 23:34:14 +00:00
ability_map: &AbilityMap,
2021-03-02 00:08:46 +00:00
msm: &MaterialStatManifest,
) -> Option<Item> {
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
if item.is_stackable() && item.amount() > 1 {
2021-04-29 23:34:14 +00:00
let mut return_item = item.duplicate(ability_map, msm);
2021-03-02 00:08:46 +00:00
let returning_amount = item.amount() / 2;
item.decrease_amount(returning_amount).ok()?;
return_item.set_amount(returning_amount).expect(
"return_item.amount() = item.amount() / 2 < item.amount() (since \
item.amount() 1) item.max_amount() = return_item.max_amount(), since \
return_item is a duplicate of item",
);
2021-03-02 00:08:46 +00:00
Some(return_item)
} else {
self.remove(inv_slot_id)
}
} else {
None
}
}
/// 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.
pub fn item_count(&self, item_def: &ItemDef) -> u64 {
2020-07-14 20:11:39 +00:00
self.slots()
.flatten()
.filter(|it| it.is_same_item_def(item_def))
.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,
) -> Result<HashMap<InvSlotId, u32>, Vec<(&'a RecipeInput, u32)>> {
let mut slot_claims = HashMap::<InvSlotId, u32>::new();
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;
for (inv_slot_id, slot) in self.slots_with_id() {
if let Some(item) = slot
.as_ref()
.filter(|item| item.matches_recipe_input(&*input))
{
let claim = slot_claims.entry(inv_slot_id).or_insert(0);
// FIXME: Fishy, looks like it can underflow before min which can trigger an
// overflow check.
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)
}
}
/// Adds a new item to the first empty slot of the inventory. Returns the
/// item again in an Err if no free slot was found, otherwise returns a
/// reference to the item.
fn insert(&mut self, item: Item) -> Result<(), Item> {
match self.slots_mut().find(|slot| slot.is_none()) {
Some(slot) => {
*slot = Some(item);
Ok(())
},
None => Err(item),
}
}
pub fn slot(&self, inv_slot_id: InvSlotId) -> Option<&InvSlot> {
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),
}
}
pub fn slot_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut InvSlot> {
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"]
pub fn equip(&mut self, inv_slot: InvSlotId) -> Vec<Item> {
self.get(inv_slot)
.and_then(|item| self.loadout.get_slot_to_equip_into(item.kind()))
.map(|equip_slot| {
2021-05-09 21:18:36 +00:00
// Special case when equipping into active main hand - swap with active offhand
// first
if equip_slot == EquipSlot::ActiveMainhand {
self.loadout
2021-05-09 21:18:36 +00:00
.swap_slots(EquipSlot::ActiveMainhand, EquipSlot::InactiveMainhand);
}
self.swap_inventory_loadout(inv_slot, equip_slot)
})
.unwrap_or_else(Vec::new)
}
/// 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)
.and_then(|item| self.loadout.get_slot_to_equip_into(item.kind()))
.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> {
if item.is_stackable() {
return self.push(item);
}
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.
item.drain().for_each(|item| {
self.push(item).unwrap();
});
self.push(item)
}
/// 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("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),
Ok(()) => None,
}
}))
}
/// 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"]
pub fn swap(&mut self, slot_a: Slot, slot_b: Slot) -> Vec<Item> {
match (slot_a, slot_b) {
(Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => {
self.swap_slots(slot_a, slot_b);
Vec::new()
},
(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);
Vec::new()
},
}
}
/// 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,
) -> Vec<Item> {
if !self.can_swap(inv_slot_id, equip_slot) {
return Vec::new();
}
// 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);
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("Unable to push to inventory, no slots (bug in can_swap()?)");
}
items
})
.unwrap_or_default();
// 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
match self.push_all(unloaded_items.into_iter()) {
Err(Error::Full(leftovers)) => leftovers,
Ok(()) => Vec::new(),
}
}
/// 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
}
2021-03-02 00:45:02 +00:00
pub fn equipped_items_of_kind(&self, item_kind: ItemKind) -> impl Iterator<Item = &Item> {
self.loadout.equipped_items_of_kind(item_kind)
}
2019-07-25 14:02:05 +00:00
}
impl Component for Inventory {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InventoryUpdateEvent {
Init,
Used,
Consumed(String),
Gave,
Given,
Swapped,
Dropped,
Collected(Item),
CollectFailed,
Possession,
Debug,
2020-07-14 20:11:39 +00:00
Craft,
}
impl Default for InventoryUpdateEvent {
fn default() -> Self { Self::Init }
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct InventoryUpdate {
event: InventoryUpdateEvent,
}
impl InventoryUpdate {
pub fn new(event: InventoryUpdateEvent) -> Self { Self { event } }
pub fn event(&self) -> InventoryUpdateEvent { self.event.clone() }
}
impl Component for InventoryUpdate {
type Storage = IdvStorage<Self>;
}