azalea_client/plugins/
attack.rs

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