diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-12-09 13:29:59 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-09 13:29:59 -0600 |
| commit | 26d619c9a329087a23d6577ee74bd764f50cd773 (patch) | |
| tree | 8020fe902257764a23a445c6ed9987ea4848189d /azalea-entity/src | |
| parent | 84cd261118c9d1e3145d4d1751c0d22098cd8cd8 (diff) | |
| download | azalea-drasl-26d619c9a329087a23d6577ee74bd764f50cd773.tar.xz | |
Enchantments (#286)
* start implementing enchants
* store parsed registries
* more work on enchants
* implement deserializer for some entity effects
* mostly working definitions for enchants
* fix tests
* detect equipment changes
* fix errors
* update changelog
* fix some imports
* remove outdated todo
* add basic test for enchants applying attributes
* use git simdnbt
Diffstat (limited to 'azalea-entity/src')
| -rw-r--r-- | azalea-entity/src/attributes.rs | 45 | ||||
| -rw-r--r-- | azalea-entity/src/enchantments.rs | 8 | ||||
| -rw-r--r-- | azalea-entity/src/inventory.rs | 722 | ||||
| -rw-r--r-- | azalea-entity/src/lib.rs | 29 | ||||
| -rw-r--r-- | azalea-entity/src/mining.rs | 21 |
5 files changed, 775 insertions, 50 deletions
diff --git a/azalea-entity/src/attributes.rs b/azalea-entity/src/attributes.rs index ca02e639..d0bd2c21 100644 --- a/azalea-entity/src/attributes.rs +++ b/azalea-entity/src/attributes.rs @@ -2,17 +2,25 @@ use std::collections::{HashMap, hash_map}; -use azalea_buf::AzBuf; -use azalea_core::identifier::Identifier; +use azalea_core::{ + attribute_modifier_operation::AttributeModifierOperation, identifier::Identifier, +}; +use azalea_inventory::components::AttributeModifier; +use azalea_registry::Attribute; use bevy_ecs::component::Component; use thiserror::Error; +/// A component that contains the current attribute values for an entity. +/// +/// Each attribute can have multiple modifiers, and these modifiers are the +/// result of things like sprinting or enchantments. #[derive(Clone, Debug, Component)] pub struct Attributes { pub movement_speed: AttributeInstance, pub sneaking_speed: AttributeInstance, pub attack_speed: AttributeInstance, pub water_movement_efficiency: AttributeInstance, + pub mining_efficiency: AttributeInstance, pub block_interaction_range: AttributeInstance, pub entity_interaction_range: AttributeInstance, @@ -20,6 +28,25 @@ pub struct Attributes { pub step_height: AttributeInstance, } +impl Attributes { + /// Returns a mutable reference to the [`AttributeInstance`] for the given + /// attribute, or `None` if the attribute isn't implemented. + pub fn get_mut(&mut self, attribute: Attribute) -> Option<&mut AttributeInstance> { + let value = match attribute { + Attribute::MovementSpeed => &mut self.movement_speed, + Attribute::SneakingSpeed => &mut self.sneaking_speed, + Attribute::AttackSpeed => &mut self.attack_speed, + Attribute::WaterMovementEfficiency => &mut self.water_movement_efficiency, + Attribute::MiningEfficiency => &mut self.mining_efficiency, + Attribute::BlockInteractionRange => &mut self.block_interaction_range, + Attribute::EntityInteractionRange => &mut self.entity_interaction_range, + Attribute::StepHeight => &mut self.step_height, + _ => return None, + }; + Some(value) + } +} + #[derive(Clone, Debug)] pub struct AttributeInstance { pub base: f64, @@ -78,20 +105,6 @@ impl AttributeInstance { } } -#[derive(Clone, Debug, AzBuf, PartialEq)] -pub struct AttributeModifier { - pub id: Identifier, - pub amount: f64, - pub operation: AttributeModifierOperation, -} - -#[derive(Clone, Debug, Copy, AzBuf, PartialEq)] -pub enum AttributeModifierOperation { - AddValue, - AddMultipliedBase, - AddMultipliedTotal, -} - pub fn sprinting_modifier() -> AttributeModifier { AttributeModifier { id: Identifier::new("sprinting"), diff --git a/azalea-entity/src/enchantments.rs b/azalea-entity/src/enchantments.rs deleted file mode 100644 index d51cf752..00000000 --- a/azalea-entity/src/enchantments.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub fn _get_enchant_level( - _enchantment: azalea_registry::Enchantment, - _player_inventory: &azalea_inventory::Menu, -) -> u32 { - // TODO - - 0 -} diff --git a/azalea-entity/src/inventory.rs b/azalea-entity/src/inventory.rs new file mode 100644 index 00000000..5dd933a2 --- /dev/null +++ b/azalea-entity/src/inventory.rs @@ -0,0 +1,722 @@ +use std::{cmp, collections::HashSet}; + +use azalea_chat::FormattedText; +use azalea_inventory::{ + ItemStack, ItemStackData, Menu, + components::EquipmentSlot, + item::MaxStackSizeExt, + operations::{ + ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus, + QuickCraftStatusKind, QuickMoveClick, ThrowClick, + }, +}; +use bevy_ecs::prelude::*; + +use crate::PlayerAbilities; + +/// A component present on all local players that have an inventory. +#[derive(Component, Debug, Clone)] +pub struct Inventory { + /// The player's inventory menu. This is guaranteed to be a `Menu::Player`. + /// + /// We keep it as a [`Menu`] since `Menu` has some useful functions that + /// bare [`azalea_inventory::Player`] doesn't have. + pub inventory_menu: azalea_inventory::Menu, + + /// The ID of the container that's currently open. + /// + /// Its value is not guaranteed to be anything specific, and it may change + /// every time you open a container (unless it's 0, in which case it + /// means that no container is open). + pub id: i32, + /// The current container menu that the player has open, or `None` if no + /// container is open. + pub container_menu: Option<azalea_inventory::Menu>, + /// The custom name of the menu that's currently open. + /// + /// This can only be `Some` when `container_menu` is `Some`. + pub container_menu_title: Option<FormattedText>, + /// The item that is currently held by the cursor, or `Slot::Empty` if + /// nothing is currently being held. + /// + /// This is different from [`Self::selected_hotbar_slot`], which is the + /// item that's selected in the hotbar. + pub carried: ItemStack, + /// An identifier used by the server to track client inventory desyncs. + /// + /// This is sent on every container click, and it's only ever updated when + /// the server sends a new container update. + pub state_id: u32, + + pub quick_craft_status: QuickCraftStatusKind, + pub quick_craft_kind: QuickCraftKind, + /// A set of the indexes of the slots that have been right clicked in + /// this "quick craft". + pub quick_craft_slots: HashSet<u16>, + + /// The index of the item in the hotbar that's currently being held by the + /// player. This must be in the range 0..=8. + /// + /// In a vanilla client this is changed by pressing the number keys or using + /// the scroll wheel. + pub selected_hotbar_slot: u8, +} + +impl Inventory { + /// Returns a reference to the currently active menu. + /// + /// If a container is open then it'll return [`Self::container_menu`], + /// otherwise [`Self::inventory_menu`]. + /// + /// Use [`Self::menu_mut`] if you need a mutable reference. + pub fn menu(&self) -> &azalea_inventory::Menu { + match &self.container_menu { + Some(menu) => menu, + _ => &self.inventory_menu, + } + } + + /// Returns a mutable reference to the currently active menu. + /// + /// If a container is open then it'll return [`Self::container_menu`], + /// otherwise [`Self::inventory_menu`]. + /// + /// Use [`Self::menu`] if you don't need a mutable reference. + pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu { + match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + } + } + + /// Modify the inventory as if the given operation was performed on it. + pub fn simulate_click( + &mut self, + operation: &ClickOperation, + player_abilities: &PlayerAbilities, + ) { + if let ClickOperation::QuickCraft(quick_craft) = operation { + let last_quick_craft_status_tmp = self.quick_craft_status.clone(); + self.quick_craft_status = last_quick_craft_status_tmp.clone(); + let last_quick_craft_status = last_quick_craft_status_tmp; + + // no carried item, reset + if self.carried.is_empty() { + return self.reset_quick_craft(); + } + // if we were starting or ending, or now we aren't ending and the status + // changed, reset + if (last_quick_craft_status == QuickCraftStatusKind::Start + || last_quick_craft_status == QuickCraftStatusKind::End + || self.quick_craft_status != QuickCraftStatusKind::End) + && (self.quick_craft_status != last_quick_craft_status) + { + return self.reset_quick_craft(); + } + if self.quick_craft_status == QuickCraftStatusKind::Start { + self.quick_craft_kind = quick_craft.kind.clone(); + if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break + { + self.quick_craft_status = QuickCraftStatusKind::Add; + self.quick_craft_slots.clear(); + } else { + self.reset_quick_craft(); + } + return; + } + if let QuickCraftStatus::Add { slot } = quick_craft.status { + let slot_item = self.menu().slot(slot as usize); + if let Some(slot_item) = slot_item + && let ItemStack::Present(carried) = &self.carried + { + // minecraft also checks slot.may_place(carried) and + // menu.can_drag_to(slot) + // but they always return true so they're not relevant for us + if can_item_quick_replace(slot_item, &self.carried, true) + && (self.quick_craft_kind == QuickCraftKind::Right + || carried.count as usize > self.quick_craft_slots.len()) + { + self.quick_craft_slots.insert(slot); + } + } + return; + } + if self.quick_craft_status == QuickCraftStatusKind::End { + if !self.quick_craft_slots.is_empty() { + if self.quick_craft_slots.len() == 1 { + // if we only clicked one slot, then turn this + // QuickCraftClick into a PickupClick + let slot = *self.quick_craft_slots.iter().next().unwrap(); + self.reset_quick_craft(); + self.simulate_click( + &match self.quick_craft_kind { + QuickCraftKind::Left => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Right => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Middle => { + // idk just do nothing i guess + return; + } + }, + player_abilities, + ); + return; + } + + let ItemStack::Present(mut carried) = self.carried.clone() else { + // this should never happen + return self.reset_quick_craft(); + }; + + let mut carried_count = carried.count; + let mut quick_craft_slots_iter = self.quick_craft_slots.iter(); + + loop { + let mut slot: &ItemStack; + let mut slot_index: u16; + let mut item_stack: &ItemStack; + + loop { + let Some(&next_slot) = quick_craft_slots_iter.next() else { + carried.count = carried_count; + self.carried = ItemStack::Present(carried); + return self.reset_quick_craft(); + }; + + slot = self.menu().slot(next_slot as usize).unwrap(); + slot_index = next_slot; + item_stack = &self.carried; + + if slot.is_present() + && can_item_quick_replace(slot, item_stack, true) + // this always returns true in most cases + // && slot.may_place(item_stack) + && ( + self.quick_craft_kind == QuickCraftKind::Middle + || item_stack.count() >= self.quick_craft_slots.len() as i32 + ) + { + break; + } + } + + // get the ItemStackData for the slot + let ItemStack::Present(slot) = slot else { + unreachable!("the loop above requires the slot to be present to break") + }; + + // if self.can_drag_to(slot) { + let mut new_carried = carried.clone(); + let slot_item_count = slot.count; + get_quick_craft_slot_count( + &self.quick_craft_slots, + &self.quick_craft_kind, + &mut new_carried, + slot_item_count, + ); + let max_stack_size = i32::min( + new_carried.kind.max_stack_size(), + i32::min( + new_carried.kind.max_stack_size(), + slot.kind.max_stack_size(), + ), + ); + if new_carried.count > max_stack_size { + new_carried.count = max_stack_size; + } + + carried_count -= new_carried.count - slot_item_count; + // we have to inline self.menu_mut() here to avoid the borrow checker + // complaining + let menu = match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + }; + *menu.slot_mut(slot_index as usize).unwrap() = + ItemStack::Present(new_carried); + } + } + } else { + return self.reset_quick_craft(); + } + } + // the quick craft status should always be in start if we're not in quick craft + // mode + if self.quick_craft_status != QuickCraftStatusKind::Start { + return self.reset_quick_craft(); + } + + match operation { + // left clicking outside inventory + ClickOperation::Pickup(PickupClick::Left { slot: None }) => { + if self.carried.is_present() { + // vanilla has `player.drop`s but they're only used + // server-side + // they're included as comments here in case you want to adapt this for a server + // implementation + + // player.drop(self.carried, true); + self.carried = ItemStack::Empty; + } + } + ClickOperation::Pickup(PickupClick::Right { slot: None }) => { + if self.carried.is_present() { + let _item = self.carried.split(1); + // player.drop(item, true); + } + } + &ClickOperation::Pickup( + // lol + ref pickup @ (PickupClick::Left { slot: Some(slot) } + | PickupClick::Right { slot: Some(slot) }), + ) => { + let slot = slot as usize; + let Some(slot_item) = self.menu().slot(slot) else { + return; + }; + + if self.try_item_click_behavior_override(operation, slot) { + return; + } + + let is_left_click = matches!(pickup, PickupClick::Left { .. }); + + match slot_item { + ItemStack::Empty => { + if self.carried.is_present() { + let place_count = if is_left_click { + self.carried.count() + } else { + 1 + }; + self.carried = + self.safe_insert(slot, self.carried.clone(), place_count); + } + } + ItemStack::Present(_) => { + if !self.menu().may_pickup(slot) { + return; + } + if let ItemStack::Present(carried) = self.carried.clone() { + let slot_is_same_item_as_carried = slot_item + .as_present() + .is_some_and(|s| carried.is_same_item_and_components(s)); + + if self.menu().may_place(slot, &carried) { + if slot_is_same_item_as_carried { + let place_count = if is_left_click { carried.count } else { 1 }; + self.carried = + self.safe_insert(slot, self.carried.clone(), place_count); + } else if carried.count + <= self + .menu() + .max_stack_size(slot) + .min(carried.kind.max_stack_size()) + { + // swap slot_item and carried + self.carried = slot_item.clone(); + let slot_item = self.menu_mut().slot_mut(slot).unwrap(); + *slot_item = carried.into(); + } + } else if slot_is_same_item_as_carried + && let Some(removed) = self.try_remove( + slot, + slot_item.count(), + carried.kind.max_stack_size() - carried.count, + ) + { + self.carried.as_present_mut().unwrap().count += removed.count(); + // slot.onTake(player, removed); + } + } else { + let pickup_count = if is_left_click { + slot_item.count() + } else { + (slot_item.count() + 1) / 2 + }; + if let Some(new_slot_item) = + self.try_remove(slot, pickup_count, i32::MAX) + { + self.carried = new_slot_item; + // slot.onTake(player, newSlot); + } + } + } + } + } + &ClickOperation::QuickMove( + QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot }, + ) => { + // in vanilla it also tests if QuickMove has a slot index of -999 + // but i don't think that's ever possible so it's not covered here + let slot = slot as usize; + loop { + let new_slot_item = self.menu_mut().quick_move_stack(slot); + let slot_item = self.menu().slot(slot).unwrap(); + if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() { + break; + } + } + } + ClickOperation::Swap(s) => { + let source_slot_index = s.source_slot as usize; + let target_slot_index = s.target_slot as usize; + + let Some(source_slot) = self.menu().slot(source_slot_index) else { + return; + }; + let Some(target_slot) = self.menu().slot(target_slot_index) else { + return; + }; + if source_slot.is_empty() && target_slot.is_empty() { + return; + } + + if target_slot.is_empty() { + if self.menu().may_pickup(source_slot_index) { + let source_slot = source_slot.clone(); + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = source_slot; + } + } else if source_slot.is_empty() { + let target_item = target_slot + .as_present() + .expect("target slot was already checked to not be empty"); + if self.menu().may_place(source_slot_index, target_item) { + // get the target_item but mutable + let source_max_stack_size = self.menu().max_stack_size(source_slot_index); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = + target_slot.split(source_max_stack_size.try_into().unwrap()); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + } + } else if self.menu().may_pickup(source_slot_index) { + let ItemStack::Present(target_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + if self.menu().may_place(source_slot_index, target_item) { + let source_max_stack = self.menu().max_stack_size(source_slot_index); + if target_slot.count() > source_max_stack { + // if there's more than the max stack size in the target slot + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = + target_slot.split(source_max_stack.try_into().unwrap()); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + // if !self.inventory_menu.add(new_source_slot) { + // player.drop(new_source_slot, true); + // } + } else { + // normal swap + let new_target_slot = source_slot.clone(); + let new_source_slot = target_slot.clone(); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = new_target_slot; + + let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap(); + *source_slot = new_source_slot; + } + } + } + } + ClickOperation::Clone(CloneClick { slot }) => { + if !player_abilities.instant_break || self.carried.is_present() { + return; + } + let Some(source_slot) = self.menu().slot(*slot as usize) else { + return; + }; + let ItemStack::Present(source_item) = source_slot else { + return; + }; + let mut new_carried = source_item.clone(); + new_carried.count = new_carried.kind.max_stack_size(); + self.carried = ItemStack::Present(new_carried); + } + ClickOperation::Throw(c) => { + if self.carried.is_present() { + return; + } + + let (ThrowClick::Single { slot: slot_index } + | ThrowClick::All { slot: slot_index }) = c; + let slot_index = *slot_index as usize; + + let Some(slot) = self.menu_mut().slot_mut(slot_index) else { + return; + }; + let ItemStack::Present(slot_item) = slot else { + return; + }; + + let dropping_count = match c { + ThrowClick::Single { .. } => 1, + ThrowClick::All { .. } => slot_item.count, + }; + + let _dropping = slot_item.split(dropping_count as u32); + // player.drop(dropping, true); + } + ClickOperation::PickupAll(PickupAllClick { + slot: source_slot_index, + reversed, + }) => { + let source_slot_index = *source_slot_index as usize; + + let source_slot = self.menu().slot(source_slot_index).unwrap(); + let target_slot = self.carried.clone(); + + if target_slot.is_empty() + || (source_slot.is_present() && self.menu().may_pickup(source_slot_index)) + { + return; + } + + let ItemStack::Present(target_slot_item) = &target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + + for round in 0..2 { + let iterator: Box<dyn Iterator<Item = usize>> = if *reversed { + Box::new((0..self.menu().len()).rev()) + } else { + Box::new(0..self.menu().len()) + }; + + for i in iterator { + if target_slot_item.count < target_slot_item.kind.max_stack_size() { + let checking_slot = self.menu().slot(i).unwrap(); + if let ItemStack::Present(checking_item) = checking_slot + && can_item_quick_replace(checking_slot, &target_slot, true) + && self.menu().may_pickup(i) + && (round != 0 + || checking_item.count != checking_item.kind.max_stack_size()) + { + // get the checking_slot and checking_item again but mutable + let checking_slot = self.menu_mut().slot_mut(i).unwrap(); + + let taken_item = checking_slot.split(checking_slot.count() as u32); + + // now extend the carried item + let target_slot = &mut self.carried; + let ItemStack::Present(target_slot_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + target_slot_item.count += taken_item.count(); + } + } + } + } + } + _ => {} + } + } + + fn reset_quick_craft(&mut self) { + self.quick_craft_status = QuickCraftStatusKind::Start; + self.quick_craft_slots.clear(); + } + + /// Get the item in the player's hotbar that is currently being held in + /// their main hand. + pub fn held_item(&self) -> &ItemStack { + self.get_equipment(EquipmentSlot::Mainhand) + .expect("The main hand item should always be present") + } + + /// TODO: implement bundles + fn try_item_click_behavior_override( + &self, + _operation: &ClickOperation, + _slot_item_index: usize, + ) -> bool { + false + } + + fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack { + let Some(slot_item) = self.menu_mut().slot_mut(slot) else { + return src_item; + }; + let ItemStack::Present(mut src_item) = src_item else { + return src_item; + }; + + let take_count = cmp::min( + cmp::min(take_count, src_item.count), + src_item.kind.max_stack_size() - slot_item.count(), + ); + if take_count <= 0 { + return src_item.into(); + } + let take_count = take_count as u32; + + if slot_item.is_empty() { + *slot_item = src_item.split(take_count).into(); + } else if let ItemStack::Present(slot_item) = slot_item + && slot_item.is_same_item_and_components(&src_item) + { + src_item.count -= take_count as i32; + slot_item.count += take_count as i32; + } + + src_item.into() + } + + fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> { + if !self.menu().may_pickup(slot) { + return None; + } + let mut slot_item = self.menu().slot(slot)?.clone(); + if !self.menu().allow_modification(slot) && limit < slot_item.count() { + return None; + } + + let count = count.min(limit); + if count <= 0 { + return None; + } + // vanilla calls .remove here but i think it has the same behavior as split? + let removed = slot_item.split(count as u32); + + if removed.is_present() && slot_item.is_empty() { + *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty; + } + + Some(removed) + } + + /// Get the item at the given equipment slot, or `None` if the inventory + /// can't contain that slot. + pub fn get_equipment(&self, equipment_slot: EquipmentSlot) -> Option<&ItemStack> { + let player = self.inventory_menu.as_player(); + let item = match equipment_slot { + EquipmentSlot::Mainhand => { + let menu = self.menu(); + let main_hand_slot_idx = + *menu.hotbar_slots_range().start() + self.selected_hotbar_slot as usize; + menu.slot(main_hand_slot_idx)? + } + EquipmentSlot::Offhand => &player.offhand, + EquipmentSlot::Feet => &player.armor[3], + EquipmentSlot::Legs => &player.armor[2], + EquipmentSlot::Chest => &player.armor[1], + EquipmentSlot::Head => &player.armor[0], + EquipmentSlot::Body => { + // TODO: when riding entities is implemented, mount/horse inventories should be + // implemented too. note that horse inventories aren't a normal menu (they're + // not in MenuKind), maybe they should be a separate field in `Inventory`? + return None; + } + EquipmentSlot::Saddle => { + // TODO: implement riding entities, see above + return None; + } + }; + Some(item) + } +} + +fn can_item_quick_replace( + target_slot: &ItemStack, + item: &ItemStack, + ignore_item_count: bool, +) -> bool { + let ItemStack::Present(target_slot) = target_slot else { + return false; + }; + let ItemStack::Present(item) = item else { + // i *think* this is what vanilla does + // not 100% sure lol probably doesn't matter though + return false; + }; + + if !item.is_same_item_and_components(target_slot) { + return false; + } + let count = target_slot.count as u16 + + if ignore_item_count { + 0 + } else { + item.count as u16 + }; + count <= item.kind.max_stack_size() as u16 +} + +fn get_quick_craft_slot_count( + quick_craft_slots: &HashSet<u16>, + quick_craft_kind: &QuickCraftKind, + item: &mut ItemStackData, + slot_item_count: i32, +) { + item.count = match quick_craft_kind { + QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32, + QuickCraftKind::Right => 1, + QuickCraftKind::Middle => item.kind.max_stack_size(), + }; + item.count += slot_item_count; +} + +impl Default for Inventory { + fn default() -> Self { + Inventory { + inventory_menu: Menu::Player(azalea_inventory::Player::default()), + id: 0, + container_menu: None, + container_menu_title: None, + carried: ItemStack::Empty, + state_id: 0, + quick_craft_status: QuickCraftStatusKind::Start, + quick_craft_kind: QuickCraftKind::Middle, + quick_craft_slots: HashSet::new(), + selected_hotbar_slot: 0, + } + } +} + +#[cfg(test)] +mod tests { + use azalea_inventory::SlotList; + use azalea_registry::Item; + + use super::*; + + #[test] + fn test_simulate_shift_click_in_crafting_table() { + let spruce_planks = ItemStack::new(Item::SprucePlanks, 4); + + let mut inventory = Inventory { + inventory_menu: Menu::Player(azalea_inventory::Player::default()), + id: 1, + container_menu: Some(Menu::Crafting { + result: spruce_planks.clone(), + // simulate_click won't delete the items from here + grid: SlotList::default(), + player: SlotList::default(), + }), + container_menu_title: None, + carried: ItemStack::Empty, + state_id: 0, + quick_craft_status: QuickCraftStatusKind::Start, + quick_craft_kind: QuickCraftKind::Middle, + quick_craft_slots: HashSet::new(), + selected_hotbar_slot: 0, + }; + + inventory.simulate_click( + &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }), + &PlayerAbilities::default(), + ); + + let new_slots = inventory.menu().slots(); + assert_eq!(&new_slots[0], &ItemStack::Empty); + assert_eq!( + &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()], + &spruce_planks + ); + } +} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 927b4637..84e1c9f7 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -4,7 +4,7 @@ pub mod attributes; mod data; pub mod dimensions; mod effects; -mod enchantments; +pub mod inventory; pub mod metadata; pub mod mining; pub mod particle; @@ -511,7 +511,7 @@ impl EntityBundle { dimensions, direction: LookDirection::default(), - attributes: default_attributes(EntityKind::Player), + attributes: Attributes::new(EntityKind::Player), jumping: Jumping(false), crouching: Crouching(false), @@ -522,17 +522,20 @@ impl EntityBundle { } } -pub fn default_attributes(_entity_kind: EntityKind) -> Attributes { - // TODO: do the correct defaults for everything, some - // entities have different defaults - Attributes { - movement_speed: AttributeInstance::new(0.1f32 as f64), - sneaking_speed: AttributeInstance::new(0.3), - attack_speed: AttributeInstance::new(4.0), - water_movement_efficiency: AttributeInstance::new(0.0), - block_interaction_range: AttributeInstance::new(4.5), - entity_interaction_range: AttributeInstance::new(3.0), - step_height: AttributeInstance::new(0.6), +impl Attributes { + pub fn new(_entity_kind: EntityKind) -> Self { + // TODO: do the correct defaults for everything, some + // entities have different defaults + Attributes { + movement_speed: AttributeInstance::new(0.1f32 as f64), + sneaking_speed: AttributeInstance::new(0.3), + attack_speed: AttributeInstance::new(4.0), + water_movement_efficiency: AttributeInstance::new(0.0), + mining_efficiency: AttributeInstance::new(0.0), + block_interaction_range: AttributeInstance::new(4.5), + entity_interaction_range: AttributeInstance::new(3.0), + step_height: AttributeInstance::new(0.6), + } } } diff --git a/azalea-entity/src/mining.rs b/azalea-entity/src/mining.rs index 2008da34..b387367f 100644 --- a/azalea-entity/src/mining.rs +++ b/azalea-entity/src/mining.rs @@ -2,7 +2,7 @@ use azalea_block::{BlockBehavior, BlockTrait}; use azalea_core::tier::get_item_tier; use azalea_registry::{self as registry, MobEffect}; -use crate::{ActiveEffects, FluidOnEyes, Physics}; +use crate::{ActiveEffects, Attributes, FluidOnEyes, Physics}; /// How much progress is made towards mining the block per tick, as a /// percentage. @@ -17,9 +17,9 @@ use crate::{ActiveEffects, FluidOnEyes, Physics}; pub fn get_mine_progress( block: &dyn BlockTrait, held_item: registry::Item, - player_inventory: &azalea_inventory::Menu, fluid_on_eyes: &FluidOnEyes, physics: &Physics, + attributes: &Attributes, active_effects: &ActiveEffects, ) -> f32 { let block_behavior: BlockBehavior = block.behavior(); @@ -37,9 +37,9 @@ pub fn get_mine_progress( let base_destroy_speed = destroy_speed( block.as_registry_block(), held_item, - player_inventory, fluid_on_eyes, physics, + attributes, active_effects, ); (base_destroy_speed / destroy_time) / divisor as f32 @@ -81,22 +81,17 @@ fn has_correct_tool_for_drops(block: &dyn BlockTrait, tool: registry::Item) -> b fn destroy_speed( block: registry::Block, tool: registry::Item, - _player_inventory: &azalea_inventory::Menu, _fluid_on_eyes: &FluidOnEyes, physics: &Physics, + attributes: &Attributes, active_effects: &ActiveEffects, ) -> f32 { let mut base_destroy_speed = base_destroy_speed(block, tool); - // add efficiency enchantment - // TODO - // if base_destroy_speed > 1. { - // let efficiency_level = - // enchantments::get_enchant_level(registry::Enchantment::Efficiency, - // player_inventory); if efficiency_level > 0 && tool != - // registry::Item::Air { base_destroy_speed += (efficiency_level * - // efficiency_level + 1) as f32; } - // } + if base_destroy_speed > 1. { + // efficiency enchantment + base_destroy_speed += attributes.mining_efficiency.calculate() as f32; + } if let Some(dig_speed_amplifier) = active_effects.get_dig_speed_amplifier() { base_destroy_speed *= 1. + (dig_speed_amplifier + 1) as f32 * 0.2; |
