1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

Attacking (#96)

* add Client::attack

* partially implement attack cooldowns

* attack speed modifiers

* don't care clippy

---------

Co-authored-by: mat <git@matdoes.dev>
This commit is contained in:
mat 2023-07-15 04:39:43 -05:00 committed by GitHub
parent 148f203817
commit cde7e35046
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 359 additions and 65 deletions

138
azalea-client/src/attack.rs Normal file
View file

@ -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::<AttackEvent>()
.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::<AttackStrengthScale>();
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<AttackEvent>,
mut query: Query<(
&LocalGameMode,
&mut TicksSinceLastAttack,
&mut Physics,
&mut Sprinting,
&mut ShiftKeyDown,
)>,
mut send_packet_events: EventWriter<SendPacketEvent>,
mut swing_arm_event: EventWriter<SwingArmEvent>,
) {
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.)
}

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
attack::{self, AttackPlugin},
chat::ChatPlugin, chat::ChatPlugin,
disconnect::{DisconnectEvent, DisconnectPlugin}, disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents}, events::{Event, EventPlugin, LocalPlayerEvents},
@ -306,6 +307,7 @@ impl Client {
abilities: PlayerAbilities::default(), abilities: PlayerAbilities::default(),
permission_level: PermissionLevel::default(), permission_level: PermissionLevel::default(),
mining: mining::MineBundle::default(), mining: mining::MineBundle::default(),
attack: attack::AttackBundle::default(),
_local: Local, _local: Local,
}); });
@ -574,6 +576,7 @@ pub struct JoinedClientBundle {
pub permission_level: PermissionLevel, pub permission_level: PermissionLevel,
pub mining: mining::MineBundle, pub mining: mining::MineBundle,
pub attack: attack::AttackBundle,
pub _local: Local, pub _local: Local,
} }
@ -722,6 +725,7 @@ impl PluginGroup for DefaultPlugins {
.add(InteractPlugin) .add(InteractPlugin)
.add(RespawnPlugin) .add(RespawnPlugin)
.add(MinePlugin) .add(MinePlugin)
.add(AttackPlugin)
.add(TickBroadcastPlugin); .add(TickBroadcastPlugin);
#[cfg(feature = "log")] #[cfg(feature = "log")]
{ {

View file

@ -2,7 +2,9 @@ use std::ops::AddAssign;
use azalea_block::BlockState; use azalea_block::BlockState;
use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3}; 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_inventory::{ItemSlot, ItemSlotData};
use azalea_nbt::NbtList; use azalea_nbt::NbtList;
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
@ -17,6 +19,7 @@ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
event::{Event, EventReader, EventWriter}, event::{Event, EventReader, EventWriter},
query::{Changed, With},
schedule::IntoSystemConfigs, schedule::IntoSystemConfigs,
system::{Commands, Query, Res}, system::{Commands, Query, Res},
}; };
@ -39,12 +42,15 @@ impl Plugin for InteractPlugin {
.add_systems( .add_systems(
Update, Update,
( (
update_hit_result_component.after(clamp_look_direction), (
handle_block_interact_event, update_hit_result_component.after(clamp_look_direction),
handle_swing_arm_event, handle_block_interact_event,
) handle_swing_arm_event,
.before(handle_send_packet_event) )
.chain(), .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<Local>, Changed<InventoryComponent>),
>,
) {
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();
}
}

View file

@ -12,6 +12,7 @@
#![feature(type_alias_impl_trait)] #![feature(type_alias_impl_trait)]
mod account; mod account;
pub mod attack;
pub mod chat; pub mod chat;
mod client; mod client;
pub mod disconnect; pub mod disconnect;

View file

@ -18,6 +18,7 @@ use crate::{
}, },
inventory::InventoryComponent, inventory::InventoryComponent,
local_player::{LocalGameMode, SendPacketEvent}, local_player::{LocalGameMode, SendPacketEvent},
Client,
}; };
/// A plugin that allows clients to break blocks in the world. /// 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 /// Information about the block we're currently mining. This is only present if
/// we're currently mining a block. /// we're currently mining a block.
#[derive(Component)] #[derive(Component)]

View file

@ -574,13 +574,14 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::AddEntity(p) => { ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p); debug!("Got add entity packet {:?}", p);
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<( let mut system_state: SystemState<(
Commands, Commands,
Query<Option<&InstanceName>>, Query<Option<&InstanceName>>,
ResMut<InstanceContainer>, Res<InstanceContainer>,
ResMut<EntityInfos>, ResMut<EntityInfos>,
)> = SystemState::new(ecs); )> = 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); system_state.get_mut(ecs);
let instance_name = query.get_mut(player_entity).unwrap(); let instance_name = query.get_mut(player_entity).unwrap();
@ -694,9 +695,8 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::SetHealth(p) => { ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p); debug!("Got set health packet {:?}", p);
let mut system_state: SystemState<(Query<&mut Health>, EventWriter<DeathEvent>)> = let mut system_state: SystemState<Query<&mut Health>> = SystemState::new(ecs);
SystemState::new(ecs); let mut query = system_state.get_mut(ecs);
let (mut query, mut death_events) = system_state.get_mut(ecs);
let mut health = query.get_mut(player_entity).unwrap(); let mut health = query.get_mut(player_entity).unwrap();
**health = p.health; **health = p.health;

View file

@ -13,6 +13,7 @@ use uuid::{uuid, Uuid};
#[derive(Clone, Debug, Component)] #[derive(Clone, Debug, Component)]
pub struct Attributes { pub struct Attributes {
pub speed: AttributeInstance, pub speed: AttributeInstance,
pub attack_speed: AttributeInstance,
} }
#[derive(Clone, Debug)] #[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 { impl McBufReadable for AttributeModifier {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let uuid = Uuid::read_from(buf)?; let uuid = Uuid::read_from(buf)?;

