1use specs::{Join, LendJoin, WorldExt};
2use vek::*;
3
4use client::{self, Client};
5use common::{
6 comp::{self, CapsulePrism, Health, tool::ToolKind},
7 consts::{MAX_INTERACT_RANGE, MAX_PICKUP_RANGE},
8 link::Is,
9 mounting::{Mount, Rider},
10 uid::Uid,
11 util::{
12 find_dist::{Cylinder, FindDist},
13 lines::closest_points_3d,
14 },
15 vol::ReadVol,
16};
17use common_base::span;
18
19#[derive(Clone, Copy, Debug)]
20pub struct Target<T> {
21 pub kind: T,
22 pub position: Vec3<f32>,
23}
24
25#[derive(Clone, Copy, Debug)]
26pub struct Build(pub Vec3<f32>);
27
28#[derive(Clone, Copy, Debug)]
29pub struct Collectable;
30
31#[derive(Clone, Copy, Debug)]
32pub struct Entity(pub specs::Entity);
33
34#[derive(Clone, Copy, Debug)]
35pub struct Mine;
36
37#[derive(Clone, Copy, Debug)]
38pub struct Terrain;
40
41impl<T> Target<T> {
42 pub fn position_int(self) -> Vec3<i32> { self.position.map(|p| p.floor() as i32) }
43}
44
45pub const MAX_TARGET_RANGE: f32 = 500.0;
47
48pub(super) fn targets_under_cursor(
50 client: &Client,
51 cam_pos: Vec3<f32>,
52 cam_dir: Vec3<f32>,
53 can_build: bool,
54 active_mine_tool: Option<ToolKind>,
55 viewpoint_entity: specs::Entity,
56) -> (
57 Option<Target<Build>>,
58 Option<Target<Collectable>>,
59 Option<Target<Entity>>,
60 Option<Target<Mine>>,
61 Option<Target<Terrain>>,
62) {
63 span!(_guard, "targets_under_cursor");
64 let player_entity = client.entity();
66 let ecs = client.state().ecs();
67 let positions = ecs.read_storage::<comp::Pos>();
68 let player_pos = match positions.get(player_entity) {
69 Some(pos) => pos.0,
70 None => cam_pos, };
72 let scales = ecs.read_storage();
73 let colliders = ecs.read_storage();
74 let char_states = ecs.read_storage::<comp::CharacterState>();
75 let player_char_state = char_states.get(player_entity);
76 let player_scale = scales.get(player_entity).copied();
77 let player_cylinder = Cylinder::from_components(
79 player_pos,
80 player_scale,
81 colliders.get(player_entity),
82 player_char_state,
83 );
84 let eye_height = ecs
85 .read_storage::<comp::Body>()
86 .get(player_entity)
87 .map(|b| b.eye_height(player_scale.map_or(1.0, |s| s.0)))
88 .unwrap_or(0.0);
89 let eye_pos = player_pos + Vec3::unit_z() * eye_height;
90 let terrain = client.state().terrain();
91
92 const MAX_RAY_DIST: f32 = MAX_TARGET_RANGE
94 .max(MAX_PICKUP_RANGE)
95 .max(MAX_INTERACT_RANGE)
96 + 1.0;
97 let ray = terrain
98 .ray(cam_pos, cam_pos + cam_dir * MAX_RAY_DIST)
99 .max_iter(500);
100
101 let break_tgt_pos = |dist: f32| (cam_pos + cam_dir * (dist + 0.01)).map(|e| e.floor() + 0.5);
102 let place_tgt_pos = |dist: f32| (cam_pos + cam_dir * (dist - 0.01)).map(|e| e.floor() + 0.5);
103
104 let collect_cast = Some(ray.until(|b| b.is_solid() || b.is_directly_collectible()).cast())
105 .filter(|(_, b)| matches!(b, Ok(Some(b)) if b.is_directly_collectible()))
106 .filter(|(d, _)| player_pos.distance(break_tgt_pos(*d)) < MAX_INTERACT_RANGE);
108 let mine_cast = Some(ray.until(|b| b.is_solid() || b.mine_tool().is_some()).cast())
109 .filter(|(_, b)| matches!(b, Ok(Some(b)) if b.mine_tool().zip(active_mine_tool).map_or(false, |(a, b)| a == b)))
111 .filter(|(d, _)| eye_pos.distance(break_tgt_pos(*d)) < MAX_PICKUP_RANGE);
113 let build_cast = Some(ray.until(|b| b.is_solid()).cast())
114 .filter(|(d, _)| *d < MAX_TARGET_RANGE);
116 let obstacle_cast =
118 Some(ray.until(|b| b.is_filled()).cast()).filter(|(d, _)| *d < MAX_TARGET_RANGE);
119
120 let max_target_dist = obstacle_cast.map_or(MAX_TARGET_RANGE, |(d, _)| d);
123 let cam_segment = LineSegment3 {
124 start: cam_pos,
125 end: cam_pos + cam_dir * max_target_dist,
126 };
127
128 let uids = ecs.read_storage::<Uid>();
129
130 let player_wielding = player_char_state.is_some_and(|cs| cs.is_wield());
134 let mut nearby = (
135 &ecs.entities(),
136 &positions,
137 scales.maybe(),
138 &ecs.read_storage::<comp::Body>(),
139 ecs.read_storage::<comp::PickupItem>().maybe(),
140 !&ecs.read_storage::<Is<Mount>>(),
141 ecs.read_storage::<Is<Rider>>().maybe(),
142 ecs.read_storage::<Health>().maybe(),
143 )
144 .join()
145 .filter(|(e, _, _, _, _, _, _, _)| *e != viewpoint_entity)
146 .filter_map(|(e, p, s, b, i, _, is_rider, health)| {
147 const RADIUS_SCALE: f32 = 3.0;
148 let radius = s.map_or(1.0, |s| s.0) * (b.dimensions() * Vec3::new(1.0, 1.0, 0.5)).reduce_partial_max() * RADIUS_SCALE;
150 let height = s.map_or(1.0, |s| s.0) * b.height();
151 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + (height / 2.0));
153 let dist_sqr = pos.distance_squared(cam_pos);
155 let not_riding_player = is_rider.is_none_or(|is_rider| Some(&is_rider.mount) != uids.get(viewpoint_entity));
158 if (i.is_some() || !matches!(b, comp::Body::Object(_)) || health.is_some()) && not_riding_player {
159 Some((e, pos, radius, dist_sqr))
160 } else {
161 None
162 }
163 })
164 .filter(|(_, _, r, d_sqr)| *d_sqr <= max_target_dist.powi(2) + 2.0 * max_target_dist * r + r.powi(2))
166 .filter(|(_, _, r, d_sqr)| player_wielding || *d_sqr > r.powi(2))
168 .map(|(e, p, r, d_sqr)| {
170 (e, p, r, d_sqr.sqrt() - r, cam_segment.distance_to_point(p))
171 })
172 .collect::<Vec<_>>();
173
174 if player_wielding {
177 nearby.sort_unstable_by(|a, b| a.4.partial_cmp(&b.4).unwrap());
178 } else {
179 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
180 }
181
182 let seg_ray = LineSegment3 {
183 start: cam_pos,
184 end: cam_pos + cam_dir * max_target_dist,
185 };
186 let entity_target = nearby
188 .iter()
189 .map(|(e, p, r, _, _)| (e, *p, r))
190 .find(|(_, p, r)| {
192 if player_wielding {
193 seg_ray.projected_point(*p).distance_squared(*p) < (*r + cam_pos.distance(*p) / 10.0).powi(2)
194 } else {
195 seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2)
196 }
197 })
198 .and_then(|(e, p, _)| {
199 let target_cylinder = Cylinder::from_components(
201 p,
202 scales.get(*e).copied(),
203 colliders.get(*e),
204 char_states.get(*e),
205 );
206
207 if player_cylinder.min_distance(target_cylinder) < MAX_TARGET_RANGE {
208 Some(Target {
209 kind: Entity(*e),
210 position: p,
211 })
212 } else { None }
213 });
214
215 let terrain_target = obstacle_cast.map(|(d, _)| Target {
216 kind: Terrain,
217 position: break_tgt_pos(d),
218 });
219
220 let build_target = if let (true, Some((d, _))) = (can_build, build_cast) {
221 Some(Target {
222 kind: Build(place_tgt_pos(d)),
223 position: break_tgt_pos(d),
224 })
225 } else {
226 None
227 };
228
229 let collect_target = collect_cast.map(|(d, _)| Target {
230 kind: Collectable,
231 position: break_tgt_pos(d),
232 });
233
234 let mine_target = mine_cast.map(|(d, _)| Target {
235 kind: Mine,
236 position: break_tgt_pos(d),
237 });
238
239 (
242 build_target,
243 collect_target,
244 entity_target,
245 mine_target,
246 terrain_target,
247 )
248}
249
250pub(super) fn ray_entities(
251 client: &Client,
252 start: Vec3<f32>,
253 end: Vec3<f32>,
254 cast_dist: f32,
255) -> (f32, Option<Entity>) {
256 let player_entity = client.entity();
257 let ecs = client.state().ecs();
258 let positions = ecs.read_storage::<comp::Pos>();
259 let colliders = ecs.read_storage::<comp::Collider>();
260
261 let mut nearby = (
262 &ecs.entities(),
263 &positions,
264 &colliders,
265 )
266 .join()
267 .filter(|(e, _, _)| *e != player_entity)
268 .map(|(e, p, c)| {
269 let height = c.get_height();
270 let radius = c.bounding_radius().max(height / 2.0);
271 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
273 let dist_sqr = pos.distance_squared(start);
275 (e, pos, radius, dist_sqr, c)
276 })
277 .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
279 .collect::<Vec<_>>();
280 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
282
283 let seg_ray = LineSegment3 { start, end };
284
285 let entity = nearby.iter().find_map(|(e, p, r, _, c)| {
286 let nearest = seg_ray.projected_point(*p);
287
288 match c {
289 comp::Collider::CapsulePrism(CapsulePrism {
290 p0,
291 p1,
292 radius,
293 z_min,
294 z_max,
295 }) => {
296 if nearest.distance_squared(*p) > (r * 3.0_f32.sqrt()).powi(2) {
300 return None;
301 }
302
303 let entity_rotation = ecs
304 .read_storage::<comp::Ori>()
305 .get(*e)
306 .copied()
307 .unwrap_or_default();
308 let entity_position = ecs.read_storage::<comp::Pos>().get(*e).copied().unwrap();
309 let world_p0 = entity_position.0
310 + (entity_rotation.to_quat()
311 * Vec3::new(p0.x, p0.y, z_min + c.get_height() / 2.0));
312 let world_p1 = entity_position.0
313 + (entity_rotation.to_quat()
314 * Vec3::new(p1.x, p1.y, z_min + c.get_height() / 2.0));
315
316 let (p_a, p_b) = if p0 != p1 {
320 let seg_capsule = LineSegment3 {
321 start: world_p0,
322 end: world_p1,
323 };
324 closest_points_3d(seg_ray, seg_capsule)
325 } else {
326 let nearest = seg_ray.projected_point(world_p0);
327 (nearest, world_p0)
328 };
329
330 let distance = p_a.xy().distance_squared(p_b.xy());
334 if distance < radius.powi(2)
335 && p_a.z >= entity_position.0.z + z_min
336 && p_a.z <= entity_position.0.z + z_max
337 {
338 return Some((p_a.distance(start), Entity(*e)));
339 }
340
341 None
343 },
344 _ => {
346 if nearest.distance_squared(*p) < r.powi(2) {
347 return Some((nearest.distance(start), Entity(*e)));
348 }
349 None
350 },
351 }
352 });
353 entity
354 .map(|(dist, e)| (dist, Some(e)))
355 .unwrap_or((cast_dist, None))
356}