Added overhead hints, smoother mount movement

This commit is contained in:
Joshua Barretto 2022-01-16 13:57:38 +00:00
parent f33c12e976
commit 504e2a38d5
11 changed files with 169 additions and 34 deletions

View File

@ -26,7 +26,7 @@
"hud.tutorial_elements": r#"Crafting"#,
"hud.temp_quest_headline": r#"Greetings Traveller!"#,
"hud.temp_quest_text": r#"To begin your journey you could start looking through this village and gather some supplies.
"hud.temp_quest_text": r#"To begin your journey you could start looking through this village and gather some supplies.
You are welcome to take whatever you need on your journey!
@ -46,6 +46,13 @@ Whenever you feel ready, try to get even better equipment from the many challeng
"hud.free_look_indicator": "Free look active. Press {key} to disable.",
"hud.camera_clamp_indicator": "Camera vertical clamp active. Press {key} to disable.",
"hud.auto_walk_indicator": "Auto walk/swim active",
"hud.collect": "Collect",
"hud.pick_up": "Pick up",
"hud.open": "Open",
"hud.use": "Use",
"hud.talk": "Talk",
"hud.trade": "Trade",
"hud.mount": "Mount",
},

View File

@ -44,6 +44,7 @@ sum_type! {
Ori(comp::Ori),
Shockwave(comp::Shockwave),
BeamSegment(comp::BeamSegment),
Alignment(comp::Alignment),
}
}
// Automatically derive From<T> for EcsCompPhantom
@ -80,6 +81,7 @@ sum_type! {
Ori(PhantomData<comp::Ori>),
Shockwave(PhantomData<comp::Shockwave>),
BeamSegment(PhantomData<comp::BeamSegment>),
Alignment(PhantomData<comp::Alignment>),
}
}
impl sync::CompPacket for EcsCompPacket {
@ -127,6 +129,7 @@ impl sync::CompPacket for EcsCompPacket {
},
EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_insert(comp, entity, world),
}
}
@ -172,6 +175,7 @@ impl sync::CompPacket for EcsCompPacket {
},
EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_modify(comp, entity, world),
}
}
@ -211,7 +215,8 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::Vel(_) => sync::handle_interp_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_interp_remove::<comp::Ori>(entity, world),
EcsCompPhantom::Shockwave(_) => sync::handle_remove::<comp::Shockwave>(entity, world),
EcsCompPhantom::BeamSegment(_) => sync::handle_remove::<comp::Ori>(entity, world),
EcsCompPhantom::BeamSegment(_) => sync::handle_remove::<comp::BeamSegment>(entity, world),
EcsCompPhantom::Alignment(_) => sync::handle_remove::<comp::Alignment>(entity, world),
}
}
}

View File

