azalea_client/plugins/
attack.rs1use 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 Client, 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 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
46impl Client {
47 pub fn attack(&self, entity: Entity) {
52 self.ecs.lock().write_message(AttackEvent {
53 entity: self.entity,
54 target: entity,
55 });
56 }
57
58 pub fn has_attack_cooldown(&self) -> bool {
62 let Some(attack_strength_scale) = self.get_component::<AttackStrengthScale>() else {
63 return false;
66 };
67 *attack_strength_scale < 1.0
68 }
69
70 pub fn attack_cooldown_remaining_ticks(&self) -> usize {
74 let mut ecs = self.ecs.lock();
75 let Ok((attributes, ticks_since_last_attack)) = ecs
76 .query::<(&Attributes, &TicksSinceLastAttack)>()
77 .get(&ecs, self.entity)
78 else {
79 return 0;
80 };
81
82 let attack_strength_delay = get_attack_strength_delay(attributes);
83 let remaining_ticks = attack_strength_delay - **ticks_since_last_attack as f32;
84
85 remaining_ticks.max(0.).ceil() as usize
86 }
87}
88
89#[derive(Clone, Component, Debug)]
92pub struct AttackQueued {
93 pub target: Entity,
94}
95#[allow(clippy::type_complexity)]
96pub fn handle_attack_queued(
97 mut commands: Commands,
98 mut query: Query<(
99 Entity,
100 &mut TicksSinceLastAttack,
101 &mut Physics,
102 &mut Sprinting,
103 &AttackQueued,
104 &LocalGameMode,
105 &Crouching,
106 &EntityIdIndex,
107 )>,
108) {
109 for (
110 client_entity,
111 mut ticks_since_last_attack,
112 mut physics,
113 mut sprinting,
114 attack_queued,
115 game_mode,
116 crouching,
117 entity_id_index,
118 ) in &mut query
119 {
120 let target_entity = attack_queued.target;
121 let Some(target_entity_id) = entity_id_index.get_by_ecs_entity(target_entity) else {
122 warn!("tried to attack entity {target_entity} which isn't in our EntityIdIndex");
123 continue;
124 };
125
126 commands.entity(client_entity).remove::<AttackQueued>();
127
128 commands.trigger(SendGamePacketEvent::new(
129 client_entity,
130 ServerboundInteract {
131 entity_id: target_entity_id,
132 action: s_interact::ActionType::Attack,
133 using_secondary_action: **crouching,
134 },
135 ));
136 commands.trigger(SwingArmEvent {
137 entity: client_entity,
138 });
139
140 if game_mode.current == GameMode::Spectator {
143 continue;
144 };
145
146 ticks_since_last_attack.0 = 0;
147
148 physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6);
149 **sprinting = false;
150 }
151}
152
153#[derive(Message)]
156pub struct AttackEvent {
157 pub entity: Entity,
159 pub target: Entity,
161}
162pub fn handle_attack_event(mut events: MessageReader<AttackEvent>, mut commands: Commands) {
163 for event in events.read() {
164 commands.entity(event.entity).insert(AttackQueued {
165 target: event.target,
166 });
167 }
168}
169
170#[derive(Bundle, Default)]
171pub struct AttackBundle {
172 pub ticks_since_last_attack: TicksSinceLastAttack,
173 pub attack_strength_scale: AttackStrengthScale,
174}
175
176#[derive(Clone, Component, Default, Deref, DerefMut)]
177pub struct TicksSinceLastAttack(pub u32);
178pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) {
179 for mut ticks_since_last_attack in query.iter_mut() {
180 **ticks_since_last_attack += 1;
181 }
182}
183
184#[derive(Clone, Component, Default, Deref, DerefMut)]
185pub struct AttackStrengthScale(pub f32);
186pub fn update_attack_strength_scale(
187 mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>,
188) {
189 for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() {
190 **attack_strength_scale =
192 get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5);
193 }
194}
195
196pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 {
198 ((1. / attributes.attack_speed.calculate()) * 20.) as f32
199}
200
201pub fn get_attack_strength_scale(
202 ticks_since_last_attack: u32,
203 attributes: &Attributes,
204 in_ticks: f32,
205) -> f32 {
206 let attack_strength_delay = get_attack_strength_delay(attributes);
207 let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay;
208 attack_strength.clamp(0., 1.)
209}