diff --git a/CHANGELOG.md b/CHANGELOG.md index 8528091887..597b6d28ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Petting animals tamed by you or someone else! + ### Changed - Fireworks and bombs are (again) available from chests (Sahagin and above). diff --git a/assets/voxygen/i18n/en/hud/misc.ftl b/assets/voxygen/i18n/en/hud/misc.ftl index d6df56bb93..35e19879e7 100644 --- a/assets/voxygen/i18n/en/hud/misc.ftl +++ b/assets/voxygen/i18n/en/hud/misc.ftl @@ -54,6 +54,7 @@ hud-mine-needs_pickaxe = Needs Pickaxe hud-mine-needs_shovel = Needs Shovel hud-mine-needs_unhandled_case = Needs ??? hud-talk = Talk +hud-pet = Pet hud-trade = Trade hud-mount = Mount hud-follow = Follow diff --git a/client/src/lib.rs b/client/src/lib.rs index 2d544a7809..a4e7b83d38 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1419,6 +1419,16 @@ impl Client { } } + pub fn do_pet(&mut self, target_entity: EcsEntity) { + if self.is_dead() { + return; + } + + if let Some(target_uid) = self.state.read_component_copied(target_entity) { + self.control_action(ControlAction::Pet { target_uid }); + } + } + pub fn npc_interact(&mut self, npc_entity: EcsEntity, subject: Subject) { // If we're dead, exit before sending message if self.is_dead() { diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index bc98b1c522..ee8b8c78b8 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -717,6 +717,7 @@ impl From<&CharacterState> for CharacterAbilityType { | CharacterState::Climb(_) | CharacterState::Sit | CharacterState::Dance + | CharacterState::Pet(_) | CharacterState::Talk | CharacterState::Glide(_) | CharacterState::GlideWield(_) diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 08d85bc993..8e166cbe68 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -102,6 +102,7 @@ pub enum CharacterState { Sit, Dance, Talk, + Pet(pet::Data), Glide(glide::Data), GlideWield(glide_wield::Data), /// A stunned state @@ -315,6 +316,7 @@ impl CharacterState { CharacterState::Climb(_) | CharacterState::Equipping(_) | CharacterState::Dance + | CharacterState::Pet(_) | CharacterState::Glide(_) | CharacterState::GlideWield(_) | CharacterState::Talk @@ -508,6 +510,7 @@ impl CharacterState { CharacterState::Stunned(data) => data.behavior(j, output_events), CharacterState::Sit => sit::Data::behavior(&sit::Data, j, output_events), CharacterState::Dance => dance::Data::behavior(&dance::Data, j, output_events), + CharacterState::Pet(data) => data.behavior(j, output_events), CharacterState::BasicBlock(data) => data.behavior(j, output_events), CharacterState::Roll(data) => data.behavior(j, output_events), CharacterState::Wielding(data) => data.behavior(j, output_events), @@ -562,6 +565,7 @@ impl CharacterState { CharacterState::Dance => { states::dance::Data::handle_event(&dance::Data, j, output_events, action) }, + CharacterState::Pet(data) => data.handle_event(j, output_events, action), CharacterState::BasicBlock(data) => data.handle_event(j, output_events, action), CharacterState::Roll(data) => data.handle_event(j, output_events, action), CharacterState::Wielding(data) => data.handle_event(j, output_events, action), @@ -618,6 +622,7 @@ impl CharacterState { CharacterState::Stunned(_) => None, CharacterState::Sit => None, CharacterState::Dance => None, + CharacterState::Pet(_) => None, CharacterState::BasicBlock(data) => Some(data.static_data.ability_info), CharacterState::Roll(data) => Some(data.static_data.ability_info), CharacterState::Wielding(_) => None, @@ -663,6 +668,7 @@ impl CharacterState { CharacterState::Stunned(data) => Some(data.stage_section), CharacterState::Sit => None, CharacterState::Dance => None, + CharacterState::Pet(_) => None, CharacterState::BasicBlock(data) => Some(data.stage_section), CharacterState::Roll(data) => Some(data.stage_section), CharacterState::Equipping(_) => Some(StageSection::Buildup), @@ -712,6 +718,7 @@ impl CharacterState { }), CharacterState::Sit => None, CharacterState::Dance => None, + CharacterState::Pet(_) => None, CharacterState::BasicBlock(data) => Some(DurationsInfo { buildup: Some(data.static_data.buildup_duration), recover: Some(data.static_data.recover_duration), @@ -901,6 +908,7 @@ impl CharacterState { CharacterState::Stunned(data) => Some(data.timer), CharacterState::Sit => None, CharacterState::Dance => None, + CharacterState::Pet(_) => None, CharacterState::BasicBlock(data) => Some(data.timer), CharacterState::Roll(data) => Some(data.timer), CharacterState::Wielding(_) => None, @@ -946,6 +954,7 @@ impl CharacterState { CharacterState::Stunned(_) => None, CharacterState::Sit => None, CharacterState::Dance => None, + CharacterState::Pet(_) => None, CharacterState::BasicBlock(_) => None, CharacterState::Roll(_) => None, CharacterState::Wielding(_) => None, diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 6122d3d05d..679c4e5dae 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -181,6 +181,9 @@ pub enum ControlAction { Unwield, Sit, Dance, + Pet { + target_uid: Uid, + }, Sneak, Stand, Talk, diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 8bc65b4030..2aec12d8ce 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -3,18 +3,18 @@ use crate::{ self, character_state::OutputEvents, item::{tool::AbilityMap, MaterialStatManifest}, - ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, ControlAction, - Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, - InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate, - Stats, Vel, + ActiveAbilities, Alignment, Beam, Body, CharacterActivity, CharacterState, Combo, + ControlAction, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, + Inventory, InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, PreviousPhysCache, Scale, + SkillSet, Stance, StateUpdate, Stats, Vel, }, link::Is, mounting::{Rider, VolumeRider}, resources::{DeltaTime, Time}, terrain::TerrainGrid, - uid::Uid, + uid::{IdMaps, Uid}, }; -use specs::{storage::FlaggedAccessMut, Entity, LazyUpdate}; +use specs::{storage::FlaggedAccessMut, Entity, LazyUpdate, Read, ReadStorage}; use vek::*; pub trait CharacterBehavior { @@ -50,6 +50,14 @@ pub trait CharacterBehavior { fn dance(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { StateUpdate::from(data) } + fn pet( + &self, + data: &JoinData, + _output_events: &mut OutputEvents, + _target_uid: Uid, + ) -> StateUpdate { + StateUpdate::from(data) + } fn sneak(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { StateUpdate::from(data) } @@ -96,6 +104,7 @@ pub trait CharacterBehavior { ControlAction::Unwield => self.unwield(data, output_events), ControlAction::Sit => self.sit(data, output_events), ControlAction::Dance => self.dance(data, output_events), + ControlAction::Pet { target_uid } => self.pet(data, output_events, target_uid), ControlAction::Sneak => { if data.mount_data.is_none() && data.volume_mount_data.is_none() { self.sneak(data, output_events) @@ -149,6 +158,9 @@ pub struct JoinData<'a> { pub mount_data: Option<&'a Is>, pub volume_mount_data: Option<&'a Is>, pub stance: Option<&'a Stance>, + pub id_maps: &'a Read<'a, IdMaps>, + pub alignments: &'a ReadStorage<'a, Alignment>, + pub prev_phys_caches: &'a ReadStorage<'a, PreviousPhysCache>, } pub struct JoinStruct<'a> { @@ -179,6 +191,9 @@ pub struct JoinStruct<'a> { pub mount_data: Option<&'a Is>, pub volume_mount_data: Option<&'a Is>, pub stance: Option<&'a Stance>, + pub id_maps: &'a Read<'a, IdMaps>, + pub alignments: &'a ReadStorage<'a, Alignment>, + pub prev_phys_caches: &'a ReadStorage<'a, PreviousPhysCache>, } impl<'a> JoinData<'a> { @@ -223,6 +238,9 @@ impl<'a> JoinData<'a> { mount_data: j.mount_data, volume_mount_data: j.volume_mount_data, stance: j.stance, + id_maps: j.id_maps, + alignments: j.alignments, + prev_phys_caches: j.prev_phys_caches, } } } diff --git a/common/src/states/dance.rs b/common/src/states/dance.rs index c151f794fd..f3934f4ce5 100644 --- a/common/src/states/dance.rs +++ b/common/src/states/dance.rs @@ -5,6 +5,7 @@ use crate::{ behavior::{CharacterBehavior, JoinData}, idle, }, + uid::Uid, }; use serde::{Deserialize, Serialize}; @@ -50,6 +51,12 @@ impl CharacterBehavior for Data { update } + fn pet(&self, data: &JoinData, _: &mut OutputEvents, target_uid: Uid) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_pet(data, &mut update, target_uid); + update + } + fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); // Try to Fall/Stand up/Move diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs index 05d1ab9b3d..916cdaec3f 100644 --- a/common/src/states/idle.rs +++ b/common/src/states/idle.rs @@ -6,6 +6,7 @@ use crate::{ }, resources::Time, states::behavior::{CharacterBehavior, JoinData}, + uid::Uid, }; use serde::{Deserialize, Serialize}; @@ -96,6 +97,12 @@ impl CharacterBehavior for Data { update } + fn pet(&self, data: &JoinData, _: &mut OutputEvents, target_uid: Uid) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_pet(data, &mut update, target_uid); + update + } + fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle(Data { diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 5ad0cdd153..2a39c3244e 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -23,6 +23,7 @@ pub mod idle; pub mod leap_melee; pub mod leap_shockwave; pub mod music; +pub mod pet; pub mod rapid_melee; pub mod repeater_ranged; pub mod riposte_melee; diff --git a/common/src/states/pet.rs b/common/src/states/pet.rs new file mode 100644 index 0000000000..feb594d906 --- /dev/null +++ b/common/src/states/pet.rs @@ -0,0 +1,99 @@ +use super::utils::*; +use crate::{ + comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate}, + states::{ + behavior::{CharacterBehavior, JoinData}, + idle, + }, + uid::Uid, + util::Dir, +}; +use serde::{Deserialize, Serialize}; +use vek::Vec3; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StaticData { + pub target_uid: Uid, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + pub static_data: StaticData, +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + + let target_entity = data.id_maps.uid_entity(self.static_data.target_uid); + let target_pos = target_entity + .and_then(|target_entity| data.prev_phys_caches.get(target_entity)) + .and_then(|prev_phys| prev_phys.pos); + + let can_pet = target_entity.map_or(false, |target_entity| { + target_pos.zip(data.alignments.get(target_entity)).map_or( + false, + |(target_position, target_alignment)| { + can_perform_pet(*data.pos, target_position, *target_alignment) + }, + ) + }); + + // Face target if they have a position. + if let Some(target_pos) = target_pos { + let ori_dir = Dir::from_unnormalized(Vec3::from((target_pos.0 - data.pos.0).xy())); + handle_orientation(data, &mut update, 1.0, ori_dir); + } + + leave_stance(data, output_events); + handle_wield(data, &mut update); + handle_jump(data, output_events, &mut update, 1.0); + + if !can_pet { + update.character = CharacterState::Idle(idle::Data::default()); + } + + // Try to Fall/Stand up/Move + if data.physics.on_ground.is_none() || data.inputs.move_dir.magnitude_squared() > 0.0 { + update.character = CharacterState::Idle(idle::Data::default()); + } + + update + } + + fn manipulate_loadout( + &self, + data: &JoinData, + output_events: &mut OutputEvents, + inv_action: InventoryAction, + ) -> StateUpdate { + let mut update = StateUpdate::from(data); + handle_manipulate_loadout(data, output_events, &mut update, inv_action); + update + } + + fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_wield(data, &mut update); + update + } + + fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_dance(data, &mut update); + update + } + + fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sit(data, &mut update); + update + } + + fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + // Try to Fall/Stand up/Move + update.character = CharacterState::Idle(idle::Data::default()); + update + } +} diff --git a/common/src/states/talk.rs b/common/src/states/talk.rs index bab5b59f4d..5a04475ab6 100644 --- a/common/src/states/talk.rs +++ b/common/src/states/talk.rs @@ -5,6 +5,7 @@ use crate::{ behavior::{CharacterBehavior, JoinData}, idle, }, + uid::Uid, }; use serde::{Deserialize, Serialize}; @@ -54,6 +55,12 @@ impl CharacterBehavior for Data { update } + fn pet(&self, data: &JoinData, _: &mut OutputEvents, target_uid: Uid) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_pet(data, &mut update, target_uid); + update + } + fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); // Try to Fall/Stand up/Move diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index b4a3ad3657..01d6a079d2 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -15,15 +15,16 @@ use crate::{ }, quadruped_low, quadruped_medium, quadruped_small, ship, skills::{Skill, SwimSkill, SKILL_MODIFIERS}, - theropod, Body, CharacterState, Density, InputAttr, InputKind, InventoryAction, Melee, - StateUpdate, + theropod, Alignment, Body, CharacterState, Density, InputAttr, InputKind, InventoryAction, + Melee, Pos, StateUpdate, }, - consts::{FRIC_GROUND, GRAVITY, MAX_PICKUP_RANGE}, + consts::{FRIC_GROUND, GRAVITY, MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, event::{BuffEvent, ChangeStanceEvent, ComboChangeEvent, InventoryManipEvent, LocalEvent}, mounting::Volume, outcome::Outcome, states::{behavior::JoinData, utils::CharacterState::Idle, *}, terrain::{Block, TerrainGrid, UnlockKind}, + uid::Uid, util::Dir, vol::ReadVol, }; @@ -883,6 +884,34 @@ pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) { } } +pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool { + let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2); + let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame); + + within_distance && valid_alignment +} + +pub fn attempt_pet(data: &JoinData<'_>, update: &mut StateUpdate, target_uid: Uid) { + let can_pet = data + .id_maps + .uid_entity(target_uid) + .and_then(|target_entity| { + data.prev_phys_caches + .get(target_entity) + .and_then(|prev_phys| prev_phys.pos) + .zip(data.alignments.get(target_entity)) + }) + .map_or(false, |(target_position, target_alignment)| { + can_perform_pet(*data.pos, target_position, *target_alignment) + }); + + if can_pet && data.physics.on_ground.is_some() && data.body.is_humanoid() { + update.character = CharacterState::Pet(pet::Data { + static_data: pet::StaticData { target_uid }, + }); + } +} + pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) { if data.physics.on_ground.is_some() { update.character = CharacterState::Talk; diff --git a/common/src/states/wielding.rs b/common/src/states/wielding.rs index 0aac0aa902..4476820fed 100644 --- a/common/src/states/wielding.rs +++ b/common/src/states/wielding.rs @@ -9,6 +9,7 @@ use crate::{ behavior::{CharacterBehavior, JoinData}, idle, }, + uid::Uid, }; use serde::{Deserialize, Serialize}; @@ -104,6 +105,12 @@ impl CharacterBehavior for Data { update } + fn pet(&self, data: &JoinData, _: &mut OutputEvents, target_uid: Uid) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_pet(data, &mut update, target_uid); + update + } + fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); if data.physics.on_ground.is_some() && data.body.is_humanoid() { diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 3ac499e9fa..d569fac15d 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -9,7 +9,7 @@ use common::{ inventory::item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, Controller, Density, Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, - Scale, SkillSet, Stance, StateUpdate, Stats, Vel, + PreviousPhysCache, Scale, SkillSet, Stance, StateUpdate, Stats, Vel, }, event::{self, EventBus, KnockbackEvent, LocalEvent}, link::Is, @@ -21,7 +21,7 @@ use common::{ idle, }, terrain::TerrainGrid, - uid::Uid, + uid::{IdMaps, Uid}, }; use common_ecs::{Job, Origin, Phase, System}; @@ -53,6 +53,7 @@ pub struct ReadData<'a> { terrain: ReadExpect<'a, TerrainGrid>, inventories: ReadStorage<'a, Inventory>, stances: ReadStorage<'a, Stance>, + prev_phys_caches: ReadStorage<'a, PreviousPhysCache>, } /// ## Character Behavior System @@ -74,6 +75,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Controller>, WriteStorage<'a, Poise>, Read<'a, EventBus>, + Read<'a, IdMaps>, ); const NAME: &'static str = "character_behavior"; @@ -94,6 +96,7 @@ impl<'a> System<'a> for Sys { mut controllers, mut poises, outcomes, + id_maps, ): Self::SystemData, ) { let mut local_emitter = read_data.local_bus.emitter(); @@ -217,6 +220,9 @@ impl<'a> System<'a> for Sys { mount_data: read_data.is_riders.get(entity), volume_mount_data: read_data.is_volume_riders.get(entity), stance: read_data.stances.get(entity), + id_maps: &id_maps, + alignments: &read_data.alignments, + prev_phys_caches: &read_data.prev_phys_caches, }; for action in actions { diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 6d1201a153..b854d967c7 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -131,6 +131,7 @@ impl<'a> System<'a> for Sys { CharacterState::Idle(_) | CharacterState::Talk | CharacterState::Dance + | CharacterState::Pet(_) | CharacterState::Skate(_) | CharacterState::Glide(_) | CharacterState::GlideWield(_) diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 6e3945e3c5..a250adfe4c 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -445,6 +445,28 @@ impl<'a> AgentData<'a> { None => {}, } + let owner_uid = self.alignment.and_then(|alignment| match alignment { + Alignment::Owned(owner_uid) => Some(owner_uid), + _ => None, + }); + + let owner = owner_uid.and_then(|owner_uid| get_entity_by_id(*owner_uid, read_data)); + + let is_being_pet = owner + .and_then(|owner| read_data.char_states.get(owner)) + .map_or(false, |char_state| match char_state { + CharacterState::Pet(petting_data) => { + petting_data.static_data.target_uid == *self.uid + }, + _ => false, + }); + + let is_in_range = owner + .and_then(|owner| read_data.positions.get(owner)) + .map_or(false, |pos| { + pos.0.distance_squared(self.pos.0) < MAX_MOUNT_RANGE.powi(2) + }); + // 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) { @@ -452,10 +474,9 @@ impl<'a> AgentData<'a> { } else { break 'activity; } - } else if let Some(Alignment::Owned(owner_uid)) = self.alignment - && let Some(owner) = get_entity_by_id(*owner_uid, read_data) - && let Some(pos) = read_data.positions.get(owner) - && pos.0.distance_squared(self.pos.0) < MAX_MOUNT_RANGE.powi(2) + } else if let Some(owner_uid) = owner_uid + && is_in_range + && !is_being_pet && rng.gen_bool(0.01) { controller.push_event(ControlEvent::Mount(*owner_uid)); diff --git a/voxygen/anim/src/character/mod.rs b/voxygen/anim/src/character/mod.rs index 0c09b9dc3c..4e2ba6127d 100644 --- a/voxygen/anim/src/character/mod.rs +++ b/voxygen/anim/src/character/mod.rs @@ -20,6 +20,7 @@ pub mod jump; pub mod leapmelee; pub mod mount; pub mod music; +pub mod pet; pub mod rapidmelee; pub mod repeater; pub mod ripostemelee; @@ -51,10 +52,11 @@ pub use self::{ dance::DanceAnimation, dash::DashAnimation, divemelee::DiveMeleeAnimation, equip::EquipAnimation, finishermelee::FinisherMeleeAnimation, glidewield::GlideWieldAnimation, gliding::GlidingAnimation, idle::IdleAnimation, jump::JumpAnimation, leapmelee::LeapAnimation, - mount::MountAnimation, music::MusicAnimation, rapidmelee::RapidMeleeAnimation, - repeater::RepeaterAnimation, ripostemelee::RiposteMeleeAnimation, roll::RollAnimation, - run::RunAnimation, selfbuff::SelfBuffAnimation, shockwave::ShockwaveAnimation, - shoot::ShootAnimation, sit::SitAnimation, sleep::SleepAnimation, sneak::SneakAnimation, + mount::MountAnimation, music::MusicAnimation, pet::PetAnimation, + rapidmelee::RapidMeleeAnimation, repeater::RepeaterAnimation, + ripostemelee::RiposteMeleeAnimation, roll::RollAnimation, run::RunAnimation, + selfbuff::SelfBuffAnimation, shockwave::ShockwaveAnimation, shoot::ShootAnimation, + sit::SitAnimation, sleep::SleepAnimation, sneak::SneakAnimation, sneakequip::SneakEquipAnimation, sneakwield::SneakWieldAnimation, staggered::StaggeredAnimation, stand::StandAnimation, steer::SteerAnimation, stunned::StunnedAnimation, swim::SwimAnimation, swimwield::SwimWieldAnimation, diff --git a/voxygen/anim/src/character/pet.rs b/voxygen/anim/src/character/pet.rs new file mode 100644 index 0000000000..f9a6642ee1 --- /dev/null +++ b/voxygen/anim/src/character/pet.rs @@ -0,0 +1,48 @@ +use super::{ + super::{vek::*, Animation}, + CharacterSkeleton, SkeletonAttr, +}; + +use std::f32::consts::PI; + +pub struct PetAnimation; + +impl Animation for PetAnimation { + type Dependency<'a> = (Vec3, Option>, f32); + type Skeleton = CharacterSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"character_pet\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "character_pet")] + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (pos, target_pos, _global_time): Self::Dependency<'_>, + anim_time: f32, + _rate: &mut f32, + s_a: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + + let fast = (anim_time * 3.0).sin(); + let fast_offset = (anim_time * 3.0 + PI * 0.5).sin(); + + let z_diff = target_pos.map_or(0., |target_pos| target_pos.z - pos.z); + + // Tilt head down by 10 deg + next.head.orientation = Quaternion::rotation_x(-1. * PI / 2. / 9.); + + // Lift hand up and out, slight hand position change depending on height + next.hand_r.position = Vec3::new( + s_a.hand.0 + -2. * fast_offset, + s_a.hand.1 + 8.0, + s_a.hand.2 + 4.0 + 1. * fast + z_diff, + ); + + // Raise arm 90deg then up and down + next.hand_r.orientation = + Quaternion::rotation_x(PI / 2. + fast * 0.15).rotated_z(fast_offset * 0.5); + + next + } +} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index e45825b592..0fe59514b1 100755 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -2465,39 +2465,49 @@ impl Hud { ] }, Some(comp::Alignment::Owned(owner)) - if Some(*owner) == client.uid() - && dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2) => + if dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2) => { let mut options = Vec::new(); if is_mount.is_none() { - options.push(( - GameInput::Trade, - i18n.get_msg("hud-trade").to_string(), - )); - if !client.is_riding() - && is_mountable(body, bodies.get(client.entity())) - { + if Some(*owner) == client.uid() { options.push(( - GameInput::Mount, - i18n.get_msg("hud-mount").to_string(), + GameInput::Trade, + i18n.get_msg("hud-trade").to_string(), + )); + if !client.is_riding() + && is_mountable(body, bodies.get(client.entity())) + { + options.push(( + GameInput::Mount, + i18n.get_msg("hud-mount").to_string(), + )); + } + + let is_staying = character_activity + .map_or(false, |activity| activity.is_pet_staying); + + options.push(( + GameInput::StayFollow, + i18n.get_msg(if is_staying { + "hud-follow" + } else { + "hud-stay" + }) + .to_string(), )); } - let is_staying = character_activity - .map_or(false, |activity| activity.is_pet_staying); - + // Anyone can pet a tamed animal options.push(( - GameInput::StayFollow, - i18n.get_msg(if is_staying { - "hud-follow" - } else { - "hud-stay" - }) - .to_string(), + GameInput::Interact, + i18n.get_msg("hud-pet").to_string(), )); } options }, + Some(comp::Alignment::Tame) => { + vec![(GameInput::Interact, i18n.get_msg("hud-pet").to_string())] + }, _ => Vec::new(), }, &time, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index fad11fc349..49e9d645db 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -2112,6 +2112,22 @@ impl FigureMgr { skeleton_attr, ) }, + CharacterState::Pet(s) => { + let target_entity = id_maps.uid_entity(s.static_data.target_uid); + let target_pos = target_entity.and_then(|target_entity| { + ecs.read_component::() + .get(target_entity) + .map(|pos| pos.0) + }); + + anim::character::PetAnimation::update_skeleton( + &target_base, + (pos.0, target_pos, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + }, CharacterState::Music(s) => { anim::character::MusicAnimation::update_skeleton( &target_base, diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 843465fd46..d1560bfeaf 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -28,6 +28,7 @@ use common::{ mounting::{Mount, VolumePos}, outcome::Outcome, recipe, + states::utils::can_perform_pet, terrain::{Block, BlockKind}, trade::TradeResult, util::{Dir, Plane}, @@ -1074,6 +1075,22 @@ impl PlayState for SessionState { .state() .read_component_cloned::(*entity); + let pettable = client + .state() + .read_component_cloned::(*entity) + .zip(client.state().read_component_cloned::(*entity)) + .zip(client.state().read_component_cloned::(client.entity())) + .map_or( + false, + |((target_position, target_alignment), client_position)| { + can_perform_pet( + client_position, + target_position, + target_alignment, + ) + }, + ); + if client .state() .ecs() @@ -1097,6 +1114,8 @@ impl PlayState for SessionState { .flatten() { client.activate_portal(portal_uid); + } else if pettable { + client.do_pet(*entity); } else { client.npc_interact(*entity, Subject::Regular); }