From c0573cca440bcc0dfd71e7ef85ef4ad290c71119 Mon Sep 17 00:00:00 2001 From: Jesus Bracho Date: Tue, 2 Mar 2021 00:08:46 +0000 Subject: [PATCH] Implement stacking and splitting --- CHANGELOG.md | 1 + client/src/lib.rs | 24 +++++ common/src/comp/controller.rs | 8 ++ common/src/comp/inventory/mod.rs | 54 +++++++++++ server/src/events/inventory_manip.rs | 112 +++++++++++++++++++++-- voxygen/src/hud/mod.rs | 41 ++++++++- voxygen/src/session.rs | 58 ++++++++++++ voxygen/src/ui/widgets/slot.rs | 131 ++++++++++++++++++++++++--- 8 files changed, 406 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e929be2ddb..ba39d8af27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Talk animation - New bosses in 5 lower dungeons = New enemies in 5 lower dungeons +- Item stacking and splitting ### Changed diff --git a/client/src/lib.rs b/client/src/lib.rs index c30c5db091..d181381543 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -668,6 +668,30 @@ impl Client { pub fn is_dead(&self) -> bool { self.current::().map_or(false, |h| h.is_dead) } + pub fn split_swap_slots(&mut self, a: comp::slot::Slot, b: comp::slot::Slot) { + match (a, b) { + (Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => { + self.control_action(ControlAction::LoadoutManip(LoadoutManip::Swap(equip, slot))) + }, + (Slot::Inventory(inv1), Slot::Inventory(inv2)) => { + self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::SplitSwap(inv1, inv2), + ))) + }, + } + } + + pub fn split_drop_slot(&mut self, slot: comp::slot::Slot) { + match slot { + Slot::Equip(equip) => { + self.control_action(ControlAction::LoadoutManip(LoadoutManip::Drop(equip))) + }, + Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent( + ControlEvent::InventoryManip(InventoryManip::SplitDrop(inv)), + )), + } + } + pub fn pick_up(&mut self, entity: EcsEntity) { // Get the health component from the entity diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 733d47ef2e..20891432fe 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -23,7 +23,9 @@ pub enum InventoryManip { Collect(Vec3), Use(InvSlotId), Swap(InvSlotId, InvSlotId), + SplitSwap(InvSlotId, InvSlotId), Drop(InvSlotId), + SplitDrop(InvSlotId), CraftRecipe(String), } @@ -40,7 +42,9 @@ pub enum SlotManip { Collect(Vec3), Use(Slot), Swap(Slot, Slot), + SplitSwap(Slot, Slot), Drop(Slot), + SplitDrop(Slot), CraftRecipe(String), } @@ -63,7 +67,11 @@ impl From for SlotManip { InventoryManip::Swap(inv1, inv2) => { Self::Swap(Slot::Inventory(inv1), Slot::Inventory(inv2)) }, + InventoryManip::SplitSwap(inv1, inv2) => { + Self::SplitSwap(Slot::Inventory(inv1), Slot::Inventory(inv2)) + }, InventoryManip::Drop(inv) => Self::Drop(Slot::Inventory(inv)), + InventoryManip::SplitDrop(inv) => Self::SplitDrop(Slot::Inventory(inv)), InventoryManip::CraftRecipe(recipe) => Self::CraftRecipe(recipe), } } diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index df78f961a5..666de975d7 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -167,6 +167,32 @@ impl Inventory { } } + /// Merge the stack of items at src into the stack at dst if the items are + /// compatible and stackable, and return whether anything was changed + pub fn merge_stack_into(&mut self, src: InvSlotId, dst: InvSlotId) -> bool { + let mut amount = None; + if let (Some(srcitem), Some(dstitem)) = (self.get(src), self.get(dst)) { + // The equality check ensures the items have the same definition, to avoid e.g. + // transmuting coins to diamonds, and the stackable check avoids creating a + // stack of swords + if srcitem == dstitem && srcitem.is_stackable() { + amount = Some(srcitem.amount()); + } + } + if let Some(amount) = amount { + self.remove(src); + let dstitem = self + .get_mut(dst) + .expect("self.get(dst) was Some right above this"); + dstitem + .increase_amount(amount) + .expect("already checked is_stackable"); + true + } else { + false + } + } + /// Checks if inserting item exists in given cell. Inserts an item if it /// exists. pub fn insert_or_stack_at( @@ -215,6 +241,11 @@ impl Inventory { self.slot(inv_slot_id).and_then(Option::as_ref) } + /// Mutably get content of a slot + fn get_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut Item> { + self.slot_mut(inv_slot_id).and_then(Option::as_mut) + } + /// Returns a reference to the item (if any) equipped in the given EquipSlot pub fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> { self.loadout.equipped(equip_slot) @@ -275,6 +306,29 @@ impl Inventory { } } + /// Takes half of the items from a slot in the inventory + pub fn take_half( + &mut self, + inv_slot_id: InvSlotId, + msm: &MaterialStatManifest, + ) -> Option { + if let Some(Some(item)) = self.slot_mut(inv_slot_id) { + if item.is_stackable() && item.amount() > 1 { + let mut return_item = item.duplicate(msm); + let returning_amount = item.amount() / 2; + item.decrease_amount(returning_amount).ok()?; + return_item + .set_amount(returning_amount) + .expect("Items duplicated from a stackable item must be stackable."); + Some(return_item) + } else { + self.remove(inv_slot_id) + } + } else { + None + } + } + /// Takes all items from the inventory pub fn drain(&mut self) -> impl Iterator + '_ { self.slots_mut() diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 48cc8c0ede..f73e871503 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -5,7 +5,8 @@ use vek::{Rgb, Vec3}; use common::{ comp::{ - self, item, + self, + item::{self, MaterialStatManifest}, slot::{self, Slot}, }, consts::MAX_PICKUP_RANGE, @@ -429,15 +430,26 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo if let Some(pos) = ecs.read_storage::().get(entity) { if let Some(mut inventory) = ecs.write_storage::().get_mut(entity) { - dropped_items.extend(inventory.swap(a, b).into_iter().map(|x| { - ( - *pos, - state - .read_component_copied::(entity) - .unwrap_or_default(), - x, - ) - })); + let mut merged_stacks = false; + + // If both slots have items and we're attemping to drag from one stack + // into another, stack the items. + if let (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) = (a, b) { + merged_stacks |= inventory.merge_stack_into(slot_a, slot_b); + } + + // If the stacks weren't mergable carry out a swap. + if !merged_stacks { + dropped_items.extend(inventory.swap(a, b).into_iter().map(|x| { + ( + *pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + x, + ) + })); + } } } @@ -447,6 +459,55 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo ); }, + comp::SlotManip::SplitSwap(slot, target) => { + let msm = state.ecs().read_resource::(); + let mut inventories = state.ecs().write_storage::(); + let mut inventory = if let Some(inventory) = inventories.get_mut(entity) { + inventory + } else { + error!( + ?entity, + "Can't manipulate inventory, entity doesn't have one" + ); + return; + }; + + // If both slots have items and we're attemping to split from one stack + // into another, ensure that they are the same type of item. If they are + // the same type do nothing, as you don't want to overwrite the existing item. + + if let (Slot::Inventory(source_inv_slot_id), Slot::Inventory(target_inv_slot_id)) = + (slot, target) + { + if let Some(source_item) = inventory.get(source_inv_slot_id) { + if let Some(target_item) = inventory.get(target_inv_slot_id) { + if source_item != target_item { + return; + } + } + } + } + + let item = match slot { + Slot::Inventory(slot) => inventory.take_half(slot, &msm), + Slot::Equip(_) => None, + }; + + if let Some(item) = item { + if let Slot::Inventory(target) = target { + inventory.insert_or_stack_at(target, item).ok(); + } + } + drop(inventory); + drop(inventories); + drop(msm); + + state.write_component( + entity, + comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Swapped), + ); + }, + comp::SlotManip::Drop(slot) => { let item = match slot { Slot::Inventory(slot) => state @@ -480,6 +541,37 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo ); }, + comp::SlotManip::SplitDrop(slot) => { + let msm = state.ecs().read_resource::(); + let item = match slot { + Slot::Inventory(slot) => state + .ecs() + .write_storage::() + .get_mut(entity) + .and_then(|mut inv| inv.take_half(slot, &msm)), + Slot::Equip(_) => None, + }; + + // FIXME: We should really require the drop and write to be atomic! + if let (Some(mut item), Some(pos)) = + (item, state.ecs().read_storage::().get(entity)) + { + item.put_in_world(); + dropped_items.push(( + *pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + item, + )); + } + drop(msm); + state.write_component( + entity, + comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Dropped), + ); + }, + comp::SlotManip::CraftRecipe(recipe) => { if let Some(mut inv) = state .ecs() diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 706bed30b3..7550b7bbf6 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -391,7 +391,13 @@ pub enum Event { slot_b: comp::slot::Slot, bypass_dialog: bool, }, + SplitSwapSlots { + slot_a: comp::slot::Slot, + slot_b: comp::slot::Slot, + bypass_dialog: bool, + }, DropSlot(comp::slot::Slot), + SplitDropSlot(comp::slot::Slot), ChangeHotbarState(Box), TradeAction(TradeAction), Ability3(bool), @@ -769,7 +775,15 @@ impl Hud { let hotbar_state = HotbarState::new(global_state.profile.get_hotbar_slots(server, character_id)); - let slot_manager = slots::SlotManager::new(ui.id_generator(), Vec2::broadcast(40.0)); + let slot_manager = slots::SlotManager::new( + ui.id_generator(), + Vec2::broadcast(40.0) + // TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in hand". + // fonts.cyri.conrod_id, + // Vec2::new(1.0, 1.0), + // fonts.cyri.scale(12), + // TEXT_COLOR, + ); Self { ui, @@ -2788,6 +2802,31 @@ impl Hud { } } }, + slot::Event::SplitDropped(from) => { + // Drop item + if let Some(from) = to_slot(from) { + events.push(Event::SplitDropSlot(from)); + } else if let Hotbar(h) = from { + self.hotbar.clear_slot(h); + events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); + } + }, + slot::Event::SplitDragged(a, b) => { + // Swap between slots + if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) { + events.push(Event::SplitSwapSlots { + slot_a: a, + slot_b: b, + bypass_dialog: false, + }); + } else if let (Inventory(i), Hotbar(h)) = (a, b) { + self.hotbar.add_inventory_link(h, i.slot); + events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); + } else if let (Hotbar(a), Hotbar(b)) = (a, b) { + self.hotbar.swap(a, b); + events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); + } + }, slot::Event::Used(from) => { // Item used (selected and then clicked again) if let Some(from) = to_slot(from) { diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index b37dec9d5e..a7e863c183 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1136,6 +1136,57 @@ impl PlayState for SessionState { self.client.borrow_mut().swap_slots(slot_a, slot_b); } }, + HudEvent::SplitSwapSlots { + slot_a, + slot_b, + bypass_dialog, + } => { + let mut move_allowed = true; + if !bypass_dialog { + if let Some(inventory) = self + .client + .borrow() + .state() + .ecs() + .read_storage::() + .get(self.client.borrow().entity()) + { + match (slot_a, slot_b) { + (Slot::Inventory(inv_slot), Slot::Equip(equip_slot)) + | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => { + if !inventory.can_swap(inv_slot, equip_slot) { + move_allowed = false; + } else { + let slot_deficit = + inventory.free_after_swap(equip_slot, inv_slot); + if slot_deficit < 0 { + self.hud.set_prompt_dialog( + PromptDialogSettings::new( + format!( + "This will result in dropping {} \ + item(s) on the ground. Are you sure?", + slot_deficit.abs() + ), + HudEvent::SwapSlots { + slot_a, + slot_b, + bypass_dialog: true, + }, + None, + ), + ); + move_allowed = false; + } + } + }, + _ => {}, + } + } + }; + if move_allowed { + self.client.borrow_mut().split_swap_slots(slot_a, slot_b); + } + }, HudEvent::DropSlot(x) => { let mut client = self.client.borrow_mut(); client.drop_slot(x); @@ -1143,6 +1194,13 @@ impl PlayState for SessionState { client.disable_lantern(); } }, + HudEvent::SplitDropSlot(x) => { + let mut client = self.client.borrow_mut(); + client.split_drop_slot(x); + if let comp::slot::Slot::Equip(comp::slot::EquipSlot::Lantern) = x { + client.disable_lantern(); + } + }, HudEvent::ChangeHotbarState(state) => { let client = self.client.borrow(); diff --git a/voxygen/src/ui/widgets/slot.rs b/voxygen/src/ui/widgets/slot.rs index b190acb5c4..2a936c6712 100644 --- a/voxygen/src/ui/widgets/slot.rs +++ b/voxygen/src/ui/widgets/slot.rs @@ -94,7 +94,13 @@ where #[derive(Clone, Copy)] enum ManagerState { - Dragging(widget::Id, K, image::Id), + Dragging( + widget::Id, + K, + image::Id, + /// Amount of items being dragged in the stack. + Option, + ), Selected(widget::Id, K), Idle, } @@ -110,6 +116,10 @@ pub enum Event { Dragged(K, K), // Dragged to open space Dropped(K), + // Dropped half of the stack + SplitDropped(K), + // Dragged half of the stack + SplitDragged(K, K), // Clicked while selected Used(K), } @@ -127,21 +137,56 @@ pub struct SlotManager { // Note: could potentially be specialized for each slot if needed drag_img_size: Vec2, pub mouse_over_slot: Option, + /* TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in + * hand". + * + * drag_amount_id: widget::Id, + * drag_amount_shadow_id: widget::Id, */ + + /* Asset ID pointing to a font set. + * amount_font: font::Id, */ + + /* Specifies the size of the font used to display number of items held in + * a stack when dragging. + * amount_font_size: u32, */ + + /* Specifies how much space should be used in the margins of the item + * amount relative to the slot. + * amount_margins: Vec2, */ + + /* Specifies the color of the text used to display the number of items held + * in a stack when dragging. + * amount_text_color: Color, */ } impl SlotManager where S: SumSlot, { - pub fn new(mut gen: widget::id::Generator, drag_img_size: Vec2) -> Self { + pub fn new( + mut gen: widget::id::Generator, + drag_img_size: Vec2, + /* TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in + * hand". amount_font: font::Id, + * amount_margins: Vec2, + * amount_font_size: u32, + * amount_text_color: Color, */ + ) -> Self { Self { state: ManagerState::Idle, slot_ids: Vec::new(), slots: Vec::new(), events: Vec::new(), drag_id: gen.next(), - drag_img_size, mouse_over_slot: None, + // TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in + // hand". drag_amount_id: gen.next(), + // drag_amount_shadow_id: gen.next(), + // amount_font, + // amount_font_size, + // amount_margins, + // amount_text_color, + drag_img_size, } } @@ -166,8 +211,32 @@ where // If dragging and mouse is released check if there is a slot widget under the // mouse - if let ManagerState::Dragging(_, slot, content_img) = &self.state { + if let ManagerState::Dragging(_, slot, content_img, drag_amount) = &self.state { let content_img = *content_img; + let drag_amount = *drag_amount; + + // If we are dragging and we right click, drop half the stack + // on the ground or into the slot under the cursor. This only + // works with open slots or slots containing the same kind of + // item. + + if drag_amount.is_some() { + if let Some(id) = input.widget_under_mouse { + if ui.widget_input(id).clicks().right().next().is_some() { + if id == ui.window { + let temp_slot = *slot; + self.events.push(Event::SplitDropped(temp_slot)); + } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) + { + let (from, to) = (*slot, slots[idx]); + if from != to { + self.events.push(Event::SplitDragged(from, to)); + } + } + } + } + } + if let mouse::ButtonPosition::Up = input.mouse.buttons.left() { // Get widget under the mouse if let Some(id) = input.widget_under_mouse { @@ -186,6 +255,7 @@ where // Mouse released stop dragging self.state = ManagerState::Idle; } + // Draw image of contents being dragged let [mouse_x, mouse_y] = input.mouse.xy; let size = self.drag_img_size.map(|e| e as f64).into_array(); @@ -193,6 +263,34 @@ where .wh(size) .xy([mouse_x, mouse_y]) .set(self.drag_id, ui); + + // TODO(heyzoos) Will be useful for whoever works on rendering the + // number of items "in hand". + // + // if let Some(drag_amount) = drag_amount { + // Text::new(format!("{}", drag_amount).as_str()) + // .parent(self.drag_id) + // .font_id(self.amount_font) + // .font_size(self.amount_font_size) + // .bottom_right_with_margins_on( + // self.drag_id, + // self.amount_margins.x as f64, + // self.amount_margins.y as f64, + // ) + // .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + // .set(self.drag_amount_shadow_id, ui); + // Text::new(format!("{}", drag_amount).as_str()) + // .parent(self.drag_id) + // .font_id(self.amount_font) + // .font_size(self.amount_font_size) + // .bottom_right_with_margins_on( + // self.drag_id, + // self.amount_margins.x as f64, + // self.amount_margins.y as f64, + // ) + // .color(self.amount_text_color) + // .set(self.drag_amount_id, ui); + // } } std::mem::replace(&mut self.events, Vec::new()) @@ -204,6 +302,7 @@ where slot: S, ui: &conrod_core::Ui, content_img: Option>, + drag_amount: Option, ) -> Interaction { // Add to list of slots self.slot_ids.push(widget); @@ -212,7 +311,7 @@ where let filled = content_img.is_some(); // If the slot is no longer filled deselect it or cancel dragging match &self.state { - ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _) + ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _, _) if *id == widget && !filled => { self.state = ManagerState::Idle; @@ -223,7 +322,7 @@ where // If this is the selected/dragged widget make sure the slot value is up to date match &mut self.state { ManagerState::Selected(id, stored_slot) - | ManagerState::Dragging(id, stored_slot, _) + | ManagerState::Dragging(id, stored_slot, _, _) if *id == widget => { *stored_slot = slot @@ -278,24 +377,26 @@ where // If something is selected, deselect self.state = ManagerState::Idle; }, - ManagerState::Dragging(_, _, _) => {}, + ManagerState::Dragging(_, _, _, _) => {}, } } // If not dragging and there is a drag event on this slot start dragging if input.drags().left().next().is_some() - && !matches!(self.state, ManagerState::Dragging(_, _, _)) + && !matches!(self.state, ManagerState::Dragging(_, _, _, _)) { // Start dragging if widget is filled - if let Some(img) = content_img { - self.state = ManagerState::Dragging(widget, slot, img[0]); + if let Some(images) = content_img { + if !images.is_empty() { + self.state = ManagerState::Dragging(widget, slot, images[0], drag_amount); + } } } // Determine whether this slot is being interacted with match self.state { ManagerState::Selected(id, _) if id == widget => Interaction::Selected, - ManagerState::Dragging(id, _, _) if id == widget => Interaction::Dragging, + ManagerState::Dragging(id, _, _, _) if id == widget => Interaction::Dragging, _ => Interaction::None, } } @@ -488,7 +589,13 @@ where let content_images = state.cached_images.as_ref().map(|c| c.1.clone()); // Get whether this slot is selected let interaction = self.slot_manager.map_or(Interaction::None, |m| { - m.update(id, slot_key.into(), ui, content_images.clone()) + m.update( + id, + slot_key.into(), + ui, + content_images.clone(), + slot_key.amount(content_source), + ) }); // No content if it is being dragged let content_images = if let Interaction::Dragging = interaction {