1use std::f64::consts::PI;
2
3use azalea_client::interact::SwingArmEvent;
4use azalea_client::mining::Mining;
5use azalea_client::TickBroadcast;
6use azalea_core::position::{BlockPos, Vec3};
7use azalea_core::tick::GameTick;
8use azalea_entity::{
9 clamp_look_direction, metadata::Player, EyeHeight, Jumping, LocalEntity, LookDirection,
10 Position,
11};
12use azalea_physics::PhysicsSet;
13use bevy_app::Update;
14use bevy_ecs::prelude::Event;
15use bevy_ecs::schedule::IntoSystemConfigs;
16use futures_lite::Future;
17use tracing::trace;
18
19use crate::accept_resource_packs::AcceptResourcePacksPlugin;
20use crate::app::{App, Plugin, PluginGroup, PluginGroupBuilder};
21use crate::auto_respawn::AutoRespawnPlugin;
22use crate::container::ContainerPlugin;
23use crate::ecs::{
24 component::Component,
25 entity::Entity,
26 event::EventReader,
27 query::{With, Without},
28 system::{Commands, Query},
29};
30use crate::pathfinder::PathfinderPlugin;
31
32#[derive(Clone, Default)]
33pub struct BotPlugin;
34impl Plugin for BotPlugin {
35 fn build(&self, app: &mut App) {
36 app.add_event::<LookAtEvent>()
37 .add_event::<JumpEvent>()
38 .add_systems(
39 Update,
40 (
41 insert_bot,
42 look_at_listener.before(clamp_look_direction),
43 jump_listener,
44 ),
45 )
46 .add_systems(
47 GameTick,
48 stop_jumping
49 .after(PhysicsSet)
50 .after(azalea_client::movement::send_player_input_packet),
51 );
52 }
53}
54
55#[derive(Default, Component)]
58pub struct Bot {
59 jumping_once: bool,
60}
61
62#[allow(clippy::type_complexity)]
64fn insert_bot(
65 mut commands: Commands,
66 mut query: Query<Entity, (Without<Bot>, With<LocalEntity>, With<Player>)>,
67) {
68 for entity in &mut query {
69 commands.entity(entity).insert(Bot::default());
70 }
71}
72
73fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) {
74 for (mut jumping, mut bot) in &mut query {
75 if bot.jumping_once && **jumping {
76 bot.jumping_once = false;
77 **jumping = false;
78 }
79 }
80}
81
82pub trait BotClientExt {
83 fn jump(&mut self);
85 fn look_at(&mut self, pos: Vec3);
87 fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
89 fn mine(&mut self, position: BlockPos) -> impl Future<Output = ()> + Send;
94}
95
96impl BotClientExt for azalea_client::Client {
97 fn jump(&mut self) {
98 let mut ecs = self.ecs.lock();
99 ecs.send_event(JumpEvent {
100 entity: self.entity,
101 });
102 }
103
104 fn look_at(&mut self, position: Vec3) {
105 let mut ecs = self.ecs.lock();
106 ecs.send_event(LookAtEvent {
107 entity: self.entity,
108 position,
109 });
110 }
111
112 fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
131 let ecs = self.ecs.lock();
132 let tick_broadcast = ecs.resource::<TickBroadcast>();
133 tick_broadcast.subscribe()
134 }
135
136 async fn mine(&mut self, position: BlockPos) {
137 self.start_mining(position);
138 self.ecs.lock().send_event(SwingArmEvent {
140 entity: self.entity,
141 });
142
143 let mut receiver = self.get_tick_broadcaster();
144 while receiver.recv().await.is_ok() {
145 let ecs = self.ecs.lock();
146 if ecs.get::<Mining>(self.entity).is_none() {
147 break;
148 }
149 }
150 }
151}
152
153#[derive(Event)]
155pub struct JumpEvent {
156 pub entity: Entity,
157}
158
159pub fn jump_listener(
160 mut query: Query<(&mut Jumping, &mut Bot)>,
161 mut events: EventReader<JumpEvent>,
162) {
163 for event in events.read() {
164 if let Ok((mut jumping, mut bot)) = query.get_mut(event.entity) {
165 **jumping = true;
166 bot.jumping_once = true;
167 }
168 }
169}
170
171#[derive(Event)]
173pub struct LookAtEvent {
174 pub entity: Entity,
175 pub position: Vec3,
177}
178fn look_at_listener(
179 mut events: EventReader<LookAtEvent>,
180 mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
181) {
182 for event in events.read() {
183 if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
184 let new_look_direction =
185 direction_looking_at(&position.up(eye_height.into()), &event.position);
186 trace!(
187 "look at {:?} (currently at {:?})",
188 event.position,
189 **position
190 );
191 *look_direction = new_look_direction;
192 }
193 }
194}
195
196pub fn direction_looking_at(current: &Vec3, target: &Vec3) -> LookDirection {
199 let delta = target - current;
201 let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
202 let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
203 let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
204
205 let y_rot = y_rot.rem_euclid(360.0);
207 let x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
208
209 LookDirection {
210 x_rot: x_rot as f32,
211 y_rot: y_rot as f32,
212 }
213}
214
215pub struct DefaultBotPlugins;
218
219impl PluginGroup for DefaultBotPlugins {
220 fn build(self) -> PluginGroupBuilder {
221 PluginGroupBuilder::start::<Self>()
222 .add(BotPlugin)
223 .add(PathfinderPlugin)
224 .add(ContainerPlugin)
225 .add(AutoRespawnPlugin)
226 .add(AcceptResourcePacksPlugin)
227 }
228}