2020-09-17 23:02:14 +00:00
|
|
|
use core::ops::Not;
|
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-04-17 16:24:33 +00:00
|
|
|
use std::{collections::HashMap, convert::TryFrom, mem, ops::Range};
|
2021-01-08 19:12:09 +00:00
|
|
|
use tracing::{debug, trace, warn};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
comp::{
|
|
|
|
inventory::{
|
2021-04-29 23:34:14 +00:00
|
|
|
item::{tool::AbilityMap, ItemDef, ItemKind, MaterialStatManifest, TagExampleInfo},
|
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;
|
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;
|
2021-01-08 19:12:09 +00:00
|
|
|
|
|
|
|
pub type InvSlot = Option<Item>;
|
|
|
|
const DEFAULT_INVENTORY_SLOTS: usize = 18;
|
2019-05-18 16:46:14 +00:00
|
|
|
|
2021-04-09 07:34:58 +00:00
|
|
|
/// NOTE: Do not add a PartialEq instance for Inventory; that's broken!
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
2019-05-18 16:46:14 +00:00
|
|
|
pub struct Inventory {
|
2021-04-17 16:24:33 +00:00
|
|
|
next_sort_order: InventorySortOrder,
|
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-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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-04-17 16:24:33 +00:00
|
|
|
next_sort_order: InventorySortOrder::Name,
|
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
|
|
|
}
|
|
|
|
|
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
|
2021-04-09 07:34:58 +00:00
|
|
|
/// 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)
|
2020-09-17 23:02:14 +00:00
|
|
|
}
|
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 {
|
2021-04-09 07:34:58 +00:00
|
|
|
if let Err(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() {
|
2021-04-09 07:34:58 +00:00
|
|
|
if let Err(overflow) = self.push(item) {
|
|
|
|
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 {
|
|
|
|
let dstitem = self
|
|
|
|
.get_mut(dst)
|
|
|
|
.expect("self.get(dst) was Some right above this");
|
|
|
|
dstitem
|
|
|
|
.increase_amount(amount)
|
2021-04-09 07:34:58 +00:00
|
|
|
.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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-04-29 23:34:14 +00:00
|
|
|
pub fn take(
|
|
|
|
&mut self,
|
|
|
|
inv_slot_id: InvSlotId,
|
|
|
|
ability_map: &AbilityMap,
|
|
|
|
msm: &MaterialStatManifest,
|
|
|
|
) -> Option<Item> {
|
2021-01-08 19:12:09 +00:00
|
|
|
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);
|
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,
|
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()?;
|
2021-04-09 07:34:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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);
|
2021-04-09 07:34:58 +00:00
|
|
|
// FIXME: Fishy, looks like it can underflow before min which can trigger an
|
|
|
|
// overflow check.
|
2021-01-08 19:12:09 +00:00
|
|
|
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
|
2021-04-09 07:34:58 +00:00
|
|
|
/// 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> {
|
2021-01-08 19:12:09 +00:00
|
|
|
match self.slots_mut().find(|slot| slot.is_none()) {
|
|
|
|
Some(slot) => {
|
|
|
|
*slot = Some(item);
|
2021-04-09 07:34:58 +00:00
|
|
|
Ok(())
|
2021-01-08 19:12:09 +00:00
|
|
|
},
|
2021-04-09 07:34:58 +00:00
|
|
|
None => Err(item),
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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| {
|
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 {
|
2021-01-09 18:10:12 +00:00
|
|
|
self.loadout
|
2021-05-09 21:18:36 +00:00
|
|
|
.swap_slots(EquipSlot::ActiveMainhand, EquipSlot::InactiveMainhand);
|
2021-01-09 18:10:12 +00:00
|
|
|
}
|
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() {
|
2021-04-09 07:34:58 +00:00
|
|
|
return self.push(item);
|
2021-01-08 23:34:35 +00:00
|
|
|
}
|
|
|
|
|
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| {
|
2021-04-09 07:34:58 +00:00
|
|
|
self.push(item).unwrap();
|
2021-01-09 18:10:12 +00:00
|
|
|
});
|
2021-04-09 07:34:58 +00:00
|
|
|
self.push(item)
|
2021-01-08 19:12:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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)
|
2021-04-09 07:34:58 +00:00
|
|
|
.expect("Failed to push item to inventory, precondition failed?");
|
2021-01-08 19:12:09 +00:00
|
|
|
|
|
|
|
// 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)
|
2021-04-09 07:34:58 +00:00
|
|
|
.expect("Unable to push to inventory, no slots (bug in can_swap()?)");
|
2021-01-09 18:10:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2021-05-12 02:10:41 +00:00
|
|
|
#[allow(clippy::blocks_in_if_conditions)]
|
2021-01-08 19:12:09 +00:00
|
|
|
pub fn can_swap(&self, inv_slot_id: InvSlotId, equip_slot: EquipSlot) -> bool {
|
|
|
|
// Check if loadout slot can hold item
|
2021-05-12 02:10:41 +00:00
|
|
|
if !self.get(inv_slot_id).map_or(true, |item| {
|
|
|
|
self.loadout.slot_can_hold(equip_slot, &item.kind())
|
|
|
|
}) {
|
2021-01-08 19:12:09 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2021-04-09 07:34:58 +00:00
|
|
|
#[derive(Clone, Debug, 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
|
|
|
}
|