azalea_client/plugins/
attack.rs

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