View file

@ -344,6 +344,7 @@ impl EntityBundle {
// TODO: do the correct defaults for everything, some // TODO: do the correct defaults for everything, some
// entities have different defaults // entities have different defaults
speed: AttributeInstance::new(0.1), speed: AttributeInstance::new(0.1),
attack_speed: AttributeInstance::new(4.0),
}, },
jumping: Jumping(false), jumping: Jumping(false),

View file

@ -11,6 +11,8 @@ use azalea::pathfinder::BlockPosGoal;
use azalea::protocol::packets::game::ClientboundGamePacket; use azalea::protocol::packets::game::ClientboundGamePacket;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection}; use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event}; use azalea::{Account, Client, Event};
use azalea_core::Vec3;
use azalea_world::{InstanceName, MinecraftEntityId};
use std::time::Duration; use std::time::Duration;
#[derive(Default, Clone, Component)] #[derive(Default, Clone, Component)]
@ -220,6 +222,56 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
println!("no container found"); 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::<InstanceName>();
{
let mut ecs = bot.ecs.lock();
let mut query = ecs.query_filtered::<(
azalea::ecs::entity::Entity,
&MinecraftEntityId,
&Position,
&InstanceName,
&EyeHeight,
), With<MinecraftEntityId>>(
);
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");
}
}
_ => {} _ => {}
} }
} }

View file

@ -21,6 +21,5 @@ fn auto_respawn(
perform_respawn_events.send(PerformRespawnEvent { perform_respawn_events.send(PerformRespawnEvent {
entity: event.entity, entity: event.entity,
}); });
println!("auto respawning");
} }
} }

View file

@ -8,7 +8,10 @@ use crate::ecs::{
query::{With, Without}, query::{With, Without},
system::{Commands, Query}, 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::{ use azalea_entity::{
clamp_look_direction, metadata::Player, EyeHeight, Jumping, Local, LookDirection, Position, 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 { pub trait BotClientExt {
/// Queue a jump for the next tick.
fn jump(&mut self); fn jump(&mut self);
/// Turn the bot's head to look at the coordinate in the world.
fn look_at(&mut self, pos: Vec3); 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 { impl BotClientExt for azalea_client::Client {
/// Queue a jump for the next tick.
fn jump(&mut self) { fn jump(&mut self) {
let mut ecs = self.ecs.lock(); let mut ecs = self.ecs.lock();
ecs.send_event(JumpEvent(self.entity)); 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) { fn look_at(&mut self, position: Vec3) {
let mut ecs = self.ecs.lock(); let mut ecs = self.ecs.lock();
ecs.send_event(LookAtEvent { ecs.send_event(LookAtEvent {
@ -86,6 +96,40 @@ impl BotClientExt for azalea_client::Client {
position, 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::<WaitingForInventoryOpen>(self.entity).is_none() {
/// break;
/// }
/// }
/// # }
/// ```
fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
let ecs = self.ecs.lock();
let tick_broadcast = ecs.resource::<TickBroadcast>();
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::<Mining>(self.entity).is_none() {
break;
}
}
}
} }
/// Event to jump once. /// Event to jump once.

View file

@ -3,7 +3,7 @@ use std::fmt::Formatter;
use azalea_client::{ use azalea_client::{
inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent}, inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent},
packet_handling::PacketEvent, packet_handling::PacketEvent,
Client, TickBroadcast, Client,
}; };
use azalea_core::BlockPos; use azalea_core::BlockPos;
use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu}; 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 bevy_ecs::{component::Component, prelude::EventReader, system::Commands};
use std::fmt::Debug; use std::fmt::Debug;
use crate::bot::BotClientExt;
pub struct ContainerPlugin; pub struct ContainerPlugin;
impl Plugin for ContainerPlugin { impl Plugin for ContainerPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
@ -49,11 +51,7 @@ impl ContainerClientExt for Client {
.insert(WaitingForInventoryOpen); .insert(WaitingForInventoryOpen);
self.block_interact(pos); self.block_interact(pos);
let mut receiver = { let mut receiver = self.get_tick_broadcaster();
let ecs = self.ecs.lock();
let tick_broadcast = ecs.resource::<TickBroadcast>();
tick_broadcast.subscribe()
};
while receiver.recv().await.is_ok() { while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock(); let ecs = self.ecs.lock();
if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() { if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {

View file

@ -6,7 +6,6 @@
mod auto_respawn; mod auto_respawn;
mod bot; mod bot;
mod container; mod container;
pub mod mining;
pub mod pathfinder; pub mod pathfinder;
pub mod prelude; pub mod prelude;
pub mod swarm; pub mod swarm;

View file

@ -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::<TickBroadcast>();
tick_broadcast.subscribe()
};
while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock();
if ecs.get::<Mining>(self.entity).is_none() {
break;
}
}
}
}

View file

@ -2,8 +2,8 @@
//! re-exported here. //! re-exported here.
pub use crate::{ pub use crate::{
bot::BotClientExt, container::ContainerClientExt, mining::MiningExt, bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt,
pathfinder::PathfinderClientExt, ClientBuilder, ClientBuilder,
}; };
pub use azalea_client::{Account, Client, Event}; pub use azalea_client::{Account, Client, Event};
// this is necessary to make the macros that reference bevy_ecs work // this is necessary to make the macros that reference bevy_ecs work