diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b4b8c95b7..ccc3659090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - One handed weapons can now be used and found in the world - Players can now opt-in to server-authoritiative physics in gameplay settings. - Added `/server_physics` admin command. +- Sort inventory button ### Changed diff --git a/assets/voxygen/element/ui/bag/buttons/inv_sort.png b/assets/voxygen/element/ui/bag/buttons/inv_sort.png new file mode 100644 index 0000000000..e53633e873 --- /dev/null +++ b/assets/voxygen/element/ui/bag/buttons/inv_sort.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4df36780ca7d4145a5edfb06400ba3843def64d301983c77c22e992568345eb +size 263 diff --git a/assets/voxygen/element/ui/bag/buttons/inv_sort_hover.png b/assets/voxygen/element/ui/bag/buttons/inv_sort_hover.png new file mode 100644 index 0000000000..83377fb1f7 --- /dev/null +++ b/assets/voxygen/element/ui/bag/buttons/inv_sort_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:162e25d8cee5b140cec0640fbb21d9297d37239d6fd12df45c4985692bb4e270 +size 263 diff --git a/assets/voxygen/element/ui/bag/buttons/inv_sort_press.png b/assets/voxygen/element/ui/bag/buttons/inv_sort_press.png new file mode 100644 index 0000000000..77f9ae10be --- /dev/null +++ b/assets/voxygen/element/ui/bag/buttons/inv_sort_press.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e671ed5ee7aede3b6d08fb0d53284994a1e809bae73e45ff97a95c61f757abed +size 263 diff --git a/assets/voxygen/i18n/en/hud/bag.ron b/assets/voxygen/i18n/en/hud/bag.ron index 85a0be7a64..e10c54ddcd 100644 --- a/assets/voxygen/i18n/en/hud/bag.ron +++ b/assets/voxygen/i18n/en/hud/bag.ron @@ -33,6 +33,9 @@ "hud.bag.combat_rating_desc": "Calculated from your\nequipment and health.", "hud.bag.protection_desc": "Damage reduction through armor", "hud.bag.stun_res_desc": "Resilience against being stunned by consecutive hits.\nRegenerates like Stamina.", + "hud.bag.sort_by_name": "Sort by Name", + "hud.bag.sort_by_quality": "Sort by Quality", + "hud.bag.sort_by_category": "Sort by Category", }, diff --git a/client/src/lib.rs b/client/src/lib.rs index db5848dd37..4443e2638e 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -892,6 +892,10 @@ impl Client { } } + pub fn sort_inventory(&mut self) { + self.control_action(ControlAction::InventoryAction(InventoryAction::Sort)); + } + pub fn perform_trade_action(&mut self, action: TradeAction) { if let Some((id, _, _)) = self.pending_trade { if let TradeAction::Decline = action { diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 0db1f7be95..8437d9a269 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -23,6 +23,7 @@ pub enum InventoryEvent { Drop(InvSlotId), SplitDrop(InvSlotId), CraftRecipe(String), + Sort, } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] @@ -30,6 +31,7 @@ pub enum InventoryAction { Swap(EquipSlot, Slot), Drop(EquipSlot), Use(Slot), + Sort, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -42,6 +44,7 @@ pub enum InventoryManip { Drop(Slot), SplitDrop(Slot), CraftRecipe(String), + Sort, } impl From for InventoryManip { @@ -50,6 +53,7 @@ impl From for InventoryManip { InventoryAction::Use(slot) => Self::Use(slot), InventoryAction::Swap(equip, slot) => Self::Swap(Slot::Equip(equip), slot), InventoryAction::Drop(equip) => Self::Drop(Slot::Equip(equip)), + InventoryAction::Sort => Self::Sort, } } } @@ -68,6 +72,7 @@ impl From for InventoryManip { InventoryEvent::Drop(inv) => Self::Drop(Slot::Inventory(inv)), InventoryEvent::SplitDrop(inv) => Self::SplitDrop(Slot::Inventory(inv)), InventoryEvent::CraftRecipe(recipe) => Self::CraftRecipe(recipe), + InventoryEvent::Sort => Self::Sort, } } } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 117e6a43bc..f1f87c35b6 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -71,7 +71,7 @@ impl Lantern { pub struct Glider { pub kind: String, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Copy)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Copy, PartialOrd, Ord)] pub enum Quality { Low, // Grey Common, // UI Main Color diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 97c23314a6..64759c6188 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -1,15 +1,14 @@ use core::ops::Not; -use std::{collections::HashMap, convert::TryFrom, mem, ops::Range}; - use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; use specs_idvs::IdvStorage; +use std::{collections::HashMap, convert::TryFrom, mem, ops::Range}; use tracing::{debug, trace, warn}; use crate::{ comp::{ inventory::{ - item::{ItemDef, ItemKind, MaterialStatManifest}, + item::{ItemDef, ItemKind, MaterialStatManifest, TagExampleInfo}, loadout::Loadout, slot::{EquipSlot, Slot, SlotError}, }, @@ -34,6 +33,7 @@ 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 { + next_sort_order: InventorySortOrder, loadout: Loadout, /// The "built-in" slots belonging to the inventory itself, all other slots /// are provided by equipped items @@ -48,6 +48,23 @@ pub enum Error { Full(Vec), } +#[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 @@ -66,6 +83,7 @@ impl Inventory { pub fn new_with_loadout(loadout: Loadout) -> Inventory { Inventory { + next_sort_order: InventorySortOrder::Name, loadout, slots: vec![None; DEFAULT_INVENTORY_SLOTS], } @@ -99,6 +117,33 @@ impl Inventory { ) } + /// Sorts the inventory using the next sort order + pub fn sort(&mut self) { + let sort_order = self.next_sort_order; + let mut items: Vec = 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 } + /// 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. diff --git a/common/src/comp/inventory/test.rs b/common/src/comp/inventory/test.rs index 5cc97a73d4..e4dc4da265 100644 --- a/common/src/comp/inventory/test.rs +++ b/common/src/comp/inventory/test.rs @@ -15,6 +15,7 @@ lazy_static! { fn push_full() { let msm = &MaterialStatManifest::default(); let mut inv = Inventory { + next_sort_order: InventorySortOrder::Name, slots: TEST_ITEMS.iter().map(|a| Some(a.duplicate(msm))).collect(), loadout: LoadoutBuilder::new().build(), }; @@ -29,6 +30,7 @@ fn push_full() { fn push_all_full() { let msm = &MaterialStatManifest::default(); let mut inv = Inventory { + next_sort_order: InventorySortOrder::Name, slots: TEST_ITEMS.iter().map(|a| Some(a.duplicate(msm))).collect(), loadout: LoadoutBuilder::new().build(), }; @@ -50,6 +52,7 @@ fn push_all_full() { fn push_unique_all_full() { let msm = &MaterialStatManifest::default(); let mut inv = Inventory { + next_sort_order: InventorySortOrder::Name, slots: TEST_ITEMS.iter().map(|a| Some(a.duplicate(msm))).collect(), loadout: LoadoutBuilder::new().build(), }; @@ -63,6 +66,7 @@ fn push_unique_all_full() { fn push_all_empty() { let msm = &MaterialStatManifest::default(); let mut inv = Inventory { + next_sort_order: InventorySortOrder::Name, slots: vec![None, None], loadout: LoadoutBuilder::new().build(), }; @@ -76,6 +80,7 @@ fn push_all_empty() { fn push_all_unique_empty() { let msm = &MaterialStatManifest::default(); let mut inv = Inventory { + next_sort_order: InventorySortOrder::Name, slots: vec![None, None], loadout: LoadoutBuilder::new().build(), }; diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index dc4809503d..7ff3a7ac1c 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -611,7 +611,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } } }, + comp::InventoryManip::Sort => { + inventory.sort(); + drop(inventories); + }, } + // Drop items, Debug items should simply disappear when dropped for (pos, ori, mut item) in dropped_items .into_iter() diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 1ee7ec456c..82db41dfcf 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -19,6 +19,7 @@ use common::{ assets::AssetExt, combat::{combat_rating, Damage}, comp::{ + inventory::InventorySortOrder, item::{ItemDef, MaterialStatManifest, Quality}, Body, Energy, Health, Inventory, Poise, SkillSet, Stats, }, @@ -26,7 +27,7 @@ use common::{ use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, State as ConrodState, Text}, - widget_ids, Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, + widget_ids, Color, Colorable, Positionable, Scalar, Sizeable, UiCell, Widget, WidgetCommon, }; use crate::hud::slots::SlotKind; @@ -423,6 +424,7 @@ widget_ids! { space_txt, inventory_title, inventory_title_bg, + inventory_sort, scrollbar_bg, scrollbar_slots, tab_1, @@ -540,6 +542,7 @@ pub struct BagState { pub enum Event { BagExpand, Close, + SortInventory, } impl<'a> Widget for Bag<'a> { @@ -563,6 +566,7 @@ impl<'a> Widget for Bag<'a> { #[allow(clippy::useless_format)] // TODO: Pending review in #587 fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; + let i18n = &self.localized_strings; let mut event = None; let bag_tooltip = Tooltip::new({ @@ -681,21 +685,12 @@ impl<'a> Widget for Bag<'a> { } else { self.imgs.expand_btn_press }); + // Only show expand button when it's needed... - let space_max = inventory.slots().count(); - if space_max > 45 && !self.show.bag_inv { + if inventory.slots().count() > 45 || self.show.bag_inv { + let expand_btn_top = if self.show.bag_inv { 53.0 } else { 460.0 }; if expand_btn - .top_left_with_margins_on(state.bg_ids.bg_frame, 460.0, 211.5) - .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) - .set(state.ids.bag_expand_btn, ui) - .was_clicked() - { - event = Some(Event::BagExpand); - } - } else if self.show.bag_inv { - //... but always show it when the bag is expanded - if expand_btn - .top_left_with_margins_on(state.bg_ids.bg_frame, 53.0, 211.5) + .top_left_with_margins_on(state.bg_ids.bg_frame, expand_btn_top, 211.5) .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) .set(state.ids.bag_expand_btn, ui) .was_clicked() @@ -704,6 +699,30 @@ impl<'a> Widget for Bag<'a> { } } + // Sort inventory button + let inv_sort_btn_top: Scalar = if !self.show.bag_inv { 460.0 } else { 53.0 }; + if Button::image(self.imgs.inv_sort_btn) + .w_h(30.0, 17.0) + .hover_image(self.imgs.inv_sort_btn_hover) + .press_image(self.imgs.inv_sort_btn_press) + .top_left_with_margins_on(state.bg_ids.bg_frame, inv_sort_btn_top, 47.0) + .with_tooltip( + self.tooltip_manager, + match inventory.next_sort_order() { + InventorySortOrder::Name => i18n.get("hud.bag.sort_by_name"), + InventorySortOrder::Quality => i18n.get("hud.bag.sort_by_quality"), + InventorySortOrder::Tag => i18n.get("hud.bag.sort_by_category"), + }, + "", + &tooltip, + color::WHITE, + ) + .set(state.ids.inventory_sort, ui) + .was_clicked() + { + event = Some(Event::SortInventory); + } + // Armor Slots let mut slot_maker = SlotMaker { empty_slot: self.imgs.armor_slot_empty, @@ -726,7 +745,6 @@ impl<'a> Widget for Bag<'a> { slot_manager: Some(self.slot_manager), pulse: self.pulse, }; - let i18n = &self.localized_strings; let filled_slot = self.imgs.armor_slot; if !self.show.bag_inv { // Stat icons and text @@ -1179,6 +1197,7 @@ impl<'a> Widget for Bag<'a> { .set(state.ids.offhand_slot, ui) } } + // Bag 1 let bag1_item = inventory .equipped(EquipSlot::Armor(ArmorSlot::Bag1)) diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 3a3af7b30e..39d28d18f6 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -407,6 +407,9 @@ image_ids! { expand_btn: "voxygen.element.ui.bag.buttons.inv_expand", expand_btn_hover: "voxygen.element.ui.bag.buttons.inv_expand_hover", expand_btn_press: "voxygen.element.ui.bag.buttons.inv_expand_press", + inv_sort_btn: "voxygen.element.ui.bag.buttons.inv_sort", + inv_sort_btn_hover: "voxygen.element.ui.bag.buttons.inv_sort_hover", + inv_sort_btn_press: "voxygen.element.ui.bag.buttons.inv_sort_press", coin_ico: "voxygen.element.items.coin", cheese_ico: "voxygen.element.items.item_cheese", inv_bg_armor: "voxygen.element.ui.bag.inv_bg_0", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 22b707b82c..009dca6909 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -372,6 +372,7 @@ pub enum Event { }, DropSlot(comp::slot::Slot), SplitDropSlot(comp::slot::Slot), + SortInventory, ChangeHotbarState(Box), TradeAction(TradeAction), Ability3(bool), @@ -2330,6 +2331,7 @@ impl Hud { self.force_ungrab = true }; }, + Some(bag::Event::SortInventory) => self.events.push(Event::SortInventory), None => {}, } } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index c415365d64..a5ef5a2df7 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1126,6 +1126,9 @@ impl PlayState for SessionState { client.disable_lantern(); } }, + HudEvent::SortInventory => { + self.client.borrow_mut().sort_inventory(); + }, HudEvent::ChangeHotbarState(state) => { let client = self.client.borrow();