diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcce40f02..8019bf5975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Crushing damage now does poise damage to a target equal to the amount mitigated by armor - UI to select abilities and assign to hotbar - Position of abilities on hotbar is now persisted through the server +- Interation hints now appear for sprites and entities +- Players can now mount and ride pets ### Changed diff --git a/assets/voxygen/i18n/en/hud/misc.ron b/assets/voxygen/i18n/en/hud/misc.ron index d447525a9f..3a7b30298d 100644 --- a/assets/voxygen/i18n/en/hud/misc.ron +++ b/assets/voxygen/i18n/en/hud/misc.ron @@ -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,14 @@ 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.mine": "Mine", + "hud.talk": "Talk", + "hud.trade": "Trade", + "hud.mount": "Mount", }, diff --git a/client/src/lib.rs b/client/src/lib.rs index 8d5fb990c9..faa68a93ba 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -34,6 +34,8 @@ use common::{ }, event::{EventBus, LocalEvent}, grid::Grid, + link::Is, + mounting::Rider, outcome::Outcome, recipe::RecipeBook, resources::{PlayerEntity, TimeOfDay}, @@ -1152,10 +1154,10 @@ impl Client { ))); } - pub fn is_mounted(&self) -> bool { + pub fn is_riding(&self) -> bool { self.state .ecs() - .read_storage::() + .read_storage::>() .get(self.entity()) .is_some() } diff --git a/common/net/src/msg/ecs_packet.rs b/common/net/src/msg/ecs_packet.rs index efaa3fb36a..83a4007250 100644 --- a/common/net/src/msg/ecs_packet.rs +++ b/common/net/src/msg/ecs_packet.rs @@ -1,5 +1,10 @@ use crate::sync; -use common::{comp, resources::Time}; +use common::{ + comp, + link::Is, + mounting::{Mount, Rider}, + resources::Time, +}; use serde::{Deserialize, Serialize}; use specs::WorldExt; use std::marker::PhantomData; @@ -27,8 +32,8 @@ sum_type! { Item(comp::Item), Scale(comp::Scale), Group(comp::Group), - MountState(comp::MountState), - Mounting(comp::Mounting), + IsMount(Is), + IsRider(Is), Mass(comp::Mass), Density(comp::Density), Collider(comp::Collider), @@ -39,6 +44,7 @@ sum_type! { Ori(comp::Ori), Shockwave(comp::Shockwave), BeamSegment(comp::BeamSegment), + Alignment(comp::Alignment), } } // Automatically derive From for EcsCompPhantom @@ -63,8 +69,8 @@ sum_type! { Item(PhantomData), Scale(PhantomData), Group(PhantomData), - MountState(PhantomData), - Mounting(PhantomData), + IsMount(PhantomData>), + IsRider(PhantomData>), Mass(PhantomData), Density(PhantomData), Collider(PhantomData), @@ -75,6 +81,7 @@ sum_type! { Ori(PhantomData), Shockwave(PhantomData), BeamSegment(PhantomData), + Alignment(PhantomData), } } impl sync::CompPacket for EcsCompPacket { @@ -104,8 +111,8 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::IsMount(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::IsRider(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Density(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world), @@ -122,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), } } @@ -149,8 +157,8 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::IsMount(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::IsRider(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Density(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world), @@ -167,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), } } @@ -193,8 +202,8 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Group(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::MountState(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::Mounting(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::IsMount(_) => sync::handle_remove::>(entity, world), + EcsCompPhantom::IsRider(_) => sync::handle_remove::>(entity, world), EcsCompPhantom::Mass(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Density(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Collider(_) => sync::handle_remove::(entity, world), @@ -206,7 +215,10 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Vel(_) => sync::handle_interp_remove::(entity, world), EcsCompPhantom::Ori(_) => sync::handle_interp_remove::(entity, world), EcsCompPhantom::Shockwave(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::BeamSegment(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::BeamSegment(_) => { + sync::handle_remove::(entity, world) + }, + EcsCompPhantom::Alignment(_) => sync::handle_remove::(entity, world), } } } diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 7e9eb2c7fe..484d0261b7 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -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::{Deserialize, Serialize}; +use specs::{Component, DerefFlaggedStorage, Entity as EcsEntity}; 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; + type Storage = DerefFlaggedStorage>; } bitflags::bitflags! { diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 8e1c6ad3c4..33b7cb5b89 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -730,45 +730,45 @@ impl Body { ) } - /// Component of the mounting offset specific to the mountee - pub fn mountee_offset(&self) -> Vec3 { + /// Component of the mounting offset specific to the mount + pub fn mount_offset(&self) -> Vec3 { match self { Body::QuadrupedMedium(quadruped_medium) => { match (quadruped_medium.species, quadruped_medium.body_type) { - (quadruped_medium::Species::Grolgar, _) => [0.5, 0.5, 1.8], - (quadruped_medium::Species::Saber, _) => [0.3, 0.3, 1.3], - (quadruped_medium::Species::Tiger, _) => [0.2, 0.2, 1.4], - (quadruped_medium::Species::Tuskram, _) => [-0.5, -0.5, 1.5], - (quadruped_medium::Species::Lion, _) => [0.3, 0.3, 1.5], - (quadruped_medium::Species::Tarasque, _) => [0.6, 0.6, 2.0], - (quadruped_medium::Species::Wolf, _) => [0.5, 0.5, 1.3], - (quadruped_medium::Species::Frostfang, _) => [0.5, 0.5, 1.2], - (quadruped_medium::Species::Mouflon, _) => [0.3, 0.3, 1.2], + (quadruped_medium::Species::Grolgar, _) => [0.0, 0.5, 1.8], + (quadruped_medium::Species::Saber, _) => [0.0, 0.3, 1.3], + (quadruped_medium::Species::Tiger, _) => [0.0, 0.2, 1.4], + (quadruped_medium::Species::Tuskram, _) => [0.0, -0.5, 1.5], + (quadruped_medium::Species::Lion, _) => [0.0, 0.3, 1.5], + (quadruped_medium::Species::Tarasque, _) => [0.0, 0.6, 2.0], + (quadruped_medium::Species::Wolf, _) => [0.0, 0.5, 1.3], + (quadruped_medium::Species::Frostfang, _) => [0.0, 0.5, 1.2], + (quadruped_medium::Species::Mouflon, _) => [0.0, 0.3, 1.2], (quadruped_medium::Species::Catoblepas, _) => [0.0, 0.0, 2.0], - (quadruped_medium::Species::Bonerattler, _) => [0.5, 0.5, 1.2], - (quadruped_medium::Species::Deer, _) => [0.2, 0.2, 1.3], + (quadruped_medium::Species::Bonerattler, _) => [0.0, 0.5, 1.2], + (quadruped_medium::Species::Deer, _) => [0.0, 0.2, 1.3], (quadruped_medium::Species::Hirdrasil, _) => [0.0, 0.0, 1.4], - (quadruped_medium::Species::Roshwalr, _) => [0.5, 0.5, 1.8], - (quadruped_medium::Species::Donkey, _) => [0.5, 0.5, 1.5], - (quadruped_medium::Species::Camel, _) => [-0.1, -0.1, 2.8], - (quadruped_medium::Species::Zebra, _) => [0.5, 0.5, 1.8], - (quadruped_medium::Species::Antelope, _) => [0.3, 0.3, 1.4], - (quadruped_medium::Species::Kelpie, _) => [0.5, 0.5, 1.9], + (quadruped_medium::Species::Roshwalr, _) => [0.0, 0.5, 1.8], + (quadruped_medium::Species::Donkey, _) => [0.0, 0.5, 1.5], + (quadruped_medium::Species::Camel, _) => [0.0, -0.1, 2.8], + (quadruped_medium::Species::Zebra, _) => [0.0, 0.5, 1.8], + (quadruped_medium::Species::Antelope, _) => [0.0, 0.3, 1.4], + (quadruped_medium::Species::Kelpie, _) => [0.0, 0.5, 1.9], (quadruped_medium::Species::Horse, _) => [0.0, 0.0, 2.0], - (quadruped_medium::Species::Barghest, _) => [0.5, 0.5, 2.2], + (quadruped_medium::Species::Barghest, _) => [0.0, 0.5, 2.2], (quadruped_medium::Species::Cattle, quadruped_medium::BodyType::Male) => { - [0.5, 0.5, 2.6] + [0.0, 0.5, 2.6] }, (quadruped_medium::Species::Cattle, quadruped_medium::BodyType::Female) => { - [0.7, 0.7, 2.2] + [0.0, 0.7, 2.2] }, - (quadruped_medium::Species::Darkhound, _) => [0.5, 0.5, 1.4], - (quadruped_medium::Species::Highland, _) => [0.5, 0.5, 2.3], + (quadruped_medium::Species::Darkhound, _) => [0.0, 0.5, 1.4], + (quadruped_medium::Species::Highland, _) => [0.0, 0.5, 2.3], (quadruped_medium::Species::Yak, _) => [0.0, 0.0, 3.0], - (quadruped_medium::Species::Panda, _) => [-0.2, -0.2, 1.4], - (quadruped_medium::Species::Bear, _) => [-0.4, -0.4, 2.5], - (quadruped_medium::Species::Dreadhorn, _) => [0.2, 0.2, 3.5], - (quadruped_medium::Species::Moose, _) => [-0.6, -0.6, 2.1], + (quadruped_medium::Species::Panda, _) => [0.0, -0.2, 1.4], + (quadruped_medium::Species::Bear, _) => [0.0, -0.4, 2.5], + (quadruped_medium::Species::Dreadhorn, _) => [0.0, 0.2, 3.5], + (quadruped_medium::Species::Moose, _) => [0.0, -0.6, 2.1], (quadruped_medium::Species::Snowleopard, _) => [-0.5, -0.5, 1.4], (quadruped_medium::Species::Mammoth, _) => [0.0, 4.9, 7.2], (quadruped_medium::Species::Ngoubou, _) => [0.0, 0.3, 2.0], @@ -789,8 +789,8 @@ impl Body { .into() } - /// Component of the mounting offset specific to the mounter - pub fn mounter_offset(&self) -> Vec3 { + /// Component of the mounting offset specific to the rider + pub fn rider_offset(&self) -> Vec3 { match self { Body::Humanoid(_) => [0.0, 0.0, 0.0], _ => [0.0, 0.0, 0.0], diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 46350e7cca..10671b318e 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -243,6 +243,32 @@ impl CharacterState { || matches!(self, CharacterState::Roll(s) if s.stage_section == StageSection::Movement) } + pub fn can_perform_mounted(&self) -> bool { + matches!( + self, + CharacterState::Idle(_) + | CharacterState::Sit + | CharacterState::Talk + | CharacterState::GlideWield(_) + | CharacterState::Stunned(_) + | CharacterState::BasicBlock(_) + | CharacterState::Equipping(_) + | CharacterState::Wielding(_) + | CharacterState::BasicMelee(_) + | CharacterState::BasicRanged(_) + | CharacterState::ComboMelee(_) + | CharacterState::ChargedRanged(_) + | CharacterState::RepeaterRanged(_) + | CharacterState::BasicBeam(_) + | CharacterState::BasicAura(_) + | CharacterState::BasicSummon(_) + | CharacterState::SelfBuff(_) + | CharacterState::SpriteSummon(_) + | CharacterState::UseItem(_) + | CharacterState::SpriteInteract(_) + ) + } + pub fn is_sitting(&self) -> bool { use use_item::{Data, ItemUseKind, StaticData}; matches!( diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index fe803208ea..2a1ba9789b 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -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::*; @@ -284,20 +284,3 @@ impl Controller { impl Component for Controller { type Storage = IdvStorage; } - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum MountState { - Unmounted, - MountedBy(Uid), -} - -impl Component for MountState { - type Storage = DerefFlaggedStorage>; -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Mounting(pub Uid); - -impl Component for Mounting { - type Storage = DerefFlaggedStorage>; -} diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 2619eea576..39110bb48c 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -153,6 +153,19 @@ pub fn make_potion_bag(quantity: u32) -> Item { bag } +#[must_use] +pub fn make_food_bag(quantity: u32) -> Item { + let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch"); + if let Some(i) = bag.slots_mut().iter_mut().next() { + let mut food = Item::new_from_asset_expect("common.items.food.apple_stick"); + if let Err(e) = food.set_amount(quantity) { + warn!("Failed to set food quantity: {:?}", e); + } + *i = Some(food); + } + bag +} + #[must_use] // We have many species so this function is long // Also we are using default tools for un-specified species so diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index b33dfbe589..a75ebfb387 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -73,8 +73,7 @@ pub use self::{ combo::Combo, controller::{ Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InputAttr, - InputKind, InventoryAction, InventoryEvent, InventoryManip, MountState, Mounting, - UtteranceKind, + InputKind, InventoryAction, InventoryEvent, InventoryManip, UtteranceKind, }, energy::Energy, fluid_dynamics::Fluid, diff --git a/common/src/consts.rs b/common/src/consts.rs index 8d3d8dee8f..88895900f5 100644 --- a/common/src/consts.rs +++ b/common/src/consts.rs @@ -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; diff --git a/common/src/lib.rs b/common/src/lib.rs index e6109bb5c4..ac0470bff2 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -12,7 +12,9 @@ trait_alias, type_alias_impl_trait, extend_one, - arbitrary_enum_discriminant + arbitrary_enum_discriminant, + generic_associated_types, + arbitrary_self_types )] #![feature(hash_drain_filter)] @@ -46,8 +48,11 @@ pub mod figure; #[cfg(not(target_arch = "wasm32"))] pub mod generation; #[cfg(not(target_arch = "wasm32"))] pub mod grid; +#[cfg(not(target_arch = "wasm32"))] pub mod link; #[cfg(not(target_arch = "wasm32"))] pub mod lottery; +#[cfg(not(target_arch = "wasm32"))] +pub mod mounting; #[cfg(not(target_arch = "wasm32"))] pub mod npc; #[cfg(not(target_arch = "wasm32"))] pub mod outcome; diff --git a/common/src/link.rs b/common/src/link.rs new file mode 100644 index 0000000000..9cdee123c0 --- /dev/null +++ b/common/src/link.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use specs::{Component, DerefFlaggedStorage, SystemData}; +use specs_idvs::IdvStorage; +use std::{ops::Deref, sync::Arc}; + +pub trait Link: Sized + Send + Sync + 'static { + type Error; + + type CreateData<'a>: SystemData<'a>; + fn create(this: &LinkHandle, data: Self::CreateData<'_>) -> Result<(), Self::Error>; + + type PersistData<'a>: SystemData<'a>; + fn persist(this: &LinkHandle, data: Self::PersistData<'_>) -> bool; + + type DeleteData<'a>: SystemData<'a>; + fn delete(this: &LinkHandle, data: Self::DeleteData<'_>); +} + +pub trait Role { + type Link: Link; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Is { + #[serde(bound(serialize = "R::Link: Serialize"))] + #[serde(bound(deserialize = "R::Link: Deserialize<'de>"))] + link: LinkHandle, +} + +impl Is { + pub fn delete(&self, data: ::DeleteData<'_>) { + R::Link::delete(&self.link, data) + } +} + +impl Clone for Is { + fn clone(&self) -> Self { + Self { + link: self.link.clone(), + } + } +} + +impl Deref for Is { + type Target = R::Link; + + fn deref(&self) -> &Self::Target { &self.link } +} + +impl Component for Is +where + R::Link: Send + Sync + 'static, +{ + type Storage = DerefFlaggedStorage>; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct LinkHandle { + link: Arc, +} + +impl Clone for LinkHandle { + fn clone(&self) -> Self { + Self { + link: Arc::clone(&self.link), + } + } +} + +impl LinkHandle { + pub fn from_link(link: L) -> Self { + Self { + link: Arc::new(link), + } + } + + pub fn make_role>(&self) -> Is { Is { link: self.clone() } } +} + +impl Deref for LinkHandle { + type Target = L; + + fn deref(&self) -> &Self::Target { &self.link } +} diff --git a/common/src/mounting.rs b/common/src/mounting.rs new file mode 100644 index 0000000000..bfc0c6b185 --- /dev/null +++ b/common/src/mounting.rs @@ -0,0 +1,144 @@ +use crate::{ + comp, + link::{Is, Link, LinkHandle, Role}, + terrain::TerrainGrid, + uid::{Uid, UidAllocator}, +}; +use serde::{Deserialize, Serialize}; +use specs::{saveload::MarkerAllocator, Entities, Read, ReadExpect, ReadStorage, WriteStorage}; +use vek::*; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Rider; + +impl Role for Rider { + type Link = Mounting; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Mount; + +impl Role for Mount { + type Link = Mounting; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Mounting { + pub mount: Uid, + pub rider: Uid, +} + +pub enum MountingError { + NoSuchEntity, + NotMountable, +} + +impl Link for Mounting { + type CreateData<'a> = ( + Read<'a, UidAllocator>, + WriteStorage<'a, Is>, + WriteStorage<'a, Is>, + ); + type DeleteData<'a> = ( + Read<'a, UidAllocator>, + WriteStorage<'a, Is>, + WriteStorage<'a, Is>, + WriteStorage<'a, comp::Pos>, + WriteStorage<'a, comp::ForceUpdate>, + ReadExpect<'a, TerrainGrid>, + ); + type Error = MountingError; + type PersistData<'a> = ( + Read<'a, UidAllocator>, + Entities<'a>, + ReadStorage<'a, comp::Health>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + ); + + fn create( + this: &LinkHandle, + (uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>, + ) -> Result<(), Self::Error> { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + if this.mount == this.rider { + // Forbid self-mounting + Err(MountingError::NotMountable) + } else if let Some((mount, rider)) = entity(this.mount).zip(entity(this.rider)) { + let can_mount_with = + |entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none(); + + // Ensure that neither mount or rider are already part of a mounting + // relationship + if can_mount_with(mount) && can_mount_with(rider) { + let _ = is_mounts.insert(mount, this.make_role()); + let _ = is_riders.insert(rider, this.make_role()); + Ok(()) + } else { + Err(MountingError::NotMountable) + } + } else { + Err(MountingError::NoSuchEntity) + } + } + + fn persist( + this: &LinkHandle, + (uid_allocator, entities, healths, is_mounts, is_riders): Self::PersistData<'_>, + ) -> bool { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + if let Some((mount, rider)) = entity(this.mount).zip(entity(this.rider)) { + let is_alive = |entity| { + entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead) + }; + + // Ensure that both entities are alive and that they continue to be linked + is_alive(mount) + && is_alive(rider) + && is_mounts.get(mount).is_some() + && is_riders.get(rider).is_some() + } else { + false + } + } + + fn delete( + this: &LinkHandle, + (uid_allocator, mut is_mounts, mut is_riders, mut positions, mut force_update, terrain): Self::DeleteData<'_>, + ) { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + let mount = entity(this.mount); + let rider = entity(this.rider); + + // Delete link components + mount.map(|mount| is_mounts.remove(mount)); + rider.map(|rider| is_riders.remove(rider)); + + // Try to move the rider to a safe place when dismounting + let safe_pos = rider + .and_then(|rider| positions.get(rider).copied()) + .filter(|rider_pos| terrain.is_space(rider_pos.0.map(|e| e.floor() as i32))) + .or_else(|| { + mount + .and_then(|mount| positions.get(mount).copied()) + .filter(|mount_pos| { + terrain.is_space( + (mount_pos.0 + Vec3::unit_z() * 0.1).map(|e| e.floor() as i32), + ) + }) + }); + rider + .and_then(|rider| Some(rider).zip(positions.get_mut(rider))) + .map(|(rider, pos)| { + let old_pos = pos.0.map(|e| e.floor() as i32); + pos.0 = safe_pos + .map(|p| p.0.map(|e| e.floor())) + .unwrap_or_else(|| terrain.find_space(old_pos).map(|e| e as f32)) + + Vec3::new(0.5, 0.5, 0.0); + let _ = force_update.insert(rider, comp::ForceUpdate); + }); + } +} diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index a620bf20b4..9d0612290a 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -158,7 +158,7 @@ impl Body { Body::Humanoid(_) => 3.5, Body::QuadrupedSmall(_) => 3.0, Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species { - quadruped_medium::Species::Mammoth => 2.2, + quadruped_medium::Species::Mammoth => 1.0, _ => 2.8, }, Body::BirdMedium(_) => 6.0, @@ -421,7 +421,14 @@ pub fn handle_orientation( // Angle factor used to keep turning rate approximately constant by // counteracting slerp turning more with a larger angle let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori)).sqrt(); - data.body.base_ori_rate() * efficiency * angle_factor + data.body.base_ori_rate() + * efficiency + * angle_factor + * if data.physics.on_ground.is_some() { + 1.0 + } else { + 0.2 + } }; update.ori = update .ori diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 2084852f56..ecaf113f74 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -162,18 +162,22 @@ impl TerrainGrid { self.try_find_space(pos).unwrap_or(pos) } + pub fn is_space(&self, pos: Vec3) -> bool { + (0..2).all(|z| { + self.get(pos + Vec3::unit_z() * z) + .map_or(true, |b| !b.is_solid()) + }) + } + pub fn try_find_space(&self, pos: Vec3) -> Option> { const SEARCH_DIST: i32 = 63; (0..SEARCH_DIST * 2 + 1) .map(|i| if i % 2 == 0 { i } else { -i } / 2) .map(|z_diff| pos + Vec3::unit_z() * z_diff) - .find(|test_pos| { - self.get(test_pos - Vec3::unit_z()) + .find(|pos| { + self.get(pos - Vec3::unit_z()) .map_or(false, |b| b.is_filled()) - && (0..2).all(|z| { - self.get(test_pos + Vec3::unit_z() * z) - .map_or(true, |b| !b.is_solid()) - }) + && self.is_space(*pos) }) } } diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 3a06dbc0a2..09bc81f754 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -8,6 +8,8 @@ use common::{ calendar::Calendar, comp, event::{EventBus, LocalEvent, ServerEvent}, + link::Is, + mounting::{Mount, Rider}, outcome::Outcome, region::RegionMap, resources::{ @@ -141,8 +143,8 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); - ecs.register::(); + ecs.register::>(); + ecs.register::>(); ecs.register::(); ecs.register::(); ecs.register::(); @@ -153,6 +155,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); // Register components send from clients -> server ecs.register::(); @@ -182,7 +185,6 @@ impl State { ecs.register::>(); ecs.register::>(); ecs.register::>(); - ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index c5c5e86ad5..cfd9e6cc1e 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -7,10 +7,12 @@ use common::{ comp::{ self, character_state::OutputEvents, inventory::item::MaterialStatManifest, ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, - Inventory, InventoryManip, Mass, Melee, Mounting, Ori, PhysicsState, Poise, Pos, SkillSet, + Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, SkillSet, StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, + link::Is, + mounting::Rider, outcome::Outcome, resources::{DeltaTime, Time}, states::{ @@ -37,7 +39,7 @@ pub struct ReadData<'a> { melee_attacks: ReadStorage<'a, Melee>, beams: ReadStorage<'a, Beam>, uids: ReadStorage<'a, Uid>, - mountings: ReadStorage<'a, Mounting>, + is_riders: ReadStorage<'a, Is>, stats: ReadStorage<'a, Stats>, skill_sets: ReadStorage<'a, SkillSet>, active_abilities: ReadStorage<'a, ActiveAbilities>, @@ -110,7 +112,7 @@ impl<'a> System<'a> for Sys { health, body, physics, - (stat, skill_set, active_abilities), + (stat, skill_set, active_abilities, is_rider), combo, ) in ( &read_data.entities, @@ -131,6 +133,7 @@ impl<'a> System<'a> for Sys { &read_data.stats, &read_data.skill_sets, &read_data.active_abilities, + read_data.is_riders.maybe(), ), &read_data.combos, ) @@ -207,11 +210,9 @@ impl<'a> System<'a> for Sys { // Mounted occurs after control actions have been handled // If mounted, character state is controlled by mount - if let Some(Mounting(_)) = read_data.mountings.get(entity) { - let idle_state = CharacterState::Idle(idle::Data { is_sneaking: false }); - if *join_struct.char_state != idle_state { - *join_struct.char_state = idle_state; - } + if is_rider.is_some() && !join_struct.char_state.can_perform_mounted() { + // TODO: A better way to swap between mount inputs and rider inputs + *join_struct.char_state = CharacterState::Idle(idle::Data { is_sneaking: false }); continue; } diff --git a/common/systems/src/lib.rs b/common/systems/src/lib.rs index 1a2de0b900..a88b20b9b9 100644 --- a/common/systems/src/lib.rs +++ b/common/systems/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(bool_to_option)] +#![feature(bool_to_option, let_else, btree_drain_filter)] #![allow(clippy::option_map_unit_fn)] mod aura; diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index f2c8a69c98..82b43dbf13 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,7 @@ use common::{ - comp::{Body, Controller, MountState, Mounting, Ori, Pos, Vel}, + comp::{Body, Controller, InputKind, Ori, Pos, Vel}, + link::Is, + mounting::Mount, uid::UidAllocator, }; use common_ecs::{Job, Origin, Phase, System}; @@ -18,8 +20,7 @@ impl<'a> System<'a> for Sys { Read<'a, UidAllocator>, Entities<'a>, WriteStorage<'a, Controller>, - WriteStorage<'a, MountState>, - WriteStorage<'a, Mounting>, + ReadStorage<'a, Is>, WriteStorage<'a, Pos>, WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, @@ -36,69 +37,51 @@ impl<'a> System<'a> for Sys { uid_allocator, entities, mut controllers, - mut mount_state, - mut mountings, + is_mounts, mut positions, mut velocities, mut orientations, bodies, ): Self::SystemData, ) { - // Mounted entities. - for (entity, mut mount_states, body) in (&entities, &mut mount_state, bodies.maybe()).join() - { - match *mount_states { - MountState::Unmounted => {}, - MountState::MountedBy(mounter_uid) => { - // Note: currently controller events are not passed through since none of them - // are currently relevant to controlling the mounted entity - if let Some((inputs, queued_inputs, mounter)) = uid_allocator - .retrieve_entity_internal(mounter_uid.id()) - .and_then(|mounter| { - controllers - .get(mounter) - .map(|c| (c.inputs.clone(), c.queued_inputs.clone(), mounter)) + // For each mount... + for (entity, is_mount, body) in (&entities, &is_mounts, bodies.maybe()).join() { + // ...find the rider... + let Some((inputs, queued_inputs, rider)) = uid_allocator + .retrieve_entity_internal(is_mount.rider.id()) + .and_then(|rider| { + controllers + .get_mut(rider) + .map(|c| { + let queued_inputs = c.queued_inputs + // TODO: Formalise ways to pass inputs to mounts + .drain_filter(|i, _| matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll)) + .collect(); + (c.inputs.clone(), queued_inputs, rider) }) - { - // TODO: consider joining on these? (remember we can use .maybe()) - let pos = positions.get(entity).copied(); - let ori = orientations.get(entity).copied(); - let vel = velocities.get(entity).copied(); - if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) { - let mounter_body = bodies.get(mounter); - let mounting_offset = body.map_or(Vec3::unit_z(), Body::mountee_offset) - + mounter_body.map_or(Vec3::zero(), Body::mounter_offset); - let _ = positions - .insert(mounter, Pos(pos.0 + ori.to_quat() * mounting_offset)); - let _ = orientations.insert(mounter, ori); - let _ = velocities.insert(mounter, vel); - } - if let Some(controller) = controllers.get_mut(entity) { - *controller = Controller { - inputs, - queued_inputs, - ..Default::default() - } - } - } else { - *mount_states = MountState::Unmounted; - } - }, - } - } + }) + else { continue }; - let mut to_unmount = Vec::new(); - for (entity, Mounting(mountee_uid)) in (&entities, &mountings).join() { - if uid_allocator - .retrieve_entity_internal(mountee_uid.id()) - .filter(|mountee| entities.is_alive(*mountee)) - .is_none() - { - to_unmount.push(entity); + // ...apply the mount's position/ori/velocity to the rider... + let pos = positions.get(entity).copied(); + let ori = orientations.get(entity).copied(); + let vel = velocities.get(entity).copied(); + if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) { + let mounter_body = bodies.get(rider); + let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset) + + mounter_body.map_or(Vec3::zero(), Body::rider_offset); + let _ = positions.insert(rider, Pos(pos.0 + ori.to_quat() * mounting_offset)); + let _ = orientations.insert(rider, ori); + let _ = velocities.insert(rider, vel); + } + // ...and apply the rider's inputs to the mount's controller. + if let Some(controller) = controllers.get_mut(entity) { + *controller = Controller { + inputs, + queued_inputs, + ..Default::default() + } } - } - for entity in to_unmount { - mountings.remove(entity); } } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 42ebfc587b..7a47c1400e 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -2,11 +2,13 @@ use common::{ comp::{ body::ship::figuredata::{VoxelCollider, VOXEL_COLLIDER_MANIFEST}, fluid_dynamics::{Fluid, LiquidKind, Wings}, - Body, CharacterState, Collider, Density, Mass, Mounting, Ori, PhysicsState, Pos, - PosVelOriDefer, PreviousPhysCache, Projectile, Scale, Stats, Sticky, Vel, + Body, CharacterState, Collider, Density, Mass, Ori, PhysicsState, Pos, PosVelOriDefer, + PreviousPhysCache, Projectile, Scale, Stats, Sticky, Vel, }, consts::{AIR_DENSITY, FRIC_GROUND, GRAVITY}, event::{EventBus, ServerEvent}, + link::Is, + mounting::Rider, outcome::Outcome, resources::DeltaTime, states, @@ -111,7 +113,7 @@ pub struct PhysicsRead<'a> { stickies: ReadStorage<'a, Sticky>, masses: ReadStorage<'a, Mass>, colliders: ReadStorage<'a, Collider>, - mountings: ReadStorage<'a, Mounting>, + is_ridings: ReadStorage<'a, Is>, projectiles: ReadStorage<'a, Projectile>, char_states: ReadStorage<'a, CharacterState>, bodies: ReadStorage<'a, Body>, @@ -169,10 +171,9 @@ impl<'a> PhysicsData<'a> { &self.write.velocities, &self.write.positions, !&self.write.previous_phys_cache, - !&self.read.mountings, ) .join() - .map(|(e, _, _, _, _, _)| e) + .map(|(e, _, _, _, _)| e) .collect::>() { let _ = self @@ -192,7 +193,7 @@ impl<'a> PhysicsData<'a> { } // Update PreviousPhysCache - for (_, vel, position, ori, mut phys_cache, collider, scale, cs, _) in ( + for (_, vel, position, ori, mut phys_cache, collider, scale, cs) in ( &self.read.entities, &self.write.velocities, &self.write.positions, @@ -201,7 +202,6 @@ impl<'a> PhysicsData<'a> { &self.read.colliders, self.read.scales.maybe(), self.read.char_states.maybe(), - !&self.read.mountings, ) .join() { @@ -292,14 +292,12 @@ impl<'a> PhysicsData<'a> { let lg2_large_cell_size = 6; let radius_cutoff = 8; let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); - for (entity, pos, phys_cache, _, _, _, _) in ( + for (entity, pos, phys_cache, _, _) in ( &read.entities, &write.positions, &write.previous_phys_cache, write.velocities.mask(), !&read.projectiles, // Not needed because they are skipped in the inner loop below - !&read.mountings, - read.colliders.mask(), ) .join() { @@ -328,7 +326,7 @@ impl<'a> PhysicsData<'a> { previous_phys_cache, &read.masses, &read.colliders, - !&read.mountings, + read.is_ridings.maybe(), read.stickies.maybe(), &mut write.physics_states, // TODO: if we need to avoid collisions for other things consider @@ -338,9 +336,6 @@ impl<'a> PhysicsData<'a> { read.char_states.maybe(), ) .par_join() - .map(|(e, p, v, vd, m, c, _, sticky, ph, pr, c_s)| { - (e, p, v, vd, m, c, sticky, ph, pr, c_s) - }) .map_init( || { prof_span!(guard, "physics e<>e rayon job"); @@ -354,6 +349,7 @@ impl<'a> PhysicsData<'a> { previous_cache, mass, collider, + is_riding, sticky, physics, projectile, @@ -404,6 +400,7 @@ impl<'a> PhysicsData<'a> { mass, collider, read.char_states.get(entity), + read.is_ridings.get(entity), )) }) .for_each( @@ -415,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; @@ -476,6 +474,8 @@ impl<'a> PhysicsData<'a> { collider_other, *mass, *mass_other, + vel, + is_riding.is_some() || other_is_riding_maybe.is_some(), ); } }, @@ -570,7 +570,7 @@ impl<'a> PhysicsData<'a> { write.physics_states.mask(), !&write.pos_vel_ori_defers, // This is the one we are adding write.previous_phys_cache.mask(), - !&read.mountings, + !&read.is_ridings, ) .join() .map(|t| (t.0, *t.2, *t.3, *t.4)) @@ -601,7 +601,7 @@ impl<'a> PhysicsData<'a> { &write.physics_states, &read.masses, &read.densities, - !&read.mountings, + !&read.is_ridings, ) .par_join() .for_each_init( @@ -730,7 +730,7 @@ impl<'a> PhysicsData<'a> { &mut write.physics_states, &mut write.pos_vel_ori_defers, previous_phys_cache, - !&read.mountings, + !&read.is_ridings, ) .par_join() .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities) @@ -1792,6 +1792,8 @@ fn resolve_e2e_collision( collider_other: &Collider, mass: Mass, mass_other: Mass, + vel: &Vel, + is_riding: bool, ) -> bool { // Find the distance betwen our collider and // collider we collide with and get vector of pushback. @@ -1849,7 +1851,8 @@ fn resolve_e2e_collision( // // This allows using e2e pushback to gain speed by jumping out of a roll // while in the middle of a collider, this is an intentional combat mechanic. - let forced_movement = matches!(char_state_maybe, Some(cs) if cs.is_forced_movement()); + let forced_movement = + matches!(char_state_maybe, Some(cs) if cs.is_forced_movement()) || is_riding; // Don't apply repulsive force to projectiles, // or if we're colliding with a terrain-like entity, @@ -1869,7 +1872,16 @@ fn resolve_e2e_collision( let distance_coefficient = collision_dist - diff.magnitude(); let force = ELASTIC_FORCE_COEFFICIENT * distance_coefficient * mass_coefficient; - *vel_delta += Vec3::from(diff.normalized()) * force * step_delta; + let diff = diff.normalized(); + + *vel_delta += Vec3::from(diff) + * force + * step_delta + * vel + .0 + .xy() + .try_normalized() + .map_or(1.0, |dir| diff.dot(-dir).max(0.025)); } *collision_registered = true; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 049569d062..c9a4787b08 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -34,10 +34,12 @@ use common::{ effect::Effect, event::{EventBus, ServerEvent}, generation::{EntityConfig, EntityInfo}, + link::Is, + mounting::Rider, npc::{self, get_npc_name}, resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, - uid::Uid, + uid::{Uid, UidAllocator}, vol::{ReadVol, RectVolSize}, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; @@ -50,7 +52,9 @@ use core::{cmp::Ordering, convert::TryFrom, time::Duration}; use hashbrown::{HashMap, HashSet}; use humantime::Duration as HumanDuration; use rand::Rng; -use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt}; +use specs::{ + saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt, +}; use std::{str::FromStr, sync::Arc}; use vek::*; use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement}; @@ -201,11 +205,35 @@ fn position_mut( descriptor: &str, f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T, ) -> CmdResult { - let mut pos_storage = server.state.ecs_mut().write_storage::(); - pos_storage + let entity = server + .state + .ecs() + .read_storage::>() + .get(entity) + .and_then(|is_rider| { + server + .state + .ecs() + .read_resource::() + .retrieve_entity_internal(is_rider.mount.into()) + }) + .unwrap_or(entity); + + let res = server + .state + .ecs() + .write_storage::() .get_mut(entity) .map(f) - .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor)) + .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor)); + if res.is_ok() { + let _ = server + .state + .ecs() + .write_storage::() + .insert(entity, comp::ForceUpdate); + } + res } fn insert_or_replace_component( @@ -622,8 +650,7 @@ fn handle_make_npc( .create_npc(pos, stats, skill_set, health, poise, inventory, body) .with(alignment) .with(scale) - .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0))) - .with(comp::MountState::Unmounted); + .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0))); if let Some(agent) = agent { entity_builder = entity_builder.with(agent); @@ -767,8 +794,7 @@ fn handle_jump( if let (Some(x), Some(y), Some(z)) = parse_args!(args, f32, f32, f32) { position_mut(server, target, "target", |current_pos| { current_pos.0 += Vec3::new(x, y, z) - })?; - insert_or_replace_component(server, target, comp::ForceUpdate, "target") + }) } else { Err(action.help_string()) } @@ -784,8 +810,7 @@ fn handle_goto( if let (Some(x), Some(y), Some(z)) = parse_args!(args, f32, f32, f32) { position_mut(server, target, "target", |current_pos| { current_pos.0 = Vec3::new(x, y, z) - })?; - insert_or_replace_component(server, target, comp::ForceUpdate, "target") + }) } else { Err(action.help_string()) } @@ -820,8 +845,7 @@ fn handle_site( position_mut(server, target, "target", |current_pos| { current_pos.0 = site_pos - })?; - insert_or_replace_component(server, target, comp::ForceUpdate, "target") + }) } else { Err(action.help_string()) } @@ -848,8 +872,7 @@ fn handle_home( target, comp::Waypoint::temp_new(home_pos, time), "target", - )?; - insert_or_replace_component(server, target, comp::ForceUpdate, "target") + ) } fn handle_kill( @@ -1119,8 +1142,7 @@ fn handle_tp( let player_pos = position(server, player, "player")?; position_mut(server, target, "target", |target_pos| { *target_pos = player_pos - })?; - insert_or_replace_component(server, target, comp::ForceUpdate, "target") + }) } fn handle_spawn( @@ -1168,7 +1190,6 @@ fn handle_spawn( body, ) .with(comp::Vel(vel)) - .with(comp::MountState::Unmounted) .with(alignment); if ai { @@ -1251,7 +1272,6 @@ fn handle_spawn_training_dummy( body, ) .with(comp::Vel(vel)) - .with(comp::MountState::Unmounted) .build(); server.notify_client( diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index d3d92106e9..2a9a6325bf 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -15,6 +15,8 @@ use common::{ Inventory, Pos, SkillGroupKind, }, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, + link::Is, + mounting::{Mount, Mounting, Rider}, outcome::Outcome, terrain::{Block, SpriteKind}, uid::Uid, @@ -97,62 +99,48 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en } } -/// FIXME: Make mounting more robust, avoid bidirectional links. -pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) { +pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { let state = server.state_mut(); - if state - .ecs() - .read_storage::() - .get(mounter) - .is_none() - { - let not_mounting_yet = matches!( - state.ecs().read_storage::().get(mountee), - Some(comp::MountState::Unmounted) - ); + if state.ecs().read_storage::>().get(rider).is_none() { + let not_mounting_yet = state.ecs().read_storage::>().get(mount).is_none(); let within_range = || { let positions = state.ecs().read_storage::(); - within_mounting_range(positions.get(mounter), positions.get(mountee)) + within_mounting_range(positions.get(rider), positions.get(mount)) }; let healths = state.ecs().read_storage::(); let alive = |e| healths.get(e).map_or(true, |h| !h.is_dead); - if not_mounting_yet && within_range() && alive(mounter) && alive(mountee) { + if not_mounting_yet && within_range() && alive(rider) && alive(mount) { let uids = state.ecs().read_storage::(); - if let (Some(mounter_uid), Some(mountee_uid)) = - (uids.get(mounter).copied(), uids.get(mountee).copied()) + if let (Some(rider_uid), Some(mount_uid)) = + (uids.get(rider).copied(), uids.get(mount).copied()) { - drop(uids); - drop(healths); - // We know the entities must exist to be able to look up their UIDs, so these - // are guaranteed to work; hence we can ignore possible errors here. - state.write_component_ignore_entity_dead( - mountee, - comp::MountState::MountedBy(mounter_uid), + let is_pet = matches!( + state + .ecs() + .read_storage::() + .get(mount), + Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, ); - state.write_component_ignore_entity_dead(mounter, comp::Mounting(mountee_uid)); + + if is_pet { + drop(uids); + drop(healths); + let _ = state.link(Mounting { + mount: mount_uid, + rider: rider_uid, + }); + } } } } } -pub fn handle_unmount(server: &mut Server, mounter: EcsEntity) { +pub fn handle_unmount(server: &mut Server, rider: EcsEntity) { let state = server.state_mut(); - let mountee_entity = state - .ecs() - .write_storage::() - .get(mounter) - .and_then(|mountee| state.ecs().entity_from_uid(mountee.0.into())); - if let Some(mountee_entity) = mountee_entity { - state - .ecs() - .write_storage::() - .get_mut(mountee_entity) - .map(|mut ms| *ms = comp::MountState::Unmounted); - } - state.delete_component::(mounter); + state.ecs().write_storage::>().remove(rider); } /// FIXME: This code is dangerous and needs to be refactored. We can't just diff --git a/server/src/lib.rs b/server/src/lib.rs index fa19904156..b0d2d7de8e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -8,7 +8,8 @@ drain_filter, never_type, option_zip, - unwrap_infallible + unwrap_infallible, + let_else )] #![cfg_attr(not(feature = "worldgen"), feature(const_panic))] @@ -663,6 +664,9 @@ impl Server { // will be processed once handle_events() is called below let disconnect_type = self.disconnect_all_clients_if_requested(); + // Handle entity links (such as mounting) + self.state.maintain_links(); + // Handle game events frontend_events.append(&mut self.handle_events()); diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 7c0e29a312..beb7ccdffe 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -1,6 +1,9 @@ use super::*; use common::{ - comp::inventory::{loadout_builder::make_potion_bag, slot::ArmorSlot}, + comp::inventory::{ + loadout_builder::{make_food_bag, make_potion_bag}, + slot::ArmorSlot, + }, resources::Time, rtsim::{Memory, MemoryItem}, store::Id, @@ -142,8 +145,9 @@ impl Entity { // give potions to traveler humanoids or return loadout as is otherwise match (body, kind) { - (comp::Body::Humanoid(_), RtSimEntityKind::Random) => { - |l, _| l.bag(ArmorSlot::Bag1, Some(make_potion_bag(100))) + (comp::Body::Humanoid(_), RtSimEntityKind::Random) => |l, _| { + l.bag(ArmorSlot::Bag1, Some(make_potion_bag(100))) + .bag(ArmorSlot::Bag2, Some(make_food_bag(100))) }, (_, RtSimEntityKind::Merchant) => { |l, trade| l.with_creator(world::site::settlement::merchant_loadout, trade) @@ -163,7 +167,9 @@ impl Entity { .iter() .filter(|s| s.1.is_settlement() || s.1.is_castle()) .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32 }) .map(|(id, _)| id) @@ -173,7 +179,9 @@ impl Entity { // with at least one path, we need to get them to a town that does. let nearest_site = &world.civs().sites[nearest_site_id]; let site_wpos = - nearest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + nearest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = site_wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; if dist < 64_u32.pow(2) { @@ -204,7 +212,9 @@ impl Entity { }) .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; dist + if dist < 96_u32.pow(2) { 100_000_000 } else { 0 } @@ -215,8 +225,10 @@ impl Entity { (Normal::new(0.0, 64.0), Normal::new(0.0, 256.0)) { let mut path = Vec::>::default(); - let target_site_pos = site.center.map(|e| e as f32) - * TerrainChunk::RECT_SIZE.map(|e| e as f32); + let target_site_pos = + site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e * sz as i32 + sz as i32 / 2) as f32 + }); let offset_site_pos = target_site_pos.map(|v| v + normalpos.sample(&mut rng)); let offset_dir = (offset_site_pos - self.pos.xy()).normalized(); @@ -252,7 +264,9 @@ impl Entity { }) .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } @@ -275,7 +289,9 @@ impl Entity { .neighbors(site_id) .filter(|sid| { let site = world.civs().sites.get(*sid); - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; dist > 96_u32.pow(2) }) @@ -310,7 +326,9 @@ impl Entity { .filter(|s| s.1.is_settlement() | s.1.is_castle()) .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } }) @@ -333,7 +351,9 @@ impl Entity { .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; if dist < 64_u32.pow(2) { @@ -435,7 +455,9 @@ impl Entity { }; if let Some(sim_pos) = track.path().iter().nth(nth) { - let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let chunkpos = sim_pos.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let wpos = if let Some(pathdata) = world.sim().get_nearest_path(chunkpos) { pathdata.1.map(|e| e as i32) } else { @@ -523,7 +545,9 @@ impl Entity { .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); - let wpos = dest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let wpos = dest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }); let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; // Once at site, stay for a bit, then move to other site diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 3aa1aa030e..7c32742f1b 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -18,6 +18,8 @@ use common::{ Group, Inventory, Poise, }, effect::Effect, + link::{Link, LinkHandle}, + mounting::Mounting, resources::{Time, TimeOfDay}, slowjob::SlowJobPool, uid::{Uid, UidAllocator}, @@ -110,6 +112,11 @@ pub trait StateExt { fn send_chat(&self, msg: comp::UnresolvedChatMsg); fn notify_players(&self, msg: ServerGeneral); fn notify_in_game_clients(&self, msg: ServerGeneral); + /// Create a new link between entities (see [`common::mounting`] for an + /// example). + fn link(&mut self, link: L) -> Result<(), L::Error>; + /// Maintain active links between entities + fn maintain_links(&mut self); /// Delete an entity, recording the deletion in [`DeletedEntities`] fn delete_entity_recorded( &mut self, @@ -271,7 +278,7 @@ impl StateExt for State { mountable: bool, ) -> EcsEntityBuilder { let body = comp::Body::Ship(ship); - let mut builder = self + let builder = self .ecs_mut() .create_entity_synced() .with(pos) @@ -294,7 +301,7 @@ impl StateExt for State { .with(comp::Combo::default()); if mountable { - builder = builder.with(comp::MountState::Unmounted); + // TODO: Re-add mounting check } builder } @@ -825,6 +832,36 @@ impl StateExt for State { } } + fn link(&mut self, link: L) -> Result<(), L::Error> { + let linker = LinkHandle::from_link(link); + + L::create(&linker, self.ecs().system_data())?; + + self.ecs_mut() + .entry::>>() + .or_insert_with(Vec::new) + .push(linker); + + Ok(()) + } + + fn maintain_links(&mut self) { + fn maintain_link(state: &State) { + if let Some(mut handles) = state.ecs().try_fetch_mut::>>() { + handles.retain(|link| { + if L::persist(link, state.ecs().system_data()) { + true + } else { + L::delete(link, state.ecs().system_data()); + false + } + }); + } + } + + maintain_link::(self); + } + fn delete_entity_recorded( &mut self, entity: EcsEntity, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 171b1b44bf..c6f91d81b5 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -21,12 +21,14 @@ use common::{ skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, AbilityInput, ActiveAbilities, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, Combo, ControlAction, ControlEvent, Controller, Energy, - Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, + Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, Ori, PhysicsState, Pos, Scale, SkillSet, Stats, UnresolvedChatMsg, UtteranceKind, Vel, }, consts::GRAVITY, effect::{BuffEffect, Effect}, event::{Emitter, EventBus, ServerEvent}, + link::Is, + mounting::Mount, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent}, @@ -152,7 +154,7 @@ pub struct ReadData<'a> { terrain: ReadExpect<'a, TerrainGrid>, alignments: ReadStorage<'a, Alignment>, bodies: ReadStorage<'a, Body>, - mount_states: ReadStorage<'a, MountState>, + is_mounts: ReadStorage<'a, Is>, time_of_day: Read<'a, TimeOfDay>, light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] @@ -174,6 +176,7 @@ const MAX_FLEE_DIST: f32 = 20.0; const AVG_FOLLOW_DIST: f32 = 6.0; const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0; const HEALING_ITEM_THRESHOLD: f32 = 0.5; +const IDLE_HEALING_ITEM_THRESHOLD: f32 = 0.999; const DEFAULT_ATTACK_RANGE: f32 = 2.0; const AWARENESS_INVESTIGATE_THRESHOLD: f32 = 1.0; const AWARENESS_DECREMENT_CONSTANT: f32 = 0.07; @@ -224,15 +227,9 @@ impl<'a> System<'a> for Sys { &mut controllers, read_data.light_emitter.maybe(), read_data.groups.maybe(), - read_data.mount_states.maybe(), + !&read_data.is_mounts, ) .par_join() - .filter(|(_, _, _, _, _, _, _, _, _, _, _, _, mount_state)| { - // Skip mounted entities - mount_state - .map(|ms| *ms == MountState::Unmounted) - .unwrap_or(true) - }) .for_each_init( || { prof_span!(guard, "agent rayon job"); @@ -675,7 +672,7 @@ impl<'a> AgentData<'a> { read_data: &ReadData, event_emitter: &mut Emitter<'_, ServerEvent>, ) { - if self.damage < HEALING_ITEM_THRESHOLD && self.heal_self(agent, controller) { + if self.damage < HEALING_ITEM_THRESHOLD && self.heal_self(agent, controller, false) { agent.action_state.timer = 0.01; return; } @@ -813,7 +810,7 @@ impl<'a> AgentData<'a> { } }; - if self.damage < HEALING_ITEM_THRESHOLD && self.heal_self(agent, controller) { + if self.damage < IDLE_HEALING_ITEM_THRESHOLD && self.heal_self(agent, controller, true) { agent.action_state.timer = 0.01; return; } @@ -1431,53 +1428,57 @@ impl<'a> AgentData<'a> { /// Attempt to consume a healing item, and return whether any healing items /// were queued. Callers should use this to implement a delay so that - /// the healing isn't interrupted. - fn heal_self(&self, _agent: &mut Agent, controller: &mut Controller) -> bool { + /// the healing isn't interrupted. If `relaxed` is `true`, we allow eating + /// food and prioritise healing. + fn heal_self(&self, _agent: &mut Agent, controller: &mut Controller, relaxed: bool) -> bool { let healing_value = |item: &Item| { let mut value = 0.0; - if let ItemKind::Consumable { - kind: ConsumableKind::Drink, - effects, - .. - } = &item.kind - { - for effect in effects.iter() { - use BuffKind::*; - match effect { - Effect::Health(HealthChange { amount, .. }) => { - value += *amount; - }, - Effect::Buff(BuffEffect { kind, data, .. }) - if matches!(kind, Regeneration | Saturation | Potion) => - { - value += - data.strength * data.duration.map_or(0.0, |d| d.as_secs() as f32); - }, - _ => {}, + if let ItemKind::Consumable { kind, effects, .. } = &item.kind { + if matches!(kind, ConsumableKind::Drink) + || (relaxed && matches!(kind, ConsumableKind::Food)) + { + for effect in effects.iter() { + use BuffKind::*; + match effect { + Effect::Health(HealthChange { amount, .. }) => { + value += *amount; + }, + Effect::Buff(BuffEffect { kind, data, .. }) + if matches!(kind, Regeneration | Saturation | Potion) => + { + value += data.strength + * data.duration.map_or(0.0, |d| d.as_secs() as f32); + }, + _ => {}, + } } } } value as i32 }; - let mut consumables: Vec<_> = self + let item = self .inventory .slots_with_id() .filter_map(|(id, slot)| match slot { Some(item) if healing_value(item) > 0 => Some((id, item)), _ => None, }) - .collect(); + .max_by_key(|(_, item)| { + if relaxed { + -healing_value(item) + } else { + healing_value(item) + } + }); - consumables.sort_by_key(|(_, item)| healing_value(item)); - - if let Some((id, _)) = consumables.last() { + if let Some((id, _)) = item { use comp::inventory::slot::Slot; controller .actions .push(ControlAction::InventoryAction(InventoryAction::Use( - Slot::Inventory(*id), + Slot::Inventory(id), ))); true } else { diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index 9810b30954..b15dbf8682 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -7,6 +7,8 @@ use crate::{ use common::{ calendar::Calendar, comp::{Collider, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel}, + link::Is, + mounting::Rider, outcome::Outcome, region::{Event as RegionEvent, RegionMap}, resources::{PlayerPhysicsSettings, TimeOfDay}, @@ -49,6 +51,7 @@ impl<'a> System<'a> for Sys { Write<'a, DeletedEntities>, Write<'a, Vec>, Read<'a, PlayerPhysicsSettings>, + ReadStorage<'a, Is>, ReadStorage<'a, Player>, TrackedComps<'a>, ReadTrackers<'a>, @@ -83,6 +86,7 @@ impl<'a> System<'a> for Sys { mut deleted_entities, mut outcomes, player_physics_settings, + is_rider, players, tracked_comps, trackers, @@ -248,7 +252,9 @@ impl<'a> System<'a> for Sys { // Don't send client physics updates about itself unless force update is // set or the client is subject to // server-authoritative physics - force_update.is_some() || player_physics_setting.server_authoritative() + force_update.is_some() + || player_physics_setting.server_authoritative() + || is_rider.get(entity).is_some() } else if matches!(collider, Some(Collider::Voxel { .. })) { // Things with a voxel collider (airships, etc.) need to have very // stable physics so we always send updated diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index e38dd9c6aa..f8be0042bb 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -7,6 +7,8 @@ use common::{ Vel, }, event::{EventBus, ServerEvent}, + link::Is, + mounting::Rider, resources::PlayerPhysicsSettings, terrain::TerrainGrid, vol::ReadVol, @@ -32,6 +34,7 @@ impl Sys { maybe_presence: &mut Option<&mut Presence>, terrain: &ReadExpect<'_, TerrainGrid>, can_build: &ReadStorage<'_, CanBuild>, + is_rider: &ReadStorage<'_, Is>, force_updates: &ReadStorage<'_, ForceUpdate>, skill_sets: &mut WriteStorage<'_, SkillSet>, healths: &ReadStorage<'_, Health>, @@ -119,6 +122,7 @@ impl Sys { if matches!(presence.kind, PresenceKind::Character(_)) && force_updates.get(entity).is_none() && healths.get(entity).map_or(true, |h| !h.is_dead) + && is_rider.get(entity).is_none() && player_physics_setting .as_ref() .map_or(true, |s| s.client_authoritative()) @@ -307,6 +311,7 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, TerrainGrid>, ReadStorage<'a, CanBuild>, ReadStorage<'a, ForceUpdate>, + ReadStorage<'a, Is>, WriteStorage<'a, SkillSet>, ReadStorage<'a, Health>, Write<'a, BlockChange>, @@ -336,6 +341,7 @@ impl<'a> System<'a> for Sys { terrain, can_build, force_updates, + is_rider, mut skill_sets, healths, mut block_changes, @@ -372,6 +378,7 @@ impl<'a> System<'a> for Sys { &mut maybe_presence.as_deref_mut(), &terrain, &can_build, + &is_rider, &force_updates, &mut skill_sets, &healths, diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 74f77376d0..bae8b38b6b 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,10 +2,12 @@ use common::{ comp::{ item::{tool::AbilityMap, MaterialStatManifest}, - ActiveAbilities, Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, - Combo, Density, Energy, Group, Health, Inventory, Item, LightEmitter, Mass, MountState, - Mounting, Ori, Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky, Vel, + ActiveAbilities, Alignment, 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, }, + link::Is, + mounting::{Mount, Rider}, uid::Uid, }; use common_ecs::{Job, Origin, Phase, System}; @@ -56,8 +58,8 @@ pub struct TrackedComps<'a> { pub light_emitter: ReadStorage<'a, LightEmitter>, pub item: ReadStorage<'a, Item>, pub scale: ReadStorage<'a, Scale>, - pub mounting: ReadStorage<'a, Mounting>, - pub mount_state: ReadStorage<'a, MountState>, + pub is_mount: ReadStorage<'a, Is>, + pub is_rider: ReadStorage<'a, Is>, pub group: ReadStorage<'a, Group>, pub mass: ReadStorage<'a, Mass>, pub density: ReadStorage<'a, Density>, @@ -67,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>, } @@ -137,11 +141,11 @@ impl<'a> TrackedComps<'a> { .get(entity) .copied() .map(|c| comps.push(c.into())); - self.mounting + self.is_mount .get(entity) .cloned() .map(|c| comps.push(c.into())); - self.mount_state + self.is_rider .get(entity) .cloned() .map(|c| comps.push(c.into())); @@ -178,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())); @@ -205,8 +213,8 @@ pub struct ReadTrackers<'a> { pub inventory: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, pub scale: ReadExpect<'a, UpdateTracker>, - pub mounting: ReadExpect<'a, UpdateTracker>, - pub mount_state: ReadExpect<'a, UpdateTracker>, + pub is_mount: ReadExpect<'a, UpdateTracker>>, + pub is_rider: ReadExpect<'a, UpdateTracker>>, pub group: ReadExpect<'a, UpdateTracker>, pub mass: ReadExpect<'a, UpdateTracker>, pub density: ReadExpect<'a, UpdateTracker>, @@ -215,6 +223,7 @@ pub struct ReadTrackers<'a> { pub character_state: ReadExpect<'a, UpdateTracker>, pub shockwave: ReadExpect<'a, UpdateTracker>, pub beam_segment: ReadExpect<'a, UpdateTracker>, + pub alignment: ReadExpect<'a, UpdateTracker>, } impl<'a> ReadTrackers<'a> { pub fn create_sync_packages( @@ -251,8 +260,8 @@ impl<'a> ReadTrackers<'a> { ) .with_component(&comps.uid, &*self.item, &comps.item, filter) .with_component(&comps.uid, &*self.scale, &comps.scale, filter) - .with_component(&comps.uid, &*self.mounting, &comps.mounting, filter) - .with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter) + .with_component(&comps.uid, &*self.is_mount, &comps.is_mount, filter) + .with_component(&comps.uid, &*self.is_rider, &comps.is_rider, filter) .with_component(&comps.uid, &*self.group, &comps.group, filter) .with_component(&comps.uid, &*self.mass, &comps.mass, filter) .with_component(&comps.uid, &*self.density, &comps.density, filter) @@ -266,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) } @@ -290,8 +300,8 @@ pub struct WriteTrackers<'a> { light_emitter: WriteExpect<'a, UpdateTracker>, item: WriteExpect<'a, UpdateTracker>, scale: WriteExpect<'a, UpdateTracker>, - mounting: WriteExpect<'a, UpdateTracker>, - mount_state: WriteExpect<'a, UpdateTracker>, + is_mounts: WriteExpect<'a, UpdateTracker>>, + is_riders: WriteExpect<'a, UpdateTracker>>, group: WriteExpect<'a, UpdateTracker>, mass: WriteExpect<'a, UpdateTracker>, density: WriteExpect<'a, UpdateTracker>, @@ -301,6 +311,7 @@ pub struct WriteTrackers<'a> { character_state: WriteExpect<'a, UpdateTracker>, shockwave: WriteExpect<'a, UpdateTracker>, beam: WriteExpect<'a, UpdateTracker>, + alignment: WriteExpect<'a, UpdateTracker>, } fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { @@ -323,8 +334,8 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.light_emitter.record_changes(&comps.light_emitter); trackers.item.record_changes(&comps.item); trackers.scale.record_changes(&comps.scale); - trackers.mounting.record_changes(&comps.mounting); - trackers.mount_state.record_changes(&comps.mount_state); + trackers.is_mounts.record_changes(&comps.is_mount); + trackers.is_riders.record_changes(&comps.is_rider); trackers.group.record_changes(&comps.group); trackers.mass.record_changes(&comps.mass); trackers.density.record_changes(&comps.density); @@ -336,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 { @@ -366,8 +378,8 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { log_counts!(light_emitter, "Light emitters"); log_counts!(item, "Items"); log_counts!(scale, "Scales"); - log_counts!(mounting, "Mountings"); - log_counts!(mount_state, "Mount States"); + log_counts!(is_mounts, "mounts"); + log_counts!(is_riders, "riders"); log_counts!(mass, "Masses"); log_counts!(mass, "Densities"); log_counts!(collider, "Colliders"); @@ -376,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"); */ } @@ -396,8 +409,8 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); - world.register_tracker::(); - world.register_tracker::(); + world.register_tracker::>(); + world.register_tracker::>(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); @@ -407,6 +420,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); + world.register_tracker::(); } /// Deleted entities grouped by region diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 84ac7ceee4..becf603e3b 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -31,10 +31,11 @@ tracy = ["profiling", "profiling/profile-with-tracy", "common-frontend/tracy", " tracy-memory = ["tracy"] # enables heap profiling with tracy plugins = ["client/plugins"] egui-ui = ["voxygen-egui", "egui", "egui_wgpu_backend", "egui_winit_platform"] +shaderc-from-source = ["shaderc/build-from-source"] # We don't ship egui with published release builds so a separate feature is required that excludes it. default-publish = ["singleplayer", "native-dialog", "plugins", "simd"] -default = ["default-publish", "egui-ui", "hot-reloading"] +default = ["default-publish", "egui-ui", "hot-reloading", "shaderc-from-source"] [dependencies] client = {package = "veloren-client", path = "../client"} diff --git a/voxygen/anim/src/biped_large/mod.rs b/voxygen/anim/src/biped_large/mod.rs index e712e3c272..76f7a3cefb 100644 --- a/voxygen/anim/src/biped_large/mod.rs +++ b/voxygen/anim/src/biped_large/mod.rs @@ -132,7 +132,7 @@ impl Skeleton for BipedLargeSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::BipedLarge(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/biped_small/mod.rs b/voxygen/anim/src/biped_small/mod.rs index 2c533da2d6..e827241a30 100644 --- a/voxygen/anim/src/biped_small/mod.rs +++ b/voxygen/anim/src/biped_small/mod.rs @@ -73,7 +73,7 @@ impl Skeleton for BipedSmallSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::BipedSmall(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/bird_large/mod.rs b/voxygen/anim/src/bird_large/mod.rs index 31569d6fd8..fd6b6198f0 100644 --- a/voxygen/anim/src/bird_large/mod.rs +++ b/voxygen/anim/src/bird_large/mod.rs @@ -101,7 +101,7 @@ impl Skeleton for BirdLargeSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::BirdLarge(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/bird_medium/mod.rs b/voxygen/anim/src/bird_medium/mod.rs index cb7a995b9b..dab98a6819 100644 --- a/voxygen/anim/src/bird_medium/mod.rs +++ b/voxygen/anim/src/bird_medium/mod.rs @@ -55,7 +55,7 @@ impl Skeleton for BirdMediumSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::BirdMedium(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/character/mod.rs b/voxygen/anim/src/character/mod.rs index d768e22488..d176eb326c 100644 --- a/voxygen/anim/src/character/mod.rs +++ b/voxygen/anim/src/character/mod.rs @@ -149,7 +149,7 @@ impl Skeleton for CharacterSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::Humanoid(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/character/mount.rs b/voxygen/anim/src/character/mount.rs index 8d5fc33535..db18826172 100644 --- a/voxygen/anim/src/character/mount.rs +++ b/voxygen/anim/src/character/mount.rs @@ -43,10 +43,6 @@ impl Animation for MountAnimation { ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let slow = (anim_time * 1.0).sin(); - let slowa = (anim_time * 1.0 + PI / 2.0).sin(); - let stop = (anim_time * 3.0).min(PI / 2.0).sin(); - let head_look = Vec2::new( (global_time * 0.05 + anim_time / 15.0) .floor() @@ -65,7 +61,7 @@ impl Animation for MountAnimation { let speed = (Vec2::::from(velocity).magnitude()).min(24.0); let canceler = (speed / 24.0).powf(0.6); let _x_tilt = avg_vel.z.atan2(avg_vel.xy().magnitude()) * canceler; - let _tilt = if ::vek::Vec2::new(ori, last_ori) + let tilt = if ::vek::Vec2::new(ori, last_ori) .map(|o| o.magnitude_squared()) .map(|m| m > 0.001 && m.is_finite()) .reduce_and() @@ -82,49 +78,41 @@ impl Animation for MountAnimation { next.hand_l.scale = Vec3::one() * 1.04; next.hand_r.scale = Vec3::one() * 1.04; next.back.scale = Vec3::one() * 1.02; + next.belt.scale = Vec3::one() * 1.02; next.hold.scale = Vec3::one() * 0.0; next.lantern.scale = Vec3::one() * 0.65; next.shoulder_l.scale = Vec3::one() * 1.1; next.shoulder_r.scale = Vec3::one() * 1.1; - next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1 + slow * 0.1 + stop * -0.8); - next.head.orientation = Quaternion::rotation_z(head_look.x + slow * 0.2 - slow * 0.1) - * Quaternion::rotation_x((0.4 + slowa * -0.1 + slow * 0.1 + head_look.y).abs()); + next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1); + next.head.orientation = Quaternion::rotation_z(head_look.x + tilt * -2.0) + * Quaternion::rotation_x((0.35 + head_look.y + tilt.abs() * 1.2).abs()); next.chest.position = Vec3::new(0.0, s_a.chest.0, s_a.chest.1); - next.chest.orientation = Quaternion::rotation_x(-0.6 + stop * 0.15); + next.chest.orientation = + Quaternion::rotation_x(-0.4 + tilt.abs() * -1.5) * Quaternion::rotation_y(tilt * 2.0); - next.belt.position = Vec3::new(0.0, s_a.belt.0 + stop * 1.2, s_a.belt.1); - next.belt.orientation = Quaternion::rotation_x(stop * 0.3); + next.belt.position = Vec3::new(0.0, s_a.belt.0 + 0.5, s_a.belt.1 + 0.5); + next.belt.orientation = Quaternion::rotation_x(0.2) * Quaternion::rotation_y(tilt * -0.5); next.back.position = Vec3::new(0.0, s_a.back.0, s_a.back.1); - next.shorts.position = Vec3::new(0.0, s_a.shorts.0 + stop * 2.5, s_a.shorts.1 + stop * 0.6); - next.shorts.orientation = Quaternion::rotation_x(stop * 0.6); + next.shorts.position = Vec3::new(0.0, s_a.shorts.0 + 1.0, s_a.shorts.1 + 1.0); + next.shorts.orientation = Quaternion::rotation_x(0.3) * Quaternion::rotation_y(tilt * -1.0); - next.hand_l.position = Vec3::new( - -s_a.hand.0 + 4.0, - s_a.hand.1 + slowa * 0.15 + stop * 8.0, - s_a.hand.2 + slow * 0.7 + stop * 4.0, - ); + next.hand_l.position = Vec3::new(-s_a.hand.0 + 3.0, s_a.hand.1 + 9.0, s_a.hand.2 + 4.0); next.hand_l.orientation = - Quaternion::rotation_x(PI / 2.0) * Quaternion::rotation_z(-PI / 2.0); + Quaternion::rotation_x(PI / 2.0) * Quaternion::rotation_z(-PI / 2.0 + 0.5); - next.hand_r.position = Vec3::new( - s_a.hand.0 - 4.0, - s_a.hand.1 + slowa * 0.15 + stop * 8.0, - s_a.hand.2 + slow * 0.7 + stop * 4.0, - ); + next.hand_r.position = Vec3::new(s_a.hand.0 - 3.0, s_a.hand.1 + 9.0, s_a.hand.2 + 4.0); next.hand_r.orientation = - Quaternion::rotation_x(PI / 2.0) * Quaternion::rotation_z(PI / 2.0); + Quaternion::rotation_x(PI / 2.0) * Quaternion::rotation_z(PI / 2.0 - 0.5); next.foot_l.position = Vec3::new(-s_a.foot.0 - 2.0, 4.0 + s_a.foot.1, s_a.foot.2); - next.foot_l.orientation = Quaternion::rotation_x(slow * 0.1 + stop * 0.4 + slow * 0.1) - * Quaternion::rotation_y(0.5); + next.foot_l.orientation = Quaternion::rotation_x(0.5) * Quaternion::rotation_y(0.5); next.foot_r.position = Vec3::new(s_a.foot.0 + 2.0, 4.0 + s_a.foot.1, s_a.foot.2); - next.foot_r.orientation = Quaternion::rotation_x(slowa * 0.1 + stop * 0.4 + slowa * 0.1) - * Quaternion::rotation_y(-0.5); + next.foot_r.orientation = Quaternion::rotation_x(0.5) * Quaternion::rotation_y(-0.5); next.shoulder_l.position = Vec3::new(-s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); next.shoulder_l.orientation = Quaternion::rotation_x(0.0); @@ -132,8 +120,6 @@ impl Animation for MountAnimation { next.shoulder_r.position = Vec3::new(s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); next.shoulder_r.orientation = Quaternion::rotation_x(0.0); - next.torso.position = Vec3::new(0.0, 0.0, stop * -1.76); - if skeleton.holding_lantern { next.hand_r.position = Vec3::new( s_a.hand.0 + 1.0 - head_look.x * 8.0, @@ -152,7 +138,9 @@ impl Animation for MountAnimation { next.lantern.orientation = next.hand_r.orientation.inverse() * Quaternion::rotation_x(fast * 0.1) * Quaternion::rotation_y(fast2 * 0.1); - } + } else { + next.lantern.position = Vec3::new(s_a.lantern.0, s_a.lantern.1, s_a.lantern.2); + }; next.glider.position = Vec3::new(0.0, 0.0, 10.0); next.glider.scale = Vec3::one() * 0.0; diff --git a/voxygen/anim/src/character/repeater.rs b/voxygen/anim/src/character/repeater.rs index 3da66f57a0..74e7bac3ac 100644 --- a/voxygen/anim/src/character/repeater.rs +++ b/voxygen/anim/src/character/repeater.rs @@ -5,6 +5,7 @@ use super::{ use common::{ comp::item::{Hands, ToolKind}, states::utils::{AbilityInfo, StageSection}, + util::Dir, }; use core::f32::consts::PI; @@ -16,6 +17,8 @@ impl Animation for RepeaterAnimation { Option, (Option, Option), Vec3, + Dir, + Vec3, f32, Option, ); @@ -27,7 +30,7 @@ impl Animation for RepeaterAnimation { #[cfg_attr(feature = "be-dyn-lib", export_name = "character_repeater")] fn update_skeleton_inner<'a>( skeleton: &Self::Skeleton, - (ability_info, hands, velocity, _global_time, stage_section): Self::Dependency<'a>, + (ability_info, hands, orientation,look_dir, velocity, _global_time, stage_section): Self::Dependency<'a>, anim_time: f32, rate: &mut f32, s_a: &SkeletonAttr, @@ -35,7 +38,9 @@ impl Animation for RepeaterAnimation { *rate = 1.0; let mut next = (*skeleton).clone(); let speed = Vec2::::from(velocity).magnitude(); - + let ori_angle = orientation.y.atan2(orientation.x); + let lookdir_angle = look_dir.y.atan2(look_dir.x); + let swivel = lookdir_angle - ori_angle; let (move1base, move2base, move3base, move4) = match stage_section { Some(StageSection::Movement) => (anim_time, 0.0, 0.0, 0.0), Some(StageSection::Buildup) => (1.0, anim_time, 0.0, 0.0), @@ -60,6 +65,10 @@ impl Animation for RepeaterAnimation { next.hold.position = Vec3::new(0.0, -1.0 + move3 * 2.0, -5.2); next.hold.orientation = Quaternion::rotation_x(-PI / 2.0) * Quaternion::rotation_z(0.0); next.hold.scale = Vec3::one() * (1.0); + + next.chest.orientation = Quaternion::rotation_z(swivel * 0.8); + next.torso.orientation = Quaternion::rotation_z(swivel * 0.2); + if speed < 0.5 { next.foot_l.position = Vec3::new( -s_a.foot.0 + move1 * -0.75, diff --git a/voxygen/anim/src/character/shoot.rs b/voxygen/anim/src/character/shoot.rs index a21f623f37..b29298afb2 100644 --- a/voxygen/anim/src/character/shoot.rs +++ b/voxygen/anim/src/character/shoot.rs @@ -46,7 +46,7 @@ impl Animation for ShootAnimation { s_a: &SkeletonAttr, ) -> Self::Skeleton { *rate = 1.0; - let speed = Vec2::::from(velocity).magnitude(); + let _speed = Vec2::::from(velocity).magnitude(); let mut next = (*skeleton).clone(); @@ -64,7 +64,9 @@ impl Animation for ShootAnimation { } else { 0.0 } * 1.3; - + let ori_angle = orientation.y.atan2(orientation.x); + let lookdir_angle = look_dir.y.atan2(look_dir.x); + let swivel = lookdir_angle - ori_angle; match ability_info.and_then(|a| a.tool) { Some(ToolKind::Staff) | Some(ToolKind::Sceptre) => { let (move1, move2, move3) = match stage_section { @@ -96,29 +98,14 @@ impl Animation for ShootAnimation { * Quaternion::rotation_z( s_a.stc.5 - (0.2 + move1 * -0.5 + move2 * 0.8) * (1.0 - move3), ); - next.chest.orientation = - Quaternion::rotation_z((move1 * 0.3 + move2 * 0.2) * (1.0 - move3)); + next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1); next.head.orientation = Quaternion::rotation_x(look_dir.z * 0.7) * Quaternion::rotation_z( tilt * -2.5 + (move1 * -0.2 + move2 * -0.4) * (1.0 - move3), ); - - if speed < 0.5 { - next.belt.orientation = - Quaternion::rotation_x(0.07) * Quaternion::rotation_z(0.0); - - next.shorts.orientation = - Quaternion::rotation_x(0.08) * Quaternion::rotation_z(0.0); - - next.foot_l.position = Vec3::new(-s_a.foot.0, s_a.foot.1 - 5.0, s_a.foot.2); - next.foot_l.orientation = Quaternion::rotation_x(-0.5); - - next.foot_r.position = Vec3::new(s_a.foot.0, s_a.foot.1 + 3.0, s_a.foot.2); - next.foot_r.orientation = - Quaternion::rotation_x(0.5) * Quaternion::rotation_z(0.3); - } else { - }; + next.chest.orientation = Quaternion::rotation_z(swivel * 0.8); + next.torso.orientation = Quaternion::rotation_z(swivel * 0.2); }, Some(ToolKind::Bow) => { let (_move1, move2, _move3) = match stage_section { @@ -148,28 +135,18 @@ impl Animation for ShootAnimation { s_a.bc.1 + 2.0 + (look_dir.z * -5.0).min(-2.0) + move2 * -1.0, s_a.bc.2 + 8.0 + (look_dir.z * 15.0).max(-8.0), ); - next.control.orientation = Quaternion::rotation_x(look_dir.z + move2 * -0.0) - * Quaternion::rotation_y(-look_dir.z + s_a.bc.4 - 1.25 + move2 * -0.0) + next.control.orientation = Quaternion::rotation_x(look_dir.z) + * Quaternion::rotation_y(-look_dir.z + s_a.bc.4 - 1.25) * Quaternion::rotation_z(s_a.bc.5 - 0.2 + move2 * -0.1); - next.chest.orientation = Quaternion::rotation_z(0.8 + move2 * 0.5); - next.head.position = Vec3::new(0.0 - 2.0, s_a.head.0, s_a.head.1); - next.head.orientation = Quaternion::rotation_x(look_dir.z * 0.7) - * Quaternion::rotation_z(tilt * -2.5 - 0.5 + (move2 * -0.2).sin()); - next.chest.orientation = Quaternion::rotation_z(0.8 + move2 * 0.2); - next.belt.orientation = Quaternion::rotation_z(move2 * 0.3); - next.shorts.orientation = Quaternion::rotation_z(move2 * 0.5); + next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1); + + next.head.orientation = + Quaternion::rotation_x(look_dir.z * 0.7) * Quaternion::rotation_z(tilt * -0.0); + next.chest.orientation = Quaternion::rotation_z(swivel * 0.8 + 0.8 + move2 * 0.5); + next.torso.orientation = Quaternion::rotation_z(swivel * 0.2); + next.shoulder_l.orientation = Quaternion::rotation_x(move2 * 0.5); - - if speed < 0.5 { - next.foot_l.position = Vec3::new(-s_a.foot.0, s_a.foot.1 - 5.0, s_a.foot.2); - next.foot_l.orientation = Quaternion::rotation_x(-0.5); - - next.foot_r.position = Vec3::new(s_a.foot.0, s_a.foot.1 + 3.0, s_a.foot.2); - next.foot_r.orientation = - Quaternion::rotation_x(0.5) * Quaternion::rotation_z(0.3); - } else { - }; }, _ => {}, } diff --git a/voxygen/anim/src/dragon/mod.rs b/voxygen/anim/src/dragon/mod.rs index 135ad304ac..7b82a7cc6a 100644 --- a/voxygen/anim/src/dragon/mod.rs +++ b/voxygen/anim/src/dragon/mod.rs @@ -76,7 +76,7 @@ impl Skeleton for DragonSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::Dragon(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/fish_medium/mod.rs b/voxygen/anim/src/fish_medium/mod.rs index 9675f3322c..80685b7ea3 100644 --- a/voxygen/anim/src/fish_medium/mod.rs +++ b/voxygen/anim/src/fish_medium/mod.rs @@ -55,7 +55,7 @@ impl Skeleton for FishMediumSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::FishMedium(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/fish_small/mod.rs b/voxygen/anim/src/fish_small/mod.rs index 8935974732..a24dca196c 100644 --- a/voxygen/anim/src/fish_small/mod.rs +++ b/voxygen/anim/src/fish_small/mod.rs @@ -46,7 +46,7 @@ impl Skeleton for FishSmallSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::FishSmall(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/golem/mod.rs b/voxygen/anim/src/golem/mod.rs index 1b3cc3bc97..e32f1360eb 100644 --- a/voxygen/anim/src/golem/mod.rs +++ b/voxygen/anim/src/golem/mod.rs @@ -78,7 +78,7 @@ impl Skeleton for GolemSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::Golem(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/object/mod.rs b/voxygen/anim/src/object/mod.rs index 2125edd791..7d4cbbb2aa 100644 --- a/voxygen/anim/src/object/mod.rs +++ b/voxygen/anim/src/object/mod.rs @@ -44,7 +44,7 @@ impl Skeleton for ObjectSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::Object(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/anim/src/quadruped_low/mod.rs b/voxygen/anim/src/quadruped_low/mod.rs index 46d32dc596..cd34de941a 100644 --- a/voxygen/anim/src/quadruped_low/mod.rs +++ b/voxygen/anim/src/quadruped_low/mod.rs @@ -33,6 +33,7 @@ skeleton_impls!(struct QuadrupedLowSkeleton { + foot_fr, + foot_bl, + foot_br, + mount, }); impl Skeleton for QuadrupedLowSkeleton { @@ -69,15 +70,32 @@ impl Skeleton for QuadrupedLowSkeleton { make_bone(chest_mat * Mat4::::from(self.foot_bl)), make_bone(chest_mat * Mat4::::from(self.foot_br)), ]; + //let (mount_bone_mat, mount_bone_ori) = (chest_mat, self.chest.orientation); + // Offset from the mounted bone's origin. + // Note: This could be its own bone if we need to animate it independently. + + // NOTE: We apply the ori from base_mat externally so we don't need to worry + // about it here for now. + + use comp::quadruped_low::Species::*; + let (mount_bone_mat, mount_bone_ori) = match (body.species, body.body_type) { + (Maneater, _) => ( + head_upper_mat, + self.chest.orientation * self.head_lower.orientation * self.head_upper.orientation, + ), + _ => (chest_mat, self.chest.orientation), + }; + let mount_position = (mount_bone_mat * Vec4::from_point(mount_point(&body))) + .homogenized() + .xyz(); + let mount_orientation = mount_bone_ori; + Offsets { lantern: None, - // TODO: see quadruped_medium for how to animate this mount_bone: Transform { - position: common::comp::Body::QuadrupedLow(body) - .mountee_offset() - .into_tuple() - .into(), - ..Default::default() + position: mount_position, + orientation: mount_orientation, + scale: Vec3::one(), }, } } @@ -311,3 +329,25 @@ impl<'a> From<&'a Body> for SkeletonAttr { } } } +fn mount_point(body: &Body) -> Vec3 { + use comp::quadruped_low::{BodyType::*, Species::*}; + match (body.species, body.body_type) { + (Crocodile, _) => (0.0, 4.5, -2.0), + (Alligator, _) => (0.0, 4.25, -2.0), + (Salamander, Male) => (0.0, 5.0, -1.0), + (Salamander, Female) => (0.0, 5.0, -1.0), + (Monitor, _) => (0.0, 2.0, -2.0), + (Asp, _) => (0.0, 2.0, 0.0), + (Tortoise, _) => (0.0, -7.0, -1.0), + (Rocksnapper, _) => (0.0, -7.0, 4.5), + (Pangolin, _) => (0.0, -6.5, -2.0), + (Maneater, _) => (0.0, 4.0, -11.5), + (Sandshark, _) => (0.0, -4.0, -2.0), + (Hakulaq, _) => (0.0, 4.0, -4.5), + (Lavadrake, _) => (0.0, 2.0, -2.5), + (Icedrake, _) => (0.0, -8.0, 2.5), + (Basilisk, _) => (0.0, -2.0, 2.0), + (Deadwood, _) => (0.0, -2.0, -3.0), + } + .into() +} diff --git a/voxygen/anim/src/quadruped_medium/jump.rs b/voxygen/anim/src/quadruped_medium/jump.rs index 850bdc5dd8..c00d8fbc45 100644 --- a/voxygen/anim/src/quadruped_medium/jump.rs +++ b/voxygen/anim/src/quadruped_medium/jump.rs @@ -6,7 +6,7 @@ use super::{ pub struct JumpAnimation; impl Animation for JumpAnimation { - type Dependency<'a> = f32; + type Dependency<'a> = (f32, Vec3, Vec3); type Skeleton = QuadrupedMediumSkeleton; #[cfg(feature = "use-dyn-lib")] @@ -15,7 +15,7 @@ impl Animation for JumpAnimation { #[cfg_attr(feature = "be-dyn-lib", export_name = "quadruped_medium_jump")] fn update_skeleton_inner<'a>( skeleton: &Self::Skeleton, - _global_time: Self::Dependency<'a>, + (_global_time, velocity, avg_vel): Self::Dependency<'a>, _anim_time: f32, _rate: &mut f32, s_a: &SkeletonAttr, @@ -33,38 +33,61 @@ impl Animation for JumpAnimation { next.foot_bl.scale = Vec3::one() * 0.96; next.foot_br.scale = Vec3::one() * 0.96; next.ears.scale = Vec3::one() * 1.02; + let speed = Vec2::::from(velocity).magnitude(); + let velocityalt = speed.max(3.0); + let normalize = velocityalt / 22.0; + + let x_tilt = (avg_vel.z.atan2(avg_vel.xy().magnitude()) * normalize).max(-0.28); + let x_tilt = if velocityalt < 3.5 { + x_tilt.abs() + } else { + x_tilt + }; next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1); + next.head.orientation = Quaternion::rotation_x(x_tilt * -0.5); next.neck.position = Vec3::new(0.0, s_a.neck.0, s_a.neck.1); + next.neck.orientation = Quaternion::rotation_x(x_tilt * -1.0); next.jaw.position = Vec3::new(0.0, s_a.jaw.0, s_a.jaw.1); next.jaw.orientation = Quaternion::rotation_x(0.0); next.tail.position = Vec3::new(0.0, s_a.tail.0, s_a.tail.1); + next.tail.orientation = Quaternion::rotation_x(-0.6 * normalize + x_tilt * 2.0); next.torso_front.position = Vec3::new(0.0, s_a.torso_front.0, s_a.torso_front.1); - next.torso_front.orientation = Quaternion::rotation_y(0.0); + next.torso_front.orientation = Quaternion::rotation_x(x_tilt * 3.5); next.torso_back.position = Vec3::new(0.0, s_a.torso_back.0, s_a.torso_back.1); + next.torso_back.orientation = Quaternion::rotation_x(x_tilt * -1.2); next.ears.position = Vec3::new(0.0, s_a.ears.0, s_a.ears.1); + next.ears.orientation = Quaternion::rotation_x(x_tilt * 1.5); next.leg_fl.position = Vec3::new(-s_a.leg_f.0, s_a.leg_f.1, s_a.leg_f.2); + next.leg_fl.orientation = Quaternion::rotation_x(1.2 * normalize + x_tilt.abs() * 0.8); next.leg_fr.position = Vec3::new(s_a.leg_f.0, s_a.leg_f.1, s_a.leg_f.2); + next.leg_fr.orientation = Quaternion::rotation_x(1.2 * normalize + x_tilt.abs() * 0.8); next.leg_bl.position = Vec3::new(-s_a.leg_b.0, s_a.leg_b.1, s_a.leg_b.2); + next.leg_bl.orientation = Quaternion::rotation_x(-0.8 * normalize + x_tilt * -0.8); next.leg_br.position = Vec3::new(s_a.leg_b.0, s_a.leg_b.1, s_a.leg_b.2); + next.leg_br.orientation = Quaternion::rotation_x(-0.8 * normalize + x_tilt * -0.8); next.foot_fl.position = Vec3::new(-s_a.feet_f.0, s_a.feet_f.1, s_a.feet_f.2); + next.foot_fl.orientation = Quaternion::rotation_x(-0.4 * normalize + x_tilt * -0.4); next.foot_fr.position = Vec3::new(s_a.feet_f.0, s_a.feet_f.1, s_a.feet_f.2); + next.foot_fr.orientation = Quaternion::rotation_x(-0.4 * normalize + x_tilt * -0.4); next.foot_bl.position = Vec3::new(-s_a.feet_b.0, s_a.feet_b.1, s_a.feet_b.2); + next.foot_bl.orientation = Quaternion::rotation_x(-0.4 * normalize + x_tilt * -0.4); next.foot_br.position = Vec3::new(s_a.feet_b.0, s_a.feet_b.1, s_a.feet_b.2); + next.foot_br.orientation = Quaternion::rotation_x(-0.4 * normalize + x_tilt * -0.4); next } diff --git a/voxygen/anim/src/quadruped_medium/mod.rs b/voxygen/anim/src/quadruped_medium/mod.rs index aeafe12ccb..a55e58b6e2 100644 --- a/voxygen/anim/src/quadruped_medium/mod.rs +++ b/voxygen/anim/src/quadruped_medium/mod.rs @@ -701,42 +701,42 @@ impl<'a> From<&'a Body> for SkeletonAttr { fn mount_point(body: &Body) -> Vec3 { use comp::quadruped_medium::{BodyType::*, Species::*}; match (body.species, body.body_type) { - (Grolgar, _) => (0.0, -6.0, 6.0), - (Saber, _) => (0.0, -12.0, 4.0), - (Tuskram, _) => (0.0, -17.0, 2.0), - (Lion, _) => (0.0, -8.0, 4.0), - (Tarasque, _) => (0.0, -6.0, 4.0), - (Tiger, _) => (0.0, -8.0, 4.0), - (Wolf, _) => (0.0, -7.0, 3.0), - (Frostfang, _) => (0.0, -3.0, 4.0), - (Mouflon, _) => (0.0, -8.0, 2.0), - (Catoblepas, _) => (0.0, -8.0, 2.0), - (Bonerattler, _) => (0.0, -1.0, 4.0), - (Deer, _) => (0.0, -9.0, 3.0), - (Hirdrasil, _) => (0.0, -11.0, 3.0), - (Roshwalr, _) => (0.0, -1.0, 7.0), - (Donkey, _) => (0.0, -5.0, 2.0), - (Camel, _) => (0.0, -13.0, 5.0), - (Zebra, _) => (0.0, -6.0, 3.0), - (Antelope, _) => (0.0, -8.0, 3.0), - (Kelpie, _) => (0.0, -6.0, 3.0), - (Horse, _) => (0.0, -8.0, 3.0), - (Barghest, _) => (0.0, -8.0, 5.0), - (Cattle, Male) => (0.0, -3.0, 8.0), - (Cattle, Female) => (0.0, -2.0, 6.0), - (Darkhound, _) => (0.0, -2.0, 3.0), - (Highland, _) => (0.0, -3.0, 8.0), - (Yak, _) => (0.0, -8.0, 9.0), - (Panda, _) => (0.0, -10.0, 5.0), - (Bear, _) => (0.0, -11.0, 6.0), - (Dreadhorn, _) => (0.0, 0.0, 10.0), - (Moose, _) => (0.0, -9.0, 6.0), - (Snowleopard, _) => (0.0, -9.0, 4.0), - (Mammoth, _) => (0.0, 5.0, 8.0), - (Ngoubou, _) => (0.0, -7.0, 6.0), - (Llama, _) => (0.0, -6.0, 5.0), - (Alpaca, _) => (0.0, -9.0, 3.0), - (Akhlut, _) => (0.0, -6.0, 4.0), + (Grolgar, _) => (0.0, -6.0, 3.0), + (Saber, _) => (0.0, -12.0, 1.0), + (Tuskram, _) => (0.0, -17.0, -1.0), + (Lion, _) => (0.0, -8.0, 1.0), + (Tarasque, _) => (0.0, -6.0, 1.0), + (Tiger, _) => (0.0, -8.0, 1.0), + (Wolf, _) => (0.0, -9.0, 0.0), + (Frostfang, _) => (0.0, -6.0, -1.0), + (Mouflon, _) => (0.0, -8.0, -1.0), + (Catoblepas, _) => (0.0, -8.0, -1.0), + (Bonerattler, _) => (0.0, -1.0, 1.0), + (Deer, _) => (0.0, -9.0, 0.0), + (Hirdrasil, _) => (0.0, -11.0, 0.0), + (Roshwalr, _) => (0.0, -1.0, 4.0), + (Donkey, _) => (0.0, -5.0, -1.0), + (Camel, _) => (0.0, -13.0, 2.0), + (Zebra, _) => (0.0, -6.0, 0.0), + (Antelope, _) => (0.0, -8.0, 0.0), + (Kelpie, _) => (0.0, -6.0, 0.0), + (Horse, _) => (0.0, -8.0, 0.0), + (Barghest, _) => (0.0, -8.0, 2.0), + (Cattle, Male) => (0.0, -3.0, 5.0), + (Cattle, Female) => (0.0, -2.0, 3.0), + (Darkhound, _) => (0.0, -2.0, 0.0), + (Highland, _) => (0.0, -3.0, 5.0), + (Yak, _) => (0.0, -8.0, 6.0), + (Panda, _) => (0.0, -10.0, 2.0), + (Bear, _) => (0.0, -11.0, 3.0), + (Dreadhorn, _) => (0.0, 0.0, 7.0), + (Moose, _) => (0.0, -9.0, 3.0), + (Snowleopard, _) => (0.0, -9.0, 1.0), + (Mammoth, _) => (0.0, 5.0, 5.0), + (Ngoubou, _) => (0.0, -7.0, 3.0), + (Llama, _) => (0.0, -6.0, 2.0), + (Alpaca, _) => (0.0, -9.0, 0.0), + (Akhlut, _) => (0.0, -6.0, 1.0), } .into() } diff --git a/voxygen/anim/src/quadruped_small/mod.rs b/voxygen/anim/src/quadruped_small/mod.rs index 578dcb955d..fb9e21ffec 100644 --- a/voxygen/anim/src/quadruped_small/mod.rs +++ b/voxygen/anim/src/quadruped_small/mod.rs @@ -25,6 +25,7 @@ skeleton_impls!(struct QuadrupedSmallSkeleton { + leg_bl, + leg_br, + tail, + mount, }); impl Skeleton for QuadrupedSmallSkeleton { @@ -45,9 +46,10 @@ impl Skeleton for QuadrupedSmallSkeleton { let chest_mat = base_mat * Mat4::scaling_3d(SkeletonAttr::from(&body).scaler / 11.0) * Mat4::::from(self.chest); + let head_mat = chest_mat * Mat4::::from(self.head); *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ - make_bone(chest_mat * Mat4::::from(self.head)), + make_bone(head_mat), make_bone(chest_mat), make_bone(chest_mat * Mat4::::from(self.leg_fl)), make_bone(chest_mat * Mat4::::from(self.leg_fr)), @@ -55,15 +57,22 @@ impl Skeleton for QuadrupedSmallSkeleton { make_bone(chest_mat * Mat4::::from(self.leg_br)), make_bone(chest_mat * Mat4::::from(self.tail)), ]; + use comp::quadruped_small::Species::*; + let (mount_bone_mat, mount_bone_ori) = match (body.species, body.body_type) { + (Dodarock, _) => (head_mat, self.chest.orientation * self.head.orientation), + _ => (chest_mat, self.chest.orientation), + }; + let mount_position = (mount_bone_mat * Vec4::from_point(mount_point(&body))) + .homogenized() + .xyz(); + let mount_orientation = mount_bone_ori; + Offsets { lantern: None, - // TODO: see quadruped_medium for how to animate this mount_bone: Transform { - position: common::comp::Body::QuadrupedSmall(body) - .mountee_offset() - .into_tuple() - .into(), - ..Default::default() + position: mount_position, + orientation: mount_orientation, + scale: Vec3::one(), }, } } @@ -392,3 +401,37 @@ impl<'a> From<&'a Body> for SkeletonAttr { } } } +fn mount_point(body: &Body) -> Vec3 { + use comp::quadruped_small::{BodyType::*, Species::*}; + match (body.species, body.body_type) { + (Pig, _) => (0.0, -2.0, -2.5), + (Fox, _) => (0.0, -4.0, -3.5), + (Sheep, _) => (0.0, -4.0, -3.5), + (Boar, _) => (0.0, -2.0, -3.5), + (Jackalope, _) => (0.0, -4.0, -3.5), + (Skunk, _) => (0.0, -4.0, -3.5), + (Cat, _) => (0.0, -5.0, -4.0), + (Batfox, _) => (0.0, -4.0, -3.0), + (Raccoon, _) => (0.0, -4.0, -2.5), + (Quokka, _) => (0.0, -3.0, -3.5), + (Dodarock, _) => (0.0, 0.0, 2.5), + (Holladon, _) => (0.0, -2.0, -2.5), + (Hyena, _) => (0.0, -4.0, -3.5), + (Rabbit, _) => (0.0, -4.0, -3.5), + (Truffler, _) => (0.0, -6.0, 6.5), + (Frog, _) => (0.0, -4.0, -4.5), + (Rat, _) => (0.0, -4.0, -4.5), + (Axolotl, _) => (0.0, -4.0, -4.5), + (Gecko, _) => (0.0, -4.0, -4.5), + (Turtle, _) => (0.0, -4.0, -4.5), + (Squirrel, _) => (0.0, -4.0, -4.5), + (Fungome, _) => (0.0, -4.0, -4.5), + (Porcupine, _) => (0.0, -4.0, -3.5), + (Beaver, _) => (0.0, -2.0, -3.5), + (Hare, Male) => (0.0, -4.0, -4.5), + (Hare, Female) => (0.0, -4.0, -4.5), + (Dog, _) => (0.0, -4.0, -2.5), + (Goat, _) => (0.0, -4.0, -3.5), + } + .into() +} diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs index 81d09e9a90..9c02b67178 100644 --- a/voxygen/anim/src/ship/mod.rs +++ b/voxygen/anim/src/ship/mod.rs @@ -47,7 +47,7 @@ impl Skeleton for ShipSkeleton { mount_bone: Transform { position: (base_mat * scale_mat).mul_point( common::comp::Body::Ship(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ), diff --git a/voxygen/anim/src/theropod/mod.rs b/voxygen/anim/src/theropod/mod.rs index b76b0652e9..21db915f25 100644 --- a/voxygen/anim/src/theropod/mod.rs +++ b/voxygen/anim/src/theropod/mod.rs @@ -79,7 +79,7 @@ impl Skeleton for TheropodSkeleton { // TODO: see quadruped_medium for how to animate this mount_bone: Transform { position: common::comp::Body::Theropod(body) - .mountee_offset() + .mount_offset() .into_tuple() .into(), ..Default::default() diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 2b03fd478e..175bcd88e0 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -57,7 +57,10 @@ use crate::{ game_input::GameInput, hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent}, render::UiDrawer, - scene::camera::{self, Camera}, + scene::{ + camera::{self, Camera}, + terrain::Interaction, + }, session::{ interactable::Interactable, settings_change::{Chat as ChatChange, Interface as InterfaceChange, SettingsChange}, @@ -83,6 +86,8 @@ use common::{ BuffData, BuffKind, Item, }, consts::MAX_PICKUP_RANGE, + link::Is, + mounting::Mount, outcome::Outcome, slowjob::SlowJobPool, terrain::{SpriteKind, TerrainChunk}, @@ -1140,6 +1145,8 @@ impl Hud { let entities = ecs.entities(); let me = client.entity(); let poises = ecs.read_storage::(); + let alignments = ecs.read_storage::(); + let is_mount = ecs.read_storage::>(); // Check if there was a persistence load error of the skillset, and if so // display a dialog prompt @@ -1664,31 +1671,33 @@ 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 text = if item.amount() > 1 { - format!("{} x {}", item.amount(), item.name()) - } else { - item.name().to_string() + let make_overitem = + |item: &Item, pos, distance, properties, fonts, interaction_options| { + let text = if item.amount() > 1 { + format!("{} x {}", item.amount(), item.name()) + } else { + item.name().to_string() + }; + + let quality = get_quality_col(item); + + // Item + overitem::Overitem::new( + text.into(), + quality, + distance, + fonts, + i18n, + &global_state.settings.controls, + properties, + pulse, + &global_state.window.key_layout, + interaction_options, + ) + .x_y(0.0, 100.0) + .position_ingame(pos) }; - let quality = get_quality_col(item); - - // Item - overitem::Overitem::new( - text.into(), - quality, - distance, - fonts, - i18n, - &global_state.settings.controls, - properties, - pulse, - &global_state.window.key_layout, - ) - .x_y(0.0, 100.0) - .position_ingame(pos) - }; - self.failed_block_pickups .retain(|_, t| pulse - *t < overitem::PICKUP_FAILED_FADE_OUT_TIME); self.failed_entity_pickups @@ -1714,12 +1723,13 @@ 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); } // Render overtime for an interactable block - if let Some(Interactable::Block(block, pos, _)) = interactable { + if let Some(Interactable::Block(block, pos, interaction)) = interactable { let overitem_id = overitem_walker.next( &mut self.ids.overitems, &mut ui_widgets.widget_id_generator(), @@ -1733,9 +1743,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 +1754,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 +1766,17 @@ impl Hud { pos.distance_squared(player_pos), overitem_properties, &self.fonts, + match interaction { + Interaction::Collect => { + vec![(GameInput::Interact, i18n.get("hud.collect").to_string())] + }, + Interaction::Craft(_) => { + vec![(GameInput::Interact, i18n.get("hud.use").to_string())] + }, + Interaction::Mine => { + vec![(GameInput::Primary, i18n.get("hud.mine").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 +1791,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 +1802,22 @@ 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 +1833,7 @@ impl Hud { &inventories, players.maybe(), poises.maybe(), + (alignments.maybe(), is_mount.maybe()), ) .join() .filter(|t| { @@ -1818,6 +1857,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 +1919,7 @@ impl Hud { (info.is_some() || bubble.is_some()).then(|| { ( + entity, pos, info, bubble, @@ -1889,6 +1930,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 +1955,32 @@ 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() + && !client.is_riding() + && 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) diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index 1c5c05db0f..d0b94fc028 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -4,18 +4,20 @@ 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 common::comp::{Buffs, Energy, Health, SpeechBubble, SpeechBubbleType}; use conrod_core::{ color, position::Align, - widget::{self, Image, Rectangle, Text}, + widget::{self, Image, Rectangle, RoundedRectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; use i18n::Localization; +use keyboard_keynames::key_layout::KeyLayout; const MAX_BUBBLE_WIDTH: f64 = 250.0; widget_ids! { @@ -53,6 +55,10 @@ widget_ids! { buffs_align, buffs[], buff_timers[], + + // Interaction hints + interaction_hints, + interaction_hints_bg, } } @@ -84,14 +90,18 @@ 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, + interaction_options: Vec<(GameInput, String)>, #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Overhead<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( info: Option>, bubble: Option<&'a SpeechBubble>, @@ -99,8 +109,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, + interaction_options: Vec<(GameInput, String)>, ) -> Self { Self { info, @@ -109,8 +122,11 @@ impl<'a> Overhead<'a> { settings, pulse, i18n, + controls, imgs, fonts, + key_layout, + interaction_options, common: widget::CommonBuilder::default(), } } @@ -157,6 +173,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 +465,68 @@ 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::>() + .join("\n"); + + let scale = 30.0; + let btn_rect_size = scale * 0.8; + let btn_font_size = scale * 0.6; + let btn_radius = btn_rect_size / 5.0; + let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8); + + 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 { diff --git a/voxygen/src/hud/overitem.rs b/voxygen/src/hud/overitem.rs index 7522e04f06..968405a783 100644 --- a/voxygen/src/hud/overitem.rs +++ b/voxygen/src/hud/overitem.rs @@ -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, Sizeable, Widget, WidgetCommon, }; 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, + 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, + 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::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,50 @@ 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::>() + .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 + 2.0) - .parent(id) - .set(state.ids.btn, ui); + .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_bg, ui); } if let Some(time) = self.properties.pickup_failed_pulse { //should never exceed 1.0, but just in case diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 7208365b6c..46bfce12a0 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -33,8 +33,10 @@ use common::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, ToolKind}, Body, CharacterState, Collider, Controller, Health, Inventory, Item, Last, LightAnimation, - LightEmitter, Mounting, Ori, PhysicsState, PoiseState, Pos, Scale, Vel, + LightEmitter, Ori, PhysicsState, PoiseState, Pos, Scale, Vel, }, + link::Is, + mounting::Rider, resources::{DeltaTime, Time}, states::{equipping, idle, utils::StageSection, wielding}, terrain::TerrainChunk, @@ -458,9 +460,11 @@ impl FigureMgr { } let dt = ecs.fetch::().0; let updater = ecs.read_resource::(); - for (entity, light_emitter_opt, body, mut light_anim) in ( + for (entity, light_emitter_opt, interpolated, pos, body, mut light_anim) in ( &ecs.entities(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), + &ecs.read_storage::(), ecs.read_storage::().maybe(), &mut ecs.write_storage::(), ) @@ -483,7 +487,18 @@ impl FigureMgr { }; if let Some(lantern_offset) = body .and_then(|body| self.states.get_mut(body, &entity)) - .and_then(|state| state.lantern_offset) + .and_then(|state| { + // Calculate the correct lantern position + let pos = anim::vek::Vec3::from( + interpolated.map(|i| i.pos).unwrap_or(pos.0).into_array(), + ); + Some( + state.mount_world_pos + + state.mount_transform.orientation + * anim::vek::Vec3::from(state.lantern_offset?.into_array()) + - pos, + ) + }) { light_anim.offset = vek::Vec3::from(lantern_offset); } else if let Some(body) = body { @@ -626,7 +641,7 @@ impl FigureMgr { inventory, item, light_emitter, - mountings, + is_rider, collider, ), ) in ( @@ -644,7 +659,7 @@ impl FigureMgr { ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), + ecs.read_storage::>().maybe(), ecs.read_storage::().maybe(), ) .join() @@ -731,7 +746,9 @@ impl FigureMgr { let (in_frustum, lpindex) = if let Some(ref mut meta) = state { let (in_frustum, lpindex) = BoundingSphere::new(pos.0.into_array(), radius) .coherent_test_against_frustum(frustum, meta.lpindex); - let in_frustum = in_frustum || matches!(body, Body::Ship(_)); + let in_frustum = in_frustum + || matches!(body, Body::Ship(_)) + || pos.0.distance_squared(focus_pos) < 32.0f32.powi(2); meta.visible = in_frustum; meta.lpindex = lpindex; if in_frustum { @@ -791,12 +808,12 @@ impl FigureMgr { let hands = (active_tool_hand, second_tool_hand); - // If a mountee exists, get its animated mounting transform and its position + // If a mount exists, get its animated mounting transform and its position let mount_transform_pos = (|| -> Option<_> { - let Mounting(entity) = mountings?; - let entity = uid_allocator.retrieve_entity_internal((*entity).into())?; - let body = *bodies.get(entity)?; - let meta = self.states.get_mut(&body, &entity)?; + let mount = is_rider?.mount; + let mount = uid_allocator.retrieve_entity_internal(mount.into())?; + let body = *bodies.get(mount)?; + let meta = self.states.get_mut(&body, &mount)?; Some((meta.mount_transform, meta.mount_world_pos)) })(); @@ -869,7 +886,7 @@ impl FigureMgr { physics.on_ground.is_some(), rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid().is_some(), // In water - mountings.is_some(), + is_rider.is_some(), ) { // Standing (true, false, false, false) => { @@ -1140,6 +1157,8 @@ impl FigureMgr { ( Some(s.static_data.ability_info), hands, + ori * anim::vek::Vec3::::unit_y(), + look_dir, rel_vel, time, Some(s.stage_section), @@ -1779,9 +1798,17 @@ impl FigureMgr { skeleton_attr, ), // In air - (false, _, false) => anim::quadruped_small::JumpAnimation::update_skeleton( + (false, _, false) => anim::quadruped_small::RunAnimation::update_skeleton( &QuadrupedSmallSkeleton::default(), - (rel_vel.magnitude(), time), + ( + rel_vel.magnitude(), + // TODO: Update to use the quaternion. + ori * anim::vek::Vec3::::unit_y(), + state.last_ori * anim::vek::Vec3::::unit_y(), + time, + rel_avg_vel, + state.acc_vel, + ), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1972,7 +1999,7 @@ impl FigureMgr { (false, _, false) => { anim::quadruped_medium::JumpAnimation::update_skeleton( &QuadrupedMediumSkeleton::default(), - time, + (time, rel_vel, rel_avg_vel), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -2282,9 +2309,17 @@ impl FigureMgr { skeleton_attr, ), // In air - (false, _, false) => anim::quadruped_low::JumpAnimation::update_skeleton( + (false, _, false) => anim::quadruped_low::RunAnimation::update_skeleton( &QuadrupedLowSkeleton::default(), - (rel_vel.magnitude(), time), + ( + rel_vel.magnitude(), + // TODO: Update to use the quaternion. + ori * anim::vek::Vec3::::unit_y(), + state.last_ori * anim::vek::Vec3::::unit_y(), + time, + rel_avg_vel, + state.acc_vel, + ), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -5519,9 +5554,9 @@ impl FigureColLights { pub struct FigureStateMeta { lantern_offset: Option>, - // Animation to be applied to mounter of this entity + // Animation to be applied to rider of this entity mount_transform: anim::vek::Transform, - // Contains the position of this figure or if it is a mounter it will contain the mountee's + // Contains the position of this figure or if it is a rider it will contain the mount's // mount_world_pos // Unlike the interpolated position stored in the ecs this will be propagated along // mount chains @@ -5635,7 +5670,7 @@ impl FigureState { model: Option<&FigureModelEntry>, // TODO: there is the potential to drop the optional body from the common params and just // use this one but we need to add a function to the skelton trait or something in order to - // get the mounter offset + // get the rider offset skel_body: S::Body, ) { // NOTE: As long as update() always gets called after get_or_create_model(), and @@ -5672,26 +5707,26 @@ impl FigureState { let scale_mat = anim::vek::Mat4::scaling_3d(anim::vek::Vec3::from(*scale)); if let Some((transform, _)) = *mount_transform_pos { // Note: if we had a way to compute a "default" transform of the bones then in - // the animations we could make use of the mountee_offset from common by - // computing what the offset of the mounter is from the mounted - // bone in its default position when the mounter has the mount + // the animations we could make use of the mount_offset from common by + // computing what the offset of the rider is from the mounted + // bone in its default position when the rider has the mount // offset in common applied to it. Since we don't have this // right now we instead need to recreate the same effect in the // animations and keep it in sync. // - // Component of mounting offset specific to the mounter. - let mounter_offset = anim::vek::Mat4::::translation_3d( - body.map_or_else(Vec3::zero, |b| b.mounter_offset()), + // Component of mounting offset specific to the rider. + let rider_offset = anim::vek::Mat4::::translation_3d( + body.map_or_else(Vec3::zero, |b| b.rider_offset()), ); // NOTE: It is kind of a hack to use this entity's ori here if it is // mounted on another but this happens to match the ori of the - // mountee so it works, change this if it causes jankiness in the future. + // mount so it works, change this if it causes jankiness in the future. let transform = anim::vek::Transform { orientation: *ori * transform.orientation, ..transform }; - anim::vek::Mat4::from(transform) * mounter_offset * scale_mat + anim::vek::Mat4::from(transform) * rider_offset * scale_mat } else { let ori_mat = anim::vek::Mat4::from(*ori); ori_mat * scale_mat diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index 7bc085167d..17d5acb77f 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -9,6 +9,7 @@ use vek::*; pub enum Interaction { Collect, Craft(CraftingTab), + Mine, } #[derive(Default)] diff --git a/voxygen/src/session/interactable.rs b/voxygen/src/session/interactable.rs index 39a8d5b926..b11270d6e3 100644 --- a/voxygen/src/session/interactable.rs +++ b/voxygen/src/session/interactable.rs @@ -22,7 +22,7 @@ use crate::scene::{terrain::Interaction, Scene}; // enum since they don't use the interaction key #[derive(Clone, Copy, Debug)] pub enum Interactable { - Block(Block, Vec3, Option), + Block(Block, Vec3, Interaction), Entity(specs::Entity), } @@ -81,9 +81,8 @@ pub(super) fn select_interactable( .or_else(|| { collect_target.and_then(|t| { if Some(t.distance) == nearest_dist { - get_block(client, t).map(|b| { - Interactable::Block(b, t.position_int(), Some(Interaction::Collect)) - }) + get_block(client, t) + .map(|b| Interactable::Block(b, t.position_int(), Interaction::Collect)) } else { None } @@ -99,7 +98,7 @@ pub(super) fn select_interactable( // elements (e.g. minerals). The mineable weakrock are used // in the terrain selected_pos, but is not an interactable. if b.mine_tool().is_some() && b.is_air() { - Some(Interactable::Block(b, t.position_int(), None)) + Some(Interactable::Block(b, t.position_int(), Interaction::Mine)) } else { None } @@ -200,7 +199,7 @@ pub(super) fn select_interactable( .get(block_pos) .ok() .copied() - .map(|b| Interactable::Block(b, block_pos, Some(*interaction))) + .map(|b| Interactable::Block(b, block_pos, *interaction)) }) .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 359a90e01c..143946cdfc 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -22,6 +22,8 @@ use common::{ ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, Stats, UtteranceKind, Vel, }, consts::MAX_MOUNT_RANGE, + link::Is, + mounting::Mount, outcome::Outcome, terrain::{Block, BlockKind}, trade::TradeResult, @@ -670,7 +672,7 @@ impl PlayState for SessionState { }, GameInput::Mount if state => { let mut client = self.client.borrow_mut(); - if client.is_mounted() { + if client.is_riding() { client.unmount(); } else { let player_pos = client @@ -683,17 +685,14 @@ impl PlayState for SessionState { let closest_mountable_entity = ( &client.state().ecs().entities(), &client.state().ecs().read_storage::(), - &client - .state() - .ecs() - .read_storage::(), + // TODO: More cleverly filter by things that can actually be mounted + !&client.state().ecs().read_storage::>(), + client.state().ecs().read_storage::().maybe(), ) .join() - .filter(|(entity, _, mount_state)| { - *entity != client.entity() - && **mount_state == comp::MountState::Unmounted - }) - .map(|(entity, pos, _)| { + .filter(|(entity, _, _, _)| *entity != client.entity()) + .filter(|(_, _, _, alignment)| matches!(alignment, Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid())) + .map(|(entity, pos, _, _)| { (entity, player_pos.0.distance_squared(pos.0)) }) .filter(|(_, dist_sqr)| { @@ -714,18 +713,18 @@ impl PlayState for SessionState { match interactable { Interactable::Block(block, pos, interaction) => { match interaction { - Some(Interaction::Collect) => { + Interaction::Collect => { if block.is_collectible() { client.collect_block(pos); } }, - Some(Interaction::Craft(tab)) => { + Interaction::Craft(tab) => { self.hud.show.open_crafting_tab( tab, block.get_sprite().map(|s| (pos, s)), ) }, - _ => {}, + Interaction::Mine => {}, } }, Interactable::Entity(entity) => {