choose nearest target, from a specific subset of targets, for scene highlighting vs interactable vs game primary/secondary key input

This commit is contained in:
anomaluridae 2021-09-14 23:10:55 -07:00
parent 6362df4ffc
commit aa86c86cb6
3 changed files with 72 additions and 61 deletions

View File

@ -2,7 +2,10 @@ use ordered_float::OrderedFloat;
use specs::{Join, WorldExt};
use vek::*;
use super::target::{self, MAX_TARGET_RANGE, Target};
use super::{
find_shortest_distance,
target::{self, Target, MAX_TARGET_RANGE},
};
use client::Client;
use common::{
comp,
@ -34,11 +37,11 @@ impl Interactable {
/// Select interactable to hightlight, display interaction text for, and to
/// interact with if the interact key is pressed
/// Selected in the following order
/// 1) Targeted items, in order of preference:
/// Selected in the following order:
/// 1) Targeted items, in order of nearest under cursor:
/// (a) entity (if within range)
/// (b) collectable
/// (c) can be mined
/// (c) can be mined, and is a mine sprite (Air) not a weak rock.
/// 2) outside of targeted cam ray
/// -> closest of nearest interactable entity/block
pub(super) fn select_interactable(
@ -51,6 +54,12 @@ pub(super) fn select_interactable(
span!(_guard, "select_interactable");
use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
let nearest_dist = find_shortest_distance(&mut [
mine_target.map(|t| t.distance),
entity_target.map(|t| t.distance),
collect_target.map(|t| t.distance),
]);
fn get_block<T>(client: &Client, target: Target<T>) -> Option<Block> {
client
.state()
@ -62,7 +71,7 @@ pub(super) fn select_interactable(
if let Some(interactable) = entity_target
.and_then(|t| {
if t.distance < MAX_TARGET_RANGE {
if t.distance < MAX_TARGET_RANGE && Some(t.distance) == nearest_dist {
let entity = t.kind.0;
Some(Interactable::Entity(entity))
} else {
@ -71,24 +80,33 @@ pub(super) fn select_interactable(
})
.or_else(|| {
collect_target.and_then(|t| {
get_block(client, t)
.map(|b| Interactable::Block(b, t.position_int(), Some(Interaction::Collect)))
if Some(t.distance) == nearest_dist {
get_block(client, t).map(|b| {
Interactable::Block(b, t.position_int(), Some(Interaction::Collect))
})
} else {
None
}
})
})
.or_else(|| {
mine_target.and_then(|t| {
get_block(client, t).and_then(|b| {
// Handling edge detection. sometimes the casting (in Target mod) returns a
// position which is actually empty, which we do not want labeled as an
// interactable. We are only returning the mineable air
// elements (e.g. minerals). The mineable weakrock are used
// in the terrain selected_pos, but is not an interactable.
if b.mine_tool().is_some() && b.is_air() {
Some(Interactable::Block(b, t.position_int(), None))
} else {
None
}
})
if Some(t.distance) == nearest_dist {
get_block(client, t).and_then(|b| {
// Handling edge detection. sometimes the casting (in Target mod) returns a
// position which is actually empty, which we do not want labeled as an
// interactable. We are only returning the mineable air
// elements (e.g. minerals). The mineable weakrock are used
// in the terrain selected_pos, but is not an interactable.
if b.mine_tool().is_some() && b.is_air() {
Some(Interactable::Block(b, t.position_int(), None))
} else {
None
}
})
} else {
None
}
})
})
{

View File

@ -50,7 +50,7 @@ use crate::{
use hashbrown::HashMap;
use interactable::{select_interactable, Interactable};
use settings_change::Language::ChangeLanguage;
use target::{targets_under_cursor, Target};
use target::targets_under_cursor;
#[cfg(feature = "egui-ui")]
use voxygen_egui::EguiDebugInfo;
@ -412,14 +412,8 @@ impl PlayState for SessionState {
&& client.is_wielding() == Some(true);
// Check to see whether we're aiming at anything
let (
build_target,
collect_target,
entity_target,
mine_target,
terrain_target,
shortest_dist,
) = targets_under_cursor(&client, cam_pos, cam_dir, can_build, is_mining);
let (build_target, collect_target, entity_target, mine_target, terrain_target) =
targets_under_cursor(&client, cam_pos, cam_dir, can_build, is_mining);
self.interactable = select_interactable(
&client,
@ -431,23 +425,37 @@ impl PlayState for SessionState {
drop(client);
fn is_nearest_target<T>(shortest_dist: f32, target: Target<T>) -> bool {
target.distance <= shortest_dist
}
// Only highlight terrain blocks which can be interacted with
if let Some(mt) =
mine_target.filter(|mt| is_mining && is_nearest_target(shortest_dist, *mt))
// Nearest block to consider with GameInput primary or secondary key.
let nearest_block_dist = find_shortest_distance(&mut [
mine_target.filter(|_| is_mining).map(|t| t.distance),
build_target.filter(|_| can_build).map(|t| t.distance),
]);
// Nearest block to be highlighted in the scene (self.scene.set_select_pos).
let nearest_scene_dist = find_shortest_distance(&mut [
nearest_block_dist,
collect_target.filter(|_| !is_mining).map(|t| t.distance),
]);
// Set break_block_pos only if mining is closest.
self.inputs.break_block_pos = if let Some(mt) =
mine_target.filter(|mt| is_mining && nearest_scene_dist == Some(mt.distance))
{
self.scene.set_select_pos(Some(mt.position_int()));
self.inputs.break_block_pos = Some(mt.position);
Some(mt.position)
} else if let Some(bt) =
build_target.filter(|bt| can_build && is_nearest_target(shortest_dist, *bt))
build_target.filter(|bt| can_build && nearest_scene_dist == Some(bt.distance))
{
self.scene.set_select_pos(Some(bt.position_int()));
None
} else if let Some(ct) =
collect_target.filter(|ct| nearest_scene_dist == Some(ct.distance))
{
self.scene.set_select_pos(Some(ct.position_int()));
None
} else {
self.scene.set_select_pos(None);
}
None
};
// filled block in line of sight
let default_select_pos = terrain_target.map(|tt| tt.position);
@ -479,7 +487,7 @@ impl PlayState for SessionState {
// precedence.
// Order of precedence: build, then mining, then attack.
if let Some(build_target) = build_target.filter(|bt| {
state && can_build && is_nearest_target(shortest_dist, *bt)
state && can_build && nearest_block_dist == Some(bt.distance)
}) {
client.remove_block(build_target.position_int());
} else {
@ -494,7 +502,7 @@ impl PlayState for SessionState {
GameInput::Secondary => {
let mut client = self.client.borrow_mut();
if let Some(build_target) = build_target.filter(|bt| {
state && can_build && is_nearest_target(shortest_dist, *bt)
state && can_build && nearest_block_dist == Some(bt.distance)
}) {
let selected_pos = build_target.kind.0;
client.place_block(
@ -1533,3 +1541,9 @@ impl PlayState for SessionState {
fn egui_enabled(&self) -> bool { true }
}
fn find_shortest_distance(arr: &mut [Option<f32>]) -> Option<f32> {
arr.iter()
.filter_map(|x| *x)
.min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
}

View File

@ -1,4 +1,3 @@
use ordered_float::OrderedFloat;
use specs::{Join, WorldExt};
use vek::*;
@ -56,7 +55,6 @@ pub(super) fn targets_under_cursor(
Option<Target<Entity>>,
Option<Target<Mine>>,
Option<Target<Terrain>>,
f32,
) {
span!(_guard, "targets_under_cursor");
// Choose a spot above the player's head for item distance checks
@ -107,24 +105,6 @@ pub(super) fn targets_under_cursor(
.unwrap_or((None, None, None));
let (solid_pos, place_block_pos, solid_cam_ray) = find_pos(|b: Block| b.is_solid());
// Find shortest cam_dist of non-entity targets.
// Note that some of these targets can technically be in Air, such as the
// collectable.
let shortest_cam_dist = [&collect_cam_ray, &solid_cam_ray]
.iter()
.chain(
is_mining
.then(|| [&mine_cam_ray])
.unwrap_or([&solid_cam_ray])
.iter(),
)
.filter_map(|x| match **x {
Some((d, Ok(Some(_)))) => Some(d),
_ => None,
})
.min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
.unwrap_or(MAX_PICKUP_RANGE);
// See if ray hits entities
// Don't cast through blocks, (hence why use shortest_cam_dist from non-entity
// targets) Could check for intersection with entity from last frame to
@ -246,6 +226,5 @@ pub(super) fn targets_under_cursor(
entity_target,
mine_target,
terrain_target,
shortest_cam_dist,
)
}