azalea_client/plugins/
attack.rs

1use azalea_core::{game_type::GameMode, tick::GameTick};
2use azalea_entity::{
3    Attributes, Crouching, Physics, indexing::EntityIdIndex, metadata::Sprinting,
4    update_bounding_box,
5};
6use azalea_physics::PhysicsSystems;
7use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract};
8use bevy_app::{App, Plugin, Update};
9use bevy_ecs::prelude::*;
10use derive_more::{Deref, DerefMut};
11use tracing::warn;
12
13use super::packet::game::SendGamePacketEvent;
14use crate::{
15    Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSystems,
16    respawn::perform_respawn,
17};
18
19pub struct AttackPlugin;
20impl Plugin for AttackPlugin {
21    fn build(&self, app: &mut App) {
22        app.add_message::<AttackEvent>()
23            .add_systems(
24                Update,
25                handle_attack_event
26                    .before(update_bounding_box)
27                    .before(MoveEventsSystems)
28                    .after(perform_respawn),
29            )
30            .add_systems(
31                GameTick,
32                (
33                    increment_ticks_since_last_attack,
34                    update_attack_strength_scale.after(PhysicsSystems),
35                    // in vanilla, handle_attack_queued is part of `handleKeybinds`
36                    handle_attack_queued
37                        .before(super::movement::send_sprinting_if_needed)
38                        .before(super::tick_end::game_tick_packet)
39                        .before(super::movement::send_position),
40                )
41                    .chain(),
42            );
43    }
44}
45
46impl Client {
47    /// Attack an entity in the world.
48    ///
49    /// This doesn't automatically look at the entity or perform any
50    /// range/visibility checks, so it might trigger anticheats.
51    pub fn attack(&self, entity: Entity) {
52        self.ecs.lock().write_message(AttackEvent {
53            entity: self.entity,
54            target: entity,
55        });
56    }
57
58    /// Whether the player has an attack cooldown.
59    ///
60    /// Also see [`Client::attack_cooldown_remaining_ticks`].
61    pub fn has_attack_cooldown(&self) -> bool {
62        let Some(attack_strength_scale) = self.get_component::<AttackStrengthScale>() else {
63            // they don't even have an AttackStrengthScale so they probably can't even
64            // attack? whatever, just return false
65            return false;
66        };
67        *attack_strength_scale < 1.0
68    }
69
70    /// Returns the number of ticks until we can attack at full strength again.
71    ///
72    /// Also see [`Client::has_attack_cooldown`].
73    pub fn attack_cooldown_remaining_ticks(&self) -> usize {
74        let mut ecs = self.ecs.lock();
75        let Ok((attributes, ticks_since_last_attack)) = ecs
76            .query::<(&Attributes, &TicksSinceLastAttack)>()
77            .get(&ecs, self.entity)
78        else {
79            return 0;
80        };
81
82        let attack_strength_delay = get_attack_strength_delay(attributes);
83        let remaining_ticks = attack_strength_delay - **ticks_since_last_attack as f32;
84
85        remaining_ticks.max(0.).ceil() as usize
86    }
87}
88
89/// A component that indicates that this client will be attacking the given
90/// entity next tick.
91#[derive(Clone, Component, Debug)]
92pub struct AttackQueued {
93    pub target: Entity,
94}
95#[allow(clippy::type_complexity)]
96pub fn handle_attack_queued(
97    mut commands: Commands,
98    mut query: Query<(
99        Entity,
100        &mut TicksSinceLastAttack,
101        &mut Physics,
102        &mut Sprinting,
103        &AttackQueued,
104        &LocalGameMode,
105        &Crouching,
106        &EntityIdIndex,
107    )>,
108) {
109    for (
110        client_entity,
111        mut ticks_since_last_attack,
112        mut physics,
113        mut sprinting,
114        attack_queued,
115        game_mode,
116        crouching,
117        entity_id_index,
118    ) in &mut query
119    {
120        let target_entity = attack_queued.target;
121        let Some(target_entity_id) = entity_id_index.get_by_ecs_entity(target_entity) else {
122            warn!("tried to attack entity {target_entity} which isn't in our EntityIdIndex");
123            continue;
124        };
125
126        commands.entity(client_entity).remove::<AttackQueued>();
127
128        commands.trigger(SendGamePacketEvent::new(
129            client_entity,
130            ServerboundInteract {
131                entity_id: target_entity_id,
132                action: s_interact::ActionType::Attack,
133                using_secondary_action: **crouching,
134            },
135        ));
136        commands.trigger(SwingArmEvent {
137            entity: client_entity,
138        });
139
140        // we can't attack if we're in spectator mode but it still sends the attack
141        // packet
142        if game_mode.current == GameMode::Spectator {
143            continue;
144        };
145
146        ticks_since_last_attack.0 = 0;
147
148        physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6);
149        **sprinting = false;
150    }
151}
152
153/// Queues up an attack packet for next tick by inserting the [`AttackQueued`]
154/// component to our client.
155#[derive(Message)]
156pub struct AttackEvent {
157    /// Our client entity that will send the packets to attack.
158    pub entity: Entity,
159    /// The entity that will be attacked.
160    pub target: Entity,
161}
162pub fn handle_attack_event(mut events: MessageReader<AttackEvent>, mut commands: Commands) {
163    for event in events.read() {
164        commands.entity(event.entity).insert(AttackQueued {
165            target: event.target,
166        });
167    }
168}
169
170#[derive(Bundle, Default)]
171pub struct AttackBundle {
172    pub ticks_since_last_attack: TicksSinceLastAttack,
173    pub attack_strength_scale: AttackStrengthScale,
174}
175
176#[derive(Clone, Component, Default, Deref, DerefMut)]
177pub struct TicksSinceLastAttack(pub u32);
178pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) {
179    for mut ticks_since_last_attack in query.iter_mut() {
180        **ticks_since_last_attack += 1;
181    }
182}
183
184#[derive(Clone, Component, Default, Deref, DerefMut)]
185pub struct AttackStrengthScale(pub f32);
186pub fn update_attack_strength_scale(
187    mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>,
188) {
189    for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() {
190        // look 0.5 ticks into the future because that's what vanilla does
191        **attack_strength_scale =
192            get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5);
193    }
194}
195
196/// Returns how long it takes for the attack cooldown to reset (in ticks).
197pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 {
198    ((1. / attributes.attack_speed.calculate()) * 20.) as f32
199}
200
201pub fn get_attack_strength_scale(
202    ticks_since_last_attack: u32,
203    attributes: &Attributes,
204    in_ticks: f32,
205) -> f32 {
206    let attack_strength_delay = get_attack_strength_delay(attributes);
207    let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay;
208    attack_strength.clamp(0., 1.)
209}