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