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