diff --git a/azalea-client/src/attack.rs b/azalea-client/src/attack.rs new file mode 100644 index 00000000..34083ffc --- /dev/null +++ b/azalea-client/src/attack.rs @@ -0,0 +1,138 @@ +use azalea_core::GameMode; +use azalea_entity::{ + metadata::{ShiftKeyDown, Sprinting}, + Attributes, Physics, +}; +use azalea_protocol::packets::game::serverbound_interact_packet::{ + self, ServerboundInteractPacket, +}; +use azalea_world::MinecraftEntityId; +use bevy_app::{App, FixedUpdate, Plugin, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +use crate::{ + interact::SwingArmEvent, + local_player::{LocalGameMode, SendPacketEvent}, + Client, +}; + +pub struct AttackPlugin; +impl Plugin for AttackPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems(Update, handle_attack_event) + .add_systems( + FixedUpdate, + ( + increment_ticks_since_last_attack, + update_attack_strength_scale, + ) + .chain(), + ); + } +} + +impl Client { + /// Attack the entity with the given id. + pub fn attack(&mut self, entity_id: MinecraftEntityId) { + self.ecs.lock().send_event(AttackEvent { + entity: self.entity, + target: entity_id, + }); + } + + /// Whether the player has an attack cooldown. + pub fn has_attack_cooldown(&self) -> bool { + let ticks_since_last_attack = *self.component::(); + ticks_since_last_attack < 1.0 + } +} + +#[derive(Event)] +pub struct AttackEvent { + pub entity: Entity, + pub target: MinecraftEntityId, +} +pub fn handle_attack_event( + mut events: EventReader, + mut query: Query<( + &LocalGameMode, + &mut TicksSinceLastAttack, + &mut Physics, + &mut Sprinting, + &mut ShiftKeyDown, + )>, + mut send_packet_events: EventWriter, + mut swing_arm_event: EventWriter, +) { + for event in events.iter() { + let (game_mode, mut ticks_since_last_attack, mut physics, mut sprinting, sneaking) = + query.get_mut(event.entity).unwrap(); + + swing_arm_event.send(SwingArmEvent { + entity: event.entity, + }); + send_packet_events.send(SendPacketEvent { + entity: event.entity, + packet: ServerboundInteractPacket { + entity_id: *event.target, + action: serverbound_interact_packet::ActionType::Attack, + using_secondary_action: **sneaking, + } + .get(), + }); + + // we can't attack if we're in spectator mode but it still sends the attack + // packet + if game_mode.current == GameMode::Spectator { + continue; + }; + + ticks_since_last_attack.0 = 0; + + physics.delta = physics.delta.multiply(0.6, 1.0, 0.6); + **sprinting = false; + } +} + +#[derive(Default, Bundle)] +pub struct AttackBundle { + pub ticks_since_last_attack: TicksSinceLastAttack, + pub attack_strength_scale: AttackStrengthScale, +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct TicksSinceLastAttack(pub u32); +pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) { + for mut ticks_since_last_attack in query.iter_mut() { + **ticks_since_last_attack += 1; + } +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct AttackStrengthScale(pub f32); +pub fn update_attack_strength_scale( + mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>, +) { + for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() { + // look 0.5 ticks into the future because that's what vanilla does + **attack_strength_scale = + get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5); + } +} + +/// Returns how long it takes for the attack cooldown to reset (in ticks). +pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 { + ((1. / attributes.attack_speed.calculate()) * 20.) as f32 +} + +pub fn get_attack_strength_scale( + ticks_since_last_attack: u32, + attributes: &Attributes, + in_ticks: f32, +) -> f32 { + let attack_strength_delay = get_attack_strength_delay(attributes); + let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay; + attack_strength.clamp(0., 1.) +} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 301d9197..f7fcb16c 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,4 +1,5 @@ use crate::{ + attack::{self, AttackPlugin}, chat::ChatPlugin, disconnect::{DisconnectEvent, DisconnectPlugin}, events::{Event, EventPlugin, LocalPlayerEvents}, @@ -306,6 +307,7 @@ impl Client { abilities: PlayerAbilities::default(), permission_level: PermissionLevel::default(), mining: mining::MineBundle::default(), + attack: attack::AttackBundle::default(), _local: Local, }); @@ -574,6 +576,7 @@ pub struct JoinedClientBundle { pub permission_level: PermissionLevel, pub mining: mining::MineBundle, + pub attack: attack::AttackBundle, pub _local: Local, } @@ -722,6 +725,7 @@ impl PluginGroup for DefaultPlugins { .add(InteractPlugin) .add(RespawnPlugin) .add(MinePlugin) + .add(AttackPlugin) .add(TickBroadcastPlugin); #[cfg(feature = "log")] { diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index dc0213b0..3c9428ff 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -2,7 +2,9 @@ use std::ops::AddAssign; use azalea_block::BlockState; use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3}; -use azalea_entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position}; +use azalea_entity::{ + clamp_look_direction, view_vector, Attributes, EyeHeight, Local, LookDirection, Position, +}; use azalea_inventory::{ItemSlot, ItemSlotData}; use azalea_nbt::NbtList; use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; @@ -17,6 +19,7 @@ use bevy_ecs::{ component::Component, entity::Entity, event::{Event, EventReader, EventWriter}, + query::{Changed, With}, schedule::IntoSystemConfigs, system::{Commands, Query, Res}, }; @@ -39,12 +42,15 @@ impl Plugin for InteractPlugin { .add_systems( Update, ( - update_hit_result_component.after(clamp_look_direction), - handle_block_interact_event, - handle_swing_arm_event, - ) - .before(handle_send_packet_event) - .chain(), + ( + update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, + handle_swing_arm_event, + ) + .before(handle_send_packet_event) + .chain(), + update_modifiers_for_held_item, + ), ); } } @@ -305,3 +311,66 @@ fn handle_swing_arm_event( }); } } + +#[allow(clippy::type_complexity)] +fn update_modifiers_for_held_item( + mut query: Query< + (&mut Attributes, &InventoryComponent), + (With, Changed), + >, +) { + for (mut attributes, inventory) in &mut query { + let held_item = inventory.held_item(); + + use azalea_registry::Item; + let added_attack_speed = match held_item.kind() { + Item::WoodenSword => -2.4, + Item::WoodenShovel => -3.0, + Item::WoodenPickaxe => -2.8, + Item::WoodenAxe => -3.2, + Item::WoodenHoe => -3.0, + + Item::StoneSword => -2.4, + Item::StoneShovel => -3.0, + Item::StonePickaxe => -2.8, + Item::StoneAxe => -3.2, + Item::StoneHoe => -2.0, + + Item::GoldenSword => -2.4, + Item::GoldenShovel => -3.0, + Item::GoldenPickaxe => -2.8, + Item::GoldenAxe => -3.0, + Item::GoldenHoe => -3.0, + + Item::IronSword => -2.4, + Item::IronShovel => -3.0, + Item::IronPickaxe => -2.8, + Item::IronAxe => -3.1, + Item::IronHoe => -1.0, + + Item::DiamondSword => -2.4, + Item::DiamondShovel => -3.0, + Item::DiamondPickaxe => -2.8, + Item::DiamondAxe => -3.0, + Item::DiamondHoe => 0.0, + + Item::NetheriteSword => -2.4, + Item::NetheriteShovel => -3.0, + Item::NetheritePickaxe => -2.8, + Item::NetheriteAxe => -3.0, + Item::NetheriteHoe => 0.0, + + Item::Trident => -2.9, + _ => 0., + }; + attributes + .attack_speed + .remove(&azalea_entity::attributes::BASE_ATTACK_SPEED_UUID); + attributes + .attack_speed + .insert(azalea_entity::attributes::tool_attack_speed_modifier( + added_attack_speed, + )) + .unwrap(); + } +} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index e36cb846..e8698fc7 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -12,6 +12,7 @@ #![feature(type_alias_impl_trait)] mod account; +pub mod attack; pub mod chat; mod client; pub mod disconnect; diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs index 049bc859..c087c467 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -18,6 +18,7 @@ use crate::{ }, inventory::InventoryComponent, local_player::{LocalGameMode, SendPacketEvent}, + Client, }; /// A plugin that allows clients to break blocks in the world. @@ -44,6 +45,15 @@ impl Plugin for MinePlugin { } } +impl Client { + pub fn start_mining(&mut self, position: BlockPos) { + self.ecs.lock().send_event(StartMiningBlockEvent { + entity: self.entity, + position, + }); + } +} + /// Information about the block we're currently mining. This is only present if /// we're currently mining a block. #[derive(Component)] diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 4ceb3999..b4f4e045 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -574,13 +574,14 @@ fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::AddEntity(p) => { debug!("Got add entity packet {:?}", p); + #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, Query>, - ResMut, + Res, ResMut, )> = SystemState::new(ecs); - let (mut commands, mut query, mut instance_container, mut entity_infos) = + let (mut commands, mut query, instance_container, mut entity_infos) = system_state.get_mut(ecs); let instance_name = query.get_mut(player_entity).unwrap(); @@ -694,9 +695,8 @@ fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::SetHealth(p) => { debug!("Got set health packet {:?}", p); - let mut system_state: SystemState<(Query<&mut Health>, EventWriter)> = - SystemState::new(ecs); - let (mut query, mut death_events) = system_state.get_mut(ecs); + let mut system_state: SystemState> = SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); let mut health = query.get_mut(player_entity).unwrap(); **health = p.health; diff --git a/azalea-entity/src/attributes.rs b/azalea-entity/src/attributes.rs index 97b890dc..18bbc348 100644 --- a/azalea-entity/src/attributes.rs +++ b/azalea-entity/src/attributes.rs @@ -13,6 +13,7 @@ use uuid::{uuid, Uuid}; #[derive(Clone, Debug, Component)] pub struct Attributes { pub speed: AttributeInstance, + pub attack_speed: AttributeInstance, } #[derive(Clone, Debug)] @@ -92,6 +93,24 @@ pub fn sprinting_modifier() -> AttributeModifier { } } +pub static BASE_ATTACK_SPEED_UUID: Uuid = uuid!("FA233E1C-4180-4865-B01B-BCCE9785ACA3"); +pub fn weapon_attack_speed_modifier(amount: f64) -> AttributeModifier { + AttributeModifier { + uuid: BASE_ATTACK_SPEED_UUID, + name: "Weapon modifier".to_string(), + amount, + operation: AttributeModifierOperation::Addition, + } +} +pub fn tool_attack_speed_modifier(amount: f64) -> AttributeModifier { + AttributeModifier { + uuid: BASE_ATTACK_SPEED_UUID, + name: "Tool modifier".to_string(), + amount, + operation: AttributeModifierOperation::Addition, + } +} + impl McBufReadable for AttributeModifier { fn read_from(buf: &mut Cursor<&[u8]>) -> Result { let uuid = Uuid::read_from(buf)?; diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 53e8bfdb..76c5220a 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -344,6 +344,7 @@ impl EntityBundle { // TODO: do the correct defaults for everything, some // entities have different defaults speed: AttributeInstance::new(0.1), + attack_speed: AttributeInstance::new(4.0), }, jumping: Jumping(false), diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs index 337ac6ec..75a6ca67 100644 --- a/azalea/examples/testbot.rs +++ b/azalea/examples/testbot.rs @@ -11,6 +11,8 @@ use azalea::pathfinder::BlockPosGoal; use azalea::protocol::packets::game::ClientboundGamePacket; use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection}; use azalea::{Account, Client, Event}; +use azalea_core::Vec3; +use azalea_world::{InstanceName, MinecraftEntityId}; use std::time::Duration; #[derive(Default, Clone, Component)] @@ -220,6 +222,56 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< println!("no container found"); } } + "attack" => { + let mut nearest_entity = None; + let mut nearest_distance = f64::INFINITY; + let mut nearest_pos = Vec3::default(); + let bot_position = bot.position(); + let bot_entity = bot.entity; + let bot_instance_name = bot.component::(); + { + let mut ecs = bot.ecs.lock(); + let mut query = ecs.query_filtered::<( + azalea::ecs::entity::Entity, + &MinecraftEntityId, + &Position, + &InstanceName, + &EyeHeight, + ), With>( + ); + for (entity, &entity_id, position, instance_name, eye_height) in + query.iter(&ecs) + { + if entity == bot_entity { + continue; + } + if instance_name != &bot_instance_name { + continue; + } + + let distance = bot_position.distance_to(position); + if distance < 4.0 && distance < nearest_distance { + nearest_entity = Some(entity_id); + nearest_distance = distance; + nearest_pos = position.up(**eye_height as f64); + } + } + } + if let Some(nearest_entity) = nearest_entity { + bot.look_at(nearest_pos); + bot.attack(nearest_entity); + bot.chat("attacking"); + let mut ticks = bot.get_tick_broadcaster(); + while ticks.recv().await.is_ok() { + if !bot.has_attack_cooldown() { + break; + } + } + bot.chat("finished attacking"); + } else { + bot.chat("no entities found"); + } + } _ => {} } } diff --git a/azalea/src/auto_respawn.rs b/azalea/src/auto_respawn.rs index 6d9c5954..77a75b4b 100644 --- a/azalea/src/auto_respawn.rs +++ b/azalea/src/auto_respawn.rs @@ -21,6 +21,5 @@ fn auto_respawn( perform_respawn_events.send(PerformRespawnEvent { entity: event.entity, }); - println!("auto respawning"); } } diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 7940e7b0..71d96c4d 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -8,7 +8,10 @@ use crate::ecs::{ query::{With, Without}, system::{Commands, Query}, }; -use azalea_core::Vec3; +use azalea_client::interact::SwingArmEvent; +use azalea_client::mining::Mining; +use azalea_client::TickBroadcast; +use azalea_core::{BlockPos, Vec3}; use azalea_entity::{ clamp_look_direction, metadata::Player, EyeHeight, Jumping, Local, LookDirection, Position, }; @@ -67,18 +70,25 @@ fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) { } pub trait BotClientExt { + /// Queue a jump for the next tick. fn jump(&mut self); + /// Turn the bot's head to look at the coordinate in the world. fn look_at(&mut self, pos: Vec3); + /// Get a receiver that will receive a message every tick. + fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; + /// Mine a block. This won't turn the bot's head towards the block, so if + /// that's necessary you'll have to do that yourself with [`look_at`]. + /// + /// [`look_at`]: crate::prelude::BotClientExt::look_at + async fn mine(&mut self, position: BlockPos); } impl BotClientExt for azalea_client::Client { - /// Queue a jump for the next tick. fn jump(&mut self) { let mut ecs = self.ecs.lock(); ecs.send_event(JumpEvent(self.entity)); } - /// Turn the bot's head to look at the coordinate in the world. fn look_at(&mut self, position: Vec3) { let mut ecs = self.ecs.lock(); ecs.send_event(LookAtEvent { @@ -86,6 +96,40 @@ impl BotClientExt for azalea_client::Client { position, }); } + + /// ``` + /// # use azalea::prelude::*; + /// # async fn example(mut bot: azalea::Client) { + /// let mut ticks = self.get_tick_broadcaster(); + /// while ticks.recv().await.is_ok() { + /// let ecs = bot.ecs.lock(); + /// if ecs.get::(self.entity).is_none() { + /// break; + /// } + /// } + /// # } + /// ``` + fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { + let ecs = self.ecs.lock(); + let tick_broadcast = ecs.resource::(); + tick_broadcast.subscribe() + } + + async fn mine(&mut self, position: BlockPos) { + self.start_mining(position); + // vanilla sends an extra swing arm packet when we start mining + self.ecs.lock().send_event(SwingArmEvent { + entity: self.entity, + }); + + let mut receiver = self.get_tick_broadcaster(); + while receiver.recv().await.is_ok() { + let ecs = self.ecs.lock(); + if ecs.get::(self.entity).is_none() { + break; + } + } + } } /// Event to jump once. diff --git a/azalea/src/container.rs b/azalea/src/container.rs index dc0ba169..2261469d 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -3,7 +3,7 @@ use std::fmt::Formatter; use azalea_client::{ inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent}, packet_handling::PacketEvent, - Client, TickBroadcast, + Client, }; use azalea_core::BlockPos; use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu}; @@ -12,6 +12,8 @@ use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::EventReader, system::Commands}; use std::fmt::Debug; +use crate::bot::BotClientExt; + pub struct ContainerPlugin; impl Plugin for ContainerPlugin { fn build(&self, app: &mut App) { @@ -49,11 +51,7 @@ impl ContainerClientExt for Client { .insert(WaitingForInventoryOpen); self.block_interact(pos); - let mut receiver = { - let ecs = self.ecs.lock(); - let tick_broadcast = ecs.resource::(); - tick_broadcast.subscribe() - }; + let mut receiver = self.get_tick_broadcaster(); while receiver.recv().await.is_ok() { let ecs = self.ecs.lock(); if ecs.get::(self.entity).is_none() { diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 297199a0..0aff2a56 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -6,7 +6,6 @@ mod auto_respawn; mod bot; mod container; -pub mod mining; pub mod pathfinder; pub mod prelude; pub mod swarm; diff --git a/azalea/src/mining.rs b/azalea/src/mining.rs deleted file mode 100644 index 8ba16436..00000000 --- a/azalea/src/mining.rs +++ /dev/null @@ -1,40 +0,0 @@ -use azalea_client::{ - interact::SwingArmEvent, - mining::{Mining, StartMiningBlockEvent}, - Client, TickBroadcast, -}; -use azalea_core::BlockPos; - -pub trait MiningExt { - /// Start mining a block. - async fn mine(&mut self, position: BlockPos); -} - -impl MiningExt for Client { - /// Start mining a block. This won't turn the bot's head towards the block, - /// so you'll have to do that yourself with [`look_at`]. - /// - /// [`look_at`]: crate::prelude::BotClientExt::look_at - async fn mine(&mut self, position: BlockPos) { - self.ecs.lock().send_event(StartMiningBlockEvent { - entity: self.entity, - position, - }); - // vanilla sends an extra swing arm packet when we start mining - self.ecs.lock().send_event(SwingArmEvent { - entity: self.entity, - }); - - let mut receiver = { - let ecs = self.ecs.lock(); - let tick_broadcast = ecs.resource::(); - tick_broadcast.subscribe() - }; - while receiver.recv().await.is_ok() { - let ecs = self.ecs.lock(); - if ecs.get::(self.entity).is_none() { - break; - } - } - } -} diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index ff3c11de..87cb0b53 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -2,8 +2,8 @@ //! re-exported here. pub use crate::{ - bot::BotClientExt, container::ContainerClientExt, mining::MiningExt, - pathfinder::PathfinderClientExt, ClientBuilder, + bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt, + ClientBuilder, }; pub use azalea_client::{Account, Client, Event}; // this is necessary to make the macros that reference bevy_ecs work