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", "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]] [[package]]
name = "osascript" name = "osascript"
version = "0.3.0" version = "0.3.0"
@ -3691,7 +3700,7 @@ dependencies = [
"crossbeam-utils 0.7.2", "crossbeam-utils 0.7.2",
"linked-hash-map", "linked-hash-map",
"num_cpus", "num_cpus",
"ordered-float", "ordered-float 1.1.0",
"rustc-hash", "rustc-hash",
"stb_truetype", "stb_truetype",
] ]
@ -4962,6 +4971,7 @@ dependencies = [
"native-dialog", "native-dialog",
"num 0.2.1", "num 0.2.1",
"old_school_gfx_glutin_ext", "old_school_gfx_glutin_ext",
"ordered-float 2.0.0",
"rand 0.7.3", "rand 0.7.3",
"rodio", "rodio",
"ron", "ron",
@ -5014,7 +5024,7 @@ dependencies = [
"minifb", "minifb",
"noise", "noise",
"num 0.2.1", "num 0.2.1",
"ordered-float", "ordered-float 1.1.0",
"packed_simd_2", "packed_simd_2",
"rand 0.7.3", "rand 0.7.3",
"rand_chacha 0.2.2", "rand_chacha 0.2.2",

View File

@ -8,9 +8,6 @@ use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage, HashMapStorage}; use specs::{Component, FlaggedStorage, HashMapStorage};
use specs_idvs::IdvStorage; 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)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Inventory { pub struct Inventory {
slots: Vec<Option<Item>>, slots: Vec<Option<Item>>,

View File

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

View File

@ -5,7 +5,6 @@ use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
const MAX_ALIAS_LEN: usize = 32; const MAX_ALIAS_LEN: usize = 32;
pub const MAX_MOUNT_RANGE_SQR: i32 = 20000;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Player { 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 cmd;
pub mod combat; pub mod combat;
pub mod comp; pub mod comp;
pub mod consts;
pub mod effect; pub mod effect;
pub mod event; pub mod event;
pub mod explosion; pub mod explosion;

View File

@ -3,8 +3,9 @@ use common::{
comp::{ comp::{
self, item, self, item,
slot::{self, Slot}, slot::{self, Slot},
Pos, MAX_PICKUP_RANGE_SQR, Pos,
}, },
consts::MAX_PICKUP_RANGE,
msg::ServerGeneral, msg::ServerGeneral,
recipe::default_recipe_book, recipe::default_recipe_book,
sync::{Uid, WorldSyncExt}, 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 { fn within_pickup_range(player_position: Option<&Pos>, item_position: Option<&Pos>) -> bool {
match (player_position, item_position) { 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, _ => 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"]} image = {version = "0.23.8", default-features = false, features = ["ico", "png"]}
native-dialog = { version = "0.4.2", default-features = false, optional = true } native-dialog = { version = "0.4.2", default-features = false, optional = true }
num = "0.2" num = "0.2"
ordered-float = "2.0.0"
rand = "0.7" rand = "0.7"
rodio = {version = "0.11", default-features = false, features = ["wav", "vorbis"]} rodio = {version = "0.11", default-features = false, features = ["wav", "vorbis"]}
ron = {version = "0.6", default-features = false} ron = {version = "0.6", default-features = false}

View File

@ -1123,7 +1123,7 @@ impl Hud {
for (pos, item, distance) in (&entities, &pos, &items) for (pos, item, distance) in (&entities, &pos, &items)
.join() .join()
.map(|(_, pos, item)| (pos, item, pos.0.distance_squared(player_pos))) .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( let overitem_id = overitem_walker.next(
&mut self.ids.overitems, &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 // scale at max distance is 10, and at min distance is 30
let scale: f64 = let scale: f64 = ((1.5
((1.5 - (self.distance_from_player_sqr / common::comp::MAX_PICKUP_RANGE_SQR)) * 20.0) - (self.distance_from_player_sqr / common::consts::MAX_PICKUP_RANGE.powi(2)))
.into(); * 20.0)
.into();
let text_font_size = scale * 1.0; let text_font_size = scale * 1.0;
let text_pos_y = scale * 1.2; let text_pos_y = scale * 1.2;
let btn_rect_size = scale * 0.8; let btn_rect_size = scale * 0.8;

View File

@ -13,6 +13,9 @@ pub struct BlocksOfInterest {
pub beehives: Vec<Vec3<i32>>, pub beehives: Vec<Vec3<i32>>,
pub reeds: Vec<Vec3<i32>>, pub reeds: Vec<Vec3<i32>>,
pub flowers: 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 { impl BlocksOfInterest {
@ -24,6 +27,7 @@ impl BlocksOfInterest {
let mut beehives = Vec::new(); let mut beehives = Vec::new();
let mut reeds = Vec::new(); let mut reeds = Vec::new();
let mut flowers = Vec::new(); let mut flowers = Vec::new();
let mut interactables = Vec::new();
chunk chunk
.vol_iter( .vol_iter(
@ -34,29 +38,34 @@ impl BlocksOfInterest {
chunk.get_max_z(), chunk.get_max_z(),
), ),
) )
.for_each(|(pos, block)| match block.kind() { .for_each(|(pos, block)| {
BlockKind::Leaves => { match block.kind() {
if thread_rng().gen_range(0, 16) == 0 { BlockKind::Leaves => {
leaves.push(pos) if thread_rng().gen_range(0, 16) == 0 {
} leaves.push(pos)
}, }
BlockKind::Grass => { },
if thread_rng().gen_range(0, 16) == 0 { BlockKind::Grass => {
grass.push(pos) if thread_rng().gen_range(0, 16) == 0 {
} grass.push(pos)
}, }
_ => match block.get_sprite() { },
Some(SpriteKind::Ember) => embers.push(pos), _ => match block.get_sprite() {
Some(SpriteKind::Beehive) => beehives.push(pos), Some(SpriteKind::Ember) => embers.push(pos),
Some(SpriteKind::Reed) => reeds.push(pos), Some(SpriteKind::Beehive) => beehives.push(pos),
Some(SpriteKind::PinkFlower) => flowers.push(pos), Some(SpriteKind::Reed) => reeds.push(pos),
Some(SpriteKind::PurpleFlower) => flowers.push(pos), Some(SpriteKind::PinkFlower) => flowers.push(pos),
Some(SpriteKind::RedFlower) => flowers.push(pos), Some(SpriteKind::PurpleFlower) => flowers.push(pos),
Some(SpriteKind::WhiteFlower) => flowers.push(pos), Some(SpriteKind::RedFlower) => flowers.push(pos),
Some(SpriteKind::YellowFlower) => flowers.push(pos), Some(SpriteKind::WhiteFlower) => flowers.push(pos),
Some(SpriteKind::Sunflower) => flowers.push(pos), Some(SpriteKind::YellowFlower) => flowers.push(pos),
_ => {}, Some(SpriteKind::Sunflower) => flowers.push(pos),
}, _ => {},
},
}
if block.is_collectible() {
interactables.push(pos);
}
}); });
Self { Self {
@ -66,6 +75,7 @@ impl BlocksOfInterest {
beehives, beehives,
reeds, reeds,
flowers, flowers,
interactables,
} }
} }
} }

View File

@ -16,9 +16,9 @@ use common::{
assets::Asset, assets::Asset,
comp, comp,
comp::{ comp::{
ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel, MAX_MOUNT_RANGE_SQR, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel,
MAX_PICKUP_RANGE_SQR,
}, },
consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
event::EventBus, event::EventBus,
outcome::Outcome, outcome::Outcome,
span, span,
@ -26,6 +26,7 @@ use common::{
util::Dir, util::Dir,
vol::ReadVol, vol::ReadVol,
}; };
use ordered_float::OrderedFloat;
use specs::{Join, WorldExt}; use specs::{Join, WorldExt};
use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration}; use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
use tracing::{error, info}; use tracing::{error, info};
@ -205,6 +206,7 @@ impl PlayState for SessionState {
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult { fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
span!(_guard, "tick", "<Session as PlayState>::tick"); 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. // NOTE: Not strictly necessary, but useful for hotloading translation changes.
self.voxygen_i18n = VoxygenLocalization::load_expect(&i18n_asset_key( self.voxygen_i18n = VoxygenLocalization::load_expect(&i18n_asset_key(
&global_state.settings.language.selected_language, &global_state.settings.language.selected_language,
@ -272,16 +274,18 @@ impl PlayState for SessionState {
.get(self.client.borrow().entity()) .get(self.client.borrow().entity())
.is_some(); .is_some();
// Only highlight collectables let interactable = select_interactable(&self.client.borrow(), self.target_entity, select_pos, &self.scene);
self.scene.set_select_pos(select_pos.filter(|sp| {
self.client // Only highlight interactables
.borrow() // unless in build mode where select_pos highlighted
.state() self.scene.set_select_pos(
.terrain() select_pos
.get(*sp) .filter(|_| can_build)
.map(|b| b.is_collectible() || can_build) .or_else(|| match interactable {
.unwrap_or(false) Some(Interactable::Block(_, block_pos)) => Some(block_pos),
})); _ => None,
})
);
// Handle window events. // Handle window events.
for event in events { for event in events {
@ -457,36 +461,19 @@ impl PlayState for SessionState {
.copied(); .copied();
if let Some(player_pos) = player_pos { if let Some(player_pos) = player_pos {
// Find closest mountable entity // Find closest mountable entity
let mut closest_mountable: Option<(specs::Entity, i32)> = None; let closest_mountable_entity = (
for (entity, pos, ms) in (
&client.state().ecs().entities(), &client.state().ecs().entities(),
&client.state().ecs().read_storage::<comp::Pos>(), &client.state().ecs().read_storage::<comp::Pos>(),
&client.state().ecs().read_storage::<comp::MountState>(), &client.state().ecs().read_storage::<comp::MountState>(),
) )
.join() .join()
.filter(|(entity, _, _)| *entity != client.entity()) .filter(|(entity, _, mount_state)| *entity != client.entity()
{ && **mount_state == comp::MountState::Unmounted
if comp::MountState::Unmounted != *ms { )
continue; .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));
let dist = if let Some((mountee_entity, _)) = closest_mountable_entity {
(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 {
client.mount(mountee_entity); client.mount(mountee_entity);
} }
} }
@ -498,40 +485,21 @@ impl PlayState for SessionState {
self.key_state.collect = state; self.key_state.collect = state;
if state { if state {
let mut client = self.client.borrow_mut(); if let Some(interactable) = interactable {
let mut client = self.client.borrow_mut();
// Collect terrain sprites match interactable {
if let Some(select_pos) = self.scene.select_pos() { Interactable::Block(block, pos) => if block.is_collectible() {
client.collect_block(select_pos); client.collect_block(pos);
} },
Interactable::Entity(entity) => if client
// Collect lootable entities .state()
let player_pos = client .ecs()
.state() .read_storage::<comp::Item>()
.read_storage::<comp::Pos>() .get(entity)
.get(client.entity()) .is_some()
.copied(); {
client.pick_up(entity);
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);
} }
} }
} }
@ -1162,6 +1130,7 @@ fn under_cursor(
Option<Vec3<i32>>, Option<Vec3<i32>>,
Option<(specs::Entity, f32)>, Option<(specs::Entity, f32)>,
) { ) {
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 player_pos = match client
@ -1184,7 +1153,7 @@ 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_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)), 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 // TODO: consider setting build/select to None when targeting an entity
(build_pos, select_pos, target_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)))
})
})
}