Implement stacking and splitting

This commit is contained in:
Jesus Bracho 2021-03-02 00:08:46 +00:00 committed by Ben Wallis
parent e114786fdb
commit c0573cca44
8 changed files with 406 additions and 23 deletions

View File

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

View File

@ -668,6 +668,30 @@ impl Client {
pub fn is_dead(&self) -> bool { self.current::<comp::Health>().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

View File

@ -23,7 +23,9 @@ pub enum InventoryManip {
Collect(Vec3<i32>),
Use(InvSlotId),
Swap(InvSlotId, InvSlotId),
SplitSwap(InvSlotId, InvSlotId),
Drop(InvSlotId),
SplitDrop(InvSlotId),
CraftRecipe(String),
}
@ -40,7 +42,9 @@ pub enum SlotManip {
Collect(Vec3<i32>),
Use(Slot),
Swap(Slot, Slot),
SplitSwap(Slot, Slot),
Drop(Slot),
SplitDrop(Slot),
CraftRecipe(String),
}
@ -63,7 +67,11 @@ impl From<InventoryManip> 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),
}
}

View File

@ -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<Item> {
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<Item = Item> + '_ {
self.slots_mut()

View File

@ -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::<comp::Pos>().get(entity) {
if let Some(mut inventory) = ecs.write_storage::<comp::Inventory>().get_mut(entity)
{
dropped_items.extend(inventory.swap(a, b).into_iter().map(|x| {
(
*pos,
state
.read_component_copied::<comp::Ori>(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::<comp::Ori>(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::<MaterialStatManifest>();
let mut inventories = state.ecs().write_storage::<comp::Inventory>();
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::<MaterialStatManifest>();
let item = match slot {
Slot::Inventory(slot) => state
.ecs()
.write_storage::<comp::Inventory>()
.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::<comp::Pos>().get(entity))
{
item.put_in_world();
dropped_items.push((
*pos,
state
.read_component_copied::<comp::Ori>(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()

View File

@ -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<HotbarState>),
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) {

View File

@ -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::<comp::Inventory>()
.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();

View File

@ -94,7 +94,13 @@ where
#[derive(Clone, Copy)]
enum ManagerState<K> {
Dragging(widget::Id, K, image::Id),
Dragging(
widget::Id,
K,
image::Id,
/// Amount of items being dragged in the stack.
Option<u32>,
),
Selected(widget::Id, K),
Idle,
}
@ -110,6 +116,10 @@ pub enum Event<K> {
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<S: SumSlot> {
// Note: could potentially be specialized for each slot if needed
drag_img_size: Vec2<f32>,
pub mouse_over_slot: Option<S>,
/* 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<f32>, */
/* Specifies the color of the text used to display the number of items held
* in a stack when dragging.
* amount_text_color: Color, */
}
impl<S> SlotManager<S>
where
S: SumSlot,
{
pub fn new(mut gen: widget::id::Generator, drag_img_size: Vec2<f32>) -> Self {
pub fn new(
mut gen: widget::id::Generator,
drag_img_size: Vec2<f32>,
/* TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in
* hand". amount_font: font::Id,
* amount_margins: Vec2<f32>,
* 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<Vec<image::Id>>,
drag_amount: Option<u32>,
) -> 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 {