Merge branch 'fix-hotbar-bug' into 'master'

Fix hotbar bug

See merge request veloren/veloren!3066
This commit is contained in:
Imbris 2022-01-15 01:37:27 +00:00
commit 4b1900d3ea
10 changed files with 123 additions and 81 deletions

View File

@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Poise damage dealt to a target that is in a stunned state is now converted to health damage at an efficiency dependent on the severity of the stunned state - Poise damage dealt to a target that is in a stunned state is now converted to health damage at an efficiency dependent on the severity of the stunned state
- You are now immune to poise damage for 1 second after leaving a stunned state - You are now immune to poise damage for 1 second after leaving a stunned state
- Removed or reduced poise damage from most abilities - Removed or reduced poise damage from most abilities
- Made the hotbar link to items by item definition id and component composition instead of specific inventory slots.
### Removed ### Removed
@ -75,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Merchant cost percentages displayed as floored, whole numbers - Merchant cost percentages displayed as floored, whole numbers
- Bodies of water no longer contain black chunks on the voxel minimap. - Bodies of water no longer contain black chunks on the voxel minimap.
- Agents can flee once again, and more appropriately - Agents can flee once again, and more appropriately
- Items in hotbar no longer change when sorting inventory
## [0.11.0] - 2021-09-11 ## [0.11.0] - 2021-09-11

View File

@ -76,11 +76,6 @@ impl Alignment {
_ => false, _ => false,
} }
} }
// TODO: Remove this hack
pub fn is_friendly_to_players(&self) -> bool {
matches!(self, Alignment::Npc | Alignment::Tame | Alignment::Owned(_))
}
} }
impl Component for Alignment { impl Component for Alignment {

View File

@ -23,7 +23,7 @@ use crossbeam_utils::atomic::AtomicCell;
use serde::{de, Deserialize, Serialize, Serializer}; use serde::{de, Deserialize, Serialize, Serializer};
use specs::{Component, DerefFlaggedStorage}; use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
use std::{fmt, sync::Arc}; use std::{collections::hash_map::DefaultHasher, fmt, sync::Arc};
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use tracing::error; use tracing::error;
use vek::Rgb; use vek::Rgb;
@ -369,6 +369,17 @@ pub struct Item {
/// The slots for items that this item has /// The slots for items that this item has
slots: Vec<InvSlot>, slots: Vec<InvSlot>,
item_config: Option<Box<ItemConfig>>, item_config: Option<Box<ItemConfig>>,
hash: u64,
}
use std::hash::{Hash, Hasher};
// Used to find inventory item corresponding to hotbar slot
impl Hash for Item {
fn hash<H: Hasher>(&self, state: &mut H) {
self.item_def.item_definition_id.hash(state);
self.components.hash(state);
}
} }
// Custom serialization for ItemDef, we only want to send the item_definition_id // Custom serialization for ItemDef, we only want to send the item_definition_id
@ -633,6 +644,12 @@ impl Item {
.map(|comp| comp.duplicate(ability_map, msm)), .map(|comp| comp.duplicate(ability_map, msm)),
); );
} }
let item_hash = {
let mut s = DefaultHasher::new();
inner_item.item_definition_id.hash(&mut s);
components.hash(&mut s);
s.finish()
};
let mut item = Item { let mut item = Item {
item_id: Arc::new(AtomicCell::new(None)), item_id: Arc::new(AtomicCell::new(None)),
@ -641,6 +658,7 @@ impl Item {
slots: vec![None; inner_item.slots as usize], slots: vec![None; inner_item.slots as usize],
item_def: inner_item, item_def: inner_item,
item_config: None, item_config: None,
hash: item_hash,
}; };
item.update_item_config(ability_map, msm); item.update_item_config(ability_map, msm);
item item
@ -856,6 +874,8 @@ impl Item {
} }
pub fn ability_spec(&self) -> Option<&AbilitySpec> { self.item_def.ability_spec.as_ref() } pub fn ability_spec(&self) -> Option<&AbilitySpec> { self.item_def.ability_spec.as_ref() }
pub fn item_hash(&self) -> u64 { self.hash }
} }
/// Provides common methods providing details about an item definition /// Provides common methods providing details about an item definition

View File

