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    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
46/// A component that indicates that this client will be attacking the given
47/// entity next tick.
48#[derive(Clone, Component, Debug)]
49pub struct AttackQueued {
50    pub target: Entity,
51}
52#[allow(clippy::type_complexity)]
53pub fn handle_attack_queued(
54    mut commands: Commands,
55    mut query: Query<(
56        Entity,
57        &mut TicksSinceLastAttack,
58        &mut Physics,
59        &mut Sprinting,
60        &AttackQueued,
61        &LocalGameMode,
62        &Crouching,
63        &EntityIdIndex,
64    )>,
65) {
66    for (
67        client_entity,
68        mut ticks_since_last_attack,
69        mut physics,
70        mut sprinting,
71        attack_queued,
72        game_mode,
73        crouching,
74        entity_id_index,
75    ) in &mut query
76    {
77        let target_entity = attack_queued.target;
78        let Some(target_entity_id) = entity_id_index.get_by_ecs_entity(target_entity) else {
79            warn!("tried to attack entity {target_entity} which isn't in our EntityIdIndex");
80            continue;
81        };
82
83        commands.entity(client_entity).remove::<AttackQueued>();
84
85        commands.trigger(SendGamePacketEvent::new(
86            client_entity,
87            ServerboundInteract {
88                entity_id: target_entity_id,
89                action: s_interact::ActionType::Attack,
90                using_secondary_action: **crouching,
91            },
92        ));
93        commands.trigger(SwingArmEvent {
94            entity: client_entity,
95        });
96
97        // we can't attack if we're in spectator mode but it still sends the attack
98        // packet
99        if game_mode.current == GameMode::Spectator {
100            continue;
101        };
102
103        ticks_since_last_attack.0 = 0;
104
105        physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6);
106        **sprinting = false;
107    }
108}
109
110/// Queues up an attack packet for next tick by inserting the [`AttackQueued`]
111/// component to our client.
112#[derive(Message)]
113pub struct AttackEvent {
114    /// Our client entity that will send the packets to attack.
115    pub entity: Entity,
116    /// The entity that will be attacked.
117    pub target: Entity,
118}
119pub fn handle_attack_event(mut events: MessageReader<AttackEvent>, mut commands: Commands) {
120    for event in events.read() {
121        commands.entity(event.entity).insert(AttackQueued {
122            target: event.target,
123        });
124    }
125}
126
127#[derive(Bundle, Default)]
128pub struct AttackBundle {
129    pub ticks_since_last_attack: TicksSinceLastAttack,
130    pub attack_strength_scale: AttackStrengthScale,
131}
132
133#[derive(Clone, Component, Default, Deref, DerefMut)]
134pub struct TicksSinceLastAttack(pub u32);
135pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) {
136    for mut ticks_since_last_attack in query.iter_mut() {
137        **ticks_since_last_attack += 1;
138    }
139}
140
141#[derive(Clone, Component, Default, Deref, DerefMut)]
142pub struct AttackStrengthScale(pub f32);
143pub fn update_attack_strength_scale(
144    mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>,
145) {
146    for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() {
147        // look 0.5 ticks into the future because that's what vanilla does
148        **attack_strength_scale =
149            get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5);
150    }
151}
152
153/// Returns how long it takes for the attack cooldown to reset (in ticks).
154pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 {
155    ((1. / attributes.attack_speed.calculate()) * 20.) as f32
156}
157
158pub fn get_attack_strength_scale(
159    ticks_since_last_attack: u32,
160    attributes: &Attributes,
161    in_ticks: f32,
162) -> f32 {
163    let attack_strength_delay = get_attack_strength_delay(attributes);
164    let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay;
165    attack_strength.clamp(0., 1.)
166}