Merge branch 'zesterer/mount-everest' into 'master'

Mount Everest

See merge request veloren/veloren!3101
This commit is contained in:
Joshua Barretto 2022-01-17 13:21:38 +00:00
commit 9c565dadc5
57 changed files with 1211 additions and 533 deletions

View File

@ -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

View File

@ -26,7 +26,7 @@
"hud.tutorial_elements": r#"Crafting"#,
"hud.temp_quest_headline": r#"Greetings Traveller!"#,
"hud.temp_quest_text": r#"To begin your journey you could start looking through this village and gather some supplies.
"hud.temp_quest_text": r#"To begin your journey you could start looking through this village and gather some supplies.
You are welcome to take whatever you need on your journey!
@ -46,6 +46,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",
},

View File

@ -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::<comp::Mounting>()
.read_storage::<Is<Rider>>()
.get(self.entity())
.is_some()
}

View File

@ -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<Mount>),
IsRider(Is<Rider>),
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<T> for EcsCompPhantom
@ -63,8 +69,8 @@ sum_type! {
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
Group(PhantomData<comp::Group>),
MountState(PhantomData<comp::MountState>),
Mounting(PhantomData<comp::Mounting>),
IsMount(PhantomData<Is<Mount>>),
IsRider(PhantomData<Is<Rider>>),
Mass(PhantomData<comp::Mass>),
Density(PhantomData<comp::Density>),
Collider(PhantomData<comp::Collider>),
@ -75,6 +81,7 @@ sum_type! {
Ori(PhantomData<comp::Ori>),
Shockwave(PhantomData<comp::Shockwave>),
BeamSegment(PhantomData<comp::BeamSegment>),
Alignment(PhantomData<comp::Alignment>),
}
}
impl sync::CompPacket for EcsCompPacket {
@ -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::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world),
EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
EcsCompPhantom::IsMount(_) => sync::handle_remove::<Is<Mount>>(entity, world),
EcsCompPhantom::IsRider(_) => sync::handle_remove::<Is<Rider>>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
EcsCompPhantom::Density(_) => sync::handle_remove::<comp::Density>(entity, world),
EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world),
@ -206,7 +215,10 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::Vel(_) => sync::handle_interp_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_interp_remove::<comp::Ori>(entity, world),
EcsCompPhantom::Shockwave(_) => sync::handle_remove::<comp::Shockwave>(entity, world),
EcsCompPhantom::BeamSegment(_) => sync::handle_remove::<comp::Ori>(entity, world),
EcsCompPhantom::BeamSegment(_) => {
sync::handle_remove::<comp::BeamSegment>(entity, world)
},
EcsCompPhantom::Alignment(_) => sync::handle_remove::<comp::Alignment>(entity, world),
}
}
}

View File