@ -347,6 +347,20 @@ impl Inventory {
self.slot(inv_slot_id).and_then(Option::as_ref) self.slot(inv_slot_id).and_then(Option::as_ref)
} }
/// Get item from inventory
pub fn get_by_hash(&self, item_hash: u64) -> Option<&Item> {
self.slots().flatten().find(|i| i.item_hash() == item_hash)
}
/// Get slot from hash
pub fn get_slot_from_hash(&self, item_hash: u64) -> Option<InvSlotId> {
let slot_with_id = self.slots_with_id().find(|slot| match slot.1 {
None => false,
Some(item) => item.item_hash() == item_hash,
});
slot_with_id.map(|s| s.0)
}
/// Mutably get content of a slot /// Mutably get content of a slot
fn get_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut Item> { fn get_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut Item> {
self.slot_mut(inv_slot_id).and_then(Option::as_mut) self.slot_mut(inv_slot_id).and_then(Option::as_mut)

View File

@ -1,4 +1,5 @@
use common::comp::slot::InvSlotId; use crate::hud::item_imgs::ItemKey;
use common::comp::inventory::item::Item;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@ -15,13 +16,13 @@ pub enum Slot {
Ten = 9, Ten = 9,
} }
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum SlotContents { pub enum SlotContents {
Inventory(InvSlotId), Inventory(u64, ItemKey),
Ability(usize), Ability(usize),
} }
#[derive(Clone, Copy, Default)] #[derive(Clone, Default)]
pub struct State { pub struct State {
pub slots: [Option<SlotContents>; 10], pub slots: [Option<SlotContents>; 10],
inputs: [bool; 10], inputs: [bool; 10],
@ -43,14 +44,17 @@ impl State {
just_pressed just_pressed
} }
pub fn get(&self, slot: Slot) -> Option<SlotContents> { self.slots[slot as usize] } pub fn get(&self, slot: Slot) -> Option<SlotContents> { self.slots[slot as usize].clone() }
pub fn swap(&mut self, a: Slot, b: Slot) { self.slots.swap(a as usize, b as usize); } pub fn swap(&mut self, a: Slot, b: Slot) { self.slots.swap(a as usize, b as usize); }
pub fn clear_slot(&mut self, slot: Slot) { self.slots[slot as usize] = None; } pub fn clear_slot(&mut self, slot: Slot) { self.slots[slot as usize] = None; }
pub fn add_inventory_link(&mut self, slot: Slot, inventory_pos: InvSlotId) { pub fn add_inventory_link(&mut self, slot: Slot, item: &Item) {
self.slots[slot as usize] = Some(SlotContents::Inventory(inventory_pos)); self.slots[slot as usize] = Some(SlotContents::Inventory(
item.item_hash(),
ItemKey::from(item),
));
} }
// TODO: remove pending UI // TODO: remove pending UI

View File

@ -3304,8 +3304,13 @@ impl Hud {
Hotbar(h), Hotbar(h),
) = (a, b) ) = (a, b)
{ {
self.hotbar.add_inventory_link(h, slot); if let Some(item) = inventories
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); .get(client.entity())
.and_then(|inv| inv.get(slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
}
} else if let (Hotbar(a), Hotbar(b)) = (a, b) { } else if let (Hotbar(a), Hotbar(b)) = (a, b) {
self.hotbar.swap(a, b); self.hotbar.swap(a, b);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
@ -3370,8 +3375,13 @@ impl Hud {
bypass_dialog: false, bypass_dialog: false,
}); });
} else if let (Inventory(i), Hotbar(h)) = (a, b) { } else if let (Inventory(i), Hotbar(h)) = (a, b) {
self.hotbar.add_inventory_link(h, i.slot); if let Some(item) = inventories
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); .get(client.entity())
.and_then(|inv| inv.get(i.slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
}
} else if let (Hotbar(a), Hotbar(b)) = (a, b) { } else if let (Hotbar(a), Hotbar(b)) = (a, b) {
self.hotbar.swap(a, b); self.hotbar.swap(a, b);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
@ -3419,11 +3429,16 @@ impl Hud {
} else if let Hotbar(h) = from { } else if let Hotbar(h) = from {
// Used from hotbar // Used from hotbar
self.hotbar.get(h).map(|s| match s { self.hotbar.get(h).map(|s| match s {
hotbar::SlotContents::Inventory(i) => { hotbar::SlotContents::Inventory(i, _) => {
events.push(Event::UseSlot { if let Some(slot) = inventories
slot: comp::slot::Slot::Inventory(i), .get(client.entity())
bypass_dialog: false, .and_then(|inv| inv.get_slot_from_hash(i))
}); {
events.push(Event::UseSlot {
slot: comp::slot::Slot::Inventory(slot),
bypass_dialog: false,
});
}
}, },
hotbar::SlotContents::Ability(_) => {}, hotbar::SlotContents::Ability(_) => {},
}); });
@ -3602,7 +3617,12 @@ impl Hud {
} }
} }
pub fn handle_event(&mut self, event: WinEvent, global_state: &mut GlobalState) -> bool { pub fn handle_event(
&mut self,
event: WinEvent,
global_state: &mut GlobalState,
client_inventory: Option<&comp::Inventory>,
) -> bool {
// Helper // Helper
fn handle_slot( fn handle_slot(
slot: hotbar::Slot, slot: hotbar::Slot,
@ -3610,6 +3630,7 @@ impl Hud {
events: &mut Vec<Event>, events: &mut Vec<Event>,
slot_manager: &mut slots::SlotManager, slot_manager: &mut slots::SlotManager,
hotbar: &mut hotbar::State, hotbar: &mut hotbar::State,
client_inventory: Option<&comp::Inventory>,
) { ) {
use slots::InventorySlot; use slots::InventorySlot;
if let Some(slots::SlotKind::Inventory(InventorySlot { if let Some(slots::SlotKind::Inventory(InventorySlot {
@ -3618,18 +3639,24 @@ impl Hud {
.. ..
})) = slot_manager.selected() })) = slot_manager.selected()
{ {
hotbar.add_inventory_link(slot, i); if let Some(item) = client_inventory.and_then(|inv| inv.get(i)) {
events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned()))); hotbar.add_inventory_link(slot, item);
slot_manager.idle(); events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned())));
slot_manager.idle();
}
} else { } else {
let just_pressed = hotbar.process_input(slot, state); let just_pressed = hotbar.process_input(slot, state);
hotbar.get(slot).map(|s| match s { hotbar.get(slot).map(|s| match s {
hotbar::SlotContents::Inventory(i) => { hotbar::SlotContents::Inventory(i, _) => {
if just_pressed { if just_pressed {
events.push(Event::UseSlot { if let Some(slot) =
slot: comp::slot::Slot::Inventory(i), client_inventory.and_then(|inv| inv.get_slot_from_hash(i))
bypass_dialog: false, {
}); events.push(Event::UseSlot {
slot: comp::slot::Slot::Inventory(slot),
bypass_dialog: false,
});
}
} }
}, },
hotbar::SlotContents::Ability(i) => events.push(Event::Ability(i, state)), hotbar::SlotContents::Ability(i) => events.push(Event::Ability(i, state)),
@ -3714,6 +3741,7 @@ impl Hud {
&mut self.events, &mut self.events,
&mut self.slot_manager, &mut self.slot_manager,
&mut self.hotbar, &mut self.hotbar,
client_inventory,
); );
true true
} else { } else {
@ -3817,6 +3845,7 @@ impl Hud {
&mut self.events, &mut self.events,
&mut self.slot_manager, &mut self.slot_manager,
&mut self.hotbar, &mut self.hotbar,
client_inventory,
); );
true true
} else { } else {

View File

@ -594,7 +594,7 @@ impl<'a> Skillbar<'a> {
let slot_content = |slot| { let slot_content = |slot| {
let (hotbar, inventory, ..) = content_source; let (hotbar, inventory, ..) = content_source;
hotbar.get(slot).and_then(|content| match content { hotbar.get(slot).and_then(|content| match content {
hotbar::SlotContents::Inventory(i) => inventory.get(i), hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i),
_ => None, _ => None,
}) })
}; };
@ -603,8 +603,8 @@ impl<'a> Skillbar<'a> {
let tooltip_text = |slot| { let tooltip_text = |slot| {
let (hotbar, inventory, _, _, active_abilities, _) = content_source; let (hotbar, inventory, _, _, active_abilities, _) = content_source;
hotbar.get(slot).and_then(|content| match content { hotbar.get(slot).and_then(|content| match content {
hotbar::SlotContents::Inventory(i) => inventory hotbar::SlotContents::Inventory(i, _) => inventory
.get(i) .get_by_hash(i)
.map(|item| (item.name(), item.description())), .map(|item| (item.name(), item.description())),
hotbar::SlotContents::Ability(i) => active_abilities hotbar::SlotContents::Ability(i) => active_abilities
.abilities .abilities

View File

@ -130,11 +130,15 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
&self, &self,
(hotbar, inventory, energy, skillset, active_abilities, body): &HotbarSource<'a>, (hotbar, inventory, energy, skillset, active_abilities, body): &HotbarSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> { ) -> Option<(Self::ImageKey, Option<Color>)> {
const GREYED_OUT: Color = Color::Rgba(0.3, 0.3, 0.3, 0.8);
hotbar.get(*self).and_then(|contents| match contents { hotbar.get(*self).and_then(|contents| match contents {
hotbar::SlotContents::Inventory(idx) => inventory hotbar::SlotContents::Inventory(item_hash, item_key) => {
.get(idx) let item = inventory.get_by_hash(item_hash);
.map(|item| HotbarImage::Item(item.into())) match item {
.map(|i| (i, None)), Some(item) => Some((HotbarImage::Item(item.into()), None)),
None => Some((HotbarImage::Item(item_key), Some(GREYED_OUT))),
}
},
hotbar::SlotContents::Ability(i) => { hotbar::SlotContents::Ability(i) => {
let ability_id = active_abilities let ability_id = active_abilities
.abilities .abilities
@ -157,7 +161,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
if energy.current() > ability.get_energy_cost() { if energy.current() > ability.get_energy_cost() {
Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)) Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))
} else { } else {
Some(Color::Rgba(0.3, 0.3, 0.3, 0.8)) Some(GREYED_OUT)
}, },
) )
}) })
@ -170,7 +174,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
hotbar hotbar
.get(*self) .get(*self)
.and_then(|content| match content { .and_then(|content| match content {
hotbar::SlotContents::Inventory(idx) => inventory.get(idx), hotbar::SlotContents::Inventory(item_hash, _) => inventory.get_by_hash(item_hash),
hotbar::SlotContents::Ability(_) => None, hotbar::SlotContents::Ability(_) => None,
}) })
.map(|item| item.amount()) .map(|item| item.amount())

View File

@ -1,5 +1,5 @@
use crate::hud; use crate::hud;
use common::{character::CharacterId, comp::slot::InvSlotId}; use common::character::CharacterId;
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -18,18 +18,7 @@ pub struct CharacterProfile {
} }
const fn default_slots() -> [Option<hud::HotbarSlotContents>; 10] { const fn default_slots() -> [Option<hud::HotbarSlotContents>; 10] {
[ [None, None, None, None, None, None, None, None, None, None]
None,
None,
None,
None,
None,
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))),
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))),
None,
None,
None,
]
} }
impl Default for CharacterProfile { impl Default for CharacterProfile {
@ -132,7 +121,7 @@ impl Profile {
self.servers self.servers
.get(server) .get(server)
.and_then(|s| s.characters.get(&character_id)) .and_then(|s| s.characters.get(&character_id))
.map(|c| c.hotbar_slots) .map(|c| c.hotbar_slots.clone())
.unwrap_or_else(default_slots) .unwrap_or_else(default_slots)
} }
@ -216,41 +205,18 @@ impl Profile {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use common::comp::inventory::slot::InvSlotId;
#[test] #[test]
fn test_get_slots_with_empty_profile() { fn test_get_slots_with_empty_profile() {
let profile = Profile::default(); let profile = Profile::default();
let slots = profile.get_hotbar_slots("TestServer", 12345); let slots = profile.get_hotbar_slots("TestServer", 12345);
assert_eq!(slots, [ assert_eq!(slots, [(); 10].map(|()| None))
None,
None,
None,
None,
None,
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))),
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))),
None,
None,
None,
])
} }
#[test] #[test]
fn test_set_slots_with_empty_profile() { fn test_set_slots_with_empty_profile() {
let mut profile = Profile::default(); let mut profile = Profile::default();
let slots = [ let slots = [(); 10].map(|()| None);
None,
None,
None,
None,
None,
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))),
Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))),
None,
None,
None,
];
profile.set_hotbar_slots("TestServer", 12345, slots); profile.set_hotbar_slots("TestServer", 12345, slots);
} }
} }

View File

@ -462,8 +462,16 @@ impl PlayState for SessionState {
// Handle window events. // Handle window events.
for event in events { for event in events {
// Pass all events to the ui first. // Pass all events to the ui first.
if self.hud.handle_event(event.clone(), global_state) { {
continue; let client = self.client.borrow();
let inventories = client.inventories();
let inventory = inventories.get(client.entity());
if self
.hud
.handle_event(event.clone(), global_state, inventory)
{
continue;
}
} }
match event { match event {