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::{
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();
77 let player_cylinder = Cylinder::from_components(
79 player_pos,
80 scales.get(player_entity).copied(),
81 colliders.get(player_entity),
82 char_states.get(player_entity),
83 );
84 let terrain = client.state().terrain();
85
86 let find_pos = |hit: fn(Block) -> bool| {
87 let cam_ray = terrain
88 .ray(cam_pos, cam_pos + cam_dir * 100.0)
89 .until(|block| hit(*block))
90 .cast();
91 let cam_ray = (cam_ray.0, cam_ray.1.map(|x| x.copied()));
92 let cam_dist = cam_ray.0;
93
94 if matches!(
95 cam_ray.1,
96 Ok(Some(_)) if player_cylinder.min_distance(cam_pos + cam_dir * (cam_dist + 0.01)) <= MAX_PICKUP_RANGE
97 ) {
98 (
99 Some(cam_pos + cam_dir * (cam_dist + 0.01)),
100 Some(cam_pos + cam_dir * (cam_dist - 0.01)),
101 Some(cam_ray),
102 )
103 } else {
104 (None, None, None)
105 }
106 };
107
108 let (collect_pos, _, collect_cam_ray) = find_pos(|b: Block| {
111 b.get_sprite()
112 .is_some_and(|s| s.default_tool() == Some(None))
113 });
114 let (mine_pos, _, mine_cam_ray) = if active_mine_tool.is_some() {
115 find_pos(|b: Block| b.mine_tool().is_some())
116 } else {
117 (None, None, None)
118 };
119 let (solid_pos, place_block_pos, solid_cam_ray) = find_pos(|b: Block| b.is_filled());
120
121 let cast_dist = solid_cam_ray
126 .as_ref()
127 .map(|(d, _)| d.min(MAX_TARGET_RANGE))
128 .unwrap_or(MAX_TARGET_RANGE);
129
130 let uids = ecs.read_storage::<Uid>();
131
132 let mut nearby = (
136 &ecs.entities(),
137 &positions,
138 scales.maybe(),
139 &ecs.read_storage::<comp::Body>(),
140 ecs.read_storage::<comp::PickupItem>().maybe(),
141 !&ecs.read_storage::<Is<Mount>>(),
142 ecs.read_storage::<Is<Rider>>().maybe(),
143 )
144 .join()
145 .filter(|(e, _, _, _, _, _, _)| *e != player_entity)
146 .filter_map(|(e, p, s, b, i, _, is_rider)| {
147 const RADIUS_SCALE: f32 = 3.0;
148 let radius = s.map_or(1.0, |s| s.0) * b.max_radius() * RADIUS_SCALE;
150 let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
152 let dist_sqr = pos.distance_squared(cam_pos);
154 let not_riding_player = is_rider.is_none_or(|is_rider| Some(&is_rider.mount) != uids.get(viewpoint_entity));
157 if (i.is_some() || !matches!(b, comp::Body::Object(_))) && not_riding_player {
158 Some((e, pos, radius, dist_sqr))
159 } else {
160 None
161 }
162 })
163 .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
165 .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
167 .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
169 .collect::<Vec<_>>();
170 nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
172
173 let seg_ray = LineSegment3 {
174 start: cam_pos,
175 end: cam_pos + cam_dir * cast_dist,
176 };
177 let entity_target = nearby
179 .iter()
180 .map(|(e, p, r, _)| (e, *p, r))
181 .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
183 .and_then(|(e, p, _)| {
184 let target_cylinder = Cylinder::from_components(
186 p,
187 scales.get(*e).copied(),
188 colliders.get(*e),
189 char_states.get(*e),
190 );
191
192 let dist_to_player = player_cylinder.min_distance(target_cylinder);
193 if dist_to_player < MAX_TARGET_RANGE {
194 Some(Target {
195 kind: Entity(*e),
196 position: p,
197 distance: dist_to_player,
198 })
199 } else { None }
200 });
201
202 let solid_ray_dist = solid_cam_ray.map(|r| r.0);
203 let terrain_target = if let (None, Some(distance)) = (entity_target, solid_ray_dist) {
204 solid_pos.map(|position| Target {
205 kind: Terrain,
206 distance,
207 position,
208 })
209 } else {
210 None
211 };
212
213 let build_target = if let (true, Some(distance)) = (can_build, solid_ray_dist) {
214 place_block_pos
215 .zip(solid_pos)
216 .map(|(place_pos, position)| Target {
217 kind: Build(place_pos),
218 distance,
219 position,
220 })
221 } else {
222 None
223 };
224
225 let collect_target = collect_pos
226 .zip(collect_cam_ray)
227 .map(|(position, ray)| Target {
228 kind: Collectable,
229 distance: ray.0,
230 position,
231 });
232
233 let mine_target = mine_pos.zip(mine_cam_ray).map(|(position, ray)| Target {
234 kind: Mine,
235 distance: ray.0,
236 position,
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 {
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}