diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 7308abd268..3dc58d54e8 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -1012,6 +1012,7 @@ impl Body { /// Component of the mounting offset specific to the mount pub fn mount_offset(&self) -> Vec3 { match self { + Body::Humanoid(_) => (self.dimensions() * Vec3::new(0.7, 0.0, 0.6)).into_array(), Body::QuadrupedMedium(quadruped_medium) => { match (quadruped_medium.species, quadruped_medium.body_type) { (quadruped_medium::Species::Grolgar, _) => [0.0, 0.5, 1.8], diff --git a/common/src/comp/pet.rs b/common/src/comp/pet.rs index ac617be380..47aab87d9e 100644 --- a/common/src/comp/pet.rs +++ b/common/src/comp/pet.rs @@ -62,6 +62,7 @@ pub fn is_mountable(mount: &Body, rider: Option<&Body>) -> bool { |rider: Option<&Body>| -> bool { rider.map_or(false, |b| b.mass() <= Mass(500.0)) }; match mount { + Body::Humanoid(_) => matches!(rider, Some(Body::BirdMedium(_))), Body::QuadrupedMedium(body) => match body.species { quadruped_medium::Species::Alpaca | quadruped_medium::Species::Antelope diff --git a/common/src/mounting.rs b/common/src/mounting.rs index 249a5b3720..8aa08f05c0 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -1,5 +1,5 @@ use crate::{ - comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, + comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, link::{Is, Link, LinkHandle, Role}, terrain::{Block, TerrainGrid}, uid::{Uid, UidAllocator}, @@ -59,8 +59,10 @@ impl Link for Mounting { Read<'a, UidAllocator>, Entities<'a>, ReadStorage<'a, comp::Health>, + ReadStorage<'a, comp::Body>, ReadStorage<'a, Is>, ReadStorage<'a, Is>, + ReadStorage<'a, comp::CharacterState>, ); fn create( @@ -73,15 +75,12 @@ impl Link for Mounting { // 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.contains(entity) - && !is_riders.contains(entity) - && !is_volume_rider.contains(entity) - }; - // Ensure that neither mount or rider are already part of a mounting // relationship - if can_mount_with(mount) && can_mount_with(rider) { + if !is_mounts.contains(mount) + && !is_riders.contains(rider) + && !is_volume_rider.contains(rider) + { let _ = is_mounts.insert(mount, this.make_role()); let _ = is_riders.insert(rider, this.make_role()); Ok(()) @@ -95,7 +94,7 @@ impl Link for Mounting { fn persist( this: &LinkHandle, - (uid_allocator, entities, healths, is_mounts, is_riders): Self::PersistData<'_>, + (uid_allocator, entities, healths, bodies, is_mounts, is_riders, character_states): Self::PersistData<'_>, ) -> bool { let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); @@ -104,11 +103,19 @@ impl Link for Mounting { entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead) }; + let is_in_ridable_state = character_states + .get(mount) + .map_or(false, |cs| !matches!(cs, comp::CharacterState::Roll(_))); + // 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() + && bodies.get(mount).map_or(false, |mount_body| { + is_mountable(mount_body, bodies.get(rider)) + }) + && is_in_ridable_state } else { false } diff --git a/common/systems/src/lib.rs b/common/systems/src/lib.rs index 1c4e108177..0d77b9926d 100644 --- a/common/systems/src/lib.rs +++ b/common/systems/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(drain_filter)] +#![feature(drain_filter, let_chains)] #![allow(clippy::option_map_unit_fn)] mod aura; diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index 9aa56983d5..9061380c14 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -56,19 +56,25 @@ impl<'a> System<'a> for Sys { // For each mount... for (entity, is_mount, body) in (&entities, &is_mounts, bodies.maybe()).join() { // ...find the rider... - let Some((inputs, actions, rider)) = uid_allocator + let Some((inputs_and_actions, rider)) = uid_allocator .retrieve_entity_internal(is_mount.rider.id()) .and_then(|rider| { controllers .get_mut(rider) - .map(|c| { - let actions = c.actions.drain_filter(|action| match action { - ControlAction::StartInput { input: i, .. } - | ControlAction::CancelInput(i) => matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll), - _ => false - }).collect(); - (c.inputs.clone(), actions, rider) - }) + .map(|c| ( + // Only take inputs and actions from the rider if the mount is not intelligent (TODO: expand the definition of 'intelligent'). + if !matches!(body, Some(Body::Humanoid(_))) { + let actions = c.actions.drain_filter(|action| match action { + ControlAction::StartInput { input: i, .. } + | ControlAction::CancelInput(i) => matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll), + _ => false + }).collect(); + Some((c.inputs.clone(), actions)) + } else { + None + }, + rider, + )) }) else { continue }; @@ -86,8 +92,10 @@ impl<'a> System<'a> for Sys { 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) { + // ...and apply the rider's inputs to the mount's controller + if let Some((inputs, actions)) = inputs_and_actions + && let Some(controller) = controllers.get_mut(entity) + { controller.inputs = inputs; controller.actions = actions; } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 55377a5cf2..255c4dbb09 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -25,19 +25,20 @@ use common::{ Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent, Controller, HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind, }, + consts::MAX_MOUNT_RANGE, effect::{BuffEffect, Effect}, event::{Emitter, ServerEvent}, path::TraversalConfig, rtsim::NpcActivity, states::basic_beam, - terrain::{Block, TerrainGrid}, + terrain::Block, time::DayPeriod, util::Dir, vol::ReadVol, }; use itertools::Itertools; use rand::{thread_rng, Rng}; -use specs::Entity as EcsEntity; +use specs::{saveload::Marker, Entity as EcsEntity}; use vek::*; #[cfg(feature = "use-dyn-lib")] @@ -48,7 +49,11 @@ impl<'a> AgentData<'a> { // Action Nodes //////////////////////////////////////// - pub fn glider_fall(&self, controller: &mut Controller) { + pub fn glider_fall(&self, controller: &mut Controller, read_data: &ReadData) { + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + controller.push_action(ControlAction::GlideWield); let flight_direction = @@ -66,7 +71,11 @@ impl<'a> AgentData<'a> { controller.inputs.look_dir = Dir::from_unnormalized(look_dir).unwrap_or_else(Dir::forward); } - pub fn fly_upward(&self, controller: &mut Controller) { + pub fn fly_upward(&self, controller: &mut Controller, read_data: &ReadData) { + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + controller.push_basic_input(InputKind::Fly); controller.inputs.move_z = 1.0; } @@ -86,6 +95,10 @@ impl<'a> AgentData<'a> { path: Path, speed_multiplier: Option, ) -> bool { + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + let partial_path_tgt_pos = |pos_difference: Vec3| { self.pos.0 + PARTIAL_PATH_DIST * pos_difference.try_normalized().unwrap_or_else(Vec3::zero) @@ -369,6 +382,23 @@ impl<'a> AgentData<'a> { None => {}, } + // Idle NPCs should try to jump on the shoulders of their owner, sometimes. + if read_data.is_riders.contains(*self.entity) { + if rng.gen_bool(0.0001) { + controller.push_event(ControlEvent::Unmount); + } else { + break 'activity; + } + } else if let Some(Alignment::Owned(owner_uid)) = self.alignment + && let Some(owner) = get_entity_by_id(owner_uid.id(), read_data) + && let Some(pos) = read_data.positions.get(owner) + && pos.0.distance_squared(self.pos.0) < MAX_MOUNT_RANGE.powi(2) + && rng.gen_bool(0.01) + { + controller.push_event(ControlEvent::Mount(*owner_uid)); + break 'activity; + } + // Bats should fly // Use a proportional controller as the bouncing effect mimics bat flight if self.traversal_config.can_fly @@ -488,11 +518,15 @@ impl<'a> AgentData<'a> { &self, agent: &mut Agent, controller: &mut Controller, - terrain: &TerrainGrid, + read_data: &ReadData, tgt_pos: &Pos, ) { + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + if let Some((bearing, speed)) = agent.chaser.chase( - terrain, + &*read_data.terrain, self.pos.0, self.vel.0, tgt_pos.0, @@ -536,9 +570,13 @@ impl<'a> AgentData<'a> { &self, agent: &mut Agent, controller: &mut Controller, + read_data: &ReadData, tgt_pos: &Pos, - terrain: &TerrainGrid, ) { + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + if let Some(body) = self.body { if body.can_strafe() && !self.is_gliding { controller.push_action(ControlAction::Unwield); @@ -546,7 +584,7 @@ impl<'a> AgentData<'a> { } if let Some((bearing, speed)) = agent.chaser.chase( - terrain, + &*read_data.terrain, self.pos.0, self.vel.0, // Away from the target (ironically) @@ -887,6 +925,10 @@ impl<'a> AgentData<'a> { #[cfg(feature = "be-dyn-lib")] let rng = &mut thread_rng(); + if read_data.is_riders.contains(*self.entity) { + controller.push_event(ControlEvent::Unmount); + } + let tool_tactic = |tool_kind| match tool_kind { ToolKind::Bow => Tactic::Bow, ToolKind::Staff => Tactic::Staff, @@ -1458,9 +1500,9 @@ impl<'a> AgentData<'a> { if sound_was_threatening && is_close { if !self.below_flee_health(agent) && follows_threatening_sounds { - self.follow(agent, controller, &read_data.terrain, &sound_pos); + self.follow(agent, controller, read_data, &sound_pos); } else if self.below_flee_health(agent) || !follows_threatening_sounds { - self.flee(agent, controller, &sound_pos, &read_data.terrain); + self.flee(agent, controller, read_data, &sound_pos); } else { self.idle(agent, controller, read_data, event_emitter, rng); } diff --git a/server/agent/src/lib.rs b/server/agent/src/lib.rs index 13d1629376..f0a61862f0 100644 --- a/server/agent/src/lib.rs +++ b/server/agent/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(exclusive_range_pattern)] +#![feature(exclusive_range_pattern, let_chains)] #[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))] compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once"); diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index a2170fc805..1b54f1bab0 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -113,13 +113,15 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { if let (Some(rider_uid), Some(mount_uid)) = (uids.get(rider).copied(), uids.get(mount).copied()) { - let is_pet = matches!( - state - .ecs() - .read_storage::() - .get(mount), - Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, - ); + let is_pet_of = |mount, rider_uid| { + matches!( + state + .ecs() + .read_storage::() + .get(mount), + Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, + ) + }; let can_ride = state .ecs() @@ -129,7 +131,7 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { is_mountable(mount_body, state.ecs().read_storage().get(rider)) }); - if is_pet && can_ride { + if (is_pet_of(mount, rider_uid) || is_pet_of(rider, mount_uid)) && can_ride { drop(uids); let _ = state.link(Mounting { mount: mount_uid, diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 1a04733524..6aed9a4e91 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -191,12 +191,21 @@ fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool { // But keep in mind our 25 m/s gravity let is_falling_dangerous = bdata.agent_data.vel.0.z < -20.0; - if is_falling_dangerous && bdata.agent_data.traversal_config.can_fly { - bdata.agent_data.fly_upward(bdata.controller); - return true; - } else if is_falling_dangerous && bdata.agent_data.glider_equipped { - bdata.agent_data.glider_fall(bdata.controller); - return true; + if is_falling_dangerous { + if bdata.read_data.is_riders.contains(*bdata.agent_data.entity) { + bdata.controller.push_event(ControlEvent::Unmount); + } + if bdata.agent_data.traversal_config.can_fly { + bdata + .agent_data + .fly_upward(bdata.controller, bdata.read_data); + return true; + } else if bdata.agent_data.glider_equipped { + bdata + .agent_data + .glider_fall(bdata.controller, bdata.read_data); + return true; + } } false } @@ -407,12 +416,9 @@ fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0); if dist_sqrd > (MAX_FOLLOW_DIST).powi(2) { - bdata.agent_data.follow( - bdata.agent, - bdata.controller, - &bdata.read_data.terrain, - tgt_pos, - ); + bdata + .agent_data + .follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos); return true; } } @@ -698,7 +704,7 @@ fn search_last_known_pos_if_not_alert(bdata: &mut BehaviorData) -> bool { if let Some(target) = agent.target { if let Some(last_known_pos) = target.last_known_pos { - agent_data.follow(agent, controller, &read_data.terrain, &Pos(last_known_pos)); + agent_data.follow(agent, controller, read_data, &Pos(last_known_pos)); return true; } @@ -774,11 +780,11 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { }; } else if !flee_timer_done { if within_normal_flee_dir_dist { - agent_data.flee(agent, controller, tgt_pos, &read_data.terrain); + agent_data.flee(agent, controller, read_data, tgt_pos); } else if let Some(random_pos) = agent.flee_from_pos { - agent_data.flee(agent, controller, &random_pos, &read_data.terrain); + agent_data.flee(agent, controller, read_data, &random_pos); } else { - agent_data.flee(agent, controller, tgt_pos, &read_data.terrain); + agent_data.flee(agent, controller, read_data, tgt_pos); } agent.action_state.timers diff --git a/voxygen/anim/src/character/mod.rs b/voxygen/anim/src/character/mod.rs index ba20f2d4b2..c612df1281 100644 --- a/voxygen/anim/src/character/mod.rs +++ b/voxygen/anim/src/character/mod.rs @@ -162,17 +162,25 @@ impl Skeleton for CharacterSkeleton { // FIXME: Should this be control_l_mat? make_bone(control_mat * hand_l_mat * Mat4::::from(self.hold)), ]; + + // Offset from the mounted bone's origin. + // Note: This could be its own bone if we need to animate it independently. + let mount_position = (chest_mat * Vec4::from_point(Vec3::new(5.5, 0.0, 6.5))) + .homogenized() + .xyz(); + // NOTE: We apply the ori from base_mat externally so we don't need to worry + // about it here for now. + let mount_orientation = + self.torso.orientation * self.chest.orientation * Quaternion::rotation_y(0.4); + let weapon_trails = self.main_weapon_trail || self.off_weapon_trail; Offsets { lantern: Some((lantern_mat * Vec4::new(0.0, 0.5, -6.0, 1.0)).xyz()), viewpoint: Some((head_mat * Vec4::new(0.0, 0.0, 4.0, 1.0)).xyz()), - // TODO: see quadruped_medium for how to animate this mount_bone: Transform { - position: comp::Body::Humanoid(body) - .mount_offset() - .into_tuple() - .into(), - ..Default::default() + position: mount_position, + orientation: mount_orientation, + scale: Vec3::one(), }, primary_trail_mat: if weapon_trails { self.main_weapon_trail diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 9770cd2df2..937ca37f43 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -107,7 +107,7 @@ use common::{ }, consts::MAX_PICKUP_RANGE, link::Is, - mounting::{Mount, VolumePos}, + mounting::{Mount, Rider, VolumePos}, outcome::Outcome, resources::{Secs, Time}, slowjob::SlowJobPool, @@ -1500,7 +1500,8 @@ impl Hud { let me = info.viewpoint_entity; let poises = ecs.read_storage::(); let alignments = ecs.read_storage::(); - let is_mount = ecs.read_storage::>(); + let is_mounts = ecs.read_storage::>(); + let is_riders = ecs.read_storage::>(); let stances = ecs.read_storage::(); let time = ecs.read_resource::