mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Fix issues with not being able to grab highlighted apples by unifying distance checking
This commit is contained in:
parent
dd6d81dace
commit
8d02ebf61e
150
common/src/util/find_dist.rs
Normal file
150
common/src/util/find_dist.rs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/// Calculate the shortest distance between the surfaces of two shapes
|
||||||
|
use vek::*;
|
||||||
|
|
||||||
|
pub trait FindDist<T> {
|
||||||
|
/// Compute roughly whether the other shape is out of range
|
||||||
|
/// Meant to be a cheap method for initial filtering
|
||||||
|
/// Must return true if the shape could be within the supplied distance but
|
||||||
|
/// is allowed to return true if the shape is actually just out of
|
||||||
|
/// range
|
||||||
|
fn approx_in_range(self, other: T, range: f32) -> bool;
|
||||||
|
/// Find the smallest distance between the two shapes
|
||||||
|
fn min_distance(self, other: T) -> f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A z-axis aligned cylinder
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Cylinder {
|
||||||
|
/// Center of the cylinder
|
||||||
|
pub pos: Vec3<f32>,
|
||||||
|
/// Radius of the cylinder
|
||||||
|
pub radius: f32,
|
||||||
|
/// Height of the cylinder
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cylinder {
|
||||||
|
fn aabb(&self) -> Aabb<f32> {
|
||||||
|
Aabb {
|
||||||
|
min: self.pos - Vec3::new(self.radius, self.radius, self.height) / 2.0,
|
||||||
|
max: self.pos + Vec3::new(self.radius, self.radius, self.height) / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_components(
|
||||||
|
pos: Vec3<f32>,
|
||||||
|
scale: Option<crate::comp::Scale>,
|
||||||
|
collider: Option<crate::comp::Collider>,
|
||||||
|
char_state: Option<&crate::comp::CharacterState>,
|
||||||
|
) -> Self {
|
||||||
|
let scale = scale.map_or(1.0, |s| s.0);
|
||||||
|
let radius = collider.map_or(0.5, |c| c.get_radius()) * scale;
|
||||||
|
let z_limit_modifier = char_state
|
||||||
|
.filter(|char_state| char_state.is_dodge())
|
||||||
|
.map_or(1.0, |_| 0.5)
|
||||||
|
* scale;
|
||||||
|
let (z_bottom, z_top) = collider
|
||||||
|
.map(|c| c.get_z_limits(z_limit_modifier))
|
||||||
|
.unwrap_or((-0.5 * z_limit_modifier, 0.5 * z_limit_modifier));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pos: pos + Vec3::unit_z() * (z_top + z_bottom) / 2.0,
|
||||||
|
radius,
|
||||||
|
height: z_top - z_bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An axis aligned cube
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Cube {
|
||||||
|
/// The position of min corner of the cube
|
||||||
|
pub pos: Vec3<f32>,
|
||||||
|
/// The side length of the cube
|
||||||
|
pub side_length: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindDist<Cylinder> for Cube {
|
||||||
|
#[inline]
|
||||||
|
fn approx_in_range(self, other: Cylinder, range: f32) -> bool {
|
||||||
|
let cube_plus_range_aabb = Aabb {
|
||||||
|
min: self.pos - Vec3::from(range),
|
||||||
|
max: self.pos + Vec3::from(range),
|
||||||
|
};
|
||||||
|
let cylinder_aabb = other.aabb();
|
||||||
|
|
||||||
|
cube_plus_range_aabb.collides_with_aabb(cylinder_aabb)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn min_distance(self, other: Cylinder) -> f32 {
|
||||||
|
// Distance between centers along the z-axis
|
||||||
|
let z_center_dist = (self.pos.z + self.side_length / 2.0 - other.pos.z).abs();
|
||||||
|
// Distance between surfaces projected onto the z-axis
|
||||||
|
let z_dist = (z_center_dist - (self.side_length + other.height) / 2.0).max(0.0);
|
||||||
|
// Distance between shapes projected onto the xy plane as a square/circle
|
||||||
|
let square_aabr = Aabr {
|
||||||
|
min: self.pos.xy(),
|
||||||
|
max: self.pos.xy() + self.side_length,
|
||||||
|
};
|
||||||
|
let xy_dist = (square_aabr.distance_to_point(other.pos.xy()) - other.radius).max(0.0);
|
||||||
|
// Overall distance by pythagoras
|
||||||
|
(z_dist.powi(2) + xy_dist.powi(2)).sqrt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindDist<Cube> for Cylinder {
|
||||||
|
#[inline]
|
||||||
|
fn approx_in_range(self, other: Cube, range: f32) -> bool { other.approx_in_range(self, range) }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn min_distance(self, other: Cube) -> f32 { other.min_distance(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindDist<Cylinder> for Cylinder {
|
||||||
|
#[inline]
|
||||||
|
fn approx_in_range(self, other: Cylinder, range: f32) -> bool {
|
||||||
|
let mut aabb = self.aabb();
|
||||||
|
aabb.min -= range;
|
||||||
|
aabb.max += range;
|
||||||
|
|
||||||
|
aabb.collides_with_aabb(other.aabb())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn min_distance(self, other: Cylinder) -> f32 {
|
||||||
|
// Distance between centers along the z-axis
|
||||||
|
let z_center_dist = (self.pos.z - other.pos.z).abs();
|
||||||
|
// Distance between surfaces projected onto the z-axis
|
||||||
|
let z_dist = (z_center_dist - (self.height + other.height) / 2.0).max(0.0);
|
||||||
|
// Distance between shapes projected onto the xy plane as a circles
|
||||||
|
let xy_dist =
|
||||||
|
(self.pos.xy().distance(other.pos.xy()) - self.radius - other.radius).max(0.0);
|
||||||
|
// Overall distance by pythagoras
|
||||||
|
(z_dist.powi(2) + xy_dist.powi(2)).sqrt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindDist<Vec3<f32>> for Cylinder {
|
||||||
|
#[inline]
|
||||||
|
fn approx_in_range(self, other: Vec3<f32>, range: f32) -> bool {
|
||||||
|
let mut aabb = self.aabb();
|
||||||
|
aabb.min -= range;
|
||||||
|
aabb.max += range;
|
||||||
|
|
||||||
|
aabb.contains_point(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn min_distance(self, other: Vec3<f32>) -> f32 {
|
||||||
|
// Distance between center and point along the z-axis
|
||||||
|
let z_center_dist = (self.pos.z - other.z).abs();
|
||||||
|
// Distance between surface and point projected onto the z-axis
|
||||||
|
let z_dist = (z_center_dist - self.height / 2.0).max(0.0);
|
||||||
|
// Distance between shapes projected onto the xy plane
|
||||||
|
let xy_dist = (self.pos.xy().distance(other.xy()) - self.radius).max(0.0);
|
||||||
|
// Overall distance by pythagoras
|
||||||
|
(z_dist.powi(2) + xy_dist.powi(2)).sqrt()
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
mod color;
|
mod color;
|
||||||
pub mod dir;
|
pub mod dir;
|
||||||
|
pub mod find_dist;
|
||||||
mod option;
|
mod option;
|
||||||
pub mod userdata_dir;
|
pub mod userdata_dir;
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ use common::{
|
|||||||
comp::{
|
comp::{
|
||||||
self, item,
|
self, item,
|
||||||
slot::{self, Slot},
|
slot::{self, Slot},
|
||||||
Pos,
|
|
||||||
},
|
},
|
||||||
consts::MAX_PICKUP_RANGE,
|
consts::MAX_PICKUP_RANGE,
|
||||||
msg::ServerGeneral,
|
msg::ServerGeneral,
|
||||||
recipe::default_recipe_book,
|
recipe::default_recipe_book,
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, WorldSyncExt},
|
||||||
|
util::find_dist::{self, FindDist},
|
||||||
vol::ReadVol,
|
vol::ReadVol,
|
||||||
};
|
};
|
||||||
use comp::LightEmitter;
|
use comp::LightEmitter;
|
||||||
@ -59,30 +59,50 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
.get_mut(entity),
|
.get_mut(entity),
|
||||||
) {
|
) {
|
||||||
picked_up_item = Some(item.clone());
|
picked_up_item = Some(item.clone());
|
||||||
if !within_pickup_range(
|
|
||||||
state.ecs().read_storage::<comp::Pos>().get(entity),
|
{
|
||||||
state.ecs().read_storage::<comp::Pos>().get(item_entity),
|
let ecs = state.ecs();
|
||||||
) {
|
let positions = ecs.read_storage::<comp::Pos>();
|
||||||
debug!("Failed to pick up item as not within range, Uid: {}", uid);
|
let scales = ecs.read_storage::<comp::Scale>();
|
||||||
|
let colliders = ecs.read_storage::<comp::Collider>();
|
||||||
|
let char_states = ecs.read_storage::<comp::CharacterState>();
|
||||||
|
|
||||||
|
let cylinder = |entity| {
|
||||||
|
positions.get(entity).map(|p| {
|
||||||
|
find_dist::Cylinder::from_components(
|
||||||
|
p.0,
|
||||||
|
scales.get(entity).copied(),
|
||||||
|
colliders.get(entity).copied(),
|
||||||
|
char_states.get(entity),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let entity_cylinder = cylinder(entity);
|
||||||
|
if !within_pickup_range(entity_cylinder, || cylinder(item_entity)) {
|
||||||
|
debug!(
|
||||||
|
?entity_cylinder,
|
||||||
|
"Failed to pick up item as not within range, Uid: {}", uid
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the health from the player and check if the player is dead.
|
// Grab the health from the entity and check if the entity is dead.
|
||||||
let healths = state.ecs().read_storage::<comp::Health>();
|
let healths = state.ecs().read_storage::<comp::Health>();
|
||||||
if let Some(entity_health) = healths.get(entity) {
|
if let Some(entity_health) = healths.get(entity) {
|
||||||
if entity_health.is_dead {
|
if entity_health.is_dead {
|
||||||
debug!("Failed to pick up item as the player is dead");
|
debug!("Failed to pick up item as the entity is dead");
|
||||||
return; // If dead, don't continue
|
return; // If dead, don't continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to add the item to the player's inventory
|
// Attempt to add the item to the entity's inventory
|
||||||
match inv.push(item) {
|
match inv.push(item) {
|
||||||
None => Some(item_entity),
|
None => Some(item_entity),
|
||||||
Some(_) => None, // Inventory was full
|
Some(_) => None, // Inventory was full
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Item entity/component could not be found - most likely because the player
|
// Item entity/component could not be found - most likely because the entity
|
||||||
// attempted to pick up the same item very quickly before its deletion of the
|
// attempted to pick up the same item very quickly before its deletion of the
|
||||||
// world from the first pickup attempt was processed.
|
// world from the first pickup attempt was processed.
|
||||||
debug!("Failed to get entity/component for item Uid: {}", uid);
|
debug!("Failed to get entity/component for item Uid: {}", uid);
|
||||||
@ -92,7 +112,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
let event = if let Some(item_entity) = item_entity {
|
let event = if let Some(item_entity) = item_entity {
|
||||||
if let Err(err) = state.delete_entity_recorded(item_entity) {
|
if let Err(err) = state.delete_entity_recorded(item_entity) {
|
||||||
// If this occurs it means the item was duped as it's been pushed to the
|
// If this occurs it means the item was duped as it's been pushed to the
|
||||||
// player's inventory but also left on the ground
|
// entity's inventory but also left on the ground
|
||||||
panic!("Failed to delete picked up item entity: {:?}", err);
|
panic!("Failed to delete picked up item entity: {:?}", err);
|
||||||
}
|
}
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(
|
||||||
@ -111,14 +131,34 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
if let Some(block) = block {
|
if let Some(block) = block {
|
||||||
if block.is_collectible() && state.can_set_block(pos) {
|
if block.is_collectible() && state.can_set_block(pos) {
|
||||||
// Check if the block is within pickup range
|
// Check if the block is within pickup range
|
||||||
if !within_pickup_range(
|
{
|
||||||
state.ecs().read_storage::<comp::Pos>().get(entity),
|
let ecs = state.ecs();
|
||||||
// We convert the Vec<i32> pos into a Vec<f32>, adding 0.5 to get the
|
let positions = ecs.read_storage::<comp::Pos>();
|
||||||
// center of the block
|
let scales = ecs.read_storage::<comp::Scale>();
|
||||||
Some(&Pos(pos.map(|e| e as f32 + 0.5))),
|
let colliders = ecs.read_storage::<comp::Collider>();
|
||||||
) {
|
let char_states = ecs.read_storage::<comp::CharacterState>();
|
||||||
|
|
||||||
|
let entity_cylinder = positions.get(entity).map(|p| {
|
||||||
|
find_dist::Cylinder::from_components(
|
||||||
|
p.0,
|
||||||
|
scales.get(entity).copied(),
|
||||||
|
colliders.get(entity).copied(),
|
||||||
|
char_states.get(entity),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if !within_pickup_range(entity_cylinder, || {
|
||||||
|
Some(find_dist::Cube {
|
||||||
|
pos: pos.as_(),
|
||||||
|
side_length: 1.0,
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
debug!(
|
||||||
|
?entity_cylinder,
|
||||||
|
"Failed to pick up block as not within range, block pos: {}", pos
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
|
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
|
||||||
let (event, item_was_added) = if let Some(inv) = state
|
let (event, item_was_added) = if let Some(inv) = state
|
||||||
@ -241,7 +281,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
{
|
{
|
||||||
let uid = state
|
let uid = state
|
||||||
.read_component_copied(entity)
|
.read_component_copied(entity)
|
||||||
.expect("Expected player to have a UID");
|
.expect("Expected entity to have a UID");
|
||||||
if (
|
if (
|
||||||
&state.read_storage::<comp::Alignment>(),
|
&state.read_storage::<comp::Alignment>(),
|
||||||
&state.read_storage::<comp::Agent>(),
|
&state.read_storage::<comp::Agent>(),
|
||||||
@ -529,37 +569,47 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn within_pickup_range(player_position: Option<&Pos>, item_position: Option<&Pos>) -> bool {
|
fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(
|
||||||
match (player_position, item_position) {
|
entity_cylinder: Option<find_dist::Cylinder>,
|
||||||
(Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE.powi(2),
|
shape_fn: impl FnOnce() -> Option<S>,
|
||||||
_ => false,
|
) -> bool {
|
||||||
}
|
entity_cylinder
|
||||||
|
.and_then(|entity_cylinder| {
|
||||||
|
shape_fn().map(|shape| dbg!(shape.min_distance(entity_cylinder)) < MAX_PICKUP_RANGE)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use common::comp::Pos;
|
use common::comp::Pos;
|
||||||
|
use find_dist::*;
|
||||||
use vek::Vec3;
|
use vek::Vec3;
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
fn test_cylinder(pos: comp::Pos) -> Option<Cylinder> {
|
||||||
|
Some(Cylinder::from_components(pos.0, None, None, None))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pickup_distance_within_range() {
|
fn pickup_distance_within_range() {
|
||||||
let player_position = Pos(Vec3::zero());
|
let position = Pos(Vec3::zero());
|
||||||
let item_position = Pos(Vec3::one());
|
let item_position = Pos(Vec3::one());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
within_pickup_range(Some(&player_position), Some(&item_position)),
|
within_pickup_range(test_cylinder(position), || test_cylinder(item_position),),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pickup_distance_not_within_range() {
|
fn pickup_distance_not_within_range() {
|
||||||
let player_position = Pos(Vec3::zero());
|
let position = Pos(Vec3::zero());
|
||||||
let item_position = Pos(Vec3::one() * 500.0);
|
let item_position = Pos(Vec3::one() * 500.0);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
within_pickup_range(Some(&player_position), Some(&item_position)),
|
within_pickup_range(test_cylinder(position), || test_cylinder(item_position),),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,10 @@ use common::{
|
|||||||
outcome::Outcome,
|
outcome::Outcome,
|
||||||
span,
|
span,
|
||||||
terrain::{Block, BlockKind},
|
terrain::{Block, BlockKind},
|
||||||
util::Dir,
|
util::{
|
||||||
|
find_dist::{Cube, Cylinder, FindDist},
|
||||||
|
Dir,
|
||||||
|
},
|
||||||
vol::ReadVol,
|
vol::ReadVol,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
@ -55,6 +58,7 @@ pub struct SessionState {
|
|||||||
is_aiming: bool,
|
is_aiming: bool,
|
||||||
target_entity: Option<specs::Entity>,
|
target_entity: Option<specs::Entity>,
|
||||||
selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
||||||
|
interactable: Option<Interactable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents an active game session (i.e., the one being played).
|
/// Represents an active game session (i.e., the one being played).
|
||||||
@ -95,6 +99,7 @@ impl SessionState {
|
|||||||
is_aiming: false,
|
is_aiming: false,
|
||||||
target_entity: None,
|
target_entity: None,
|
||||||
selected_entity: None,
|
selected_entity: None,
|
||||||
|
interactable: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,9 +279,9 @@ impl PlayState for SessionState {
|
|||||||
.get(self.client.borrow().entity())
|
.get(self.client.borrow().entity())
|
||||||
.is_some();
|
.is_some();
|
||||||
|
|
||||||
let interactable = select_interactable(
|
self.interactable = select_interactable(
|
||||||
&self.client.borrow(),
|
&self.client.borrow(),
|
||||||
self.target_entity,
|
target_entity,
|
||||||
select_pos,
|
select_pos,
|
||||||
&self.scene,
|
&self.scene,
|
||||||
);
|
);
|
||||||
@ -284,14 +289,12 @@ impl PlayState for SessionState {
|
|||||||
// Only highlight interactables
|
// Only highlight interactables
|
||||||
// unless in build mode where select_pos highlighted
|
// unless in build mode where select_pos highlighted
|
||||||
self.scene
|
self.scene
|
||||||
.set_select_pos(
|
.set_select_pos(select_pos.filter(|_| can_build).or_else(
|
||||||
select_pos
|
|| match self.interactable {
|
||||||
.filter(|_| can_build)
|
|
||||||
.or_else(|| match interactable {
|
|
||||||
Some(Interactable::Block(_, block_pos)) => Some(block_pos),
|
Some(Interactable::Block(_, block_pos)) => Some(block_pos),
|
||||||
_ => None,
|
_ => None,
|
||||||
}),
|
},
|
||||||
);
|
));
|
||||||
|
|
||||||
// Handle window events.
|
// Handle window events.
|
||||||
for event in events {
|
for event in events {
|
||||||
@ -497,7 +500,7 @@ impl PlayState for SessionState {
|
|||||||
self.key_state.collect = state;
|
self.key_state.collect = state;
|
||||||
|
|
||||||
if state {
|
if state {
|
||||||
if let Some(interactable) = interactable {
|
if let Some(interactable) = self.interactable {
|
||||||
let mut client = self.client.borrow_mut();
|
let mut client = self.client.borrow_mut();
|
||||||
match interactable {
|
match interactable {
|
||||||
Interactable::Block(block, pos) => {
|
Interactable::Block(block, pos) => {
|
||||||
@ -1052,7 +1055,8 @@ impl PlayState for SessionState {
|
|||||||
let scene_data = SceneData {
|
let scene_data = SceneData {
|
||||||
state: client.state(),
|
state: client.state(),
|
||||||
player_entity: client.entity(),
|
player_entity: client.entity(),
|
||||||
target_entity: self.target_entity,
|
// Only highlight if interactable
|
||||||
|
target_entity: self.interactable.and_then(Interactable::entity),
|
||||||
loaded_distance: client.loaded_distance(),
|
loaded_distance: client.loaded_distance(),
|
||||||
view_distance: client.view_distance().unwrap_or(1),
|
view_distance: client.view_distance().unwrap_or(1),
|
||||||
tick: client.get_tick(),
|
tick: client.get_tick(),
|
||||||
@ -1116,7 +1120,8 @@ impl PlayState for SessionState {
|
|||||||
let scene_data = SceneData {
|
let scene_data = SceneData {
|
||||||
state: client.state(),
|
state: client.state(),
|
||||||
player_entity: client.entity(),
|
player_entity: client.entity(),
|
||||||
target_entity: self.target_entity,
|
// Only highlight if interactable
|
||||||
|
target_entity: self.interactable.and_then(Interactable::entity),
|
||||||
loaded_distance: client.loaded_distance(),
|
loaded_distance: client.loaded_distance(),
|
||||||
view_distance: client.view_distance().unwrap_or(1),
|
view_distance: client.view_distance().unwrap_or(1),
|
||||||
tick: client.get_tick(),
|
tick: client.get_tick(),
|
||||||
@ -1158,14 +1163,22 @@ fn under_cursor(
|
|||||||
span!(_guard, "under_cursor");
|
span!(_guard, "under_cursor");
|
||||||
// Choose a spot above the player's head for item distance checks
|
// Choose a spot above the player's head for item distance checks
|
||||||
let player_entity = client.entity();
|
let player_entity = client.entity();
|
||||||
let player_pos = match client
|
let ecs = client.state().ecs();
|
||||||
.state()
|
let positions = ecs.read_storage::<comp::Pos>();
|
||||||
.read_storage::<comp::Pos>()
|
let player_pos = match positions.get(player_entity) {
|
||||||
.get(player_entity)
|
Some(pos) => pos.0,
|
||||||
{
|
None => cam_pos, // Should never happen, but a safe fallback
|
||||||
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
|
|
||||||
_ => cam_pos, // Should never happen, but a safe fallback
|
|
||||||
};
|
};
|
||||||
|
let scales = ecs.read_storage();
|
||||||
|
let colliders = ecs.read_storage();
|
||||||
|
let char_states = ecs.read_storage();
|
||||||
|
// Get the player's cylinder
|
||||||
|
let player_cylinder = Cylinder::from_components(
|
||||||
|
player_pos,
|
||||||
|
scales.get(player_entity).copied(),
|
||||||
|
colliders.get(player_entity).copied(),
|
||||||
|
char_states.get(player_entity),
|
||||||
|
);
|
||||||
let terrain = client.state().terrain();
|
let terrain = client.state().terrain();
|
||||||
|
|
||||||
let cam_ray = terrain
|
let cam_ray = terrain
|
||||||
@ -1177,8 +1190,8 @@ fn under_cursor(
|
|||||||
|
|
||||||
// The ray hit something, is it within range?
|
// The ray hit something, is it within range?
|
||||||
let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if
|
let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if
|
||||||
player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
|
player_cylinder.min_distance(cam_pos + cam_dir * (cam_dist + 0.01))
|
||||||
<= MAX_PICKUP_RANGE.powi(2))
|
<= MAX_PICKUP_RANGE)
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
|
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
|
||||||
@ -1190,7 +1203,6 @@ fn under_cursor(
|
|||||||
|
|
||||||
// See if ray hits entities
|
// See if ray hits entities
|
||||||
// Currently treated as spheres
|
// Currently treated as spheres
|
||||||
let ecs = client.state().ecs();
|
|
||||||
// Don't cast through blocks
|
// Don't cast through blocks
|
||||||
// Could check for intersection with entity from last frame to narrow this down
|
// Could check for intersection with entity from last frame to narrow this down
|
||||||
let cast_dist = if let Ok(Some(_)) = cam_ray.1 {
|
let cast_dist = if let Ok(Some(_)) = cam_ray.1 {
|
||||||
@ -1204,14 +1216,15 @@ fn under_cursor(
|
|||||||
// on final result)
|
// on final result)
|
||||||
let mut nearby = (
|
let mut nearby = (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
&ecs.read_storage::<comp::Pos>(),
|
&positions,
|
||||||
ecs.read_storage::<comp::Scale>().maybe(),
|
scales.maybe(),
|
||||||
&ecs.read_storage::<comp::Body>()
|
&ecs.read_storage::<comp::Body>()
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
.filter(|(e, _, _, _)| *e != player_entity)
|
.filter(|(e, _, _, _)| *e != player_entity)
|
||||||
.map(|(e, p, s, b)| {
|
.map(|(e, p, s, b)| {
|
||||||
const RADIUS_SCALE: f32 = 3.0;
|
const RADIUS_SCALE: f32 = 3.0;
|
||||||
|
// TODO: use collider radius instead of body radius?
|
||||||
let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE;
|
let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE;
|
||||||
// Move position up from the feet
|
// Move position up from the feet
|
||||||
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
|
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
|
||||||
@ -1239,9 +1252,17 @@ fn under_cursor(
|
|||||||
.map(|(e, p, r, _)| (e, *p, r))
|
.map(|(e, p, r, _)| (e, *p, r))
|
||||||
// Find first one that intersects the ray segment
|
// Find first one that intersects the ray segment
|
||||||
.find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
|
.find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
|
||||||
.and_then(|(e, p, r)| {
|
.and_then(|(e, p, _)| {
|
||||||
let dist_to_player = p.distance(player_pos);
|
// Get the entity's cylinder
|
||||||
(dist_to_player - r < MAX_TARGET_RANGE).then_some((*e, dist_to_player))
|
let target_cylinder = Cylinder::from_components(
|
||||||
|
p,
|
||||||
|
scales.get(*e).copied(),
|
||||||
|
colliders.get(*e).copied(),
|
||||||
|
char_states.get(*e),
|
||||||
|
);
|
||||||
|
|
||||||
|
let dist_to_player = player_cylinder.min_distance(target_cylinder);
|
||||||
|
(dist_to_player < MAX_TARGET_RANGE).then_some((*e, dist_to_player))
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: consider setting build/select to None when targeting an entity
|
// TODO: consider setting build/select to None when targeting an entity
|
||||||
@ -1254,6 +1275,15 @@ enum Interactable {
|
|||||||
Entity(specs::Entity),
|
Entity(specs::Entity),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Interactable {
|
||||||
|
fn entity(self) -> Option<specs::Entity> {
|
||||||
|
match self {
|
||||||
|
Self::Entity(e) => Some(e),
|
||||||
|
Self::Block(_, _) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Select interactable to hightlight, display interaction text for, and to
|
/// Select interactable to hightlight, display interaction text for, and to
|
||||||
/// interact with if the interact key is pressed
|
/// interact with if the interact key is pressed
|
||||||
/// Selected in the following order
|
/// Selected in the following order
|
||||||
@ -1262,13 +1292,17 @@ enum Interactable {
|
|||||||
/// 3) Closest of nearest interactable entity/block
|
/// 3) Closest of nearest interactable entity/block
|
||||||
fn select_interactable(
|
fn select_interactable(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
target_entity: Option<specs::Entity>,
|
target_entity: Option<(specs::Entity, f32)>,
|
||||||
selected_pos: Option<Vec3<i32>>,
|
selected_pos: Option<Vec3<i32>>,
|
||||||
scene: &Scene,
|
scene: &Scene,
|
||||||
) -> Option<Interactable> {
|
) -> Option<Interactable> {
|
||||||
span!(_guard, "select_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};
|
use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
|
||||||
target_entity.map(Interactable::Entity)
|
target_entity
|
||||||
|
.and_then(|(e, dist_to_player)| (dist_to_player < MAX_PICKUP_RANGE).then_some(Interactable::Entity(e)))
|
||||||
.or_else(|| selected_pos.and_then(|sp|
|
.or_else(|| selected_pos.and_then(|sp|
|
||||||
client.state().terrain().get(sp).ok().copied()
|
client.state().terrain().get(sp).ok().copied()
|
||||||
.filter(Block::is_collectible).map(|b| Interactable::Block(b, sp))
|
.filter(Block::is_collectible).map(|b| Interactable::Block(b, sp))
|
||||||
@ -1276,39 +1310,44 @@ fn select_interactable(
|
|||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let ecs = client.state().ecs();
|
let ecs = client.state().ecs();
|
||||||
let player_entity = client.entity();
|
let player_entity = client.entity();
|
||||||
ecs
|
let positions = ecs.read_storage::<comp::Pos>();
|
||||||
.read_storage::<comp::Pos>()
|
let player_pos = positions.get(player_entity)?.0;
|
||||||
.get(player_entity).and_then(|player_pos| {
|
|
||||||
|
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).copied(),
|
||||||
|
char_states.get(player_entity),
|
||||||
|
);
|
||||||
|
|
||||||
let closest_interactable_entity = (
|
let closest_interactable_entity = (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
&ecs.read_storage::<comp::Pos>(),
|
&positions,
|
||||||
ecs.read_storage::<comp::Scale>().maybe(),
|
scales.maybe(),
|
||||||
&ecs.read_storage::<comp::Body>(),
|
colliders.maybe(),
|
||||||
|
char_states.maybe(),
|
||||||
// Must have this comp to be interactable (for now)
|
// Must have this comp to be interactable (for now)
|
||||||
&ecs.read_storage::<comp::Item>(),
|
&ecs.read_storage::<comp::Item>(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
.filter(|(e, _, _, _, _)| *e != player_entity)
|
.filter(|(e, _, _, _, _, _)| *e != player_entity)
|
||||||
.map(|(e, p, s, b, _)| {
|
.map(|(e, p, s, c, cs, _)| {
|
||||||
let radius = s.map_or(1.0, |s| s.0) * b.radius();
|
let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs);
|
||||||
// Distance squared from player to the entity
|
(e, cylinder)
|
||||||
// Note: the position of entities is currently at their feet so this
|
|
||||||
// distance is between their feet positions
|
|
||||||
let dist_sqr = p.0.distance_squared(player_pos.0);
|
|
||||||
(e, radius, dist_sqr)
|
|
||||||
})
|
})
|
||||||
// Roughly filter out entities farther than interaction distance
|
// Roughly filter out entities farther than interaction distance
|
||||||
.filter(|(_, r, d_sqr)| *d_sqr <= MAX_PICKUP_RANGE.powi(2) + 2.0 * MAX_PICKUP_RANGE * r + r.powi(2))
|
.filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE))
|
||||||
// Note: entities are approximated as spheres here
|
.map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder)))
|
||||||
// to determine which is closer
|
|
||||||
// Substract sphere radius from distance to the player
|
|
||||||
.map(|(e, r, d_sqr)| (e, d_sqr.sqrt() - r))
|
|
||||||
.min_by_key(|(_, dist)| OrderedFloat(*dist));
|
.min_by_key(|(_, dist)| OrderedFloat(*dist));
|
||||||
|
|
||||||
// Only search as far as closest interactable entity
|
// Only search as far as closest interactable entity
|
||||||
let search_dist = closest_interactable_entity
|
let search_dist = closest_interactable_entity
|
||||||
.map_or(MAX_PICKUP_RANGE, |(_, dist)| dist);
|
.map_or(MAX_PICKUP_RANGE, |(_, dist)| dist);
|
||||||
let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
||||||
(e.floor() as i32).div_euclid(sz as i32)
|
(e.floor() as i32).div_euclid(sz as i32)
|
||||||
});
|
});
|
||||||
let terrain = scene.terrain();
|
let terrain = scene.terrain();
|
||||||
@ -1334,22 +1373,21 @@ fn select_interactable(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(move |block_offset| chunk_pos + block_offset)
|
.map(move |block_offset| chunk_pos + block_offset)
|
||||||
})
|
})
|
||||||
// TODO: confirm that adding 0.5 here is correct
|
|
||||||
.map(|block_pos| (
|
.map(|block_pos| (
|
||||||
block_pos,
|
block_pos,
|
||||||
block_pos.map(|e| e as f32 + 0.5)
|
block_pos.map(|e| e as f32 + 0.5)
|
||||||
.distance_squared(player_pos.0)
|
.distance_squared(player_pos)
|
||||||
))
|
))
|
||||||
.min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr));
|
.min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr))
|
||||||
|
.map(|(block_pos, _)| block_pos);
|
||||||
|
|
||||||
// Pick closer one if they exist
|
// Pick closer one if they exist
|
||||||
closest_interactable_block_pos
|
closest_interactable_block_pos
|
||||||
.filter(|(_, dist_sqr)| search_dist.powi(2) > *dist_sqr)
|
.filter(|block_pos| player_cylinder.min_distance(Cube { pos: block_pos.as_(), side_length: 1.0}) < search_dist)
|
||||||
.and_then(|(block_pos, _)|
|
.and_then(|block_pos|
|
||||||
client.state().terrain().get(block_pos).ok().copied()
|
client.state().terrain().get(block_pos).ok().copied()
|
||||||
.map(|b| Interactable::Block(b, block_pos))
|
.map(|b| Interactable::Block(b, block_pos))
|
||||||
)
|
)
|
||||||
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user