diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a842d4df..66cec07206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Training dummy items - Added spin attack for axe - Creature specific stats +- Minimap compass ### Changed diff --git a/common/src/path.rs b/common/src/path.rs index 8388edb36c..d6c10bed98 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -4,7 +4,7 @@ use crate::{ vol::{BaseVol, ReadVol}, }; use hashbrown::hash_map::DefaultHashBuilder; -use rand::{thread_rng, Rng}; +use rand::prelude::*; use std::iter::FromIterator; use vek::*; @@ -56,6 +56,18 @@ impl From<Path<Vec3<i32>>> for Route { fn from(path: Path<Vec3<i32>>) -> Self { Self { path, next_idx: 0 } } } +pub struct TraversalConfig { + /// The distance to a node at which node is considered visited. + pub node_tolerance: f32, + /// The slowdown factor when following corners. + /// 0.0 = no slowdown on corners, 1.0 = total slowdown on corners. + pub slow_factor: f32, + /// Whether the agent is currently on the ground. + pub on_ground: bool, + /// The distance to the target below which it is considered reached. + pub min_tgt_dist: f32, +} + impl Route { pub fn path(&self) -> &Path<Vec3<i32>> { &self.path } @@ -70,91 +82,228 @@ impl Route { vol: &V, pos: Vec3<f32>, vel: Vec3<f32>, - traversal_tolerance: f32, + traversal_cfg: TraversalConfig, ) -> Option<(Vec3<f32>, f32)> where V: BaseVol<Vox = Block> + ReadVol, { - let next0 = self - .next(0) - .unwrap_or_else(|| pos.map(|e| e.floor() as i32)); - let next1 = self.next(1).unwrap_or(next0); - if vol.get(next0).map(|b| b.is_solid()).unwrap_or(false) { - None - } else { - let next_tgt = next0.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0); - if pos.xy().distance_squared(next_tgt.xy()) < traversal_tolerance.powf(2.0) - && next_tgt.z - pos.z < 0.2 - && next_tgt.z - pos.z > -2.2 + let (next0, next1, next_tgt, be_precise) = loop { + let next0 = self + .next(0) + .unwrap_or_else(|| pos.map(|e| e.floor() as i32)); + + // Stop using obstructed paths + if vol.get(next0).map(|b| b.is_solid()).unwrap_or(false) { + return None; + } + + let diagonals = [ + Vec2::new(1, 0), + Vec2::new(1, 1), + Vec2::new(0, 1), + Vec2::new(-1, 1), + Vec2::new(-1, 0), + Vec2::new(-1, -1), + Vec2::new(0, -1), + Vec2::new(1, -1), + ]; + + let next1 = self.next(1).unwrap_or(next0); + + let be_precise = diagonals.iter().any(|pos| { + !walkable(vol, next0 + Vec3::new(pos.x, pos.y, 0)) + && !walkable(vol, next0 + Vec3::new(pos.x, pos.y, -1)) + && !walkable(vol, next0 + Vec3::new(pos.x, pos.y, -2)) + && !walkable(vol, next0 + Vec3::new(pos.x, pos.y, 1)) + }); + + let next0_tgt = next0.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0); + let next1_tgt = next1.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0); + let next_tgt = next0_tgt; + + // Maybe skip a node (useful with traversing downhill) + let closest_tgt = if next0_tgt.distance_squared(pos) < next1_tgt.distance_squared(pos) { + next0_tgt + } else { + next1_tgt + }; + + // Determine whether we're close enough to the next to to consider it completed + let dist_sqrd = pos.xy().distance_squared(closest_tgt.xy()); + if dist_sqrd < traversal_cfg.node_tolerance.powf(2.0) * if be_precise { 0.25 } else { 1.0 } + && (pos.z - closest_tgt.z > 1.2 || (pos.z - closest_tgt.z > -0.2 && traversal_cfg.on_ground)) + && (pos.z - closest_tgt.z < 1.2 || (pos.z - closest_tgt.z < 2.9 && vel.z < -0.05)) && vel.z <= 0.0 + // Only consider the node reached if there's nothing solid between us and it && vol - .ray(pos + Vec3::unit_z() * 0.5, next_tgt + Vec3::unit_z() * 0.5) + .ray(pos + Vec3::unit_z() * 1.5, closest_tgt + Vec3::unit_z() * 1.5) .until(|block| block.is_solid()) .cast() .0 - > pos.distance(next_tgt) * 0.9 + > pos.distance(closest_tgt) * 0.9 + && self.next_idx < self.path.len() { + // Node completed, move on to the next one self.next_idx += 1; + } else { + // The next node hasn't been reached yet, use it as a target + break (next0, next1, next_tgt, be_precise); } + }; - let line = LineSegment2 { - start: pos.xy(), - end: pos.xy() + vel.xy() * 100.0, - }; - - let align = |block_pos: Vec3<i32>| { - (0..2) - .map(|i| (0..2).map(move |j| Vec2::new(i, j))) - .flatten() - .map(|rpos| block_pos + rpos) - .map(|block_pos| { - let block_posf = block_pos.xy().map(|e| e as f32); - let proj = line.projected_point(block_posf); - let clamped = proj.clamped( - block_pos.xy().map(|e| e as f32), - block_pos.xy().map(|e| e as f32), - ); - - (proj.distance_squared(clamped), clamped) - }) - .min_by_key(|(d2, _)| (d2 * 1000.0) as i32) - .unwrap() - .1 - }; - - let cb = CubicBezier2 { - start: pos.xy(), - ctrl0: pos.xy() + vel.xy().try_normalized().unwrap_or_else(Vec2::zero), - ctrl1: align(next0), - end: align(next1), - }; - - let tgt2d = cb.evaluate(0.5); - let tgt = Vec3::from(tgt2d) + Vec3::unit_z() * next_tgt.z; - let tgt_dir = (tgt - pos) - .xy() - .try_normalized() - .unwrap_or_else(Vec2::unit_y); - let next_dir = cb - .evaluate_derivative(0.5) - .try_normalized() - .unwrap_or(tgt_dir); - - //let vel_dir = vel.xy().try_normalized().unwrap_or(Vec2::zero()); - //let avg_dir = (tgt_dir * 0.2 + vel_dir * - // 0.8).try_normalized().unwrap_or(Vec2::zero()); let bearing = - // Vec3::<f32>::from(avg_dir * (tgt - pos).xy().magnitude()) + Vec3::unit_z() * - // (tgt.z - pos.z); - - Some(( - tgt - pos, - next_dir - .dot(vel.xy().try_normalized().unwrap_or_else(Vec2::zero)) - .max(0.0) - * 0.75 - + 0.25, - )) + fn gradient(line: LineSegment2<f32>) -> f32 { + let r = (line.start.y - line.end.y) / (line.start.x - line.end.x); + if r.is_nan() { 100000.0 } else { r } } + + fn intersect(a: LineSegment2<f32>, b: LineSegment2<f32>) -> Option<Vec2<f32>> { + let ma = gradient(a); + let mb = gradient(b); + + let ca = a.start.y - ma * a.start.x; + let cb = b.start.y - mb * b.start.x; + + if (ma - mb).abs() < 0.0001 || (ca - cb).abs() < 0.0001 { + None + } else { + let x = (cb - ca) / (ma - mb); + let y = ma * x + ca; + + Some(Vec2::new(x, y)) + } + } + + // We don't always want to aim for the centre of block since this can create + // jerky zig-zag movement. This function attempts to find a position + // inside a target block's area that aligned nicely with our velocity. + // This has a twofold benefit: + // + // 1. Entities can move at any angle when + // running on a flat surface + // + // 2. We don't have to search diagonals when + // pathfinding - cartesian positions are enough since this code will + // make the entity move smoothly along them + let corners = [ + Vec2::new(0, 0), + Vec2::new(1, 0), + Vec2::new(1, 1), + Vec2::new(0, 1), + Vec2::new(0, 0), // Repeated start + ]; + + let vel_line = LineSegment2 { + start: pos.xy(), + end: pos.xy() + vel.xy() * 100.0, + }; + + let align = |block_pos: Vec3<i32>, precision: f32| { + let lerp_block = + |x, precision| Lerp::lerp(x, block_pos.xy().map(|e| e as f32), precision); + + (0..4) + .filter_map(|i| { + let edge_line = LineSegment2 { + start: lerp_block( + (block_pos.xy() + corners[i]).map(|e| e as f32), + precision, + ), + end: lerp_block( + (block_pos.xy() + corners[i + 1]).map(|e| e as f32), + precision, + ), + }; + intersect(vel_line, edge_line).filter(|intersect| { + intersect + .clamped( + block_pos.xy().map(|e| e as f32), + block_pos.xy().map(|e| e as f32 + 1.0), + ) + .distance_squared(*intersect) + < 0.001 + }) + }) + .min_by_key(|intersect: &Vec2<f32>| { + (intersect.distance_squared(vel_line.end) * 1000.0) as i32 + }) + .unwrap_or_else(|| { + (0..2) + .map(|i| (0..2).map(move |j| Vec2::new(i, j))) + .flatten() + .map(|rpos| block_pos + rpos) + .map(|block_pos| { + let block_posf = block_pos.xy().map(|e| e as f32); + let proj = vel_line.projected_point(block_posf); + let clamped = lerp_block( + proj.clamped( + block_pos.xy().map(|e| e as f32), + block_pos.xy().map(|e| e as f32), + ), + precision, + ); + + (proj.distance_squared(clamped), clamped) + }) + .min_by_key(|(d2, _)| (d2 * 1000.0) as i32) + .unwrap() + .1 + }) + }; + + let bez = CubicBezier2 { + start: pos.xy(), + ctrl0: pos.xy() + vel.xy().try_normalized().unwrap_or_default() * 1.0, + ctrl1: align(next0, 1.0), + end: align(next1, 1.0), + }; + + // Use a cubic spline of the next few targets to come up with a sensible target + // position. We want to use a position that gives smooth movement but is + // also accurate enough to avoid the agent getting stuck under ledges or + // falling off walls. + let next_dir = bez + .evaluate_derivative(0.85) + .try_normalized() + .unwrap_or_default(); + let straight_factor = next_dir + .dot(vel.xy().try_normalized().unwrap_or(next_dir)) + .max(0.0) + .powf(2.0); + + let bez = CubicBezier2 { + start: pos.xy(), + ctrl0: pos.xy() + vel.xy().try_normalized().unwrap_or_default() * 1.0, + ctrl1: align( + next0, + (1.0 - if (next0.z as f32 - pos.z).abs() < 0.25 && !be_precise { + straight_factor + } else { + 0.0 + }) + .max(0.1), + ), + end: align(next1, 1.0), + }; + + let tgt2d = bez.evaluate(if (next0.z as f32 - pos.z).abs() < 0.25 { + 0.25 + } else { + 0.5 + }); + let tgt = if be_precise { + next_tgt + } else { + Vec3::from(tgt2d) + Vec3::unit_z() * next_tgt.z + }; + + Some(( + tgt - pos, + // Control the entity's speed to hopefully stop us falling off walls on sharp corners. + // This code is very imperfect: it does its best but it can still fail for particularly + // fast entities. + straight_factor * traversal_cfg.slow_factor + (1.0 - traversal_cfg.slow_factor), + )) + .filter(|(bearing, _)| bearing.z < 2.1) } } @@ -178,41 +327,64 @@ impl Chaser { pos: Vec3<f32>, vel: Vec3<f32>, tgt: Vec3<f32>, - min_dist: f32, - traversal_tolerance: f32, + traversal_cfg: TraversalConfig, ) -> Option<(Vec3<f32>, f32)> where V: BaseVol<Vox = Block> + ReadVol, { let pos_to_tgt = pos.distance(tgt); - if ((pos - tgt) * Vec3::new(1.0, 1.0, 2.0)).magnitude_squared() < min_dist.powf(2.0) { + // If we're already close to the target then there's nothing to do + if ((pos - tgt) * Vec3::new(1.0, 1.0, 2.0)).magnitude_squared() + < traversal_cfg.min_tgt_dist.powf(2.0) + { + self.route = None; return None; } let bearing = if let Some(end) = self.route.as_ref().and_then(|r| r.path().end().copied()) { let end_to_tgt = end.map(|e| e as f32).distance(tgt); - if end_to_tgt > pos_to_tgt * 0.3 + 5.0 || thread_rng().gen::<f32>() < 0.005 { + // If the target has moved significantly since the path was generated then it's + // time to search for a new path. Also, do this randomly from time + // to time to avoid any edge cases that cause us to get stuck. In + // theory this shouldn't happen, but in practice the world is full + // of unpredictable obstacles that are more than willing to mess up + // our day. TODO: Come up with a better heuristic for this + if end_to_tgt > pos_to_tgt * 0.3 + 5.0 + /* || thread_rng().gen::<f32>() < 0.005 */ + { None } else { self.route .as_mut() - .and_then(|r| r.traverse(vol, pos, vel, traversal_tolerance)) + .and_then(|r| r.traverse(vol, pos, vel, traversal_cfg)) + // In theory this filter isn't needed, but in practice agents often try to take + // stale paths that start elsewhere. This code makes sure that we're only using + // paths that start near us, avoiding the agent doubling back to chase a stale + // path. + .filter(|(bearing, _)| bearing.xy() + .magnitude_squared() < 1.75f32.powf(2.0) + && thread_rng().gen::<f32>() > 0.025) } } else { None }; - // TODO: What happens when we get stuck? - if let Some(bearing) = bearing { - Some(bearing) + if let Some((bearing, speed)) = bearing { + Some((bearing, speed)) } else { + // Only search for a path if the target has moved from their last position. We + // don't want to be thrashing the pathfinding code for targets that + // we're unable to access! if self .last_search_tgt .map(|last_tgt| last_tgt.distance(tgt) > pos_to_tgt * 0.15 + 5.0) .unwrap_or(true) + || self.astar.is_some() + || self.route.is_none() { let (start_pos, path) = find_path(&mut self.astar, vol, pos, tgt); + // Don't use a stale path if start_pos.distance_squared(pos) < 4.0f32.powf(2.0) { self.route = path.map(Route::from); } else { @@ -225,6 +397,24 @@ impl Chaser { } } +#[allow(clippy::float_cmp)] // TODO: Pending review in #587 +fn walkable<V>(vol: &V, pos: Vec3<i32>) -> bool +where + V: BaseVol<Vox = Block> + ReadVol, +{ + vol.get(pos - Vec3::new(0, 0, 1)) + .map(|b| b.is_solid() && b.get_height() == 1.0) + .unwrap_or(false) + && vol + .get(pos + Vec3::new(0, 0, 0)) + .map(|b| !b.is_solid()) + .unwrap_or(true) + && vol + .get(pos + Vec3::new(0, 0, 1)) + .map(|b| !b.is_solid()) + .unwrap_or(true) +} + #[allow(clippy::float_cmp)] // TODO: Pending review in #587 fn find_path<V>( astar: &mut Option<Astar<Vec3<i32>, DefaultHashBuilder>>, @@ -235,19 +425,7 @@ fn find_path<V>( where V: BaseVol<Vox = Block> + ReadVol, { - let is_walkable = |pos: &Vec3<i32>| { - vol.get(*pos - Vec3::new(0, 0, 1)) - .map(|b| b.is_solid() && b.get_height() == 1.0) - .unwrap_or(false) - && vol - .get(*pos + Vec3::new(0, 0, 0)) - .map(|b| !b.is_solid()) - .unwrap_or(true) - && vol - .get(*pos + Vec3::new(0, 0, 1)) - .map(|b| !b.is_solid()) - .unwrap_or(true) - }; + let is_walkable = |pos: &Vec3<i32>| walkable(vol, *pos); let get_walkable_z = |pos| { let mut z_incr = 0; for _ in 0..32 { @@ -291,23 +469,23 @@ where Vec3::new(0, 0, -1), // Downwards ]; - let walkable = [ - is_walkable(&(pos + Vec3::new(1, 0, 0))), - is_walkable(&(pos + Vec3::new(-1, 0, 0))), - is_walkable(&(pos + Vec3::new(0, 1, 0))), - is_walkable(&(pos + Vec3::new(0, -1, 0))), - ]; + // let walkable = [ + // is_walkable(&(pos + Vec3::new(1, 0, 0))), + // is_walkable(&(pos + Vec3::new(-1, 0, 0))), + // is_walkable(&(pos + Vec3::new(0, 1, 0))), + // is_walkable(&(pos + Vec3::new(0, -1, 0))), + // ]; - const DIAGONALS: [(Vec3<i32>, [usize; 2]); 8] = [ - (Vec3::new(1, 1, 0), [0, 2]), - (Vec3::new(-1, 1, 0), [1, 2]), - (Vec3::new(1, -1, 0), [0, 3]), - (Vec3::new(-1, -1, 0), [1, 3]), - (Vec3::new(1, 1, 1), [0, 2]), - (Vec3::new(-1, 1, 1), [1, 2]), - (Vec3::new(1, -1, 1), [0, 3]), - (Vec3::new(-1, -1, 1), [1, 3]), - ]; + // const DIAGONALS: [(Vec3<i32>, [usize; 2]); 8] = [ + // (Vec3::new(1, 1, 0), [0, 2]), + // (Vec3::new(-1, 1, 0), [1, 2]), + // (Vec3::new(1, -1, 0), [0, 3]), + // (Vec3::new(-1, -1, 0), [1, 3]), + // (Vec3::new(1, 1, 1), [0, 2]), + // (Vec3::new(-1, 1, 1), [1, 2]), + // (Vec3::new(1, -1, 1), [0, 3]), + // (Vec3::new(-1, -1, 1), [1, 3]), + // ]; DIRS.iter() .map(move |dir| (pos, dir)) @@ -331,24 +509,26 @@ where .unwrap_or(true))) }) .map(move |(pos, dir)| pos + dir) - .chain( - DIAGONALS - .iter() - .filter(move |(dir, [a, b])| { - is_walkable(&(pos + *dir)) && walkable[*a] && walkable[*b] - }) - .map(move |(dir, _)| pos + *dir), - ) - }; - - let crow_line = LineSegment2 { - start: startf.xy(), - end: endf.xy(), + // .chain( + // DIAGONALS + // .iter() + // .filter(move |(dir, [a, b])| { + // is_walkable(&(pos + *dir)) && walkable[*a] && + // walkable[*b] }) + // .map(move |(dir, _)| pos + *dir), + // ) }; let transition = |a: &Vec3<i32>, b: &Vec3<i32>| { + let crow_line = LineSegment2 { + start: startf.xy(), + end: endf.xy(), + }; + + // Modify the heuristic a little in order to prefer paths that take us on a + // straight line toward our target. This means we get smoother movement. 1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025 - + (b.z - a.z - 1).max(0) as f32 * 3.0 + + (b.z - a.z - 1).max(0) as f32 * 10.0 }; let satisfied = |pos: &Vec3<i32>| pos == &end; diff --git a/common/src/spiral.rs b/common/src/spiral.rs index 5a13b93d9d..2a76a3484d 100644 --- a/common/src/spiral.rs +++ b/common/src/spiral.rs @@ -16,6 +16,7 @@ impl Spiral2d { impl Iterator for Spiral2d { type Item = Vec2<i32>; + #[allow(clippy::erasing_op)] fn next(&mut self) -> Option<Self::Item> { let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); if self.i >= layer_size { diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 7ebec30dc4..ce01809f80 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -31,9 +31,9 @@ impl Body { pub fn base_accel(&self) -> f32 { match self { Body::Humanoid(_) => 100.0, - Body::QuadrupedSmall(_) => 80.0, + Body::QuadrupedSmall(_) => 85.0, Body::QuadrupedMedium(_) => 180.0, - Body::BirdMedium(_) => 70.0, + Body::BirdMedium(_) => 80.0, Body::FishMedium(_) => 50.0, Body::Dragon(_) => 250.0, Body::BirdSmall(_) => 75.0, @@ -41,7 +41,7 @@ impl Body { Body::BipedLarge(_) => 120.0, Body::Object(_) => 40.0, Body::Golem(_) => 130.0, - Body::Critter(_) => 65.0, + Body::Critter(_) => 85.0, Body::QuadrupedLow(_) => 120.0, } } diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 5fcfd5d47a..36b8afd4e9 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -3,11 +3,11 @@ use crate::{ self, agent::Activity, item::{tool::ToolKind, ItemKind}, - Agent, Alignment, CharacterState, ChatMsg, ControlAction, Controller, Loadout, MountState, - Ori, Pos, Scale, Stats, Vel, + Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout, + MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel, }, event::{EventBus, ServerEvent}, - path::Chaser, + path::{Chaser, TraversalConfig}, state::{DeltaTime, Time}, sync::{Uid, UidAllocator}, terrain::TerrainGrid, @@ -38,9 +38,11 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Stats>, ReadStorage<'a, Loadout>, ReadStorage<'a, CharacterState>, + ReadStorage<'a, PhysicsState>, ReadStorage<'a, Uid>, ReadExpect<'a, TerrainGrid>, ReadStorage<'a, Alignment>, + ReadStorage<'a, Body>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, ReadStorage<'a, MountState>, @@ -62,9 +64,11 @@ impl<'a> System<'a> for Sys { stats, loadouts, character_states, + physics_states, uids, terrain, alignments, + bodies, mut agents, mut controllers, mount_states, @@ -78,6 +82,8 @@ impl<'a> System<'a> for Sys { alignment, loadout, character_state, + physics_state, + body, uid, agent, controller, @@ -90,6 +96,8 @@ impl<'a> System<'a> for Sys { alignments.maybe(), &loadouts, &character_states, + &physics_states, + bodies.maybe(), &uids, &mut agents, &mut controllers, @@ -126,7 +134,8 @@ impl<'a> System<'a> for Sys { // and so can afford to be less precise when trying to move around // the world (especially since they would otherwise get stuck on // obstacles that smaller entities would not). - let traversal_tolerance = scale + vel.0.magnitude() * 0.3; + let node_tolerance = scale + vel.0.xy().magnitude() * 0.2; + let slow_factor = body.map(|b| b.base_accel() / 250.0).unwrap_or(0.0).min(1.0); let mut do_idle = false; let mut choose_target = false; @@ -199,8 +208,12 @@ impl<'a> System<'a> for Sys { pos.0, vel.0, tgt_pos.0, - AVG_FOLLOW_DIST, - traversal_tolerance, + TraversalConfig { + node_tolerance, + slow_factor, + on_ground: physics_state.on_ground, + min_tgt_dist: AVG_FOLLOW_DIST, + }, ) { inputs.move_dir = bearing.xy().try_normalized().unwrap_or(Vec2::zero()) @@ -315,8 +328,12 @@ impl<'a> System<'a> for Sys { pos.0, vel.0, tgt_pos.0, - 1.25, - traversal_tolerance, + TraversalConfig { + node_tolerance, + slow_factor, + on_ground: physics_state.on_ground, + min_tgt_dist: 1.25, + }, ) { inputs.move_dir = Vec2::from(bearing) .try_normalized() diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 3ea09a0003..f346f1fab0 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -146,20 +146,20 @@ impl Sys { }, ClientMsg::SetViewDistance(view_distance) => { if let ClientState::Character { .. } = client.client_state { + players.get_mut(entity).map(|player| { + player.view_distance = Some( + settings + .max_view_distance + .map(|max| view_distance.min(max)) + .unwrap_or(view_distance), + ) + }); + if settings .max_view_distance - .map(|max| view_distance <= max) - .unwrap_or(true) + .map(|max| view_distance > max) + .unwrap_or(false) { - players.get_mut(entity).map(|player| { - player.view_distance = Some( - settings - .max_view_distance - .map(|max| view_distance.min(max)) - .unwrap_or(view_distance), - ) - }); - } else { client.notify(ServerMsg::SetViewDistance( settings.max_view_distance.unwrap_or(0), )); diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 3a0225539b..b696c2c66b 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -8,7 +8,7 @@ use common::{comp, terrain::TerrainChunkSize, vol::RectVolSize}; use conrod_core::{ color, position, widget::{self, Button, Image, Rectangle, Text}, - widget_ids, Colorable, Positionable, Sizeable, Widget, WidgetCommon, + widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; use specs::WorldExt; use vek::*; @@ -23,7 +23,11 @@ widget_ids! { mmap_plus, mmap_minus, grid, - indicator + indicator, + mmap_north, + mmap_east, + mmap_south, + mmap_west, } } @@ -39,6 +43,7 @@ pub struct MiniMap<'a> { fonts: &'a ConrodVoxygenFonts, #[conrod(common_builder)] common: widget::CommonBuilder, + ori: Vec3<f32>, } impl<'a> MiniMap<'a> { @@ -49,6 +54,7 @@ impl<'a> MiniMap<'a> { rot_imgs: &'a ImgsRot, world_map: &'a (img_ids::Rotations, Vec2<u32>), fonts: &'a ConrodVoxygenFonts, + ori: Vec3<f32>, ) -> Self { Self { show, @@ -58,6 +64,7 @@ impl<'a> MiniMap<'a> { world_map, fonts, common: widget::CommonBuilder::default(), + ori, } } } @@ -195,10 +202,12 @@ impl<'a> Widget for MiniMap<'a> { [w_src, h_src], ); + let map_size = Vec2::new(170.0, 170.0); + // Map Image Image::new(world_map.source_north) .middle_of(state.ids.mmap_frame_bg) - .w_h(170.0 * SCALE, 170.0 * SCALE) + .w_h(map_size.x * SCALE, map_size.y * SCALE) .parent(state.ids.mmap_frame_bg) .source_rectangle(rect_src) .set(state.ids.grid, ui); @@ -212,6 +221,37 @@ impl<'a> Widget for MiniMap<'a> { .floating(true) .parent(ui.window) .set(state.ids.indicator, ui); + + // Compass directions + let dirs = [ + (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true), + (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false), + (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false), + (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false), + ]; + for (dir, id, name, bold) in dirs.iter() { + let cardinal_dir = Vec2::unit_x().rotated_z(self.ori.x as f64) * dir.x + + Vec2::unit_y().rotated_z(self.ori.x as f64) * dir.y; + let clamped = (cardinal_dir * 3.0) + / (cardinal_dir * 3.0).map(|e| e.abs()).reduce_partial_max(); + let pos = clamped * (map_size * 0.73 - 10.0); + Text::new(name) + .x_y_position_relative_to( + state.ids.grid, + position::Relative::Scalar(pos.x), + position::Relative::Scalar(pos.y), + ) + .font_size(self.fonts.cyri.scale(18)) + .font_id(self.fonts.cyri.conrod_id) + .color(if *bold { + Color::Rgba(0.75, 0.0, 0.0, 1.0) + } else { + TEXT_COLOR + }) + .floating(true) + .parent(ui.window) + .set(*id, ui); + } } else { Image::new(self.imgs.mmap_frame_closed) .w_h(174.0 * SCALE, 18.0 * SCALE) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 5e4bc19d81..4177177ca8 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -600,6 +600,7 @@ impl Hud { debug_info: DebugInfo, dt: Duration, info: HudInfo, + camera: &Camera, ) -> Vec<Event> { let mut events = std::mem::replace(&mut self.events, Vec::new()); let (ref mut ui_widgets, ref mut tooltip_manager) = self.ui.set_widgets(); @@ -1499,6 +1500,7 @@ impl Hud { &self.rot_imgs, &self.world_map, &self.fonts, + camera.get_orientation(), ) .set(self.ids.minimap, ui_widgets) { @@ -2251,7 +2253,7 @@ impl Hud { if let Some(maybe_id) = self.to_focus.take() { self.ui.focus_widget(maybe_id); } - let events = self.update_layout(client, global_state, debug_info, dt, info); + let events = self.update_layout(client, global_state, debug_info, dt, info, camera); let camera::Dependents { view_mat, proj_mat, .. } = camera.dependents();