1use specs::{Join, LendJoin, WorldExt};
2use vek::*;
3
4use client::{self, Client};
5use common::{
6 comp::{self, CapsulePrism, tool::ToolKind},
7 consts::MAX_PICKUP_RANGE,
8 link::Is,
9 mounting::{Mount, Rider},
10 terrain::Block,
11 uid::Uid,
12 util::{
13 find_dist::{Cylinder, FindDist},
14 lines::closest_points_3d,
15 },
16 vol::ReadVol,
17};
18use common_base::span;
19
20#[derive(Clone, Copy, Debug)]
21pub struct Target<T> {
22 pub kind: T,
23 pub distance: f32,
24 pub position: Vec3<f32>,
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct Build(pub Vec3<f32>);
29
30#[derive(Clone, Copy, Debug)]
31pub struct Collectable;
32
33#[derive(Clone, Copy, Debug)]
34pub struct Entity(pub specs::Entity);
35
36#[derive(Clone, Copy, Debug)]
37pub struct Mine;
38
39#[derive(Clone, Copy, Debug)]
40pub struct Terrain;
42
43impl<T> Target<T> {
44 pub fn position_int(self) -> Vec3<i32> { self.position.map(|p| p.floor() as i32) }
45}
46
47pub const MAX_TARGET_RANGE: f32 = 300.0;
49
50pub(super) fn targets_under_cursor(
52 client: &Client,
53 cam_pos: Vec3<f32>,
54 cam_dir: Vec3<f32>,
55 can_build: bool,
56 active_mine_tool: Option<ToolKind>,
57 viewpoint_entity: specs::Entity,
58) -> (
59 Option<Target<Build>>,
60 Option<Target<Collectable>>,
61 Option<Target<Entity>>,
62 Option<Target<Mine>>,
63 Option<Target<Terrain>>,
64) {
65 span!(_guard, "targets_under_cursor");
66 let player_entity = client.entity();
68 let ecs = client.state().ecs();
69 let positions = ecs.read_storage::<comp::Pos>();
70 let player_pos = match positions.get(player_entity) {
71 Some(pos) => pos.0,
72 None => cam_pos, };
74 let scales = ecs.read_storage();
75 let colliders = ecs.read_storage();
76 let char_states = ecs.read_storage::<comp::CharacterState>();
77 let player_char_state = char_states.get(player_entity);
78 let player_cylinder = Cylinder::from_components(
80 player_pos,
81 scales.get(player_entity).copied(),
82 colliders.get(player_entity),
83 player_char_state,
84 );
85 let terrain = client.state().terrain();
86
87 let find_pos = |hit: fn(Block) -> bool, max_range: f32, start_pos: Vec3<f32>| {
88 let dist = 250.0;
89
90 let cam_ray = terrain
91 .ray(start_pos, start_pos + cam_dir * dist)
92 .max_iter(500)
93 .until(|block| hit(*block))
94 .cast();
95
96 let cam_ray = (cam_ray.0, cam_ray.1.map(|x| x.copied()));
97 let cam_dist = cam_ray.0;
98
99 if matches!(
100 cam_ray.1,
101 Ok(Some(_)) if player_cylinder.min_distance(start_pos + cam_dir * (cam_dist + 0.01)) <= max_range
102 ) {
103 (
104 Some(start_pos + cam_dir * (cam_dist + 0.01)),
105 Some(start_pos + cam_dir * (cam_dist - 0.01)),
106 Some(cam_ray),
107 )
108 } else {
109 (None, None, None)
110 }
111 };
112
113 let (collect_pos, _, collect_cam_ray) = find_pos(
114 |b: Block| b.is_directly_collectible(),
115 MAX_TARGET_RANGE,
116 cam_pos,
117 );
118 let (mine_pos, _, mine_cam_ray) = if active_mine_tool.is_some() {
119 find_pos(
120 |b: Block| b.mine_tool().is_some(),
121 MAX_TARGET_RANGE,
122 cam_pos,
123 )
124 } else {
125 (None, None, None)
126 };
127 let (build_pos, place_block_pos, build_cam_ray) =
128 find_pos(|b: Block| b.is_filled(), MAX_TARGET_RANGE, cam_pos);
129 let (solid_pos, _, solid_cam_ray) =
130 find_pos(|b: Block| b.is_filled(), MAX_PICKUP_RANGE, cam_pos);
131
132 let cast_dist = solid_cam_ray
137 .as_ref()
138 .map(|(d, _)| d.min(MAX_TARGET_RANGE))
139 .unwrap_or(MAX_TARGET_RANGE);
140
141 let uids = ecs.read_storage::<Uid>();
142
143 let mut nearby = (
147 &ecs.entities(),
148 &positions,
149 scales.maybe(),
150 &ecs.read_storage::<comp::Body>(),
151 ecs.read_storage::<comp::PickupItem>().maybe(),
152 !&ecs.read_storage::<Is<Mount>>(),
153 ecs.read_storage::<Is<Rider>>().maybe(),
154 )
155 .join()
156 .filter(|(e, _, _, _, _, _, _)| *e != viewpoint_entity)
157 .filter_map(|(e, p, s, b, i, _, is_rider)| {
158 const RADIUS_SCALE: f32 = 3.0;
159 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;
161 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
163 let dist_sqr = pos.distance_squared(cam_pos);
165 let not_riding_player = is_rider.is_none_or(|is_rider| Some(&is_rider.mount) != uids.get(viewpoint_entity));
168 if (i.is_some() || !matches!(b, comp::Body::Object(_))) && not_riding_player {
169 Some((e, pos, radius, dist_sqr))
170 } else {
171 None
172 }
173 })
174 .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
176 .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
178 .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
180 .collect::<Vec<_>>();
181 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
183
184 let seg_ray = LineSegment3 {
185 start: cam_pos,
186 end: cam_pos + cam_dir * cast_dist,
187 };
188 let entity_target = nearby
190 .iter()
191 .map(|(e, p, r, _)| (e, *p, r))
192 .find(|(_, p, r)| {
194 if player_char_state.is_some_and(|cs| cs.is_wield()) {
195 seg_ray.projected_point(*p).distance_squared(*p) < (*r + cam_pos.distance(*p) / 10.0).powi(2)
196 } else {
197 seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2)
198 }
199 })
200 .and_then(|(e, p, _)| {
201 let target_cylinder = Cylinder::from_components(
203 p,
204 scales.get(*e).copied(),
205 colliders.get(*e),
206 char_states.get(*e),
207 );
208
209 let dist_to_player = player_cylinder.min_distance(target_cylinder);
210 if dist_to_player < MAX_TARGET_RANGE {
211 Some(Target {
212 kind: Entity(*e),
213 position: p,
214 distance: dist_to_player,
215 })
216 } else { None }
217 });
218
219 let terrain_target = solid_pos.zip(solid_cam_ray).map(|(position, ray)| Target {
220 kind: Terrain,
221 distance: ray.0,
222 position,
223 });
224
225 let build_target = if let (true, Some(ray)) = (can_build, build_cam_ray) {
226 place_block_pos
227 .zip(build_pos)
228 .map(|(place_pos, position)| Target {
229 kind: Build(place_pos),
230 distance: ray.0,
231 position,
232 })
233 } else {
234 None
235 };
236
237 let collect_target = collect_pos
238 .zip(collect_cam_ray)
239 .map(|(position, ray)| Target {
240 kind: Collectable,
241 distance: ray.0,
242 position,
243 });
244
245 let mine_target = mine_pos.zip(mine_cam_ray).map(|(position, ray)| Target {
246 kind: Mine,
247 distance: ray.0,
248 position,
249 });
250
251 (
254 build_target,
255 collect_target,
256 entity_target,
257 mine_target,
258 terrain_target,
259 )
260}
261
262pub(super) fn ray_entities(
263 client: &Client,
264 start: Vec3<f32>,
265 end: Vec3<f32>,
266 cast_dist: f32,
267) -> (f32, Option<Entity>) {
268 let player_entity = client.entity();
269 let ecs = client.state().ecs();
270 let positions = ecs.read_storage::<comp::Pos>();
271 let colliders = ecs.read_storage::<comp::Collider>();
272
273 let mut nearby = (
274 &ecs.entities(),
275 &positions,
276 &colliders,
277 )
278 .join()
279 .filter(|(e, _, _)| *e != player_entity)
280 .map(|(e, p, c)| {
281 let height = c.get_height();
282 let radius = c.bounding_radius().max(height / 2.0);
283 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
285 let dist_sqr = pos.distance_squared(start);
287 (e, pos, radius, dist_sqr, c)
288 })
289 .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
291 .collect::<Vec<_>>();
292 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
294
295 let seg_ray = LineSegment3 { start, end };
296
297 let entity = nearby.iter().find_map(|(e, p, r, _, c)| {
298 let nearest = seg_ray.projected_point(*p);
299
300 match c {
301 comp::Collider::CapsulePrism(CapsulePrism {
302 p0,
303 p1,
304 radius,
305 z_min,
306 z_max,
307 }) => {
308 if nearest.distance_squared(*p) > (r * 3.0_f32.sqrt()).powi(2) {
312 return None;
313 }
314
315 let entity_rotation = ecs
316 .read_storage::<comp::Ori>()
317 .get(*e)
318 .copied()
319 .unwrap_or_default();
320 let entity_position = ecs.read_storage::<comp::Pos>().get(*e).copied().unwrap();
321 let world_p0 = entity_position.0
322 + (entity_rotation.to_quat()
323 * Vec3::new(p0.x, p0.y, z_min + c.get_height() / 2.0));
324 let world_p1 = entity_position.0
325 + (entity_rotation.to_quat()
326 * Vec3::new(p1.x, p1.y, z_min + c.get_height() / 2.0));
327
328 let (p_a, p_b) = if p0 != p1 {
332 let seg_capsule = LineSegment3 {
333 start: world_p0,
334 end: world_p1,
335 };
336 closest_points_3d(seg_ray, seg_capsule)
337 } else {
338 let nearest = seg_ray.projected_point(world_p0);
339 (nearest, world_p0)
340 };
341
342 let distance = p_a.xy().distance_squared(p_b.xy());
346 if distance < radius.powi(2)
347 && p_a.z >= entity_position.0.z + z_min
348 && p_a.z <= entity_position.0.z + z_max
349 {
350 return Some((p_a.distance(start), Entity(*e)));
351 }
352
353 None
355 },
356 _ => {
358 if nearest.distance_squared(*p) < r.powi(2) {
359 return Some((nearest.distance(start), Entity(*e)));
360 }
361 None
362 },
363 }
364 });
365 entity
366 .map(|(dist, e)| (dist, Some(e)))
367 .unwrap_or((cast_dist, None))
368}