Implemented inventory sorting

This commit is contained in:
Ben Wallis 2021-04-17 17:24:33 +01:00
parent e7e7565440
commit a4cdb89987
15 changed files with 123 additions and 19 deletions

View File

@ -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

BIN
assets/voxygen/element/ui/bag/buttons/inv_sort.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/bag/buttons/inv_sort_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/bag/buttons/inv_sort_press.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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",
},

View File

@ -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 {

View File

@ -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<InventoryAction> for InventoryManip {
@ -50,6 +53,7 @@ impl From<InventoryAction> 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<InventoryEvent> 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,
}
}
}

View File

@ -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

View File

@ -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<Item>),
}
#[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<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 }
/// 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.

View File

@ -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(),
};

View File

@ -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()

View File

@ -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>) -> 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))

View File

@ -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",

View File

@ -372,6 +372,7 @@ pub enum Event {
},
DropSlot(comp::slot::Slot),
SplitDropSlot(comp::slot::Slot),
SortInventory,
ChangeHotbarState(Box<HotbarState>),
TradeAction(TradeAction),
Ability3(bool),
@ -2330,6 +2331,7 @@ impl Hud {
self.force_ungrab = true
};
},
Some(bag::Event::SortInventory) => self.events.push(Event::SortInventory),
None => {},
}
}

View File

@ -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();