mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
make Target into a typed struct. delineate the clear difference in Target versus Interactable. comments and naming cleanup, for more explicitness.
This commit is contained in:
parent
51f38df169
commit
48cc5d3b08
@ -33,12 +33,12 @@ 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 entity (if interactable) (entities can't be target through
|
||||
/// blocks)
|
||||
/// 2) Selected block
|
||||
/// (a) if collectable
|
||||
/// (b) if can be mined
|
||||
/// 3) Closest of nearest interactable entity/block
|
||||
/// 1) Targeted items, in order of preference:
|
||||
/// (a) entity (if within range)
|
||||
/// (b) collectable
|
||||
/// (c) can be mined
|
||||
/// 2) outside of targeted cam ray
|
||||
/// -> closest of nearest interactable entity/block
|
||||
pub(super) fn select_interactable(
|
||||
client: &Client,
|
||||
collect_target: Option<Target>,
|
||||
@ -47,107 +47,117 @@ pub(super) fn select_interactable(
|
||||
scene: &Scene,
|
||||
) -> Option<Interactable> {
|
||||
span!(_guard, "select_interactable");
|
||||
// TODO: once there are multiple distances for different types of interactions
|
||||
// this code will need to be revamped to cull things by varying distances
|
||||
// based on the types of interactions available for those things
|
||||
use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
|
||||
|
||||
entity_target
|
||||
.and_then(|x| if let Target::Entity(entity, _, dist) = x {
|
||||
(dist < MAX_PICKUP_RANGE).then_some(Interactable::Entity(entity))
|
||||
} else { None })
|
||||
if let Some(interactable) = entity_target
|
||||
.and_then(|t| {
|
||||
if t.distance < MAX_PICKUP_RANGE {
|
||||
t.make_interactable(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
collect_target.and_then(|ct| {
|
||||
client.state().terrain().get(ct.position_int()).ok().copied()
|
||||
.map(|b| Interactable::Block(b, ct.position_int(), Some(Interaction::Collect)))
|
||||
collect_target
|
||||
.map(|t| t.make_interactable(client))
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.or_else(|| {
|
||||
mine_target
|
||||
.map(|t| t.make_interactable(client))
|
||||
.unwrap_or(None)
|
||||
})
|
||||
{
|
||||
Some(interactable)
|
||||
} else {
|
||||
let ecs = client.state().ecs();
|
||||
let player_entity = client.entity();
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
let player_pos = positions.get(player_entity)?.0;
|
||||
|
||||
let scales = ecs.read_storage::<comp::Scale>();
|
||||
let colliders = ecs.read_storage::<comp::Collider>();
|
||||
let char_states = ecs.read_storage::<comp::CharacterState>();
|
||||
|
||||
let player_cylinder = Cylinder::from_components(
|
||||
player_pos,
|
||||
scales.get(player_entity).copied(),
|
||||
colliders.get(player_entity),
|
||||
char_states.get(player_entity),
|
||||
);
|
||||
|
||||
let closest_interactable_entity = (
|
||||
&ecs.entities(),
|
||||
&positions,
|
||||
scales.maybe(),
|
||||
colliders.maybe(),
|
||||
char_states.maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(e, _, _, _, _)| *e != player_entity)
|
||||
.map(|(e, p, s, c, cs)| {
|
||||
let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs);
|
||||
(e, cylinder)
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
mine_target.and_then(|mt| {
|
||||
client.state().terrain().get(mt.position_int()).ok().copied()
|
||||
.map(|b| Interactable::Block(b, mt.position_int(), None))
|
||||
// Roughly filter out entities farther than interaction distance
|
||||
.filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE))
|
||||
.map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder)))
|
||||
.min_by_key(|(_, dist)| OrderedFloat(*dist));
|
||||
|
||||
// Only search as far as closest interactable entity
|
||||
let search_dist = closest_interactable_entity.map_or(MAX_PICKUP_RANGE, |(_, dist)| dist);
|
||||
let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
||||
(e.floor() as i32).div_euclid(sz as i32)
|
||||
});
|
||||
let terrain = scene.terrain();
|
||||
|
||||
// Find closest interactable block
|
||||
// TODO: consider doing this one first?
|
||||
let closest_interactable_block_pos = Spiral2d::new()
|
||||
// TODO: this formula for the number to take was guessed
|
||||
// Note: assume RECT_SIZE.x == RECT_SIZE.y
|
||||
.take(((search_dist / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2))
|
||||
.flat_map(|offset| {
|
||||
let chunk_pos = player_chunk + offset;
|
||||
let chunk_voxel_pos =
|
||||
Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
|
||||
terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos))
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
let ecs = client.state().ecs();
|
||||
let player_entity = client.entity();
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
let player_pos = positions.get(player_entity)?.0;
|
||||
// TODO: maybe we could make this more efficient by putting the
|
||||
// interactables is some sort of spatial structure
|
||||
.flat_map(|(chunk_data, chunk_pos)| {
|
||||
chunk_data
|
||||
.blocks_of_interest
|
||||
.interactables
|
||||
.iter()
|
||||
.map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
|
||||
})
|
||||
.map(|(block_pos, interaction)| (
|
||||
block_pos,
|
||||
block_pos.map(|e| e as f32 + 0.5)
|
||||
.distance_squared(player_pos),
|
||||
interaction,
|
||||
))
|
||||
.min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr))
|
||||
.map(|(block_pos, _, interaction)| (block_pos, interaction));
|
||||
|
||||
let scales = ecs.read_storage::<comp::Scale>();
|
||||
let colliders = ecs.read_storage::<comp::Collider>();
|
||||
let char_states = ecs.read_storage::<comp::CharacterState>();
|
||||
|
||||
let player_cylinder = Cylinder::from_components(
|
||||
player_pos,
|
||||
scales.get(player_entity).copied(),
|
||||
colliders.get(player_entity),
|
||||
char_states.get(player_entity),
|
||||
);
|
||||
|
||||
let closest_interactable_entity = (
|
||||
&ecs.entities(),
|
||||
&positions,
|
||||
scales.maybe(),
|
||||
colliders.maybe(),
|
||||
char_states.maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(e, _, _, _, _)| *e != player_entity)
|
||||
.map(|(e, p, s, c, cs)| {
|
||||
let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs);
|
||||
(e, cylinder)
|
||||
})
|
||||
// Roughly filter out entities farther than interaction distance
|
||||
.filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE))
|
||||
.map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder)))
|
||||
.min_by_key(|(_, dist)| OrderedFloat(*dist));
|
||||
|
||||
// Only search as far as closest interactable entity
|
||||
let search_dist = closest_interactable_entity
|
||||
.map_or(MAX_PICKUP_RANGE, |(_, dist)| dist);
|
||||
let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
||||
(e.floor() as i32).div_euclid(sz as i32)
|
||||
});
|
||||
let terrain = scene.terrain();
|
||||
|
||||
// Find closest interactable block
|
||||
// TODO: consider doing this one first?
|
||||
let closest_interactable_block_pos = Spiral2d::new()
|
||||
// TODO: this formula for the number to take was guessed
|
||||
// Note: assume RECT_SIZE.x == RECT_SIZE.y
|
||||
.take(((search_dist / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2))
|
||||
.flat_map(|offset| {
|
||||
let chunk_pos = player_chunk + offset;
|
||||
let chunk_voxel_pos =
|
||||
Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
|
||||
terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos))
|
||||
})
|
||||
// TODO: maybe we could make this more efficient by putting the
|
||||
// interactables is some sort of spatial structure
|
||||
.flat_map(|(chunk_data, chunk_pos)| {
|
||||
chunk_data
|
||||
.blocks_of_interest
|
||||
.interactables
|
||||
.iter()
|
||||
.map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
|
||||
})
|
||||
.map(|(block_pos, interaction)| (
|
||||
block_pos,
|
||||
block_pos.map(|e| e as f32 + 0.5)
|
||||
.distance_squared(player_pos),
|
||||
interaction,
|
||||
))
|
||||
.min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr))
|
||||
.map(|(block_pos, _, interaction)| (block_pos, interaction));
|
||||
|
||||
// Pick closer one if they exist
|
||||
closest_interactable_block_pos
|
||||
.filter(|(block_pos, _)| player_cylinder.min_distance(Cube { min: block_pos.as_(), side_length: 1.0}) < search_dist)
|
||||
.and_then(|(block_pos, interaction)|
|
||||
client.state().terrain().get(block_pos).ok().copied()
|
||||
.map(|b| Interactable::Block(b, block_pos, Some(*interaction)))
|
||||
)
|
||||
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
||||
})
|
||||
// return the closest, and the 2 closest inertactable options (entity or block)
|
||||
closest_interactable_block_pos
|
||||
.filter(|(block_pos, _)| {
|
||||
player_cylinder.min_distance(Cube {
|
||||
min: block_pos.as_(),
|
||||
side_length: 1.0,
|
||||
}) < search_dist
|
||||
})
|
||||
.and_then(|(block_pos, interaction)| {
|
||||
client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(block_pos)
|
||||
.ok()
|
||||
.copied()
|
||||
.map(|b| Interactable::Block(b, block_pos, Some(*interaction)))
|
||||
})
|
||||
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
||||
}
|
||||
}
|
||||
|
@ -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, Target, TargetType};
|
||||
#[cfg(feature = "egui-ui")]
|
||||
use voxygen_egui::EguiDebugInfo;
|
||||
|
||||
@ -433,7 +433,7 @@ impl PlayState for SessionState {
|
||||
|
||||
let is_nearest_target = |target: Option<Target>| {
|
||||
target
|
||||
.map(|t| (t.distance() <= shortest_dist))
|
||||
.map(|t| (t.distance <= shortest_dist))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
@ -444,11 +444,17 @@ impl PlayState for SessionState {
|
||||
build_target.map(|bt| self.scene.set_select_pos(Some(bt.position_int())));
|
||||
} else {
|
||||
self.scene.set_select_pos(None);
|
||||
self.inputs.select_pos = entity_target.map(|et| et.position());
|
||||
self.inputs.select_pos = entity_target.map(|et| et.position);
|
||||
}
|
||||
|
||||
// Throw out distance info, it will be useful in the future
|
||||
self.target_entity = entity_target.and_then(Target::entity);
|
||||
let entity_under_target =
|
||||
if let Some(TargetType::Entity(e)) = entity_target.map(|t| t.typed) {
|
||||
Some(e)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.target_entity = entity_under_target;
|
||||
|
||||
macro_rules! entity_event_handler {
|
||||
($input: expr, $pressed: expr) => {
|
||||
@ -457,7 +463,7 @@ impl PlayState for SessionState {
|
||||
$input,
|
||||
$pressed,
|
||||
self.inputs.select_pos,
|
||||
entity_target.map(Target::entity).unwrap_or(None),
|
||||
entity_under_target,
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -483,10 +489,10 @@ impl PlayState for SessionState {
|
||||
match input {
|
||||
GameInput::Primary => {
|
||||
if is_mining && is_nearest_target(mine_target) {
|
||||
self.inputs.select_pos = mine_target.map(Target::position);
|
||||
self.inputs.select_pos = mine_target.map(|t| t.position);
|
||||
entity_event_handler!(InputKind::Primary, state);
|
||||
} else if state && can_build && is_nearest_target(build_target) {
|
||||
self.inputs.select_pos = build_target.map(Target::position);
|
||||
self.inputs.select_pos = build_target.map(|t| t.position);
|
||||
let mut client = self.client.borrow_mut();
|
||||
client.remove_block(build_target.unwrap().position_int());
|
||||
} else {
|
||||
@ -496,7 +502,7 @@ impl PlayState for SessionState {
|
||||
GameInput::Secondary => {
|
||||
if state && can_build && is_nearest_target(build_target) {
|
||||
if let Some(build_target) = build_target {
|
||||
self.inputs.select_pos = Some(build_target.position());
|
||||
self.inputs.select_pos = Some(build_target.position);
|
||||
let mut client = self.client.borrow_mut();
|
||||
client.place_block(
|
||||
build_target.position_int(),
|
||||
@ -523,7 +529,7 @@ impl PlayState for SessionState {
|
||||
.copied()
|
||||
}) {
|
||||
self.inputs.select_pos =
|
||||
build_target.map(Target::position);
|
||||
build_target.map(|t| t.position);
|
||||
self.selected_block = block;
|
||||
}
|
||||
}
|
||||
@ -682,8 +688,8 @@ impl PlayState for SessionState {
|
||||
match interaction {
|
||||
Some(Interaction::Collect) => {
|
||||
if block.is_collectible() {
|
||||
self.inputs.select_pos = collect_target
|
||||
.map(Target::position);
|
||||
self.inputs.select_pos =
|
||||
collect_target.map(|t| t.position);
|
||||
client.collect_block(pos);
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,8 @@
|
||||
use specs::{Join, WorldExt};
|
||||
use vek::*;
|
||||
|
||||
use super::interactable::Interactable;
|
||||
use crate::scene::terrain::Interaction;
|
||||
use client::{self, Client};
|
||||
use common::{
|
||||
comp,
|
||||
@ -13,40 +15,43 @@ use common::{
|
||||
use common_base::span;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Target {
|
||||
Build(Vec3<f32>, Vec3<f32>, f32), // (solid_pos, build_pos, dist)
|
||||
Collectable(Vec3<f32>, f32), // (pos, dist)
|
||||
Entity(specs::Entity, Vec3<f32>, f32), // (e, pos, dist)
|
||||
Mine(Vec3<f32>, f32), // (pos, dist)
|
||||
pub enum TargetType {
|
||||
Build(Vec3<f32>),
|
||||
Collectable,
|
||||
Entity(specs::Entity),
|
||||
Mine,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Target {
|
||||
pub typed: TargetType,
|
||||
pub distance: f32,
|
||||
pub position: Vec3<f32>,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn entity(self) -> Option<specs::Entity> {
|
||||
match self {
|
||||
Self::Entity(e, _, _) => Some(e),
|
||||
_ => None,
|
||||
pub fn position_int(self) -> Vec3<i32> { self.position.map(|p| p.floor() as i32) }
|
||||
|
||||
pub fn make_interactable(self, client: &Client) -> Option<Interactable> {
|
||||
match self.typed {
|
||||
TargetType::Collectable => client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(self.position_int())
|
||||
.ok()
|
||||
.copied()
|
||||
.map(|b| Interactable::Block(b, self.position_int(), Some(Interaction::Collect))),
|
||||
TargetType::Entity(e) => Some(Interactable::Entity(e)),
|
||||
TargetType::Mine => client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(self.position_int())
|
||||
.ok()
|
||||
.copied()
|
||||
.map(|b| Interactable::Block(b, self.position_int(), None)),
|
||||
TargetType::Build(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn distance(self) -> f32 {
|
||||
match self {
|
||||
Self::Collectable(_, d)
|
||||
| Self::Entity(_, _, d)
|
||||
| Self::Mine(_, d)
|
||||
| Self::Build(_, _, d) => d,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position(self) -> Vec3<f32> {
|
||||
match self {
|
||||
Self::Collectable(sp, _)
|
||||
| Self::Entity(_, sp, _)
|
||||
| Self::Mine(sp, _)
|
||||
| Self::Build(sp, _, _) => sp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_int(self) -> Vec3<i32> { self.position().map(|p| p.floor() as i32) }
|
||||
}
|
||||
|
||||
/// Max distance an entity can be "targeted"
|
||||
@ -130,13 +135,14 @@ pub(super) fn targets_under_cursor(
|
||||
// FIXME: the `solid_pos` is used in the remove_block(). is this correct?
|
||||
let (solid_pos, build_pos, cam_ray_2) = find_pos(|b: Block| b.is_solid());
|
||||
|
||||
// collectables can be in the Air. so using solely solid_pos is not correct.
|
||||
// so, use a minimum distance of all 3
|
||||
// find shortest cam_dist of non-entity targets
|
||||
// note that some of these targets can technically be in Air, such as the
|
||||
// collectable.
|
||||
let mut cam_rays = vec![&cam_ray_0, &cam_ray_2];
|
||||
if is_mining {
|
||||
cam_rays.push(&cam_ray_1);
|
||||
}
|
||||
let cam_dist = cam_rays
|
||||
let shortest_cam_dist = cam_rays
|
||||
.iter()
|
||||
.filter_map(|x| match **x {
|
||||
(d, Ok(Some(_))) => Some(d),
|
||||
@ -146,10 +152,10 @@ pub(super) fn targets_under_cursor(
|
||||
.unwrap_or(MAX_PICKUP_RANGE);
|
||||
|
||||
// See if ray hits entities
|
||||
// Currently treated as spheres
|
||||
// Don't cast through blocks
|
||||
// Could check for intersection with entity from last frame to narrow this down
|
||||
let cast_dist = cam_dist.min(MAX_TARGET_RANGE);
|
||||
// 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
|
||||
// narrow this down
|
||||
let cast_dist = shortest_cam_dist.min(MAX_TARGET_RANGE);
|
||||
|
||||
// Need to raycast by distance to cam
|
||||
// But also filter out by distance to the player (but this only needs to be done
|
||||
@ -191,7 +197,7 @@ pub(super) fn targets_under_cursor(
|
||||
|
||||
let seg_ray = LineSegment3 {
|
||||
start: cam_pos,
|
||||
end: cam_pos + cam_dir * cam_dist,
|
||||
end: cam_pos + cam_dir * shortest_cam_dist,
|
||||
};
|
||||
// TODO: fuzzy borders
|
||||
let entity_target = nearby
|
||||
@ -209,30 +215,48 @@ pub(super) fn targets_under_cursor(
|
||||
);
|
||||
|
||||
let dist_to_player = player_cylinder.min_distance(target_cylinder);
|
||||
(dist_to_player < MAX_TARGET_RANGE).then_some(Target::Entity(*e, p, dist_to_player))
|
||||
if dist_to_player < MAX_TARGET_RANGE {
|
||||
Some(Target {
|
||||
typed: TargetType::Entity(*e),
|
||||
position: p,
|
||||
distance: dist_to_player,
|
||||
})
|
||||
} else { None }
|
||||
});
|
||||
|
||||
let build_target = if can_build {
|
||||
solid_pos.map(|p| Target::Build(p, build_pos.unwrap(), cam_ray_2.0))
|
||||
let build_target = if let (true, Some(position), Some(bp)) = (can_build, solid_pos, build_pos) {
|
||||
Some(Target {
|
||||
typed: TargetType::Build(bp),
|
||||
distance: cam_ray_2.0,
|
||||
position,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mine_target = if is_mining {
|
||||
mine_pos.map(|p| Target::Mine(p, cam_ray_1.0))
|
||||
let collect_target = collect_pos.map(|position| Target {
|
||||
typed: TargetType::Collectable,
|
||||
distance: cam_ray_0.0,
|
||||
position,
|
||||
});
|
||||
|
||||
let mine_target = if let (true, Some(position)) = (is_mining, mine_pos) {
|
||||
Some(Target {
|
||||
typed: TargetType::Mine,
|
||||
distance: cam_ray_1.0,
|
||||
position,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let shortest_distance = cam_dist;
|
||||
|
||||
// Return multiple possible targets
|
||||
// GameInput events determine which target to use.
|
||||
(
|
||||
build_target,
|
||||
collect_pos.map(|p| Target::Collectable(p, cam_ray_0.0)),
|
||||
collect_target,
|
||||
entity_target,
|
||||
mine_target,
|
||||
shortest_distance,
|
||||
shortest_cam_dist,
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user