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, 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)]
40// line of sight (if not bocked by entity). Not build/mine mode dependent.
41pub 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
47/// Max distance an entity can be "targeted"
48pub const MAX_TARGET_RANGE: f32 = 300.0;
49
50/// Calculate what the cursor is pointing at within the 3d scene
51pub(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    // Choose a spot above the player's head for item distance checks
67    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, // Should never happen, but a safe fallback
73    };
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    // Get the player's cylinder
79    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    // See if ray hits entities
133    // Don't cast through blocks, (hence why use shortest_cam_dist from non-entity
134    // targets) Could check for intersection with entity from last frame to
135    // narrow this down
136    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    // Need to raycast by distance to cam
144    // But also filter out by distance to the player (but this only needs to be done
145    // on final result)
146    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            // TODO: use collider radius instead of body radius?
160            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            // Move position up from the feet
162            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
163            // Distance squared from camera to the entity
164            let dist_sqr = pos.distance_squared(cam_pos);
165            // We only care about interacting with entities that contain items,
166            // or are not inanimate (to trade with), and are not riding the player.
167            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        // Roughly filter out entities farther than ray distance
175        .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
176        // Ignore entities intersecting the camera
177        .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
178        // Substract sphere radius from distance to the camera
179        .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
180        .collect::<Vec<_>>();
181    // Sort by distance
182    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    // TODO: fuzzy borders
189    let entity_target = nearby
190        .iter()
191        .map(|(e, p, r, _)| (e, *p, r))
192        // 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)
193        .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            // Get the entity's cylinder
202            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    // Return multiple possible targets
252    // GameInput events determine which target to use.
253    (
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            // Move position up from the feet
284            let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
285            // Distance squared from start to the entity
286            let dist_sqr = pos.distance_squared(start);
287            (e, pos, radius, dist_sqr, c)
288        })
289        // Roughly filter out entities farther than ray distance
290        .filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
291        .collect::<Vec<_>>();
292    // Sort by distance
293    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                // Check if the nearest point is within the capsule's inclusive radius (radius
309                // from center to furthest possible edge corner) If not, then
310                // the ray doesn't intersect the capsule at all and we can skip it
311                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                // Get the closest points between the ray and the capsule's line segment
329                // If the capsule's line segment is a point, then the closest point is the point
330                // itself
331                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                // Check if the distance between the closest points are within the capsule
343                // prism's radius on the xy plane and if the closest points are
344                // within the capsule prism's z range
345                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                // If all else fails, then the ray doesn't intersect the capsule
354                None
355            },
356            // TODO: handle other collider types, for now just use the bounding sphere
357            _ => {
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}