veloren_voxygen/session/
target.rs

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)]
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 = 300.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();
75    // Get the player's cylinder
76    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    // See if ray hits entities
115    // Don't cast through blocks, (hence why use shortest_cam_dist from non-entity
116    // targets) Could check for intersection with entity from last frame to
117    // narrow this down
118    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    // Need to raycast by distance to cam
126    // But also filter out by distance to the player (but this only needs to be done
127    // on final result)
128    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            // TODO: use collider radius instead of body radius?
142            let radius = s.map_or(1.0, |s| s.0) * b.max_radius() * RADIUS_SCALE;
143            // Move position up from the feet
144            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
145            // Distance squared from camera to the entity
146            let dist_sqr = pos.distance_squared(cam_pos);
147            // We only care about interacting with entities that contain items,
148            // or are not inanimate (to trade with), and are not riding the player.
149            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        // Roughly filter out entities farther than ray distance
157        .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
158        // Ignore entities intersecting the camera
159        .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
160        // Substract sphere radius from distance to the camera
161        .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
162        .collect::<Vec<_>>();
163    // Sort by distance
164    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    // TODO: fuzzy borders
171    let entity_target = nearby
172        .iter()
173        .map(|(e, p, r, _)| (e, *p, r))
174        // Find first one that intersects the ray segment
175        .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
176        .and_then(|(e, p, _)| {
177            // Get the entity's cylinder
178            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    // Return multiple possible targets
233    // GameInput events determine which target to use.
234    (
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            // Move position up from the feet
265            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
266            // Distance squared from start to the entity
267            let dist_sqr = pos.distance_squared(start);
268            (e, pos, radius, dist_sqr, c)
269        })
270        // Roughly filter out entities farther than ray distance
271        .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
272        .collect::<Vec<_>>();
273    // Sort by distance
274    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                // Check if the nearest point is within the capsule's inclusive radius (radius
290                // from center to furthest possible edge corner) If not, then
291                // the ray doesn't intersect the capsule at all and we can skip it
292                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                // Get the closest points between the ray and the capsule's line segment
310                // If the capsule's line segment is a point, then the closest point is the point
311                // itself
312                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                // Check if the distance between the closest points are within the capsule
324                // prism's radius on the xy plane and if the closest points are
325                // within the capsule prism's z range
326                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                // If all else fails, then the ray doesn't intersect the capsule
335                None
336            },
337            // TODO: handle other collider types, for now just use the bounding sphere
338            _ => {
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}