During a trade, allow requesting items from the counterparty's inventory (prequisite for NPC trading).

This commit is contained in:
Avi Weinstock 2021-02-27 21:44:57 -05:00
parent b9b36e0bf5
commit 7e458ecd40
8 changed files with 548 additions and 279 deletions

View File

@ -58,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved network efficiency by ≈ factor 10 by using tokio. - Improved network efficiency by ≈ factor 10 by using tokio.
- Added item tooltips to trade window. - Added item tooltips to trade window.
- "Quest" given to new players converted to being a short tutorial - "Quest" given to new players converted to being a short tutorial
- Items can be requested from the counterparty's inventory during trade.
### Removed ### Removed

View File

@ -4,7 +4,7 @@
( (
string_map: { string_map: {
"hud.trade.trade_window": "Trade window", "hud.trade.trade_window": "Trade window",
"hud.trade.phase1_description": "Drag the items you want to trade\n into your area.", "hud.trade.phase1_description": "Drag the items you want to trade\n into the corresponding area.",
"hud.trade.phase2_description": "The trade is now locked to give you\n time to review it.", "hud.trade.phase2_description": "The trade is now locked to give you\n time to review it.",
/// Phase3 should only be visible for a few milliseconds if everything is working properly, but is included for completeness /// Phase3 should only be visible for a few milliseconds if everything is working properly, but is included for completeness
"hud.trade.phase3_description": "Trade is being processed.", "hud.trade.phase3_description": "Trade is being processed.",

View File

@ -21,10 +21,12 @@ pub enum TradeAction {
AddItem { AddItem {
item: InvSlotId, item: InvSlotId,
quantity: u32, quantity: u32,
ours: bool,
}, },
RemoveItem { RemoveItem {
item: InvSlotId, item: InvSlotId,
quantity: u32, quantity: u32,
ours: bool,
}, },
/// Accept needs the phase indicator to avoid progressing too far in the /// Accept needs the phase indicator to avoid progressing too far in the
/// trade if there's latency and a player presses the accept button /// trade if there's latency and a player presses the accept button
@ -118,16 +120,26 @@ impl PendingTrade {
/// - Modifications can only happen in phase 1 /// - Modifications can only happen in phase 1
/// - Whenever a trade is modified, both accept flags get reset /// - Whenever a trade is modified, both accept flags get reset
/// - Accept flags only get set for the current phase /// - Accept flags only get set for the current phase
pub fn process_trade_action(&mut self, who: usize, action: TradeAction, inventory: &Inventory) { pub fn process_trade_action(
&mut self,
mut who: usize,
action: TradeAction,
inventories: &[&Inventory],
) {
use TradeAction::*; use TradeAction::*;
match action { match action {
AddItem { AddItem {
item, item,
quantity: delta, quantity: delta,
ours,
} => { } => {
if self.phase() == TradePhase::Mutate && delta > 0 { if self.phase() == TradePhase::Mutate && delta > 0 {
if !ours {
who = 1 - who;
}
let total = self.offers[who].entry(item).or_insert(0); let total = self.offers[who].entry(item).or_insert(0);
let owned_quantity = inventory.get(item).map(|i| i.amount()).unwrap_or(0); let owned_quantity =
inventories[who].get(item).map(|i| i.amount()).unwrap_or(0);
*total = total.saturating_add(delta).min(owned_quantity); *total = total.saturating_add(delta).min(owned_quantity);
self.accept_flags = [false, false]; self.accept_flags = [false, false];
} }
@ -135,8 +147,12 @@ impl PendingTrade {
RemoveItem { RemoveItem {
item, item,
quantity: delta, quantity: delta,
ours,
} => { } => {
if self.phase() == TradePhase::Mutate { if self.phase() == TradePhase::Mutate {
if !ours {
who = 1 - who;
}
self.offers[who] self.offers[who]
.entry(item) .entry(item)
.and_replace_entry_with(|_, mut total| { .and_replace_entry_with(|_, mut total| {
@ -180,17 +196,24 @@ impl Trades {
id id
} }
pub fn process_trade_action( pub fn process_trade_action<'a, F: Fn(Uid) -> Option<&'a Inventory>>(
&mut self, &mut self,
id: TradeId, id: TradeId,
who: Uid, who: Uid,
action: TradeAction, action: TradeAction,
inventory: &Inventory, get_inventory: F,
) { ) {
trace!("for trade id {:?}, message {:?}", id, action); trace!("for trade id {:?}, message {:?}", id, action);
if let Some(trade) = self.trades.get_mut(&id) { if let Some(trade) = self.trades.get_mut(&id) {
if let Some(party) = trade.which_party(who) { if let Some(party) = trade.which_party(who) {
trade.process_trade_action(party, action, inventory); let mut inventories = Vec::new();
for party in trade.parties.iter() {
match get_inventory(*party) {
Some(inventory) => inventories.push(inventory),
None => return,
}
}
trade.process_trade_action(party, action, &*inventories);
} else { } else {
warn!( warn!(
"An entity who is not a party to trade {:?} tried to modify it", "An entity who is not a party to trade {:?} tried to modify it",

View File

@ -3,7 +3,10 @@ use common::{
comp::inventory::{item::MaterialStatManifest, Inventory}, comp::inventory::{item::MaterialStatManifest, Inventory},
trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades}, trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades},
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{
msg::ServerGeneral,
sync::{Uid, WorldSyncExt},
};
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
use specs::{world::WorldExt, Entity as EcsEntity}; use specs::{world::WorldExt, Entity as EcsEntity};
use std::cmp::Ordering; use std::cmp::Ordering;
@ -26,8 +29,17 @@ pub fn handle_process_trade_action(
server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined)) server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined))
}); });
} else { } else {
if let Some(inv) = server.state.ecs().read_component::<Inventory>().get(entity) { {
trades.process_trade_action(trade_id, uid, action, inv); let ecs = server.state.ecs();
let inventories = ecs.read_component::<Inventory>();
let get_inventory = |uid: Uid| {
if let Some(entity) = ecs.entity_from_uid(uid.0) {
inventories.get(entity)
} else {
None
}
};
trades.process_trade_action(trade_id, uid, action, get_inventory);
} }
if let Entry::Occupied(entry) = trades.trades.entry(trade_id) { if let Entry::Occupied(entry) = trades.trades.entry(trade_id) {
let parties = entry.get().parties; let parties = entry.get().parties;

View File

@ -17,23 +17,25 @@ use crate::{
}; };
use client::Client; use client::Client;
use common::{ use common::{
assets::AssetExt,
combat::{combat_rating, Damage}, combat::{combat_rating, Damage},
comp::{ comp::{
item::{MaterialStatManifest, Quality}, item::{ItemDef, MaterialStatManifest, Quality},
Body, Energy, Health, Stats, Body, Energy, Health, Inventory, Stats,
}, },
}; };
use conrod_core::{ use conrod_core::{
color, color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget::{self, Button, Image, Rectangle, Scrollbar, State as ConrodState, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
}; };
use crate::hud::slots::SlotKind; use crate::hud::slots::SlotKind;
use std::sync::Arc;
use vek::Vec2; use vek::Vec2;
widget_ids! { widget_ids! {
pub struct Ids { pub struct InventoryScrollerIds {
test, test,
bag_close, bag_close,
inv_alignment, inv_alignment,
@ -53,6 +55,351 @@ widget_ids! {
inventory_title_bg, inventory_title_bg,
scrollbar_bg, scrollbar_bg,
scrollbar_slots, scrollbar_slots,
}
}
pub struct InventoryScrollerState {
ids: InventoryScrollerIds,
}
#[derive(WidgetCommon)]
pub struct InventoryScroller<'a> {
imgs: &'a Imgs,
item_imgs: &'a ItemImgs,
fonts: &'a Fonts,
#[conrod(common_builder)]
common: widget::CommonBuilder,
tooltip_manager: &'a mut TooltipManager,
slot_manager: &'a mut SlotManager,
pulse: f32,
localized_strings: &'a Localization,
show_stats: bool,
show_bag_inv: bool,
msm: &'a MaterialStatManifest,
on_right: bool,
item_tooltip: &'a Tooltip<'a>,
playername: String,
is_us: bool,
inventory: &'a Inventory,
bg_ids: &'a BackgroundIds,
}
impl<'a> InventoryScroller<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
imgs: &'a Imgs,
item_imgs: &'a ItemImgs,
fonts: &'a Fonts,
tooltip_manager: &'a mut TooltipManager,
slot_manager: &'a mut SlotManager,
pulse: f32,
localized_strings: &'a Localization,
show_stats: bool,
show_bag_inv: bool,
msm: &'a MaterialStatManifest,
on_right: bool,
item_tooltip: &'a Tooltip<'a>,
playername: String,
is_us: bool,
inventory: &'a Inventory,
bg_ids: &'a BackgroundIds,
) -> Self {
InventoryScroller {
imgs,
item_imgs,
fonts,
common: widget::CommonBuilder::default(),
tooltip_manager,
slot_manager,
pulse,
localized_strings,
show_stats,
show_bag_inv,
msm,
on_right,
item_tooltip,
playername,
is_us,
inventory,
bg_ids,
}
}
fn background(&mut self, ui: &mut UiCell<'_>) {
let mut bg = Image::new(if self.show_stats {
self.imgs.inv_bg_stats
} else if self.show_bag_inv {
self.imgs.inv_bg_bag
} else {
self.imgs.inv_bg_armor
})
.w_h(424.0, 708.0);
if self.on_right {
bg = bg.bottom_right_with_margins_on(ui.window, 60.0, 5.0);
} else {
bg = bg.bottom_left_with_margins_on(ui.window, 60.0, 5.0);
}
bg.color(Some(UI_MAIN)).set(self.bg_ids.bg, ui);
Image::new(if self.show_bag_inv {
self.imgs.inv_frame_bag
} else {
self.imgs.inv_frame
})
.w_h(424.0, 708.0)
.middle_of(self.bg_ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.set(self.bg_ids.bg_frame, ui);
}
fn title(&mut self, state: &mut ConrodState<'_, InventoryScrollerState>, ui: &mut UiCell<'_>) {
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &*self.playername),
)
.mid_top_with_margin_on(self.bg_ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.inventory_title_bg, ui);
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &*self.playername),
)
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.color(TEXT_COLOR)
.set(state.ids.inventory_title, ui);
}
fn scrollbar_and_slots(
&mut self,
state: &mut ConrodState<'_, InventoryScrollerState>,
ui: &mut UiCell<'_>,
) {
let space_max = self.inventory.slots().count();
// Slots Scrollbar
if space_max > 45 && !self.show_bag_inv {
// Scrollbar-BG
Image::new(self.imgs.scrollbar_bg)
.w_h(9.0, 173.0)
.bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.scrollbar_bg, ui);
// Scrollbar
Scrollbar::y_axis(state.ids.inv_alignment)
.thickness(5.0)
.h(123.0)
.color(UI_MAIN)
.middle_of(state.ids.scrollbar_bg)
.set(state.ids.scrollbar_slots, ui);
} else if space_max > 135 {
// Scrollbar-BG
Image::new(self.imgs.scrollbar_bg_big)
.w_h(9.0, 592.0)
.bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.scrollbar_bg, ui);
// Scrollbar
Scrollbar::y_axis(state.ids.inv_alignment)
.thickness(5.0)
.h(542.0)
.color(UI_MAIN)
.middle_of(state.ids.scrollbar_bg)
.set(state.ids.scrollbar_slots, ui);
};
// Alignment for Grid
Rectangle::fill_with(
[362.0, if self.show_bag_inv { 600.0 } else { 200.0 }],
color::TRANSPARENT,
)
.bottom_left_with_margins_on(self.bg_ids.bg_frame, 29.0, 46.5)
.scroll_kids_vertically()
.set(state.ids.inv_alignment, ui);
// Bag Slots
// Create available inventory slot widgets
if state.ids.inv_slots.len() < self.inventory.capacity() {
state.update(|s| {
s.ids
.inv_slots
.resize(self.inventory.capacity(), &mut ui.widget_id_generator());
});
}
// Determine the range of inventory slots that are provided by the loadout item
// that the mouse is over
let mouseover_loadout_slots = self
.slot_manager
.mouse_over_slot
.and_then(|x| {
if let SlotKind::Equip(e) = x {
self.inventory.get_slot_range_for_equip_slot(e)
} else {
None
}
})
.unwrap_or(0usize..0usize);
// Display inventory contents
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
selected_slot: self.imgs.inv_slot_sel,
background_color: Some(UI_MAIN),
content_size: ContentSize {
width_height_ratio: 1.0,
max_fraction: 0.75,
},
selected_content_scale: 1.067,
amount_font: self.fonts.cyri.conrod_id,
amount_margins: Vec2::new(-4.0, 0.0),
amount_font_size: self.fonts.cyri.scale(12),
amount_text_color: TEXT_COLOR,
content_source: self.inventory,
image_source: self.item_imgs,
slot_manager: Some(self.slot_manager),
pulse: self.pulse,
};
for (i, (pos, item)) in self.inventory.slots_with_id().enumerate() {
let x = i % 9;
let y = i / 9;
// Slot
let mut slot_widget = slot_maker
.fabricate(
InventorySlot {
slot: pos,
ours: self.is_us,
},
[40.0; 2],
)
.top_left_with_margins_on(
state.ids.inv_alignment,
0.0 + y as f64 * (40.0),
0.0 + x as f64 * (40.0),
);
// Highlight slots are provided by the loadout item that the mouse is over
if mouseover_loadout_slots.contains(&i) {
slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
}
if let Some(item) = item {
let (title, desc) = super::util::item_text(item, &self.msm);
let quality_col = get_quality_col(item);
let quality_col_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey,
Quality::Common => self.imgs.inv_slot,
Quality::Moderate => self.imgs.inv_slot_green,
Quality::High => self.imgs.inv_slot_blue,
Quality::Epic => self.imgs.inv_slot_purple,
Quality::Legendary => self.imgs.inv_slot_gold,
Quality::Artifact => self.imgs.inv_slot_orange,
_ => self.imgs.inv_slot_red,
};
slot_widget
.filled_slot(quality_col_img)
.with_tooltip(
self.tooltip_manager,
title,
&*desc,
self.item_tooltip,
quality_col,
)
.set(state.ids.inv_slots[i], ui);
} else {
slot_widget.set(state.ids.inv_slots[i], ui);
}
}
}
fn footer_metrics(
&mut self,
state: &mut ConrodState<'_, InventoryScrollerState>,
ui: &mut UiCell<'_>,
) {
let space_used = self.inventory.populated_slots();
let space_max = self.inventory.slots().count();
let bag_space = format!("{}/{}", space_used, space_max);
let bag_space_percentage = space_used as f32 / space_max as f32;
let coin_itemdef = Arc::<ItemDef>::load_expect_cloned("common.items.utility.coins");
let currency = self.inventory.item_count(&coin_itemdef);
// Coin Icon and Currency Text
Image::new(self.imgs.coin_ico)
.w_h(16.0, 17.0)
.bottom_left_with_margins_on(self.bg_ids.bg_frame, 2.0, 43.0)
.set(state.ids.coin_ico, ui);
Text::new(&format!("{}", currency))
.bottom_left_with_margins_on(self.bg_ids.bg_frame, 6.0, 64.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
.set(state.ids.currency_txt, ui);
//Free Bag-Space
Text::new(&bag_space)
.bottom_right_with_margins_on(self.bg_ids.bg_frame, 6.0, 43.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.color(if bag_space_percentage < 0.8 {
TEXT_COLOR
} else if bag_space_percentage < 1.0 {
LOW_HP_COLOR
} else {
CRITICAL_HP_COLOR
})
.set(state.ids.space_txt, ui);
}
}
impl<'a> Widget for InventoryScroller<'a> {
type Event = ();
type State = InventoryScrollerState;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
InventoryScrollerState {
ids: InventoryScrollerIds::new(id_gen),
}
}
fn style(&self) -> Self::Style {}
fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
self.background(ui);
self.title(state, ui);
self.scrollbar_and_slots(state, ui);
self.footer_metrics(state, ui);
}
}
widget_ids! {
pub struct BackgroundIds {
bg,
bg_frame,
}
}
widget_ids! {
pub struct BagIds {
test,
inventory_scroller,
bag_close,
//tooltip[],
char_ico,
coin_ico,
space_txt,
currency_txt,
inventory_title,
inventory_title_bg,
scrollbar_bg,
scrollbar_slots,
tab_1, tab_1,
tab_2, tab_2,
tab_3, tab_3,
@ -148,8 +495,9 @@ impl<'a> Bag<'a> {
} }
const STATS: [&str; 4] = ["Health", "Stamina", "Protection", "Combat Rating"]; const STATS: [&str; 4] = ["Health", "Stamina", "Protection", "Combat Rating"];
pub struct State { pub struct BagState {
ids: Ids, ids: BagIds,
bg_ids: BackgroundIds,
} }
pub enum Event { pub enum Event {
@ -159,12 +507,16 @@ pub enum Event {
impl<'a> Widget for Bag<'a> { impl<'a> Widget for Bag<'a> {
type Event = Option<Event>; type Event = Option<Event>;
type State = State; type State = BagState;
type Style = (); type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
State { BagState {
ids: Ids::new(id_gen), bg_ids: BackgroundIds {
bg: id_gen.next(),
bg_frame: id_gen.next(),
},
ids: BagIds::new(id_gen),
} }
} }
@ -199,12 +551,6 @@ impl<'a> Widget for Bag<'a> {
None => return None, None => return None,
}; };
let space_used = inventory.populated_slots();
let space_max = inventory.slots().count();
let bag_space = format!("{}/{}", space_used, space_max);
let bag_space_percentage = space_used as f32 / space_max as f32;
let currency = 0; // TODO: Add as a Stat
// Tooltips // Tooltips
let item_tooltip = Tooltip::new({ let item_tooltip = Tooltip::new({
// Edge images [t, b, r, l] // Edge images [t, b, r, l]
@ -223,117 +569,32 @@ impl<'a> Widget for Bag<'a> {
.desc_font_size(self.fonts.cyri.scale(12)) .desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR); .desc_text_color(TEXT_COLOR);
// BG
Image::new(if self.show.stats { InventoryScroller::new(
self.imgs.inv_bg_stats self.imgs,
} else if self.show.bag_inv { self.item_imgs,
self.imgs.inv_bg_bag self.fonts,
} else { self.tooltip_manager,
self.imgs.inv_bg_armor self.slot_manager,
}) self.pulse,
.w_h(424.0, 708.0) self.localized_strings,
.bottom_right_with_margins_on(ui.window, 60.0, 5.0) self.show.stats,
.color(Some(UI_MAIN)) self.show.bag_inv,
.set(state.ids.bg, ui); self.msm,
Image::new(if self.show.bag_inv { true,
self.imgs.inv_frame_bag &item_tooltip,
} else { self.stats.name.to_string(),
self.imgs.inv_frame true,
}) &inventory,
.w_h(424.0, 708.0) &state.bg_ids,
.middle_of(state.ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.bg_frame, ui);
// Title
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &self.stats.name.to_string().as_str()),
) )
.mid_top_with_margin_on(state.ids.bg_frame, 9.0) .set(state.ids.inventory_scroller, ui);
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.inventory_title_bg, ui);
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &self.stats.name.to_string().as_str()),
)
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(state.ids.inventory_title, ui);
// Slots Scrollbar
if space_max > 45 && !self.show.bag_inv {
// Scrollbar-BG
Image::new(self.imgs.scrollbar_bg)
.w_h(9.0, 173.0)
.bottom_right_with_margins_on(state.ids.bg_frame, 42.0, 3.0)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.scrollbar_bg, ui);
// Scrollbar
Scrollbar::y_axis(state.ids.inv_alignment)
.thickness(5.0)
.h(123.0)
.color(UI_MAIN)
.middle_of(state.ids.scrollbar_bg)
.set(state.ids.scrollbar_slots, ui);
} else if space_max > 135 {
// Scrollbar-BG
Image::new(self.imgs.scrollbar_bg_big)
.w_h(9.0, 592.0)
.bottom_right_with_margins_on(state.ids.bg_frame, 42.0, 3.0)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.scrollbar_bg, ui);
// Scrollbar
Scrollbar::y_axis(state.ids.inv_alignment)
.thickness(5.0)
.h(542.0)
.color(UI_MAIN)
.middle_of(state.ids.scrollbar_bg)
.set(state.ids.scrollbar_slots, ui);
};
// Char Pixel-Art // Char Pixel-Art
Image::new(self.imgs.char_art) Image::new(self.imgs.char_art)
.w_h(40.0, 37.0) .w_h(40.0, 37.0)
.top_left_with_margins_on(state.ids.bg, 4.0, 2.0) .top_left_with_margins_on(state.bg_ids.bg, 4.0, 2.0)
.set(state.ids.char_ico, ui); .set(state.ids.char_ico, ui);
// Coin Icon and Currency Text
Image::new(self.imgs.coin_ico)
.w_h(16.0, 17.0)
.bottom_left_with_margins_on(state.ids.bg_frame, 2.0, 43.0)
.set(state.ids.coin_ico, ui);
Text::new(&format!("{}", currency))
.bottom_left_with_margins_on(state.ids.bg_frame, 6.0, 64.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
.set(state.ids.currency_txt, ui);
//Free Bag-Space
Text::new(&bag_space)
.bottom_right_with_margins_on(state.ids.bg_frame, 6.0, 43.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.color(if bag_space_percentage < 0.8 {
TEXT_COLOR
} else if bag_space_percentage < 1.0 {
LOW_HP_COLOR
} else {
CRITICAL_HP_COLOR
})
.set(state.ids.space_txt, ui);
// Alignment for Grid
Rectangle::fill_with(
[362.0, if self.show.bag_inv { 600.0 } else { 200.0 }],
color::TRANSPARENT,
)
.bottom_left_with_margins_on(state.ids.bg_frame, 29.0, 46.5)
.scroll_kids_vertically()
.set(state.ids.inv_alignment, ui);
// Button to expand bag // Button to expand bag
let txt = if self.show.bag_inv { let txt = if self.show.bag_inv {
"Show Loadout" "Show Loadout"
@ -357,9 +618,10 @@ impl<'a> Widget for Bag<'a> {
self.imgs.expand_btn_press self.imgs.expand_btn_press
}); });
// Only show expand button when it's needed... // Only show expand button when it's needed...
let space_max = inventory.slots().count();
if space_max > 45 && !self.show.bag_inv { if space_max > 45 && !self.show.bag_inv {
if expand_btn if expand_btn
.top_left_with_margins_on(state.ids.bg_frame, 460.0, 211.5) .top_left_with_margins_on(state.bg_ids.bg_frame, 460.0, 211.5)
.with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR)
.set(state.ids.bag_expand_btn, ui) .set(state.ids.bag_expand_btn, ui)
.was_clicked() .was_clicked()
@ -369,7 +631,7 @@ impl<'a> Widget for Bag<'a> {
} else if self.show.bag_inv { } else if self.show.bag_inv {
//... but always show it when the bag is expanded //... but always show it when the bag is expanded
if expand_btn if expand_btn
.top_left_with_margins_on(state.ids.bg_frame, 53.0, 211.5) .top_left_with_margins_on(state.bg_ids.bg_frame, 53.0, 211.5)
.with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR)
.set(state.ids.bag_expand_btn, ui) .set(state.ids.bag_expand_btn, ui)
.was_clicked() .was_clicked()
@ -378,29 +640,6 @@ impl<'a> Widget for Bag<'a> {
} }
} }
// Title
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &self.stats.name.to_string().as_str()),
)
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.inventory_title_bg, ui);
Text::new(
&self
.localized_strings
.get("hud.bag.inventory")
.replace("{playername}", &self.stats.name.to_string().as_str()),
)
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.color(TEXT_COLOR)
.set(state.ids.inventory_title, ui);
// Armor Slots // Armor Slots
let mut slot_maker = SlotMaker { let mut slot_maker = SlotMaker {
empty_slot: self.imgs.armor_slot_empty, empty_slot: self.imgs.armor_slot_empty,
@ -464,7 +703,7 @@ impl<'a> Widget for Bag<'a> {
let combat_rating_txt = format!("{}", (combat_rating * 10.0) as usize); let combat_rating_txt = format!("{}", (combat_rating * 10.0) as usize);
let btn = if i.0 == 0 { let btn = if i.0 == 0 {
btn.top_left_with_margins_on(state.ids.bg_frame, 55.0, 10.0) btn.top_left_with_margins_on(state.bg_ids.bg_frame, 55.0, 10.0)
} else { } else {
btn.down_from(state.ids.stat_icons[i.0 - 1], 7.0) btn.down_from(state.ids.stat_icons[i.0 - 1], 7.0)
}; };
@ -517,7 +756,7 @@ impl<'a> Widget for Bag<'a> {
.unwrap_or(QUALITY_COMMON); .unwrap_or(QUALITY_COMMON);
slot_maker slot_maker
.fabricate(EquipSlot::Armor(ArmorSlot::Head), [45.0; 2]) .fabricate(EquipSlot::Armor(ArmorSlot::Head), [45.0; 2])
.mid_top_with_margin_on(state.ids.bg_frame, 60.0) .mid_top_with_margin_on(state.bg_ids.bg_frame, 60.0)
.with_icon(self.imgs.head_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN)) .with_icon(self.imgs.head_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN))
.filled_slot(filled_slot) .filled_slot(filled_slot)
.with_tooltip( .with_tooltip(
@ -771,7 +1010,7 @@ impl<'a> Widget for Bag<'a> {
.unwrap_or(QUALITY_COMMON); .unwrap_or(QUALITY_COMMON);
slot_maker slot_maker
.fabricate(EquipSlot::Lantern, [45.0; 2]) .fabricate(EquipSlot::Lantern, [45.0; 2])
.top_right_with_margins_on(state.ids.bg_frame, 60.0, 5.0) .top_right_with_margins_on(state.bg_ids.bg_frame, 60.0, 5.0)
.with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN)) .with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN))
.filled_slot(filled_slot) .filled_slot(filled_slot)
.with_tooltip( .with_tooltip(
@ -888,7 +1127,7 @@ impl<'a> Widget for Bag<'a> {
slot_maker slot_maker
.fabricate(EquipSlot::Armor(ArmorSlot::Bag1), [35.0; 2]) .fabricate(EquipSlot::Armor(ArmorSlot::Bag1), [35.0; 2])
.bottom_left_with_margins_on( .bottom_left_with_margins_on(
state.ids.bg_frame, state.bg_ids.bg_frame,
if self.show.bag_inv { 600.0 } else { 167.0 }, if self.show.bag_inv { 600.0 } else { 167.0 },
3.0, 3.0,
) )
@ -971,102 +1210,13 @@ impl<'a> Widget for Bag<'a> {
bag4_q_col, bag4_q_col,
) )
.set(state.ids.bag4_slot, ui); .set(state.ids.bag4_slot, ui);
// Bag Slots
// Create available inventory slot widgets
if state.ids.inv_slots.len() < inventory.capacity() {
state.update(|s| {
s.ids
.inv_slots
.resize(inventory.capacity(), &mut ui.widget_id_generator());
});
}
// Determine the range of inventory slots that are provided by the loadout item
// that the mouse is over
let mouseover_loadout_slots = self
.slot_manager
.mouse_over_slot
.and_then(|x| {
if let SlotKind::Equip(e) = x {
inventory.get_slot_range_for_equip_slot(e)
} else {
None
}
})
.unwrap_or(0usize..0usize);
// Display inventory contents
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
selected_slot: self.imgs.inv_slot_sel,
background_color: Some(UI_MAIN),
content_size: ContentSize {
width_height_ratio: 1.0,
max_fraction: 0.75,
},
selected_content_scale: 1.067,
amount_font: self.fonts.cyri.conrod_id,
amount_margins: Vec2::new(-4.0, 0.0),
amount_font_size: self.fonts.cyri.scale(12),
amount_text_color: TEXT_COLOR,
content_source: inventory,
image_source: self.item_imgs,
slot_manager: Some(self.slot_manager),
pulse: self.pulse,
};
for (i, (pos, item)) in inventory.slots_with_id().enumerate() {
let x = i % 9;
let y = i / 9;
// Slot
let mut slot_widget = slot_maker
.fabricate(InventorySlot(pos), [40.0; 2])
.top_left_with_margins_on(
state.ids.inv_alignment,
0.0 + y as f64 * (40.0),
0.0 + x as f64 * (40.0),
);
// Highlight slots are provided by the loadout item that the mouse is over
if mouseover_loadout_slots.contains(&i) {
slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
}
if let Some(item) = item {
let (title, desc) = super::util::item_text(item, &self.msm);
let quality_col = get_quality_col(item);
let quality_col_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey,
Quality::Common => self.imgs.inv_slot,
Quality::Moderate => self.imgs.inv_slot_green,
Quality::High => self.imgs.inv_slot_blue,
Quality::Epic => self.imgs.inv_slot_purple,
Quality::Legendary => self.imgs.inv_slot_gold,
Quality::Artifact => self.imgs.inv_slot_orange,
_ => self.imgs.inv_slot_red,
};
slot_widget
.filled_slot(quality_col_img)
.with_tooltip(
self.tooltip_manager,
title,
&*desc,
&item_tooltip,
quality_col,
)
.set(state.ids.inv_slots[i], ui);
} else {
slot_widget.set(state.ids.inv_slots[i], ui);
}
}
// Close button // Close button
if Button::image(self.imgs.close_btn) if Button::image(self.imgs.close_btn)
.w_h(24.0, 25.0) .w_h(24.0, 25.0)
.hover_image(self.imgs.close_btn_hover) .hover_image(self.imgs.close_btn_hover)
.press_image(self.imgs.close_btn_press) .press_image(self.imgs.close_btn_press)
.top_right_with_margins_on(state.ids.bg, 0.0, 0.0) .top_right_with_margins_on(state.bg_ids.bg, 0.0, 0.0)
.set(state.ids.bag_close, ui) .set(state.ids.bag_close, ui)
.was_clicked() .was_clicked()
{ {

View File

@ -2716,9 +2716,10 @@ impl Hud {
// Maintain slot manager // Maintain slot manager
for event in self.slot_manager.maintain(ui_widgets) { for event in self.slot_manager.maintain(ui_widgets) {
use comp::slot::Slot; use comp::slot::Slot;
use slots::SlotKind::*; use slots::{InventorySlot, SlotKind::*};
let to_slot = |slot_kind| match slot_kind { let to_slot = |slot_kind| match slot_kind {
Inventory(i) => Some(Slot::Inventory(i.0)), Inventory(InventorySlot { slot, ours: true }) => Some(Slot::Inventory(slot)),
Inventory(InventorySlot { ours: false, .. }) => None,
Equip(e) => Some(Slot::Equip(e)), Equip(e) => Some(Slot::Equip(e)),
Hotbar(_) => None, Hotbar(_) => None,
Trade(_) => None, Trade(_) => None,
@ -2732,29 +2733,37 @@ impl Hud {
slot_b: b, slot_b: b,
bypass_dialog: false, bypass_dialog: false,
}); });
} else if let (Inventory(i), Hotbar(h)) = (a, b) { } else if let (Inventory(InventorySlot { slot, ours: true }), Hotbar(h)) =
self.hotbar.add_inventory_link(h, i.0); (a, b)
{
self.hotbar.add_inventory_link(h, slot);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); 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())));
} else if let (Inventory(i), Trade(_)) = (a, b) { } else if let (Inventory(i), Trade(t)) = (a, b) {
if let Some(inventory) = inventories.get(entity) { if i.ours == t.ours {
if let Some(inventory) = inventories.get(t.entity) {
events.push(Event::TradeAction(TradeAction::AddItem { events.push(Event::TradeAction(TradeAction::AddItem {
item: i.0, item: i.slot,
quantity: i.amount(inventory).unwrap_or(1), quantity: i.amount(inventory).unwrap_or(1),
ours: i.ours,
})); }));
} }
} else if let (Trade(t), Inventory(_)) = (a, b) { }
if let Some(inventory) = inventories.get(entity) { } else if let (Trade(t), Inventory(i)) = (a, b) {
if i.ours == t.ours {
if let Some(inventory) = inventories.get(t.entity) {
if let Some(invslot) = t.invslot { if let Some(invslot) = t.invslot {
events.push(Event::TradeAction(TradeAction::RemoveItem { events.push(Event::TradeAction(TradeAction::RemoveItem {
item: invslot, item: invslot,
quantity: t.amount(inventory).unwrap_or(1), quantity: t.amount(inventory).unwrap_or(1),
ours: t.ours,
})); }));
} }
} }
} }
}
}, },
slot::Event::Dropped(from) => { slot::Event::Dropped(from) => {
// Drop item // Drop item
@ -2763,6 +2772,16 @@ impl Hud {
} else if let Hotbar(h) = from { } else if let Hotbar(h) = from {
self.hotbar.clear_slot(h); self.hotbar.clear_slot(h);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
} else if let Trade(t) = from {
if let Some(inventory) = inventories.get(t.entity) {
if let Some(invslot) = t.invslot {
events.push(Event::TradeAction(TradeAction::RemoveItem {
item: invslot,
quantity: t.amount(inventory).unwrap_or(1),
ours: t.ours,
}));
}
}
} }
}, },
slot::Event::Used(from) => { slot::Event::Used(from) => {
@ -2835,8 +2854,13 @@ impl Hud {
slot_manager: &mut slots::SlotManager, slot_manager: &mut slots::SlotManager,
hotbar: &mut hotbar::State, hotbar: &mut hotbar::State,
) { ) {
if let Some(slots::SlotKind::Inventory(i)) = slot_manager.selected() { use slots::InventorySlot;
hotbar.add_inventory_link(slot, i.0); if let Some(slots::SlotKind::Inventory(InventorySlot {
slot: i,
ours: true,
})) = slot_manager.selected()
{
hotbar.add_inventory_link(slot, i);
events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned()))); events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned())));
slot_manager.idle(); slot_manager.idle();
} else { } else {

View File

@ -13,6 +13,7 @@ use common::comp::{
Energy, Inventory, Energy, Inventory,
}; };
use conrod_core::{image, Color}; use conrod_core::{image, Color};
use specs::Entity as EcsEntity;
pub use common::comp::slot::{ArmorSlot, EquipSlot}; pub use common::comp::slot::{ArmorSlot, EquipSlot};
@ -28,18 +29,21 @@ pub enum SlotKind {
pub type SlotManager = slot::SlotManager<SlotKind>; pub type SlotManager = slot::SlotManager<SlotKind>;
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct InventorySlot(pub InvSlotId); pub struct InventorySlot {
pub slot: InvSlotId,
pub ours: bool,
}
impl SlotKey<Inventory, ItemImgs> for InventorySlot { impl SlotKey<Inventory, ItemImgs> for InventorySlot {
type ImageKey = ItemKey; type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> { fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
source.get(self.0).map(|i| (i.into(), None)) source.get(self.slot).map(|i| (i.into(), None))
} }
fn amount(&self, source: &Inventory) -> Option<u32> { fn amount(&self, source: &Inventory) -> Option<u32> {
source source
.get(self.0) .get(self.slot)
.map(|item| item.amount()) .map(|item| item.amount())
.filter(|amount| *amount > 1) .filter(|amount| *amount > 1)
} }
@ -69,19 +73,32 @@ pub struct TradeSlot {
pub index: usize, pub index: usize,
pub quantity: u32, pub quantity: u32,
pub invslot: Option<InvSlotId>, pub invslot: Option<InvSlotId>,
pub entity: EcsEntity,
pub ours: bool,
} }
impl SlotKey<Inventory, ItemImgs> for TradeSlot { impl SlotKey<Inventory, ItemImgs> for TradeSlot {
type ImageKey = ItemKey; type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> { fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
self.invslot self.invslot.and_then(|inv_id| {
.and_then(|inv_id| InventorySlot(inv_id).image_key(source)) InventorySlot {
slot: inv_id,
ours: self.ours,
}
.image_key(source)
})
} }
fn amount(&self, source: &Inventory) -> Option<u32> { fn amount(&self, source: &Inventory) -> Option<u32> {
self.invslot self.invslot
.and_then(|inv_id| InventorySlot(inv_id).amount(source)) .and_then(|inv_id| {
InventorySlot {
slot: inv_id,
ours: self.ours,
}
.amount(source)
})
.map(|x| x.min(self.quantity)) .map(|x| x.min(self.quantity))
} }

View File

@ -6,6 +6,7 @@ use super::{
TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
}; };
use crate::{ use crate::{
hud::bag::{BackgroundIds, InventoryScroller},
i18n::Localization, i18n::Localization,
ui::{ ui::{
fonts::Fonts, fonts::Fonts,
@ -28,10 +29,12 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, State as ConrodState, Text}, widget::{self, Button, Image, Rectangle, State as ConrodState, Text},
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
}; };
use specs::Entity as EcsEntity;
use vek::*; use vek::*;
pub struct State { pub struct State {
ids: Ids, ids: Ids,
bg_ids: BackgroundIds,
} }
widget_ids! { widget_ids! {
@ -49,6 +52,7 @@ widget_ids! {
phase_indicator, phase_indicator,
accept_button, accept_button,
decline_button, decline_button,
inventory_scroller,
} }
} }
@ -157,6 +161,7 @@ impl<'a> Trade<'a> {
let inventories = self.client.inventories(); let inventories = self.client.inventories();
let uid = trade.parties[who]; let uid = trade.parties[who];
let entity = self.client.state().ecs().entity_from_uid(uid.0)?; let entity = self.client.state().ecs().entity_from_uid(uid.0)?;
let ours = entity == self.client.entity();
// TODO: update in accordence with https://gitlab.com/veloren/veloren/-/issues/960 // TODO: update in accordence with https://gitlab.com/veloren/veloren/-/issues/960
let inventory = inventories.get(entity)?; let inventory = inventories.get(entity)?;
@ -215,13 +220,15 @@ impl<'a> Trade<'a> {
index, index,
quantity, quantity,
invslot: Some(k), invslot: Some(k),
ours,
entity,
}) })
.collect(); .collect();
if matches!(trade.phase(), TradePhase::Mutate) { if matches!(trade.phase(), TradePhase::Mutate) {
self.phase1_itemwidget(state, ui, inventory, who, &tradeslots); self.phase1_itemwidget(state, ui, inventory, who, ours, entity, name, &tradeslots);
} else { } else {
self.phase2_itemwidget(state, ui, inventory, who, &tradeslots); self.phase2_itemwidget(state, ui, inventory, who, ours, entity, &tradeslots);
} }
None None
@ -233,6 +240,9 @@ impl<'a> Trade<'a> {
ui: &mut UiCell<'_>, ui: &mut UiCell<'_>,
inventory: &Inventory, inventory: &Inventory,
who: usize, who: usize,
ours: bool,
entity: EcsEntity,
name: String,
tradeslots: &[TradeSlot], tradeslots: &[TradeSlot],
) { ) {
let item_tooltip = Tooltip::new({ let item_tooltip = Tooltip::new({
@ -253,6 +263,28 @@ impl<'a> Trade<'a> {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR); .desc_text_color(TEXT_COLOR);
if !ours {
InventoryScroller::new(
self.imgs,
self.item_imgs,
self.fonts,
self.tooltip_manager,
self.slot_manager,
self.pulse,
self.localized_strings,
false,
true,
self.msm,
false,
&item_tooltip,
name,
false,
&inventory,
&state.bg_ids,
)
.set(state.ids.inventory_scroller, ui);
}
let mut slot_maker = SlotMaker { let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot, empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot, filled_slot: self.imgs.inv_slot,
@ -289,6 +321,8 @@ impl<'a> Trade<'a> {
index: i, index: i,
quantity: 0, quantity: 0,
invslot: None, invslot: None,
ours,
entity,
}); });
// Slot // Slot
let slot_widget = slot_maker let slot_widget = slot_maker
@ -334,6 +368,8 @@ impl<'a> Trade<'a> {
ui: &mut UiCell<'_>, ui: &mut UiCell<'_>,
inventory: &Inventory, inventory: &Inventory,
who: usize, who: usize,
ours: bool,
entity: EcsEntity,
tradeslots: &[TradeSlot], tradeslots: &[TradeSlot],
) { ) {
if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS { if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS {
@ -348,6 +384,8 @@ impl<'a> Trade<'a> {
index: i, index: i,
quantity: 0, quantity: 0,
invslot: None, invslot: None,
ours,
entity,
}); });
let itemname = slot let itemname = slot
.invslot .invslot
@ -435,8 +473,12 @@ impl<'a> Widget for Trade<'a> {
type State = State; type State = State;
type Style = (); type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
State { State {
bg_ids: BackgroundIds {
bg: id_gen.next(),
bg_frame: id_gen.next(),
},
ids: Ids::new(id_gen), ids: Ids::new(id_gen),
} }
} }