@ -8,8 +8,8 @@ use crate::{
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid,
};
use serde::Deserialize;
use specs::{Component, Entity as EcsEntity};
use serde::{Serialize, Deserialize};
use specs::{Component, Entity as EcsEntity, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::{collections::VecDeque, fmt};
use strum::IntoEnumIterator;
@ -21,7 +21,7 @@ use super::dialogue::Subject;
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Alignment {
/// Wild animals and gentle giants
Wild,
@ -79,7 +79,7 @@ impl Alignment {
}
impl Component for Alignment {
type Storage = IdvStorage<Self>;
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
bitflags::bitflags! {

View File

@ -10,7 +10,7 @@ use crate::{
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs::Component;
use specs_idvs::IdvStorage;
use std::collections::BTreeMap;
use vek::*;

View File

@ -1,6 +1,6 @@
// The limit on distance between the entity and a collectible (squared)
pub const MAX_PICKUP_RANGE: f32 = 5.0;
pub const MAX_MOUNT_RANGE: f32 = 14.0;
pub const MAX_MOUNT_RANGE: f32 = 5.0;
pub const MAX_TRADE_RANGE: f32 = 20.0;
pub const GRAVITY: f32 = 25.0;

View File

@ -158,6 +158,7 @@ impl State {
ecs.register::<comp::Shockwave>();
ecs.register::<comp::ShockwaveHitEntities>();
ecs.register::<comp::BeamSegment>();
ecs.register::<comp::Alignment>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();
@ -187,7 +188,6 @@ impl State {
ecs.register::<comp::Last<comp::Pos>>();
ecs.register::<comp::Last<comp::Vel>>();
ecs.register::<comp::Last<comp::Ori>>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Agent>();
ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>();

View File

@ -400,6 +400,7 @@ impl<'a> PhysicsData<'a> {
mass,
collider,
read.char_states.get(entity),
read.is_ridings.get(entity),
))
})
.for_each(
@ -411,6 +412,7 @@ impl<'a> PhysicsData<'a> {
mass_other,
collider_other,
char_state_other_maybe,
other_is_riding_maybe,
)| {
let collision_boundary = previous_cache.collision_boundary
+ previous_cache_other.collision_boundary;
@ -473,7 +475,7 @@ impl<'a> PhysicsData<'a> {
*mass,
*mass_other,
vel,
is_riding.is_some(),
is_riding.is_some() || other_is_riding_maybe.is_some(),
);
}
},

View File

@ -4,7 +4,7 @@ use common::{
item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider,
Combo, Density, Energy, Group, Health, Inventory, Item, LightEmitter, Mass,
Ori, Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky, Vel,
Ori, Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky, Vel, Alignment,
},
uid::Uid,
mounting::{Mount, Rider},
@ -69,6 +69,8 @@ pub struct TrackedComps<'a> {
pub character_state: ReadStorage<'a, CharacterState>,
pub shockwave: ReadStorage<'a, Shockwave>,
pub beam_segment: ReadStorage<'a, BeamSegment>,
pub alignment: ReadStorage<'a, Alignment>,
pub ability_map: ReadExpect<'a, AbilityMap>,
pub msm: ReadExpect<'a, MaterialStatManifest>,
}
@ -180,6 +182,10 @@ impl<'a> TrackedComps<'a> {
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.alignment
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
// Add untracked comps
pos.map(|c| comps.push(c.into()));
vel.map(|c| comps.push(c.into()));
@ -217,6 +223,7 @@ pub struct ReadTrackers<'a> {
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub shockwave: ReadExpect<'a, UpdateTracker<Shockwave>>,
pub beam_segment: ReadExpect<'a, UpdateTracker<BeamSegment>>,
pub alignment: ReadExpect<'a, UpdateTracker<Alignment>>,
}
impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages(
@ -268,7 +275,8 @@ impl<'a> ReadTrackers<'a> {
filter,
)
.with_component(&comps.uid, &*self.shockwave, &comps.shockwave, filter)
.with_component(&comps.uid, &*self.beam_segment, &comps.beam_segment, filter);
.with_component(&comps.uid, &*self.beam_segment, &comps.beam_segment, filter)
.with_component(&comps.uid, &*self.alignment, &comps.alignment, filter);
(entity_sync_package, comp_sync_package)
}
@ -303,6 +311,7 @@ pub struct WriteTrackers<'a> {
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
shockwave: WriteExpect<'a, UpdateTracker<Shockwave>>,
beam: WriteExpect<'a, UpdateTracker<BeamSegment>>,
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
}
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@ -338,6 +347,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
.record_changes(&comps.character_state);
trackers.shockwave.record_changes(&comps.shockwave);
trackers.beam.record_changes(&comps.beam_segment);
trackers.alignment.record_changes(&comps.alignment);
// Debug how many updates are being sent
/*
macro_rules! log_counts {
@ -378,6 +388,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
log_counts!(character_state, "Character States");
log_counts!(shockwave, "Shockwaves");
log_counts!(beam, "Beams");
log_counts!(alignment, "Alignments");
*/
}
@ -409,6 +420,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<CharacterState>();
world.register_tracker::<Shockwave>();
world.register_tracker::<BeamSegment>();
world.register_tracker::<Alignment>();
}
/// Deleted entities grouped by region

