azalea/
nearest_entity.rs

1use azalea_core::entity_id::MinecraftEntityId;
2use azalea_entity::Position;
3use azalea_world::WorldName;
4use bevy_ecs::{
5    prelude::Entity,
6    query::{QueryFilter, With},
7    system::{Query, SystemParam},
8};
9
10/// This system parameter can be used as a shorthand for quickly finding an
11/// entity, (or several) close to a given position.
12///
13/// This system parameter allows for additional filtering of entities based off
14/// of ECS marker components, such as `With<T>`, `Without<T>`, or `Added<T>`,
15/// etc. All functions used by this system parameter instance will respect the
16/// applied filter.
17///
18/// ```
19/// use azalea::{chat::SendChatEvent, nearest_entity::EntityFinder};
20/// use azalea_entity::{
21///     LocalEntity,
22///     metadata::{AbstractMonster, Player},
23/// };
24/// use bevy_ecs::{
25///     prelude::{Entity, MessageWriter},
26///     query::With,
27///     system::Query,
28/// };
29///
30/// /// All bots near aggressive mobs will scream in chat.
31/// pub fn bots_near_aggressive_mobs(
32///     bots: Query<Entity, (With<LocalEntity>, With<Player>)>,
33///     entity_finder: EntityFinder<With<AbstractMonster>>,
34///     mut chat_events: MessageWriter<SendChatEvent>,
35/// ) {
36///     for bot_id in bots.iter() {
37///         let Some(nearest) = entity_finder.nearest_to_entity(bot_id, 16.0) else {
38///             continue;
39///         };
40///
41///         chat_events.write(SendChatEvent {
42///             entity: bot_id,
43///             content: String::from("Ahhh!"),
44///         });
45///     }
46/// }
47/// ```
48#[derive(SystemParam)]
49pub struct EntityFinder<'w, 's, F = ()>
50where
51    F: QueryFilter + 'static,
52{
53    all_entities: Query<'w, 's, (&'static Position, &'static WorldName), With<MinecraftEntityId>>,
54
55    filtered_entities: Query<
56        'w,
57        's,
58        (Entity, &'static WorldName, &'static Position),
59        (With<MinecraftEntityId>, F),
60    >,
61}
62
63impl<'a, F> EntityFinder<'_, '_, F>
64where
65    F: QueryFilter + 'static,
66{
67    /// Gets the nearest entity to the given position and with the given world
68    /// name.
69    ///
70    /// This method will return `None` if there are no entities within range. If
71    /// multiple entities are within range, only the closest one is returned.
72    pub fn nearest_to_position(
73        &'a self,
74        position: Position,
75        world_name: &WorldName,
76        max_distance: f64,
77    ) -> Option<Entity> {
78        let mut nearest_entity = None;
79        let mut min_distance = max_distance;
80
81        for (target_entity, e_world, e_pos) in self.filtered_entities.iter() {
82            if e_world != world_name {
83                continue;
84            }
85
86            let target_distance = position.distance_to(**e_pos);
87            if target_distance < min_distance {
88                nearest_entity = Some(target_entity);
89                min_distance = target_distance;
90            }
91        }
92
93        nearest_entity
94    }
95
96    /// Gets the nearest entity to the given entity.
97    ///
98    /// This method will return `None` if there are no entities within range. If
99    /// multiple entities are within range, only the closest one is
100    /// returned.
101    pub fn nearest_to_entity(&'a self, entity: Entity, max_distance: f64) -> Option<Entity> {
102        let Ok((position, world_name)) = self.all_entities.get(entity) else {
103            return None;
104        };
105
106        let mut nearest_entity = None;
107        let mut min_distance = max_distance;
108
109        for (target_entity, e_world, e_pos) in self.filtered_entities.iter() {
110            if entity == target_entity {
111                continue;
112            };
113
114            if e_world != world_name {
115                continue;
116            }
117
118            let target_distance = position.distance_to(**e_pos);
119            if target_distance < min_distance {
120                nearest_entity = Some(target_entity);
121                min_distance = target_distance;
122            }
123        }
124
125        nearest_entity
126    }
127
128    /// This function get an iterator over all nearby entities to the given
129    /// position within the given maximum distance.
130    ///
131    /// The entities in this iterator are not returned in any specific order.
132    ///
133    /// This function returns the Entity ID of nearby entities and their
134    /// distance away.
135    pub fn nearby_entities_to_position(
136        &'a self,
137        position: &'a Position,
138        world_name: &'a WorldName,
139        max_distance: f64,
140    ) -> impl Iterator<Item = (Entity, f64)> + 'a {
141        self.filtered_entities
142            .iter()
143            .filter_map(move |(target_entity, e_world, e_pos)| {
144                if e_world != world_name {
145                    return None;
146                }
147
148                let distance = position.distance_to(**e_pos);
149                if distance < max_distance {
150                    Some((target_entity, distance))
151                } else {
152                    None
153                }
154            })
155    }
156
157    /// This function get an iterator over all nearby entities to the given
158    /// entity within the given maximum distance.
159    ///
160    /// The entities in this iterator are not returned in any specific order.
161    ///
162    /// This function returns the Entity ID of nearby entities and their
163    /// distance away.
164    pub fn nearby_entities_to_entity(
165        &'a self,
166        entity: Entity,
167        max_distance: f64,
168    ) -> impl Iterator<Item = (Entity, f64)> + 'a {
169        let position;
170        let world_name;
171        if let Ok((p, w)) = self.all_entities.get(entity) {
172            position = *p;
173            world_name = Some(w);
174        } else {
175            position = Position::default();
176            world_name = None;
177        };
178
179        self.filtered_entities
180            .iter()
181            .filter_map(move |(target_entity, e_world, e_pos)| {
182                if entity == target_entity {
183                    return None;
184                }
185
186                if Some(e_world) != world_name {
187                    return None;
188                }
189
190                let distance = position.distance_to(**e_pos);
191                if distance < max_distance {
192                    Some((target_entity, distance))
193                } else {
194                    None
195                }
196            })
197    }
198}