diff --git a/common/src/comp/inventory/loadout.rs b/common/src/comp/inventory/loadout.rs index 3c2f01576a..5cef680415 100644 --- a/common/src/comp/inventory/loadout.rs +++ b/common/src/comp/inventory/loadout.rs @@ -1,18 +1,26 @@ -use crate::comp::{ - inventory::{ - item::{self, tool::Tool, Hands, ItemKind}, - slot::{ArmorSlot, EquipSlot}, - InvSlot, +use crate::{ + comp::{ + inventory::{ + item::{self, tool::Tool, Hands, ItemDefinitionIdOwned, ItemKind}, + slot::{ArmorSlot, EquipSlot}, + InvSlot, + }, + Item, }, - Item, + resources::Time, }; +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use std::ops::Range; use tracing::warn; +pub(super) const UNEQUIP_TRACKING_DURATION: f64 = 60.0; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Loadout { slots: Vec, + // Includes time that item was unequipped at + pub(super) recently_unequipped_items: HashMap, } /// NOTE: Please don't derive a PartialEq Instance for this; that's broken! @@ -83,16 +91,36 @@ impl Loadout { .into_iter() .map(|(equip_slot, persistence_key)| LoadoutSlot::new(equip_slot, persistence_key)) .collect(), + recently_unequipped_items: HashMap::new(), } } /// Replaces the item in the Loadout slot that corresponds to the given /// EquipSlot and returns the previous item if any - pub(super) fn swap(&mut self, equip_slot: EquipSlot, item: Option) -> Option { - self.slots + pub(super) fn swap( + &mut self, + equip_slot: EquipSlot, + item: Option, + time: Time, + ) -> Option { + self.recently_unequipped_items.retain(|def, unequip_time| { + let item_reequipped = item + .as_ref() + .map_or(false, |i| def.as_ref() == i.item_definition_id()); + let old_unequip = time.0 - unequip_time.0 > UNEQUIP_TRACKING_DURATION; + // Stop tracking item if it is re-equipped or if was unequipped a while ago + !(item_reequipped || old_unequip) + }); + let unequipped_item = self + .slots .iter_mut() .find(|x| x.equip_slot == equip_slot) - .and_then(|x| core::mem::replace(&mut x.slot, item)) + .and_then(|x| core::mem::replace(&mut x.slot, item)); + if let Some(unequipped_item) = unequipped_item.as_ref() { + self.recently_unequipped_items + .insert(unequipped_item.item_definition_id().to_owned(), time); + } + unequipped_item } /// Returns a reference to the item (if any) equipped in the given EquipSlot @@ -154,7 +182,12 @@ impl Loadout { } /// Swaps the contents of two loadout slots - pub(super) fn swap_slots(&mut self, equip_slot_a: EquipSlot, equip_slot_b: EquipSlot) { + pub(super) fn swap_slots( + &mut self, + equip_slot_a: EquipSlot, + equip_slot_b: EquipSlot, + time: Time, + ) { if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() { // Currently all loadouts contain slots for all EquipSlots so this can never // happen, but if loadouts with alternate slot combinations are @@ -163,9 +196,9 @@ impl Loadout { return; } - let item_a = self.swap(equip_slot_a, None); - let item_b = self.swap(equip_slot_b, item_a); - assert_eq!(self.swap(equip_slot_a, item_b), None); + let item_a = self.swap(equip_slot_a, None, time); + let item_b = self.swap(equip_slot_b, item_a, time); + assert_eq!(self.swap(equip_slot_a, item_b, time), None); // Check if items are valid in their new positions if !self.slot_can_hold( @@ -176,9 +209,9 @@ impl Loadout { self.equipped(equip_slot_b).map(|x| x.kind()).as_deref(), ) { // If not, revert the swap - let item_a = self.swap(equip_slot_a, None); - let item_b = self.swap(equip_slot_b, item_a); - assert_eq!(self.swap(equip_slot_a, item_b), None); + let item_a = self.swap(equip_slot_a, None, time); + let item_b = self.swap(equip_slot_b, item_a, time); + assert_eq!(self.swap(equip_slot_a, item_b, time), None); } } @@ -367,7 +400,7 @@ impl Loadout { (None, None)) } - pub(super) fn swap_equipped_weapons(&mut self) { + pub(super) fn swap_equipped_weapons(&mut self, time: Time) { // Checks if a given slot can hold an item right now, defaults to true if // nothing is equipped in slot let valid_slot = |equip_slot| { @@ -385,25 +418,25 @@ impl Loadout { && valid_slot(EquipSlot::InactiveOffhand) { // Get weapons from each slot - let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None); - let active_offhand = self.swap(EquipSlot::ActiveOffhand, None); - let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None); - let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None); + let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None, time); + let active_offhand = self.swap(EquipSlot::ActiveOffhand, None, time); + let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None, time); + let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None, time); // Equip weapons into new slots assert!( - self.swap(EquipSlot::ActiveMainhand, inactive_mainhand) + self.swap(EquipSlot::ActiveMainhand, inactive_mainhand, time) .is_none() ); assert!( - self.swap(EquipSlot::ActiveOffhand, inactive_offhand) + self.swap(EquipSlot::ActiveOffhand, inactive_offhand, time) .is_none() ); assert!( - self.swap(EquipSlot::InactiveMainhand, active_mainhand) + self.swap(EquipSlot::InactiveMainhand, active_mainhand, time) .is_none() ); assert!( - self.swap(EquipSlot::InactiveOffhand, active_offhand) + self.swap(EquipSlot::InactiveOffhand, active_offhand, time) .is_none() ); } diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 4fce8c76fc..42af30bc91 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -9,6 +9,7 @@ use crate::{ item::{self, Item}, object, quadruped_low, quadruped_medium, theropod, Body, }, + resources::Time, trade::SiteInformation, }; use rand::{self, distributions::WeightedError, seq::SliceRandom, Rng}; @@ -1148,7 +1149,11 @@ impl LoadoutBuilder { .map_or(true, |item| equip_slot.can_hold(&item.kind())) ); - self.0.swap(equip_slot, item); + // Used when creating a loadout, so time not needed as it is used to check when + // stuff gets unequipped. A new loadout has never unequipped an item. + let time = Time(0.0); + + self.0.swap(equip_slot, item, time); self } diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 51d1db3ead..4240bff167 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -17,6 +17,7 @@ use crate::{ slot::{InvSlotId, SlotId}, Item, }, + resources::Time, uid::Uid, LoadoutBuilder, }; @@ -615,18 +616,19 @@ impl Inventory { &mut self, equip_slot: EquipSlot, replacement_item: Option, + time: Time, ) -> Option { - self.loadout.swap(equip_slot, replacement_item) + self.loadout.swap(equip_slot, replacement_item, time) } /// 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 { + pub fn equip(&mut self, inv_slot: InvSlotId, time: Time) -> Vec { self.get(inv_slot) .and_then(|item| self.loadout.get_slot_to_equip_into(&item.kind())) - .map(|equip_slot| self.swap_inventory_loadout(inv_slot, equip_slot)) + .map(|equip_slot| self.swap_inventory_loadout(inv_slot, equip_slot, time)) .unwrap_or_else(Vec::new) } @@ -680,7 +682,11 @@ impl Inventory { /// equipped if inventory has no slots available. #[must_use = "Returned items will be lost if not used"] #[allow(clippy::needless_collect)] // This is a false positive, the collect is needed - pub fn unequip(&mut self, equip_slot: EquipSlot) -> Result>, SlotError> { + pub fn unequip( + &mut self, + equip_slot: EquipSlot, + time: Time, + ) -> Result>, 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); @@ -688,7 +694,7 @@ impl Inventory { Ok(self .loadout - .swap(equip_slot, None) + .swap(equip_slot, None, time) .and_then(|mut unequipped_item| { let unloaded_items: Vec = unequipped_item.drain().collect(); self.push(unequipped_item) @@ -721,7 +727,7 @@ impl Inventory { /// 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 { + pub fn swap(&mut self, slot_a: Slot, slot_b: Slot, time: Time) -> Vec { match (slot_a, slot_b) { (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => { self.swap_slots(slot_a, slot_b); @@ -729,10 +735,10 @@ impl Inventory { }, (Slot::Inventory(inv_slot), Slot::Equip(equip_slot)) | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => { - self.swap_inventory_loadout(inv_slot, equip_slot) + self.swap_inventory_loadout(inv_slot, equip_slot, time) }, (Slot::Equip(slot_a), Slot::Equip(slot_b)) => { - self.loadout.swap_slots(slot_a, slot_b); + self.loadout.swap_slots(slot_a, slot_b, time); Vec::new() }, } @@ -768,6 +774,7 @@ impl Inventory { &mut self, inv_slot_id: InvSlotId, equip_slot: EquipSlot, + time: Time, ) -> Vec { if !self.can_swap(inv_slot_id, equip_slot) { return Vec::new(); @@ -777,7 +784,7 @@ impl 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 from_equip = self.loadout.swap(equip_slot, from_inv, time); let unloaded_items = from_equip .map(|mut from_equip| { @@ -803,10 +810,10 @@ impl Inventory { if self.loadout.equipped(EquipSlot::ActiveMainhand).is_none() && self.loadout.equipped(EquipSlot::ActiveOffhand).is_some() { - let offhand = self.loadout.swap(EquipSlot::ActiveOffhand, None); + let offhand = self.loadout.swap(EquipSlot::ActiveOffhand, None, time); assert!( self.loadout - .swap(EquipSlot::ActiveMainhand, offhand) + .swap(EquipSlot::ActiveMainhand, offhand, time) .is_none() ); } @@ -815,10 +822,10 @@ impl Inventory { if self.loadout.equipped(EquipSlot::InactiveMainhand).is_none() && self.loadout.equipped(EquipSlot::InactiveOffhand).is_some() { - let offhand = self.loadout.swap(EquipSlot::InactiveOffhand, None); + let offhand = self.loadout.swap(EquipSlot::InactiveOffhand, None, time); assert!( self.loadout - .swap(EquipSlot::InactiveMainhand, offhand) + .swap(EquipSlot::InactiveMainhand, offhand, time) .is_none() ); } @@ -867,7 +874,7 @@ impl Inventory { self.loadout.equipped_items_replaceable_by(item_kind) } - pub fn swap_equipped_weapons(&mut self) { self.loadout.swap_equipped_weapons() } + pub fn swap_equipped_weapons(&mut self, time: Time) { self.loadout.swap_equipped_weapons(time) } /// Update internal computed state of all top level items in this loadout. /// Used only when loading in persistence code. @@ -883,13 +890,48 @@ impl Inventory { }); } - /// Increments durability of all valid items equipped in loaodut by 1 + /// Increments durability of all valid items equipped in loaodut and + /// recently unequipped from loadout by 1 pub fn damage_items( &mut self, ability_map: &item::tool::AbilityMap, msm: &item::MaterialStatManifest, + time: Time, ) { - self.loadout.damage_items(ability_map, msm) + self.loadout.damage_items(ability_map, msm); + self.loadout + .recently_unequipped_items + .retain(|_item, unequip_time| { + time.0 - unequip_time.0 <= loadout::UNEQUIP_TRACKING_DURATION + }); + let inv_slots = self + .loadout + .recently_unequipped_items + .keys() + .filter_map(|item_def_id| { + self.slots_with_id() + .find(|&(_, item)| { + if let Some(item) = item { + // Find an item with the matching item definition id and that is not yet + // at maximum durability lost + item.item_definition_id() == *item_def_id + && item + .durability() + .map_or(true, |dur| dur < Item::MAX_DURABILITY) + } else { + false + } + }) + .map(|(slot, _)| slot) + }) + .collect::>(); + for inv_slot in inv_slots.iter() { + if let Some(Some(item)) = self.slot_mut(*inv_slot) { + if item.has_durability() { + item.increment_damage(ability_map, msm); + } + } + } } /// Resets durability of item in specified slot diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 9e39fa1198..554221f3e3 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -508,9 +508,11 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt // Modify durability on all equipped items if let Some(mut inventory) = state.ecs().write_storage::().get_mut(entity) { - let ability_map = state.ecs().read_resource::(); - let msm = state.ecs().read_resource::(); - inventory.damage_items(&ability_map, &msm); + let ecs = state.ecs(); + let ability_map = ecs.read_resource::(); + let msm = ecs.read_resource::(); + let time = ecs.read_resource::