View File

@ -90,6 +90,8 @@ use common::{
uid::Uid,
util::{srgba_to_linear, Dir},
vol::RectRasterableVol,
mounting::Mount,
link::Is,
};
use common_base::{prof_span, span};
use common_net::{
@ -1140,6 +1142,8 @@ impl Hud {
let entities = ecs.entities();
let me = client.entity();
let poises = ecs.read_storage::<comp::Poise>();
let alignments = ecs.read_storage::<comp::Alignment>();
let is_mount = ecs.read_storage::<Is<Mount>>();
// Check if there was a persistence load error of the skillset, and if so
// display a dialog prompt
@ -1664,7 +1668,7 @@ impl Hud {
let mut sct_bg_walker = self.ids.sct_bgs.walk();
let pulse = self.pulse;
let make_overitem = |item: &Item, pos, distance, properties, fonts| {
let make_overitem = |item: &Item, pos, distance, properties, fonts, interaction_options| {
let text = if item.amount() > 1 {
format!("{} x {}", item.amount(), item.name())
} else {
@ -1684,6 +1688,7 @@ impl Hud {
properties,
pulse,
&global_state.window.key_layout,
interaction_options,
)
.x_y(0.0, 100.0)
.position_ingame(pos)
@ -1714,6 +1719,7 @@ impl Hud {
pickup_failed_pulse: self.failed_entity_pickups.get(&entity).copied(),
},
&self.fonts,
vec![(GameInput::Interact, i18n.get("hud.pick_up").to_string())],
)
.set(overitem_id, ui_widgets);
}
@ -1733,9 +1739,9 @@ impl Hud {
let over_pos = pos + Vec3::unit_z() * 0.7;
// This is only done once per frame, so it's not a performance issue
if block.get_sprite().map_or(false, |s| s.is_container()) {
if let Some(sprite) = block.get_sprite().filter(|s| s.is_container()) {
overitem::Overitem::new(
"???".into(),
format!("{:?}", sprite).into(),
overitem::TEXT_COLOR,
pos.distance_squared(player_pos),
&self.fonts,
@ -1744,6 +1750,7 @@ impl Hud {
overitem_properties,
self.pulse,
&global_state.window.key_layout,
vec![(GameInput::Interact, i18n.get("hud.open").to_string())],
)
.x_y(0.0, 100.0)
.position_ingame(over_pos)
@ -1755,6 +1762,7 @@ impl Hud {
pos.distance_squared(player_pos),
overitem_properties,
&self.fonts,
vec![(GameInput::Interact, i18n.get("hud.collect").to_string())],
)
.set(overitem_id, ui_widgets);
} else if let Some(desc) = block.get_sprite().and_then(|s| get_sprite_desc(s, i18n))
@ -1769,6 +1777,7 @@ impl Hud {
overitem_properties,
self.pulse,
&global_state.window.key_layout,
vec![(GameInput::Interact, i18n.get("hud.use").to_string())],
)
.x_y(0.0, 100.0)
.position_ingame(over_pos)
@ -1779,7 +1788,7 @@ impl Hud {
let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars
for (pos, info, bubble, _, _, health, _, height_offset, hpfl, in_group) in (
for (entity, pos, info, bubble, _, _, health, _, height_offset, hpfl, in_group, dist_sqr, alignment, is_mount) in (
&entities,
&pos,
interpolated.maybe(),
@ -1795,6 +1804,7 @@ impl Hud {
&inventories,
players.maybe(),
poises.maybe(),
(alignments.maybe(), is_mount.maybe()),
)
.join()
.filter(|t| {
@ -1818,6 +1828,7 @@ impl Hud {
inventory,
player,
poise,
(alignment, is_mount),
)| {
// Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos);
@ -1879,6 +1890,7 @@ impl Hud {
(info.is_some() || bubble.is_some()).then(|| {
(
entity,
pos,
info,
bubble,
@ -1889,6 +1901,9 @@ impl Hud {
body.height() * scale.map_or(1.0, |s| s.0) + 0.5,
hpfl,
in_group,
dist_sqr,
alignment,
is_mount,
)
})
},
@ -1911,8 +1926,24 @@ impl Hud {
&global_state.settings.interface,
self.pulse,
i18n,
&global_state.settings.controls,
&self.imgs,
&self.fonts,
&global_state.window.key_layout,
match alignment {
// TODO: Don't use `MAX_MOUNT_RANGE` here, add dedicated interaction range
Some(comp::Alignment::Npc) if dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2)
&& interactable.as_ref().and_then(|i| i.entity()) == Some(entity) =>
vec![
(GameInput::Interact, i18n.get("hud.talk").to_string()),
(GameInput::Trade, i18n.get("hud.trade").to_string()),
],
Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid()
&& is_mount.is_none()
&& dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2) =>
vec![(GameInput::Mount, i18n.get("hud.mount").to_string())],
_ => Vec::new(),
},
)
.x_y(0.0, 100.0)
.position_ingame(ingame_pos)

View File

@ -4,15 +4,17 @@ use super::{
TEXT_BG, TEXT_COLOR,
};
use crate::{
game_input::GameInput,
hud::{get_buff_image, get_buff_info},
settings::InterfaceSettings,
settings::{ControlSettings, InterfaceSettings},
ui::{fonts::Fonts, Ingameable},
};
use keyboard_keynames::key_layout::KeyLayout;
use common::comp::{Buffs, Energy, Health, SpeechBubble, SpeechBubbleType};
use conrod_core::{
color,
position::Align,
widget::{self, Image, Rectangle, Text},
widget::{self, Image, Rectangle, Text, RoundedRectangle},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use i18n::Localization;
@ -53,6 +55,10 @@ widget_ids! {
buffs_align,
buffs[],
buff_timers[],
// Interaction hints
interaction_hints,
interaction_hints_bg,
}
}
@ -84,8 +90,11 @@ pub struct Overhead<'a> {
settings: &'a InterfaceSettings,
pulse: f32,
i18n: &'a Localization,
controls: &'a ControlSettings,
imgs: &'a Imgs,
fonts: &'a Fonts,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
#[conrod(common_builder)]
common: widget::CommonBuilder,
@ -99,8 +108,11 @@ impl<'a> Overhead<'a> {
settings: &'a InterfaceSettings,
pulse: f32,
i18n: &'a Localization,
controls: &'a ControlSettings,
imgs: &'a Imgs,
fonts: &'a Fonts,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
) -> Self {
Self {
info,
@ -109,8 +121,11 @@ impl<'a> Overhead<'a> {
settings,
pulse,
i18n,
controls,
imgs,
fonts,
key_layout,
interaction_options,
common: widget::CommonBuilder::default(),
}
}
@ -157,6 +172,7 @@ impl<'a> Ingameable for Overhead<'a> {
} else {
0
}
+ (!self.interaction_options.is_empty()) as usize * 2
}) + if self.bubble.is_some() { 13 } else { 0 }
}
}
@ -448,6 +464,55 @@ impl<'a> Widget for Overhead<'a> {
},
_ => {},
}
// Interaction hints
if !self.interaction_options.is_empty() {
let text = self.interaction_options
.iter()
.filter_map(|(input, action)| Some((self
.controls
.get_binding(*input)?, action)))
.map(|(input, action)| format!("{} {}", input.display_string(self.key_layout).as_str(), action))
.collect::<Vec<_>>()
.join("\n");
let scale = 30.0;
let btn_rect_size = scale * 0.8;
let btn_font_size = scale * 0.6;
let btn_rect_pos_y = 0.0;
let btn_text_pos_y = btn_rect_pos_y + ((btn_rect_size - btn_font_size) * 0.5);
let btn_radius = btn_rect_size / 5.0;
let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
// RoundedRectangle::fill_with([btn_rect_size, btn_rect_size], btn_radius, btn_color)
// .x_y(0.0, btn_rect_pos_y)
// .depth(self.distance_from_player_sqr + 2.0)
// .parent(id)
// .set(state.ids.btn_bg, ui);
let hints_text = Text::new(&text)
.font_id(self.fonts.cyri.conrod_id)
.font_size(btn_font_size as u32)
.color(TEXT_COLOR)
.parent(id)
.down_from(self.info.map_or(state.ids.name, |info| if info.health.map_or(false, should_show_healthbar) {
if info.energy.is_some() { state.ids.mana_bar } else { state.ids.health_bar }
} else {
state.ids.name
}), 12.0)
.align_middle_x_of(state.ids.name)
.depth(1.0);
let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
hints_text.set(state.ids.interaction_hints, ui);
RoundedRectangle::fill_with([w + btn_radius * 2.0, h + btn_radius * 2.0], btn_radius, btn_color)
.depth(2.0)
.middle_of(state.ids.interaction_hints)
.align_middle_y_of(state.ids.interaction_hints)
.parent(id)
.set(state.ids.interaction_hints_bg, ui);
}
}
// Speech bubble
if let Some(bubble) = self.bubble {

View File

@ -6,7 +6,7 @@ use crate::{
use conrod_core::{
color,
widget::{self, RoundedRectangle, Text},
widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon,
widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon, Sizeable,
};
use i18n::Localization;
use std::borrow::Cow;
@ -21,7 +21,7 @@ widget_ids! {
// Name
name_bg,
name,
// Key
// Interaction hints
btn_bg,
btn,
// Inventory full
@ -45,6 +45,7 @@ pub struct Overitem<'a> {
properties: OveritemProperties,
pulse: f32,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
}
impl<'a> Overitem<'a> {
@ -58,6 +59,7 @@ impl<'a> Overitem<'a> {
properties: OveritemProperties,
pulse: f32,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
) -> Self {
Self {
name,
@ -70,6 +72,7 @@ impl<'a> Overitem<'a> {
properties,
pulse,
key_layout,
interaction_options,
}
}
}
@ -120,7 +123,7 @@ impl<'a> Widget for Overitem<'a> {
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { id, state, ui, .. } = args;
let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
// Example:
// MUSHROOM
@ -167,25 +170,35 @@ impl<'a> Widget for Overitem<'a> {
.parent(id)
.set(state.ids.name, ui);
// Pickup Button
if let Some(key_button) = self
.controls
.get_binding(GameInput::Interact)
.filter(|_| self.properties.active)
{
RoundedRectangle::fill_with([btn_rect_size, btn_rect_size], btn_radius, btn_color)
.x_y(0.0, btn_rect_pos_y)
.depth(self.distance_from_player_sqr + 1.0)
.parent(id)
.set(state.ids.btn_bg, ui);
Text::new(key_button.display_string(self.key_layout).as_str())
// Interaction hints
if !self.interaction_options.is_empty() {
let text = self.interaction_options
.iter()
.filter_map(|(input, action)| Some((self
.controls
.get_binding(*input)
.filter(|_| self.properties.active)?, action)))
.map(|(input, action)| format!("{} {}", input.display_string(self.key_layout).as_str(), action))
.collect::<Vec<_>>()
.join("\n");
let hints_text = Text::new(&text)
.font_id(self.fonts.cyri.conrod_id)
.font_size(btn_font_size as u32)
.color(TEXT_COLOR)
.x_y(0.0, btn_text_pos_y)
.depth(self.distance_from_player_sqr + 1.0)
.parent(id);
let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
hints_text.set(state.ids.btn, ui);
RoundedRectangle::fill_with([w + btn_radius * 2.0, h + btn_radius * 2.0], btn_radius, btn_color)
.x_y(0.0, btn_rect_pos_y)
.depth(self.distance_from_player_sqr + 2.0)
.parent(id)
.set(state.ids.btn, ui);
.set(state.ids.btn_bg, ui);
}
if let Some(time) = self.properties.pickup_failed_pulse {
//should never exceed 1.0, but just in case