Skip to main content

veloren_voxygen/session/
target.rs

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)]
38// line of sight (if not bocked by entity). Not build/mine mode dependent.
39pub 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
45/// Max distance an entity can be "targeted"
46pub const MAX_TARGET_RANGE: f32 = 500.0;
47
48/// Calculate what the cursor is pointing at within the 3d scene
49pub(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    // Choose a spot above the player's head for item distance checks
65    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, // Should never happen, but a safe fallback
71    };
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    // Get the player's cylinder
78    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    // The maximum distance we might need to go, plus a safe threshold
93    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        // Collection is limited by the player's actual position
107        .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        // Mining is limited by the target block being mineable with the active mining tool...
110        .filter(|(_, b)| matches!(b, Ok(Some(b)) if b.mine_tool().zip(active_mine_tool).map_or(false, |(a, b)| a == b)))
111        // ...and by the distance to the player's eye position
112        .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        // Building is limited by the maximum target distance
115        .filter(|(d, _)| *d < MAX_TARGET_RANGE);
116    // Visual obstacles are limited by filled blocks
117    let obstacle_cast =
118        Some(ray.until(|b| b.is_filled()).cast()).filter(|(d, _)| *d < MAX_TARGET_RANGE);
119
120    // The maximum distance at which entities can be targetted is based on the
121    // distance to the nearest terrain obstacle being looked at
122    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    // Need to raycast by distance to cam
131    // But also filter out by distance to the player (but this only needs to be done
132    // on final result)
133    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            // TODO: use collider radius instead of body radius?
149            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            // Move position up from the feet
152            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + (height / 2.0));
153            // Distance squared from camera to the entity
154            let dist_sqr = pos.distance_squared(cam_pos);
155            // We only care about interacting with entities that contain items,
156            // or are not inanimate (to trade with), or have health (and thus can be targeted by abilities); and are not riding the player.
157            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        // Roughly filter out entities farther than ray distance
165        .filter(|(_, _, r, d_sqr)| *d_sqr <= max_target_dist.powi(2) + 2.0 * max_target_dist * r + r.powi(2))
166        // Ignore entities intersecting the camera, unless the player is wielding (heuristic used to decide if player is trying to target an entity with an ability)
167        .filter(|(_, _, r, d_sqr)| player_wielding || *d_sqr > r.powi(2))
168        // Substract sphere radius from distance to the camera
169        .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 is wielding, sort by distance to the ray, otherwise sort by
175    // distance to the camera
176    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    // TODO: fuzzy borders
187    let entity_target = nearby
188        .iter()
189        .map(|(e, p, r, _, _)| (e, *p, r))
190        // Find first one that intersects the ray segment, allow for entities nearby to the camera ray when wielding a weapon (as some abilities target an entity)
191        .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            // Get the entity's cylinder
200            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    // Return multiple possible targets
240    // GameInput events determine which target to use.
241    (
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            // Move position up from the feet
272            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
273            // Distance squared from start to the entity
274            let dist_sqr = pos.distance_squared(start);
275            (e, pos, radius, dist_sqr, c)
276        })
277        // Roughly filter out entities farther than ray distance
278        .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
279        .collect::<Vec<_>>();
280    // Sort by distance
281    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                // Check if the nearest point is within the capsule's inclusive radius (radius
297                // from center to furthest possible edge corner) If not, then
298                // the ray doesn't intersect the capsule at all and we can skip it
299                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                // Get the closest points between the ray and the capsule's line segment
317                // If the capsule's line segment is a point, then the closest point is the point
318                // itself
319                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                // Check if the distance between the closest points are within the capsule
331                // prism's radius on the xy plane and if the closest points are
332                // within the capsule prism's z range
333                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                // If all else fails, then the ray doesn't intersect the capsule
342                None
343            },
344            // TODO: handle other collider types, for now just use the bounding sphere
345            _ => {
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}