Skip to main content

azalea_client/plugins/
attack.rs

1use azalea_core::{game_type::GameMode, tick::GameTick};
2use azalea_entity::{
3    Attributes, Physics, indexing::EntityIdIndex, metadata::Sprinting, update_bounding_box,
4};
5use azalea_physics::PhysicsSystems;
6use azalea_protocol::packets::game::ServerboundAttack;
7use bevy_app::{App, Plugin, Update};
8use bevy_ecs::prelude::*;
9use derive_more::{Deref, DerefMut};
10use tracing::warn;
11
12use super::packet::game::SendGamePacketEvent;
13use crate::{
14    interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSystems,
15    respawn::perform_respawn,
16};
17
18pub struct AttackPlugin;
19impl Plugin for AttackPlugin {
20    fn build(&self, app: &mut App) {
21        app.add_message::<AttackEvent>()
22            .add_systems(
23                Update,
24                handle_attack_event
25                    .before(update_bounding_box)
26                    .before(MoveEventsSystems)
27                    .after(perform_respawn),
28            )
29            .add_systems(
30                GameTick,
31                (
32                    increment_ticks_since_last_attack,
33                    update_attack_strength_scale.after(PhysicsSystems),
34                    // in vanilla, handle_attack_queued is part of `handleKeybinds`
35                    handle_attack_queued
36                        .before(super::movement::send_sprinting_if_needed)
37                        .before(super::tick_end::game_tick_packet)
38                        .before(super::movement::send_position),
39                )
40                    .chain(),
41            );
42    }
43}
44
45/// A component that indicates that this client will be attacking the given
46/// entity next tick.
47#[derive(Clone, Component, Debug)]
48pub struct AttackQueued {
49    pub target: Entity,
50}
51#[allow(clippy::type_complexity)]
52pub fn handle_attack_queued(
53    mut commands: Commands,
54    mut query: Query<(
55        Entity,
56        &mut TicksSinceLastAttack,
57        &mut Physics,
58        &mut Sprinting,
59        &AttackQueued,
60        &LocalGameMode,
61        &EntityIdIndex,
62    )>,
63) {
64    for (
65        client_entity,
66        mut ticks_since_last_attack,
67        mut physics,
68        mut sprinting,
69        attack_queued,
70        game_mode,
71        entity_id_index,
72    ) in &mut query
73    {
74        let target_entity = attack_queued.target;
75        let Some(target_entity_id) = entity_id_index.get_by_ecs_entity(target_entity) else {
76            warn!("tried to attack entity {target_entity} which isn't in our EntityIdIndex");
77            continue;
78        };
79
80        commands.entity(client_entity).remove::<AttackQueued>();
81
82        commands.trigger(SendGamePacketEvent::new(
83            client_entity,
84            ServerboundAttack {
85                entity_id: target_entity_id,
86            },
87        ));
88        commands.trigger(SwingArmEvent {
89            entity: client_entity,
90        });
91
92        // we can't attack if we're in spectator mode but it still sends the attack
93        // packet
94        if game_mode.current == GameMode::Spectator {
95            continue;
96        };
97
98        ticks_since_last_attack.0 = 0;
99
100        physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6);
101        **sprinting = false;
102    }
103}
104
105/// Queues up an attack packet for next tick by inserting the [`AttackQueued`]
106/// component to our client.
107#[derive(Message)]
108pub struct AttackEvent {
109    /// Our client entity that will send the packets to attack.
110    pub entity: Entity,
111    /// The entity that will be attacked.
112    pub target: Entity,
113}
114pub fn handle_attack_event(mut events: MessageReader<AttackEvent>, mut commands: Commands) {
115    for event in events.read() {
116        commands.entity(event.entity).insert(AttackQueued {
117            target: event.target,
118        });
119    }
120}
121
122#[derive(Bundle, Default)]
123pub struct AttackBundle {
124    pub ticks_since_last_attack: TicksSinceLastAttack,
125    pub attack_strength_scale: AttackStrengthScale,
126}
127
128#[derive(Clone, Component, Default, Deref, DerefMut)]
129pub struct TicksSinceLastAttack(pub u32);
130pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) {
131    for mut ticks_since_last_attack in query.iter_mut() {
132        **ticks_since_last_attack += 1;
133    }
134}
135
136#[derive(Clone, Component, Default, Deref, DerefMut)]
137pub struct AttackStrengthScale(pub f32);
138pub fn update_attack_strength_scale(
139    mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>,
140) {
141    for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() {
142        // look 0.5 ticks into the future because that's what vanilla does
143        **attack_strength_scale =
144            get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5);
145    }
146}
147
148/// Returns how long it takes for the attack cooldown to reset (in ticks).
149pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 {
150    ((1. / attributes.attack_speed.calculate()) * 20.) as f32
151}
152
153pub fn get_attack_strength_scale(
154    ticks_since_last_attack: u32,
155    attributes: &Attributes,
156    in_ticks: f32,
157) -> f32 {
158    let attack_strength_delay = get_attack_strength_delay(attributes);
159    let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay;
160    attack_strength.clamp(0., 1.)
161}