1use azalea_core::{
2 aabb::Aabb,
3 direction::Direction,
4 hit_result::{BlockHitResult, EntityHitResult, HitResult},
5 position::Vec3,
6};
7use azalea_entity::{
8 Attributes, Dead, LocalEntity, LookDirection, Physics, Position,
9 dimensions::EntityDimensions,
10 metadata::{
11 AbstractArrow, AbstractBoat, AbstractLiving, AbstractMinecart, ArmorStand,
12 ArmorStandMarker, EndCrystal, FallingBlock, InGround, Interaction, ShulkerBullet, Tnt,
13 },
14 view_vector,
15};
16use azalea_physics::{
17 clip::{BlockShapeType, ClipContext, FluidPickType},
18 collision::entity_collisions::{AabbQuery, get_entities},
19};
20use azalea_world::{Instance, InstanceContainer, InstanceName};
21use bevy_ecs::prelude::*;
22use derive_more::{Deref, DerefMut};
23
24#[doc(alias("looking at", "looking at block", "crosshair"))]
27#[derive(Component, Clone, Debug, Deref, DerefMut)]
28pub struct HitResultComponent(HitResult);
29
30#[allow(clippy::type_complexity)]
31pub fn update_hit_result_component(
32 mut commands: Commands,
33 mut query: Query<
34 (
35 Entity,
36 Option<&mut HitResultComponent>,
37 &Position,
38 &EntityDimensions,
39 &LookDirection,
40 &InstanceName,
41 &Physics,
42 &Attributes,
43 ),
44 With<LocalEntity>,
45 >,
46 instance_container: Res<InstanceContainer>,
47 aabb_query: AabbQuery,
48 pickable_query: MaybePickableEntityQuery,
49) {
50 for (
51 entity,
52 hit_result_ref,
53 position,
54 dimensions,
55 look_direction,
56 world_name,
57 physics,
58 attributes,
59 ) in &mut query
60 {
61 let block_pick_range = attributes.block_interaction_range.calculate();
62 let entity_pick_range = attributes.entity_interaction_range.calculate();
63
64 let eye_position = position.up(dimensions.eye_height.into());
65
66 let Some(world_lock) = instance_container.get(world_name) else {
67 continue;
68 };
69 let world = world_lock.read();
70
71 let hit_result = pick(PickOpts {
72 source_entity: entity,
73 look_direction: *look_direction,
74 eye_position,
75 aabb: &physics.bounding_box,
76 world: &world,
77 entity_pick_range,
78 block_pick_range,
79 aabb_query: &aabb_query,
80 pickable_query: &pickable_query,
81 });
82 if let Some(mut hit_result_ref) = hit_result_ref {
83 **hit_result_ref = hit_result;
84 } else {
85 commands
86 .entity(entity)
87 .insert(HitResultComponent(hit_result));
88 }
89 }
90}
91
92pub type MaybePickableEntityQuery<'world, 'state, 'a> = Query<
93 'world,
94 'state,
95 (Option<&'a ArmorStandMarker>, Option<&'a InGround>),
96 (
98 Or<(
99 (With<Tnt>, Without<Dead>),
102 (With<FallingBlock>, Without<Dead>),
103 (With<AbstractMinecart>, Without<Dead>),
104 (With<AbstractBoat>, Without<Dead>),
105 With<ArmorStand>,
106 With<EndCrystal>,
107 With<Interaction>,
108 With<ShulkerBullet>,
109 (With<AbstractLiving>, Without<Dead>),
110 With<AbstractArrow>,
111 )>,
112 ),
113>;
114
115pub struct PickOpts<'world, 'state, 'a, 'b, 'c> {
116 source_entity: Entity,
117 look_direction: LookDirection,
118 eye_position: Vec3,
119 aabb: &'a Aabb,
120 world: &'a Instance,
121 entity_pick_range: f64,
122 block_pick_range: f64,
123 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
124 pickable_query: &'a MaybePickableEntityQuery<'world, 'state, 'c>,
125}
126
127pub fn pick(opts: PickOpts<'_, '_, '_, '_, '_>) -> HitResult {
135 let mut max_range = opts.entity_pick_range.max(opts.block_pick_range);
140 let mut max_range_squared = max_range.powi(2);
141
142 let block_hit_result = pick_block(
143 opts.look_direction,
144 opts.eye_position,
145 &opts.world.chunks,
146 max_range,
147 );
148 let block_hit_result_dist_squared = block_hit_result
149 .location
150 .distance_squared_to(opts.eye_position);
151 if !block_hit_result.miss {
152 max_range_squared = block_hit_result_dist_squared;
153 max_range = block_hit_result_dist_squared.sqrt();
154 }
155
156 let view_vector = view_vector(opts.look_direction);
157 let end_position = opts.eye_position + (view_vector * max_range);
158 let inflate_by = 1.;
159 let pick_aabb = opts
160 .aabb
161 .expand_towards(view_vector * max_range)
162 .inflate_all(inflate_by);
163
164 let is_pickable = |entity: Entity| {
165 if entity == opts.source_entity {
166 return false;
167 }
168
169 if let Ok((armor_stand_marker, arrow_in_ground)) = opts.pickable_query.get(entity) {
172 !(armor_stand_marker == Some(&ArmorStandMarker(true))
173 || arrow_in_ground == Some(&InGround(true)))
174 } else {
175 false
176 }
177 };
178 let entity_hit_result = pick_entity(PickEntityOpts {
179 source_entity: opts.source_entity,
180 eye_position: opts.eye_position,
181 end_position,
182 world: opts.world,
183 pick_range_squared: max_range_squared,
184 predicate: &is_pickable,
185 aabb: &pick_aabb,
186 aabb_query: opts.aabb_query,
187 });
188
189 if let Some(entity_hit_result) = entity_hit_result
190 && entity_hit_result
191 .location
192 .distance_squared_to(opts.eye_position)
193 < block_hit_result_dist_squared
194 {
195 filter_hit_result(
196 HitResult::Entity(entity_hit_result),
197 opts.eye_position,
198 opts.entity_pick_range,
199 )
200 } else {
201 filter_hit_result(
202 HitResult::Block(block_hit_result),
203 opts.eye_position,
204 opts.block_pick_range,
205 )
206 }
207}
208
209fn filter_hit_result(hit_result: HitResult, eye_position: Vec3, range: f64) -> HitResult {
210 let location = hit_result.location();
211 if !location.closer_than(eye_position, range) {
212 let direction = Direction::nearest(location - eye_position);
213 HitResult::new_miss(location, direction, location.into())
214 } else {
215 hit_result
216 }
217}
218
219pub fn pick_block(
226 look_direction: LookDirection,
227 eye_position: Vec3,
228 chunks: &azalea_world::ChunkStorage,
229 pick_range: f64,
230) -> BlockHitResult {
231 let view_vector = view_vector(look_direction);
232 let end_position = eye_position + (view_vector * pick_range);
233
234 azalea_physics::clip::clip(
235 chunks,
236 ClipContext {
237 from: eye_position,
238 to: end_position,
239 block_shape_type: BlockShapeType::Outline,
240 fluid_pick_type: FluidPickType::None,
241 },
242 )
243}
244
245struct PickEntityOpts<'world, 'state, 'a, 'b> {
246 source_entity: Entity,
247 eye_position: Vec3,
248 end_position: Vec3,
249 world: &'a azalea_world::Instance,
250 pick_range_squared: f64,
251 predicate: &'a dyn Fn(Entity) -> bool,
252 aabb: &'a Aabb,
253 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
254}
255
256fn pick_entity(opts: PickEntityOpts) -> Option<EntityHitResult> {
258 let mut picked_distance_squared = opts.pick_range_squared;
259 let mut result = None;
260
261 for (candidate, candidate_aabb) in get_entities(
262 opts.world,
263 Some(opts.source_entity),
264 opts.aabb,
265 opts.predicate,
266 opts.aabb_query,
267 ) {
268 let candidate_pick_radius = 0.;
272 let candidate_aabb = candidate_aabb.inflate_all(candidate_pick_radius);
273 let clip_location = candidate_aabb.clip(opts.eye_position, opts.end_position);
274
275 if candidate_aabb.contains(opts.eye_position) {
276 if picked_distance_squared >= 0. {
277 result = Some(EntityHitResult {
278 location: clip_location.unwrap_or(opts.eye_position),
279 entity: candidate,
280 });
281 picked_distance_squared = 0.;
282 }
283 } else if let Some(clip_location) = clip_location {
284 let distance_squared = opts.eye_position.distance_squared_to(clip_location);
285 if distance_squared < picked_distance_squared || picked_distance_squared == 0. {
286 result = Some(EntityHitResult {
294 location: clip_location,
295 entity: candidate,
296 });
297 picked_distance_squared = distance_squared;
298 }
299 }
300 }
301
302 result
303}