1use specs::{Join, LendJoin, WorldExt};
2use vek::*;
3
4use client::{self, Client};
5use common::{
6 comp::{self, tool::ToolKind},
7 consts::MAX_PICKUP_RANGE,
8 link::Is,
9 mounting::{Mount, Rider},
10 terrain::Block,
11 uid::Uid,
12 util::find_dist::{Cylinder, FindDist},
13 vol::ReadVol,
14};
15use common_base::span;
16use common_systems::phys::closest_points_3d;
17
18#[derive(Clone, Copy, Debug)]
19pub struct Target<T> {
20 pub kind: T,
21 pub distance: f32,
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 = 300.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();
75 let player_cylinder = Cylinder::from_components(
77 player_pos,
78 scales.get(player_entity).copied(),
79 colliders.get(player_entity),
80 char_states.get(player_entity),
81 );
82 let terrain = client.state().terrain();
83
84 let find_pos = |hit: fn(Block) -> bool| {
85 let cam_ray = terrain
86 .ray(cam_pos, cam_pos + cam_dir * 100.0)
87 .until(|block| hit(*block))
88 .cast();
89 let cam_ray = (cam_ray.0, cam_ray.1.map(|x| x.copied()));
90 let cam_dist = cam_ray.0;
91
92 if matches!(
93 cam_ray.1,
94 Ok(Some(_)) if player_cylinder.min_distance(cam_pos + cam_dir * (cam_dist + 0.01)) <= MAX_PICKUP_RANGE
95 ) {
96 (
97 Some(cam_pos + cam_dir * (cam_dist + 0.01)),
98 Some(cam_pos + cam_dir * (cam_dist - 0.01)),
99 Some(cam_ray),
100 )
101 } else {
102 (None, None, None)
103 }
104 };
105
106 let (collect_pos, _, collect_cam_ray) = find_pos(|b: Block| b.is_collectible());
107 let (mine_pos, _, mine_cam_ray) = if active_mine_tool.is_some() {
108 find_pos(|b: Block| b.mine_tool().is_some())
109 } else {
110 (None, None, None)
111 };
112 let (solid_pos, place_block_pos, solid_cam_ray) = find_pos(|b: Block| b.is_filled());
113
114 let cast_dist = solid_cam_ray
119 .as_ref()
120 .map(|(d, _)| d.min(MAX_TARGET_RANGE))
121 .unwrap_or(MAX_TARGET_RANGE);
122
123 let uids = ecs.read_storage::<Uid>();
124
125 let mut nearby = (
129 &ecs.entities(),
130 &positions,
131 scales.maybe(),
132 &ecs.read_storage::<comp::Body>(),
133 ecs.read_storage::<comp::PickupItem>().maybe(),
134 !&ecs.read_storage::<Is<Mount>>(),
135 ecs.read_storage::<Is<Rider>>().maybe(),
136 )
137 .join()
138 .filter(|(e, _, _, _, _, _, _)| *e != player_entity)
139 .filter_map(|(e, p, s, b, i, _, is_rider)| {
140 const RADIUS_SCALE: f32 = 3.0;
141 let radius = s.map_or(1.0, |s| s.0) * b.max_radius() * RADIUS_SCALE;
143 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
145 let dist_sqr = pos.distance_squared(cam_pos);
147 let not_riding_player = is_rider.is_none_or(|is_rider| Some(&is_rider.mount) != uids.get(viewpoint_entity));
150 if (i.is_some() || !matches!(b, comp::Body::Object(_))) && not_riding_player {
151 Some((e, pos, radius, dist_sqr))
152 } else {
153 None
154 }
155 })
156 .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
158 .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
160 .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
162 .collect::<Vec<_>>();
163 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
165
166 let seg_ray = LineSegment3 {
167 start: cam_pos,
168 end: cam_pos + cam_dir * cast_dist,
169 };
170 let entity_target = nearby
172 .iter()
173 .map(|(e, p, r, _)| (e, *p, r))
174 .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
176 .and_then(|(e, p, _)| {
177 let target_cylinder = Cylinder::from_components(
179 p,
180 scales.get(*e).copied(),
181 colliders.get(*e),
182 char_states.get(*e),
183 );
184
185 let dist_to_player = player_cylinder.min_distance(target_cylinder);
186 if dist_to_player < MAX_TARGET_RANGE {
187 Some(Target {
188 kind: Entity(*e),
189 position: p,
190 distance: dist_to_player,
191 })
192 } else { None }
193 });
194
195 let solid_ray_dist = solid_cam_ray.map(|r| r.0);
196 let terrain_target = if let (None, Some(distance)) = (entity_target, solid_ray_dist) {
197 solid_pos.map(|position| Target {
198 kind: Terrain,
199 distance,
200 position,
201 })
202 } else {
203 None
204 };
205
206 let build_target = if let (true, Some(distance)) = (can_build, solid_ray_dist) {
207 place_block_pos
208 .zip(solid_pos)
209 .map(|(place_pos, position)| Target {
210 kind: Build(place_pos),
211 distance,
212 position,
213 })
214 } else {
215 None
216 };
217
218 let collect_target = collect_pos
219 .zip(collect_cam_ray)
220 .map(|(position, ray)| Target {
221 kind: Collectable,
222 distance: ray.0,
223 position,
224 });
225
226 let mine_target = mine_pos.zip(mine_cam_ray).map(|(position, ray)| Target {
227 kind: Mine,
228 distance: ray.0,
229 position,
230 });
231
232 (
235 build_target,
236 collect_target,
237 entity_target,
238 mine_target,
239 terrain_target,
240 )
241}
242
243pub(super) fn ray_entities(
244 client: &Client,
245 start: Vec3<f32>,
246 end: Vec3<f32>,
247 cast_dist: f32,
248) -> (f32, Option<Entity>) {
249 let player_entity = client.entity();
250 let ecs = client.state().ecs();
251 let positions = ecs.read_storage::<comp::Pos>();
252 let colliders = ecs.read_storage::<comp::Collider>();
253
254 let mut nearby = (
255 &ecs.entities(),
256 &positions,
257 &colliders,
258 )
259 .join()
260 .filter(|(e, _, _)| *e != player_entity)
261 .map(|(e, p, c)| {
262 let height = c.get_height();
263 let radius = c.bounding_radius().max(height / 2.0);
264 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
266 let dist_sqr = pos.distance_squared(start);
268 (e, pos, radius, dist_sqr, c)
269 })
270 .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
272 .collect::<Vec<_>>();
273 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
275
276 let seg_ray = LineSegment3 { start, end };
277
278 let entity = nearby.iter().find_map(|(e, p, r, _, c)| {
279 let nearest = seg_ray.projected_point(*p);
280
281 match c {
282 comp::Collider::CapsulePrism {
283 p0,
284 p1,
285 radius,
286 z_min,
287 z_max,
288 } => {
289 if nearest.distance_squared(*p) > (r * 3.0_f32.sqrt()).powi(2) {
293 return None;
294 }
295
296 let entity_rotation = ecs
297 .read_storage::<comp::Ori>()
298 .get(*e)
299 .copied()
300 .unwrap_or_default();
301 let entity_position = ecs.read_storage::<comp::Pos>().get(*e).copied().unwrap();
302 let world_p0 = entity_position.0
303 + (entity_rotation.to_quat()
304 * Vec3::new(p0.x, p0.y, z_min + c.get_height() / 2.0));
305 let world_p1 = entity_position.0
306 + (entity_rotation.to_quat()
307 * Vec3::new(p1.x, p1.y, z_min + c.get_height() / 2.0));
308
309 let (p_a, p_b) = if p0 != p1 {
313 let seg_capsule = LineSegment3 {
314 start: world_p0,
315 end: world_p1,
316 };
317 closest_points_3d(seg_ray, seg_capsule)
318 } else {
319 let nearest = seg_ray.projected_point(world_p0);
320 (nearest, world_p0)
321 };
322
323 let distance = p_a.xy().distance_squared(p_b.xy());
327 if distance < radius.powi(2)
328 && p_a.z >= entity_position.0.z + z_min
329 && p_a.z <= entity_position.0.z + z_max
330 {
331 return Some((p_a.distance(start), Entity(*e)));
332 }
333
334 None
336 },
337 _ => {
339 if nearest.distance_squared(*p) < r.powi(2) {
340 return Some((nearest.distance(start), Entity(*e)));
341 }
342 None
343 },
344 }
345 });
346 entity
347 .map(|(dist, e)| (dist, Some(e)))
348 .unwrap_or((cast_dist, None))
349}