@ -8,8 +8,8 @@ use crate::{
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid,
};
use serde::Deserialize;
use specs::{Component, Entity as EcsEntity};
use serde::{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<Self>;
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
bitflags::bitflags! {

View File

@ -730,45 +730,45 @@ impl Body {
)
}
/// Component of the mounting offset specific to the mountee
pub fn mountee_offset(&self) -> Vec3<f32> {
/// Component of the mounting offset specific to the mount
pub fn mount_offset(&self) -> Vec3<f32> {
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<f32> {
/// Component of the mounting offset specific to the rider
pub fn rider_offset(&self) -> Vec3<f32> {
match self {
Body::Humanoid(_) => [0.0, 0.0, 0.0],
_ => [0.0, 0.0, 0.0],

View File

@ -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!(

View File

@ -10,7 +10,7 @@ use crate::{
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs::Component;
use specs_idvs::IdvStorage;
use std::collections::BTreeMap;
use vek::*;
@ -284,20 +284,3 @@ impl Controller {
impl Component for Controller {
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum MountState {
Unmounted,
MountedBy(Uid),
}
impl Component for MountState {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Mounting(pub Uid);
impl Component for Mounting {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -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

View File

@ -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,

View File

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

View File

@ -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;

84
common/src/link.rs Normal file
View File

@ -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<Self>, data: Self::CreateData<'_>) -> Result<(), Self::Error>;
type PersistData<'a>: SystemData<'a>;
fn persist(this: &LinkHandle<Self>, data: Self::PersistData<'_>) -> bool;
type DeleteData<'a>: SystemData<'a>;
fn delete(this: &LinkHandle<Self>, data: Self::DeleteData<'_>);
}
pub trait Role {
type Link: Link;
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Is<R: Role> {
#[serde(bound(serialize = "R::Link: Serialize"))]
#[serde(bound(deserialize = "R::Link: Deserialize<'de>"))]
link: LinkHandle<R::Link>,
}
impl<R: Role> Is<R> {
pub fn delete(&self, data: <R::Link as Link>::DeleteData<'_>) {
R::Link::delete(&self.link, data)
}
}
impl<R: Role> Clone for Is<R> {
fn clone(&self) -> Self {
Self {
link: self.link.clone(),
}
}
}
impl<R: Role> Deref for Is<R> {
type Target = R::Link;
fn deref(&self) -> &Self::Target { &self.link }
}
impl<R: Role + 'static> Component for Is<R>
where
R::Link: Send + Sync + 'static,
{
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LinkHandle<L: Link> {
link: Arc<L>,
}
impl<L: Link> Clone for LinkHandle<L> {
fn clone(&self) -> Self {
Self {
link: Arc::clone(&self.link),
}
}
}
impl<L: Link> LinkHandle<L> {
pub fn from_link(link: L) -> Self {
Self {
link: Arc::new(link),
}
}
pub fn make_role<R: Role<Link = L>>(&self) -> Is<R> { Is { link: self.clone() } }
}
impl<L: Link> Deref for LinkHandle<L> {
type Target = L;
fn deref(&self) -> &Self::Target { &self.link }
}

144
common/src/mounting.rs Normal file
View File

@ -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<Mount>>,
WriteStorage<'a, Is<Rider>>,
);
type DeleteData<'a> = (
Read<'a, UidAllocator>,
WriteStorage<'a, Is<Mount>>,
WriteStorage<'a, Is<Rider>>,
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<Mount>>,
ReadStorage<'a, Is<Rider>>,
);
fn create(
this: &LinkHandle<Self>,
(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<Self>,
(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<Self>,
(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);
});
}
}

View File

@ -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

View File

@ -162,18 +162,22 @@ impl TerrainGrid {
self.try_find_space(pos).unwrap_or(pos)
}
pub fn is_space(&self, pos: Vec3<i32>) -> 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<i32>) -> Option<Vec3<i32>> {
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)
})
}
}

View File

@ -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::<comp::LightEmitter>();
ecs.register::<comp::Item>();
ecs.register::<comp::Scale>();
ecs.register::<comp::Mounting>();
ecs.register::<comp::MountState>();
ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>();
ecs.register::<comp::Mass>();
ecs.register::<comp::Density>();
ecs.register::<comp::Collider>();
@ -153,6 +155,7 @@ impl State {
ecs.register::<comp::Shockwave>();
ecs.register::<comp::ShockwaveHitEntities>();
ecs.register::<comp::BeamSegment>();
ecs.register::<comp::Alignment>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();
@ -182,7 +185,6 @@ impl State {
ecs.register::<comp::Last<comp::Pos>>();
ecs.register::<comp::Last<comp::Vel>>();
ecs.register::<comp::Last<comp::Ori>>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Agent>();
ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>();

View File

@ -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<Rider>>,
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;
}

View File

@ -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;

View File

@ -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<Mount>>,
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);
}
}
}

View File

@ -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<Rider>>,
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::<Vec<_>>()
{
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;

View File

@ -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<T>(
descriptor: &str,
f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
) -> CmdResult<T> {
let mut pos_storage = server.state.ecs_mut().write_storage::<comp::Pos>();
pos_storage
let entity = server
.state
.ecs()
.read_storage::<Is<Rider>>()
.get(entity)
.and_then(|is_rider| {
server
.state
.ecs()
.read_resource::<UidAllocator>()
.retrieve_entity_internal(is_rider.mount.into())
})
.unwrap_or(entity);
let res = server
.state
.ecs()
.write_storage::<comp::Pos>()
.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::<comp::ForceUpdate>()
.insert(entity, comp::ForceUpdate);
}
res
}
fn insert_or_replace_component<C: specs::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(

View File

@ -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::<comp::Mounting>()
.get(mounter)
.is_none()
{
let not_mounting_yet = matches!(
state.ecs().read_storage::<comp::MountState>().get(mountee),
Some(comp::MountState::Unmounted)
);
if state.ecs().read_storage::<Is<Rider>>().get(rider).is_none() {
let not_mounting_yet = state.ecs().read_storage::<Is<Mount>>().get(mount).is_none();
let within_range = || {
let positions = state.ecs().read_storage::<comp::Pos>();
within_mounting_range(positions.get(mounter), positions.get(mountee))
within_mounting_range(positions.get(rider), positions.get(mount))
};
let healths = state.ecs().read_storage::<comp::Health>();
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::<Uid>();
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::<comp::Alignment>()
.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::<comp::Mounting>()
.get(mounter)
.and_then(|mountee| state.ecs().entity_from_uid(mountee.0.into()));
if let Some(mountee_entity) = mountee_entity {
state
.ecs()
.write_storage::<comp::MountState>()
.get_mut(mountee_entity)
.map(|mut ms| *ms = comp::MountState::Unmounted);
}
state.delete_component::<comp::Mounting>(mounter);
state.ecs().write_storage::<Is<Rider>>().remove(rider);
}
/// FIXME: This code is dangerous and needs to be refactored. We can't just

View File

@ -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());

View File

@ -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::<Vec2<i32>>::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

View File

@ -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<L: 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<L: 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::<Vec<LinkHandle<L>>>()
.or_insert_with(Vec::new)
.push(linker);
Ok(())
}
fn maintain_links(&mut self) {
fn maintain_link<L: Link>(state: &State) {
if let Some(mut handles) = state.ecs().try_fetch_mut::<Vec<LinkHandle<L>>>() {
handles.retain(|link| {
if L::persist(link, state.ecs().system_data()) {
true
} else {
L::delete(link, state.ecs().system_data());
false
}
});
}
}
maintain_link::<Mounting>(self);
}
fn delete_entity_recorded(
&mut self,
entity: EcsEntity,

View File

@ -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<Mount>>,
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 {

View File

@ -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<Outcome>>,
Read<'a, PlayerPhysicsSettings>,
ReadStorage<'a, Is<Rider>>,
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

View File

@ -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<Rider>>,
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<Rider>>,
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,

View File

@ -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<Mount>>,
pub is_rider: ReadStorage<'a, Is<Rider>>,
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<Inventory>>,
pub item: ReadExpect<'a, UpdateTracker<Item>>,
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
pub is_mount: ReadExpect<'a, UpdateTracker<Is<Mount>>>,
pub is_rider: ReadExpect<'a, UpdateTracker<Is<Rider>>>,
pub group: ReadExpect<'a, UpdateTracker<Group>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub density: ReadExpect<'a, UpdateTracker<Density>>,
@ -215,6 +223,7 @@ pub struct ReadTrackers<'a> {
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub shockwave: ReadExpect<'a, UpdateTracker<Shockwave>>,
pub beam_segment: ReadExpect<'a, UpdateTracker<BeamSegment>>,
pub alignment: ReadExpect<'a, UpdateTracker<Alignment>>,
}
impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages(
@ -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<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>,
scale: WriteExpect<'a, UpdateTracker<Scale>>,
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
is_mounts: WriteExpect<'a, UpdateTracker<Is<Mount>>>,
is_riders: WriteExpect<'a, UpdateTracker<Is<Rider>>>,
group: WriteExpect<'a, UpdateTracker<Group>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>,
density: WriteExpect<'a, UpdateTracker<Density>>,
@ -301,6 +311,7 @@ pub struct WriteTrackers<'a> {
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
shockwave: WriteExpect<'a, UpdateTracker<Shockwave>>,
beam: WriteExpect<'a, UpdateTracker<BeamSegment>>,
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
}
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@ -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::<LightEmitter>();
world.register_tracker::<Item>();
world.register_tracker::<Scale>();
world.register_tracker::<Mounting>();
world.register_tracker::<MountState>();
world.register_tracker::<Is<Mount>>();
world.register_tracker::<Is<Rider>>();
world.register_tracker::<Group>();
world.register_tracker::<Mass>();
world.register_tracker::<Density>();
@ -407,6 +420,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<CharacterState>();
world.register_tracker::<Shockwave>();
world.register_tracker::<BeamSegment>();
world.register_tracker::<Alignment>();
}
/// Deleted entities grouped by region

View File

@ -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"}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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::<f32>::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;

View File

@ -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<AbilityInfo>,
(Option<Hands>, Option<Hands>),
Vec3<f32>,
Dir,
Vec3<f32>,
f32,
Option<StageSection>,
);
@ -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::<f32>::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,

View File

@ -46,7 +46,7 @@ impl Animation for ShootAnimation {
s_a: &SkeletonAttr,
) -> Self::Skeleton {
*rate = 1.0;
let speed = Vec2::<f32>::from(velocity).magnitude();
let _speed = Vec2::<f32>::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 {
};
},
_ => {},
}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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::<f32>::from(self.foot_bl)),
make_bone(chest_mat * Mat4::<f32>::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<f32> {
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()
}

View File

@ -6,7 +6,7 @@ use super::{
pub struct JumpAnimation;
impl Animation for JumpAnimation {
type Dependency<'a> = f32;
type Dependency<'a> = (f32, Vec3<f32>, Vec3<f32>);
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::<f32>::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
}

View File

@ -701,42 +701,42 @@ impl<'a> From<&'a Body> for SkeletonAttr {
fn mount_point(body: &Body) -> Vec3<f32> {
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()
}

View File

@ -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::<f32>::from(self.chest);
let head_mat = chest_mat * Mat4::<f32>::from(self.head);
*(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [
make_bone(chest_mat * Mat4::<f32>::from(self.head)),
make_bone(head_mat),
make_bone(chest_mat),
make_bone(chest_mat * Mat4::<f32>::from(self.leg_fl)),
make_bone(chest_mat * Mat4::<f32>::from(self.leg_fr)),
@ -55,15 +57,22 @@ impl Skeleton for QuadrupedSmallSkeleton {
make_bone(chest_mat * Mat4::<f32>::from(self.leg_br)),
make_bone(chest_mat * Mat4::<f32>::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<f32> {
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()
}

View File

@ -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(),
),

View File

@ -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()

View File

@ -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::<comp::Poise>();
let alignments = ecs.read_storage::<comp::Alignment>();
let is_mount = ecs.read_storage::<Is<Mount>>();
// Check if there was a persistence load error of the skillset, and if so
// display a dialog prompt
@ -1664,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)

View File

@ -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<KeyLayout>,
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<Info<'a>>,
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<KeyLayout>,
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::<Vec<_>>()
.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 {

View File

@ -6,7 +6,7 @@ use crate::{
use conrod_core::{
color,
widget::{self, RoundedRectangle, Text},
widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon,
widget_ids, Color, Colorable, Positionable, 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<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
}
impl<'a> Overitem<'a> {
@ -58,6 +59,7 @@ impl<'a> Overitem<'a> {
properties: OveritemProperties,
pulse: f32,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>,
) -> Self {
Self {
name,
@ -70,6 +72,7 @@ impl<'a> Overitem<'a> {
properties,
pulse,
key_layout,
interaction_options,
}
}
}
@ -120,7 +123,7 @@ impl<'a> Widget for Overitem<'a> {
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { id, state, ui, .. } = args;
let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
// Example:
// MUSHROOM
@ -167,25 +170,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::<Vec<_>>()
.join("\n");
let hints_text = Text::new(&text)
.font_id(self.fonts.cyri.conrod_id)
.font_size(btn_font_size as u32)
.color(TEXT_COLOR)
.x_y(0.0, btn_text_pos_y)
.depth(self.distance_from_player_sqr + 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

View File

@ -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::<DeltaTime>().0;
let updater = ecs.read_resource::<LazyUpdate>();
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::<LightEmitter>().maybe(),
ecs.read_storage::<crate::ecs::comp::Interpolated>().maybe(),
&ecs.read_storage::<Pos>(),
ecs.read_storage::<Body>().maybe(),
&mut ecs.write_storage::<LightAnimation>(),
)
@ -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::<Inventory>().maybe(),
ecs.read_storage::<Item>().maybe(),
ecs.read_storage::<LightEmitter>().maybe(),
ecs.read_storage::<Mounting>().maybe(),
ecs.read_storage::<Is<Rider>>().maybe(),
ecs.read_storage::<Collider>().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::<f32>::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::<f32>::unit_y(),
state.last_ori * anim::vek::Vec3::<f32>::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::<f32>::unit_y(),
state.last_ori * anim::vek::Vec3::<f32>::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<anim::vek::Vec3<f32>>,
// Animation to be applied to mounter of this entity
// Animation to be applied to rider of this entity
mount_transform: anim::vek::Transform<f32, f32, f32>,
// 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<S: Skeleton> FigureState<S> {
model: Option<&FigureModelEntry<N>>,
// 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<S: Skeleton> FigureState<S> {
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::<f32>::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::<f32>::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

View File

@ -9,6 +9,7 @@ use vek::*;
pub enum Interaction {
Collect,
Craft(CraftingTab),
Mine,
}
#[derive(Default)]

View File

@ -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<i32>, Option<Interaction>),
Block(Block, Vec3<i32>, 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)))
}

View File

@ -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::<comp::Pos>(),
&client
.state()
.ecs()
.read_storage::<comp::MountState>(),
// TODO: More cleverly filter by things that can actually be mounted
!&client.state().ecs().read_storage::<Is<Mount>>(),
client.state().ecs().read_storage::<comp::Alignment>().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) => {