Allow interacting with nearby blocks without pointing at them, unify selection of block/entity interactors so that only one is select at once, rearrange pickup and mount range consts

This commit is contained in:
Imbris 2020-10-29 02:11:10 -04:00
parent 325695e937
commit 64def3cde4
12 changed files with 211 additions and 109 deletions

14
Cargo.lock generated
View File

@ -2963,6 +2963,15 @@ dependencies = [
"num-traits 0.2.12",
]
[[package]]
name = "ordered-float"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fe9037165d7023b1228bc4ae9a2fa1a2b0095eca6c2998c624723dfd01314a5"
dependencies = [
"num-traits 0.2.12",
]
[[package]]
name = "osascript"
version = "0.3.0"
@ -3691,7 +3700,7 @@ dependencies = [
"crossbeam-utils 0.7.2",
"linked-hash-map",
"num_cpus",
"ordered-float",
"ordered-float 1.1.0",
"rustc-hash",
"stb_truetype",
]
@ -4962,6 +4971,7 @@ dependencies = [
"native-dialog",
"num 0.2.1",
"old_school_gfx_glutin_ext",
"ordered-float 2.0.0",
"rand 0.7.3",
"rodio",
"ron",
@ -5014,7 +5024,7 @@ dependencies = [
"minifb",
"noise",
"num 0.2.1",
"ordered-float",
"ordered-float 1.1.0",
"packed_simd_2",
"rand 0.7.3",
"rand_chacha 0.2.2",

View File

@ -8,9 +8,6 @@ use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage, HashMapStorage};
use specs_idvs::IdvStorage;
// The limit on distance between the entity and a collectible (squared)
pub const MAX_PICKUP_RANGE_SQR: f32 = 64.0;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Inventory {
slots: Vec<Option<Item>>,

View File

@ -49,13 +49,13 @@ pub use inputs::CanBuild;
pub use inventory::{
item,
item::{Item, ItemDrop},
slot, Inventory, InventoryUpdate, InventoryUpdateEvent, MAX_PICKUP_RANGE_SQR,
slot, Inventory, InventoryUpdate, InventoryUpdateEvent,
};
pub use last::Last;
pub use location::{Waypoint, WaypointArea};
pub use misc::Object;
pub use phys::{Collider, ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel};
pub use player::{Player, MAX_MOUNT_RANGE_SQR};
pub use player::Player;
pub use projectile::Projectile;
pub use shockwave::{Shockwave, ShockwaveHitEntities};
pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet};

View File

@ -5,7 +5,6 @@ use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IdvStorage;
const MAX_ALIAS_LEN: usize = 32;
pub const MAX_MOUNT_RANGE_SQR: i32 = 20000;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Player {

3
common/src/consts.rs Normal file
View File

@ -0,0 +1,3 @@
// The limit on distance between the entity and a collectible (squared)
pub const MAX_PICKUP_RANGE: f32 = 8.0;
pub const MAX_MOUNT_RANGE: f32 = 14.0;

View File

@ -24,6 +24,7 @@ pub mod clock;
pub mod cmd;
pub mod combat;
pub mod comp;
pub mod consts;
pub mod effect;
pub mod event;
pub mod explosion;

View File

@ -3,8 +3,9 @@ use common::{
comp::{
self, item,
slot::{self, Slot},
Pos, MAX_PICKUP_RANGE_SQR,
Pos,
},
consts::MAX_PICKUP_RANGE,
msg::ServerGeneral,
recipe::default_recipe_book,
sync::{Uid, WorldSyncExt},
@ -512,7 +513,7 @@ 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_SQR,
(Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE.powi(2),
_ => false,
}
}

View File

@ -66,6 +66,7 @@ hashbrown = {version = "0.7.2", features = ["rayon", "serde", "nightly"]}
image = {version = "0.23.8", default-features = false, features = ["ico", "png"]}
native-dialog = { version = "0.4.2", default-features = false, optional = true }
num = "0.2"
ordered-float = "2.0.0"
rand = "0.7"
rodio = {version = "0.11", default-features = false, features = ["wav", "vorbis"]}
ron = {version = "0.6", default-features = false}

View File

@ -1123,7 +1123,7 @@ impl Hud {
for (pos, item, distance) in (&entities, &pos, &items)
.join()
.map(|(_, pos, item)| (pos, item, pos.0.distance_squared(player_pos)))
.filter(|(_, _, distance)| distance < &common::comp::MAX_PICKUP_RANGE_SQR)
.filter(|(_, _, distance)| distance < &common::consts::MAX_PICKUP_RANGE.powi(2))
{
let overitem_id = overitem_walker.next(
&mut self.ids.overitems,

View File

@ -91,9 +91,10 @@ impl<'a> Widget for Overitem<'a> {
// ———
// scale at max distance is 10, and at min distance is 30
let scale: f64 =
((1.5 - (self.distance_from_player_sqr / common::comp::MAX_PICKUP_RANGE_SQR)) * 20.0)
.into();
let scale: f64 = ((1.5
- (self.distance_from_player_sqr / common::consts::MAX_PICKUP_RANGE.powi(2)))
* 20.0)
.into();
let text_font_size = scale * 1.0;
let text_pos_y = scale * 1.2;
let btn_rect_size = scale * 0.8;

View File

@ -13,6 +13,9 @@ pub struct BlocksOfInterest {
pub beehives: Vec<Vec3<i32>>,
pub reeds: Vec<Vec3<i32>>,
pub flowers: Vec<Vec3<i32>>,
// Note: these are only needed for chunks within the iteraction range so this is a potential
// area for optimization
pub interactables: Vec<Vec3<i32>>,
}
impl BlocksOfInterest {
@ -24,6 +27,7 @@ impl BlocksOfInterest {
let mut beehives = Vec::new();
let mut reeds = Vec::new();
let mut flowers = Vec::new();
let mut interactables = Vec::new();
chunk
.vol_iter(
@ -34,29 +38,34 @@ impl BlocksOfInterest {
chunk.get_max_z(),
),
)
.for_each(|(pos, block)| match block.kind() {
BlockKind::Leaves => {
if thread_rng().gen_range(0, 16) == 0 {
leaves.push(pos)
}
},
BlockKind::Grass => {
if thread_rng().gen_range(0, 16) == 0 {
grass.push(pos)
}
},
_ => match block.get_sprite() {
Some(SpriteKind::Ember) => embers.push(pos),
Some(SpriteKind::Beehive) => beehives.push(pos),
Some(SpriteKind::Reed) => reeds.push(pos),
Some(SpriteKind::PinkFlower) => flowers.push(pos),
Some(SpriteKind::PurpleFlower) => flowers.push(pos),
Some(SpriteKind::RedFlower) => flowers.push(pos),
Some(SpriteKind::WhiteFlower) => flowers.push(pos),
Some(SpriteKind::YellowFlower) => flowers.push(pos),
Some(SpriteKind::Sunflower) => flowers.push(pos),
_ => {},
},
.for_each(|(pos, block)| {
match block.kind() {
BlockKind::Leaves => {
if thread_rng().gen_range(0, 16) == 0 {
leaves.push(pos)
}
},
BlockKind::Grass => {
if thread_rng().gen_range(0, 16) == 0 {
grass.push(pos)
}
},
_ => match block.get_sprite() {
Some(SpriteKind::Ember) => embers.push(pos),
Some(SpriteKind::Beehive) => beehives.push(pos),
Some(SpriteKind::Reed) => reeds.push(pos),
Some(SpriteKind::PinkFlower) => flowers.push(pos),
Some(SpriteKind::PurpleFlower) => flowers.push(pos),
Some(SpriteKind::RedFlower) => flowers.push(pos),
Some(SpriteKind::WhiteFlower) => flowers.push(pos),
Some(SpriteKind::YellowFlower) => flowers.push(pos),
Some(SpriteKind::Sunflower) => flowers.push(pos),
_ => {},
},
}
if block.is_collectible() {
interactables.push(pos);
}
});
Self {
@ -66,6 +75,7 @@ impl BlocksOfInterest {
beehives,
reeds,
flowers,
interactables,
}
}
}

View File

@ -16,9 +16,9 @@ use common::{
assets::Asset,
comp,
comp::{
ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel, MAX_MOUNT_RANGE_SQR,
MAX_PICKUP_RANGE_SQR,
ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel,
},
consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
event::EventBus,
outcome::Outcome,
span,
@ -26,6 +26,7 @@ use common::{
util::Dir,
vol::ReadVol,
};
use ordered_float::OrderedFloat;
use specs::{Join, WorldExt};
use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
use tracing::{error, info};
@ -205,6 +206,7 @@ impl PlayState for SessionState {
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
span!(_guard, "tick", "<Session as PlayState>::tick");
// TODO: let mut client = self.client.borrow_mut();
// NOTE: Not strictly necessary, but useful for hotloading translation changes.
self.voxygen_i18n = VoxygenLocalization::load_expect(&i18n_asset_key(
&global_state.settings.language.selected_language,
@ -272,16 +274,18 @@ impl PlayState for SessionState {
.get(self.client.borrow().entity())
.is_some();
// Only highlight collectables
self.scene.set_select_pos(select_pos.filter(|sp| {
self.client
.borrow()
.state()
.terrain()
.get(*sp)
.map(|b| b.is_collectible() || can_build)
.unwrap_or(false)
}));
let interactable = select_interactable(&self.client.borrow(), self.target_entity, select_pos, &self.scene);
// 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,
})
);
// Handle window events.
for event in events {
@ -457,36 +461,19 @@ impl PlayState for SessionState {
.copied();
if let Some(player_pos) = player_pos {
// Find closest mountable entity
let mut closest_mountable: Option<(specs::Entity, i32)> = None;
for (entity, pos, ms) in (
let closest_mountable_entity = (
&client.state().ecs().entities(),
&client.state().ecs().read_storage::<comp::Pos>(),
&client.state().ecs().read_storage::<comp::MountState>(),
)
.join()
.filter(|(entity, _, _)| *entity != client.entity())
{
if comp::MountState::Unmounted != *ms {
continue;
}
let dist =
(player_pos.0.distance_squared(pos.0) * 1000.0) as i32;
if dist > MAX_MOUNT_RANGE_SQR {
continue;
}
if let Some(previous) = closest_mountable.as_mut() {
if dist < previous.1 {
*previous = (entity, dist);
}
} else {
closest_mountable = Some((entity, dist));
}
}
if let Some((mountee_entity, _)) = closest_mountable {
.filter(|(entity, _, mount_state)| *entity != client.entity()
&& **mount_state == comp::MountState::Unmounted
)
.map(|(entity, pos, _)| (entity, player_pos.0.distance_squared(pos.0)))
.filter(|(_, dist_sqr)| *dist_sqr < MAX_MOUNT_RANGE.powi(2))
.min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr));
if let Some((mountee_entity, _)) = closest_mountable_entity {
client.mount(mountee_entity);
}
}
@ -498,40 +485,21 @@ impl PlayState for SessionState {
self.key_state.collect = state;
if state {
let mut client = self.client.borrow_mut();
// Collect terrain sprites
if let Some(select_pos) = self.scene.select_pos() {
client.collect_block(select_pos);
}
// Collect lootable entities
let player_pos = client
.state()
.read_storage::<comp::Pos>()
.get(client.entity())
.copied();
if let Some(player_pos) = player_pos {
let entity = self.target_entity.or_else(|| {
(
&client.state().ecs().entities(),
&client.state().ecs().read_storage::<comp::Pos>(),
&client.state().ecs().read_storage::<comp::Item>(),
)
.join()
.filter(|(_, pos, _)| {
pos.0.distance_squared(player_pos.0)
< MAX_PICKUP_RANGE_SQR
})
.min_by_key(|(_, pos, _)| {
(pos.0.distance_squared(player_pos.0) * 1000.0) as i32
})
.map(|(entity, _, _)| entity)
});
if let Some(entity) = entity {
client.pick_up(entity);
if let Some(interactable) = interactable {
let mut client = self.client.borrow_mut();
match interactable {
Interactable::Block(block, pos) => if block.is_collectible() {
client.collect_block(pos);
},
Interactable::Entity(entity) => if client
.state()
.ecs()
.read_storage::<comp::Item>()
.get(entity)
.is_some()
{
client.pick_up(entity);
},
}
}
}
@ -1162,6 +1130,7 @@ fn under_cursor(
Option<Vec3<i32>>,
Option<(specs::Entity, f32)>,
) {
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
@ -1184,7 +1153,7 @@ 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_SQR)
<= MAX_PICKUP_RANGE.powi(2))
{
(
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
@ -1253,3 +1222,113 @@ fn under_cursor(
// TODO: consider setting build/select to None when targeting an entity
(build_pos, select_pos, target_entity)
}
#[derive(Clone, Copy)]
enum Interactable {
Block(Block, Vec3<i32>),
Entity(specs::Entity),
}
/// 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 (if interactabl)
/// 3) Closest of nearest interactable entity/block
fn select_interactable(
client: &Client,
target_entity: Option<specs::Entity>,
selected_pos: Option<Vec3<i32>>,
scene: &Scene,
) -> Option<Interactable> {
span!(_guard, "select_interactable");
use common::{
spiral::Spiral2d,
terrain::TerrainChunk,
vol::RectRasterableVol,
};
target_entity.map(Interactable::Entity)
.or_else(|| selected_pos.and_then(|sp|
client.state().terrain().get(sp).ok().copied()
.filter(Block::is_collectible).map(|b| Interactable::Block(b, sp))
))
.or_else(|| {
let ecs = client.state().ecs();
let player_entity = client.entity();
ecs
.read_storage::<comp::Pos>()
.get(player_entity).and_then(|player_pos| {
let closest_interactable_entity = (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::Scale>().maybe(),
&ecs.read_storage::<comp::Body>(),
// Must have this comp to be interactable (for now)
&ecs.read_storage::<comp::Item>(),
)
.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));
// 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();
// 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| 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));
// 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)))
})
})
}