From 7427367d9672d29810807a877e1da4e2a7a8f3cd Mon Sep 17 00:00:00 2001 From: Imbris Date: Tue, 17 Nov 2020 03:14:55 -0500 Subject: [PATCH] Fix issues with not being able to grab highlighted apples by unifying distance checking --- common/src/util/find_dist.rs | 150 +++++++++++++++++ common/src/util/mod.rs | 1 + server/src/events/inventory_manip.rs | 112 +++++++++---- voxygen/src/session.rs | 242 ++++++++++++++++----------- 4 files changed, 372 insertions(+), 133 deletions(-) create mode 100644 common/src/util/find_dist.rs diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs new file mode 100644 index 0000000000..eec2a615a6 --- /dev/null +++ b/common/src/util/find_dist.rs @@ -0,0 +1,150 @@ +/// Calculate the shortest distance between the surfaces of two shapes +use vek::*; + +pub trait FindDist { + /// 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, + /// Radius of the cylinder + pub radius: f32, + /// Height of the cylinder + pub height: f32, +} + +impl Cylinder { + fn aabb(&self) -> Aabb { + 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, + scale: Option, + collider: Option, + 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, + /// The side length of the cube + pub side_length: f32, +} + +impl FindDist 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 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 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> for Cylinder { + #[inline] + fn approx_in_range(self, other: Vec3, 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 { + // 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() + } +} diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index 94f04ea02c..1656467664 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -1,5 +1,6 @@ mod color; pub mod dir; +pub mod find_dist; mod option; pub mod userdata_dir; diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 958a7208c0..81a3eb3e7a 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -3,12 +3,12 @@ use common::{ comp::{ self, item, slot::{self, Slot}, - Pos, }, consts::MAX_PICKUP_RANGE, msg::ServerGeneral, recipe::default_recipe_book, sync::{Uid, WorldSyncExt}, + util::find_dist::{self, FindDist}, vol::ReadVol, }; use comp::LightEmitter; @@ -59,30 +59,50 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv .get_mut(entity), ) { picked_up_item = Some(item.clone()); - if !within_pickup_range( - state.ecs().read_storage::().get(entity), - state.ecs().read_storage::().get(item_entity), - ) { - debug!("Failed to pick up item as not within range, Uid: {}", uid); - return; - }; - // Grab the health from the player and check if the player is dead. + { + let ecs = state.ecs(); + let positions = ecs.read_storage::(); + let scales = ecs.read_storage::(); + let colliders = ecs.read_storage::(); + let char_states = ecs.read_storage::(); + + 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; + }; + } + + // Grab the health from the entity and check if the entity is dead. let healths = state.ecs().read_storage::(); if let Some(entity_health) = healths.get(entity) { 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 } } - // Attempt to add the item to the player's inventory + // Attempt to add the item to the entity's inventory match inv.push(item) { None => Some(item_entity), Some(_) => None, // Inventory was full } } 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 // world from the first pickup attempt was processed. 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 { 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 - // 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); } 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 block.is_collectible() && state.can_set_block(pos) { // Check if the block is within pickup range - if !within_pickup_range( - state.ecs().read_storage::().get(entity), - // We convert the Vec pos into a Vec, adding 0.5 to get the - // center of the block - Some(&Pos(pos.map(|e| e as f32 + 0.5))), - ) { - return; - }; + { + let ecs = state.ecs(); + let positions = ecs.read_storage::(); + let scales = ecs.read_storage::(); + let colliders = ecs.read_storage::(); + let char_states = ecs.read_storage::(); + + 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; + }; + } if let Some(item) = comp::Item::try_reclaim_from_block(block) { 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 .read_component_copied(entity) - .expect("Expected player to have a UID"); + .expect("Expected entity to have a UID"); if ( &state.read_storage::(), &state.read_storage::(), @@ -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 { - match (player_position, item_position) { - (Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE.powi(2), - _ => false, - } +fn within_pickup_range>( + entity_cylinder: Option, + shape_fn: impl FnOnce() -> Option, +) -> 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)] mod tests { use super::*; use common::comp::Pos; + use find_dist::*; use vek::Vec3; + // Helper function + fn test_cylinder(pos: comp::Pos) -> Option { + Some(Cylinder::from_components(pos.0, None, None, None)) + } + #[test] fn pickup_distance_within_range() { - let player_position = Pos(Vec3::zero()); + let position = Pos(Vec3::zero()); let item_position = Pos(Vec3::one()); assert_eq!( - within_pickup_range(Some(&player_position), Some(&item_position)), + within_pickup_range(test_cylinder(position), || test_cylinder(item_position),), true ); } #[test] 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); assert_eq!( - within_pickup_range(Some(&player_position), Some(&item_position)), + within_pickup_range(test_cylinder(position), || test_cylinder(item_position),), false ); } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 98a3a075dd..5f8fb6eedb 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -22,7 +22,10 @@ use common::{ outcome::Outcome, span, terrain::{Block, BlockKind}, - util::Dir, + util::{ + find_dist::{Cube, Cylinder, FindDist}, + Dir, + }, vol::ReadVol, }; use ordered_float::OrderedFloat; @@ -55,6 +58,7 @@ pub struct SessionState { is_aiming: bool, target_entity: Option, selected_entity: Option<(specs::Entity, std::time::Instant)>, + interactable: Option, } /// Represents an active game session (i.e., the one being played). @@ -95,6 +99,7 @@ impl SessionState { is_aiming: false, target_entity: None, selected_entity: None, + interactable: None, } } @@ -274,9 +279,9 @@ impl PlayState for SessionState { .get(self.client.borrow().entity()) .is_some(); - let interactable = select_interactable( + self.interactable = select_interactable( &self.client.borrow(), - self.target_entity, + target_entity, select_pos, &self.scene, ); @@ -284,14 +289,12 @@ impl PlayState for SessionState { // Only highlight interactables // unless in build mode where select_pos highlighted self.scene - .set_select_pos( - select_pos - .filter(|_| can_build) - .or_else(|| match interactable { - Some(Interactable::Block(_, block_pos)) => Some(block_pos), - _ => None, - }), - ); + .set_select_pos(select_pos.filter(|_| can_build).or_else( + || match self.interactable { + Some(Interactable::Block(_, block_pos)) => Some(block_pos), + _ => None, + }, + )); // Handle window events. for event in events { @@ -497,7 +500,7 @@ impl PlayState for SessionState { self.key_state.collect = state; if state { - if let Some(interactable) = interactable { + if let Some(interactable) = self.interactable { let mut client = self.client.borrow_mut(); match interactable { Interactable::Block(block, pos) => { @@ -1052,7 +1055,8 @@ impl PlayState for SessionState { let scene_data = SceneData { state: client.state(), 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(), view_distance: client.view_distance().unwrap_or(1), tick: client.get_tick(), @@ -1116,7 +1120,8 @@ impl PlayState for SessionState { let scene_data = SceneData { state: client.state(), 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(), view_distance: client.view_distance().unwrap_or(1), tick: client.get_tick(), @@ -1158,14 +1163,22 @@ fn under_cursor( span!(_guard, "under_cursor"); // Choose a spot above the player's head for item distance checks let player_entity = client.entity(); - let player_pos = match client - .state() - .read_storage::() - .get(player_entity) - { - Some(pos) => pos.0 + (Vec3::unit_z() * 2.0), - _ => cam_pos, // Should never happen, but a safe fallback + let ecs = client.state().ecs(); + let positions = ecs.read_storage::(); + let player_pos = match positions.get(player_entity) { + Some(pos) => pos.0, + None => 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 cam_ray = terrain @@ -1177,8 +1190,8 @@ fn under_cursor( // The ray hit something, is it within range? let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if - player_pos.distance_squared(cam_pos + cam_dir * cam_dist) - <= MAX_PICKUP_RANGE.powi(2)) + player_cylinder.min_distance(cam_pos + cam_dir * (cam_dist + 0.01)) + <= MAX_PICKUP_RANGE) { ( 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 // Currently treated as spheres - let ecs = client.state().ecs(); // Don't cast through blocks // Could check for intersection with entity from last frame to narrow this down let cast_dist = if let Ok(Some(_)) = cam_ray.1 { @@ -1204,14 +1216,15 @@ fn under_cursor( // on final result) let mut nearby = ( &ecs.entities(), - &ecs.read_storage::(), - ecs.read_storage::().maybe(), + &positions, + scales.maybe(), &ecs.read_storage::() ) .join() .filter(|(e, _, _, _)| *e != player_entity) .map(|(e, p, s, b)| { 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; // Move position up from the feet 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)) // Find first one that intersects the ray segment .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2)) - .and_then(|(e, p, r)| { - let dist_to_player = p.distance(player_pos); - (dist_to_player - r < MAX_TARGET_RANGE).then_some((*e, dist_to_player)) + .and_then(|(e, p, _)| { + // Get the entity's cylinder + 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 @@ -1254,6 +1275,15 @@ enum Interactable { Entity(specs::Entity), } +impl Interactable { + fn entity(self) -> Option { + match self { + Self::Entity(e) => Some(e), + Self::Block(_, _) => None, + } + } +} + /// Select interactable to hightlight, display interaction text for, and to /// interact with if the interact key is pressed /// Selected in the following order @@ -1262,13 +1292,17 @@ enum Interactable { /// 3) Closest of nearest interactable entity/block fn select_interactable( client: &Client, - target_entity: Option, + target_entity: Option<(specs::Entity, f32)>, selected_pos: Option>, scene: &Scene, ) -> Option { 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}; - 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| client.state().terrain().get(sp).ok().copied() .filter(Block::is_collectible).map(|b| Interactable::Block(b, sp)) @@ -1276,80 +1310,84 @@ fn select_interactable( .or_else(|| { let ecs = client.state().ecs(); let player_entity = client.entity(); - ecs - .read_storage::() - .get(player_entity).and_then(|player_pos| { - let closest_interactable_entity = ( - &ecs.entities(), - &ecs.read_storage::(), - ecs.read_storage::().maybe(), - &ecs.read_storage::(), - // Must have this comp to be interactable (for now) - &ecs.read_storage::(), - ) - .join() - .filter(|(e, _, _, _, _)| *e != player_entity) - .map(|(e, p, s, b, _)| { - let radius = s.map_or(1.0, |s| s.0) * b.radius(); - // Distance squared from player to the entity - // 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 - .filter(|(_, r, d_sqr)| *d_sqr <= MAX_PICKUP_RANGE.powi(2) + 2.0 * MAX_PICKUP_RANGE * r + r.powi(2)) - // Note: entities are approximated as spheres here - // 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)); + let positions = ecs.read_storage::(); + let player_pos = positions.get(player_entity)?.0; - // 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.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e.floor() as i32).div_euclid(sz as i32) - }); - let terrain = scene.terrain(); + let scales = ecs.read_storage::(); + let colliders = ecs.read_storage::(); + let char_states = ecs.read_storage::(); - // 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::::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| chunk_pos + block_offset) - }) - // TODO: confirm that adding 0.5 here is correct - .map(|block_pos| ( - block_pos, - block_pos.map(|e| e as f32 + 0.5) - .distance_squared(player_pos.0) - )) - .min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr)); + let player_cylinder = Cylinder::from_components( + player_pos, + scales.get(player_entity).copied(), + colliders.get(player_entity).copied(), + char_states.get(player_entity), + ); - // Pick closer one if they exist - closest_interactable_block_pos - .filter(|(_, dist_sqr)| search_dist.powi(2) > *dist_sqr) - .and_then(|(block_pos, _)| - client.state().terrain().get(block_pos).ok().copied() - .map(|b| Interactable::Block(b, block_pos)) - ) - .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) + let closest_interactable_entity = ( + &ecs.entities(), + &positions, + scales.maybe(), + colliders.maybe(), + char_states.maybe(), + // Must have this comp to be interactable (for now) + &ecs.read_storage::(), + ) + .join() + .filter(|(e, _, _, _, _, _)| *e != player_entity) + .map(|(e, p, s, c, cs, _)| { + let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), 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::::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| chunk_pos + block_offset) + }) + .map(|block_pos| ( + block_pos, + block_pos.map(|e| e as f32 + 0.5) + .distance_squared(player_pos) + )) + .min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr)) + .map(|(block_pos, _)| block_pos); + + // Pick closer one if they exist + closest_interactable_block_pos + .filter(|block_pos| player_cylinder.min_distance(Cube { pos: block_pos.as_(), side_length: 1.0}) < search_dist) + .and_then(|block_pos| + client.state().terrain().get(block_pos).ok().copied() + .map(|b| Interactable::Block(b, block_pos)) + ) + .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) }) }