Merge branch 'TelepathicWalrus/stay_follow_pets' into 'master'

Add stay/follow option for pets

See merge request veloren/veloren!3906
This commit is contained in:
Joshua Barretto 2023-08-15 10:19:58 +00:00
commit 38c986fa59
23 changed files with 202 additions and 32 deletions

View File

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Recipe for shovel, which is used to dig in mud and graves
- Recipe for a new leather pack
- Keybinds for zooming the camera (Defaults: ']' for zooming in and '[' for zooming out)
- Added the ability to make pets sit, they wont follow nor defend you in this state
### Changed

View File

@ -33,6 +33,7 @@ gameinput-climbdown = Climb Down
gameinput-wallleap = Wall Leap
gameinput-togglelantern = Toggle Lantern
gameinput-mount = Mount
gameinput-stay = Stay/Follow
gameinput-chat = Chat
gameinput-command = Command
gameinput-escape = Escape

View File

@ -54,5 +54,7 @@ hud-mine-needs_unhandled_case = Needs ???
hud-talk = Talk
hud-trade = Trade
hud-mount = Mount
hud-follow = Follow
hud-stay= Stay
hud-sit = Sit
hud-steer = Steer

View File

@ -1477,6 +1477,14 @@ impl Client {
pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
pub fn set_pet_stay(&mut self, entity: EcsEntity, stay: bool) {
if let Some(uid) = self.state.read_component_copied(entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::SetPetStay(
uid, stay,
)));
}
}
pub fn respawn(&mut self) {
if self
.state

3
common/net/src/synced_components.rs Normal file → Executable file
View File

@ -75,7 +75,6 @@ macro_rules! reexport_comps {
pub use common::comp::*;
use common::link::Is;
use common::mounting::{Mount, Rider, VolumeRider};
// We alias these because the identifier used for the
// component's type is reused as an enum variant name
// in the macro's that we pass to `synced_components!`.
@ -105,8 +104,6 @@ synced_components!(reexport_comps);
use crate::sync::{NetSync, SyncFrom};
// These are synced from any entity within range.
impl NetSync for Body {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -595,6 +595,7 @@ pub struct Agent {
/// required and reset each time the flee timer is reset.
pub flee_from_pos: Option<Pos>,
pub awareness: Awareness,
pub stay_pos: Option<Pos>,
/// Inputs sent up to rtsim
pub rtsim_outbox: Option<VecDeque<NpcInput>>,
}
@ -700,6 +701,7 @@ impl Agent {
sounds_heard: Vec::new(),
position_pid_controller: None,
flee_from_pos: None,
stay_pos: None,
awareness: Awareness::new(0.0),
rtsim_outbox: None,
}

View File

@ -1004,6 +1004,9 @@ pub struct CharacterActivity {
/// `None` means that the look direction should be derived from the
/// orientation
pub look_dir: Option<Dir>,
/// If true, the owner has set this pet to stay at a fixed location and
/// to not engage in combat
pub is_pet_staying: bool,
}
impl Component for CharacterActivity {

View File

@ -146,6 +146,7 @@ pub enum ControlEvent {
Mount(Uid),
MountVolume(VolumePos),
Unmount,
SetPetStay(Uid, bool),
InventoryEvent(InventoryEvent),
GroupManip(GroupManip),
RemoveBuff(BuffKind),

0
common/src/comp/pet.rs Normal file → Executable file
View File

View File

@ -200,6 +200,7 @@ pub enum ServerEvent {
Mount(EcsEntity, EcsEntity),
MountVolume(EcsEntity, VolumePos),
Unmount(EcsEntity),
SetPetStay(EcsEntity, EcsEntity, bool),
Possess(Uid, Uid),
/// Inserts default components for a character when loading into the game
InitCharacterData {

View File

@ -63,6 +63,11 @@ impl<'a> System<'a> for Sys {
}
}
},
ControlEvent::SetPetStay(pet_uid, stay) => {
if let Some(pet_entity) = read_data.id_maps.uid_entity(pet_uid) {
server_emitter.emit(ServerEvent::SetPetStay(entity, pet_entity, stay));
}
},
ControlEvent::RemoveBuff(buff_id) => {
server_emitter.emit(ServerEvent::Buff {
entity,

View File

@ -3,6 +3,9 @@ pub const FLEE_DURATION: f32 = 3.0;
pub const NPC_PICKUP_RANGE: f32 = 2.5;
pub const MAX_PATROL_DIST: f32 = 50.0;
pub const MAX_PATH_DIST: f32 = 170.0;
/// If the pet is any further than this value from its stay position, it will
/// start walking back there
pub const MAX_STAY_DISTANCE: f32 = 10.0;
pub const PARTIAL_PATH_DIST: f32 = 50.0;
pub const SEPARATION_DIST: f32 = 10.0;
pub const SEPARATION_BIAS: f32 = 0.8;

0
server/agent/src/data.rs Normal file → Executable file
View File

52
server/src/events/interaction.rs Normal file → Executable file
View File

@ -17,7 +17,7 @@ use common::{
consts::{MAX_MOUNT_RANGE, MAX_SPRITE_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
event::EventBus,
link::Is,
mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
mounting::{Mount, Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
outcome::Outcome,
rtsim::RtSimVehicle,
terrain::{Block, SpriteKind},
@ -132,7 +132,15 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) {
is_mountable(mount_body, state.ecs().read_storage().get(rider))
});
if (is_pet_of(mount, rider_uid) || is_pet_of(rider, mount_uid)) && can_ride {
let is_stay = state
.ecs()
.read_storage::<comp::Agent>()
.get(mount)
.and_then(|x| x.stay_pos)
.is_some();
if (is_pet_of(mount, rider_uid) || is_pet_of(rider, mount_uid)) && can_ride && !is_stay
{
drop(uids);
let _ = state.link(Mounting {
mount: mount_uid,
@ -202,6 +210,46 @@ pub fn handle_unmount(server: &mut Server, rider: EcsEntity) {
state.ecs().write_storage::<Is<VolumeRider>>().remove(rider);
}
pub fn handle_set_pet_stay(
server: &mut Server,
command_giver: EcsEntity,
pet: EcsEntity,
stay: bool,
) {
let state = server.state_mut();
let positions = state.ecs().read_storage::<Pos>();
let is_owner = state
.ecs()
.uid_from_entity(command_giver)
.map_or(false, |owner_uid| {
matches!(
state
.ecs()
.read_storage::<comp::Alignment>()
.get(pet),
Some(comp::Alignment::Owned(pet_owner)) if *pet_owner == owner_uid,
)
});
let current_pet_position = positions.get(pet).copied();
let stay = stay && current_pet_position.is_some();
if is_owner
&& within_mounting_range(positions.get(command_giver), positions.get(pet))
&& state.ecs().read_storage::<Is<Mount>>().get(pet).is_none()
{
state
.ecs()
.write_storage::<comp::CharacterActivity>()
.get_mut(pet)
.map(|mut activity| activity.is_pet_staying = stay);
state
.ecs()
.write_storage::<comp::Agent>()
.get_mut(pet)
.map(|s| s.stay_pos = current_pet_position);
}
}
fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool {
match (player_position, mount_position) {
(Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_MOUNT_RANGE.powi(2),

View File

@ -23,7 +23,7 @@ use group_manip::handle_group;
use information::handle_site_info;
use interaction::{
handle_create_sprite, handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction,
handle_sound, handle_unmount,
handle_set_pet_stay, handle_sound, handle_unmount,
};
use inventory_manip::handle_inventory;
use invite::{handle_invite, handle_invite_response};
@ -144,6 +144,9 @@ impl Server {
handle_mount_volume(self, mounter, volume)
},
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::SetPetStay(command_giver, pet, stay) => {
handle_set_pet_stay(self, command_giver, pet, stay)
},
ServerEvent::Possess(possessor_uid, possesse_uid) => {
handle_possess(self, possessor_uid, possesse_uid)
},

12
server/src/sys/agent.rs Normal file → Executable file
View File

@ -68,9 +68,11 @@ impl<'a> System<'a> for Sys {
read_data.light_emitter.maybe(),
read_data.groups.maybe(),
read_data.rtsim_entities.maybe(),
!&read_data.is_mounts,
read_data.is_riders.maybe(),
read_data.is_volume_riders.maybe(),
(
!&read_data.is_mounts,
read_data.is_riders.maybe(),
read_data.is_volume_riders.maybe(),
),
)
.par_join()
.for_each_init(
@ -93,9 +95,7 @@ impl<'a> System<'a> for Sys {
light_emitter,
group,
rtsim_entity,
_,
is_rider,
is_volume_rider,
(_, is_rider, is_volume_rider),
)| {
let mut event_emitter = event_bus.emitter();
let mut rng = thread_rng();

37
server/src/sys/agent/behavior_tree.rs Normal file → Executable file
View File

@ -25,7 +25,7 @@ use self::interaction::{
use super::{
consts::{
DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
MAX_STAY_DISTANCE, NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
STD_AWARENESS_DECAY_RATE,
},
data::{AgentData, ReadData, TargetData},
@ -406,13 +406,30 @@ fn do_pickup_loot(bdata: &mut BehaviorData) -> bool {
fn follow_if_far_away(bdata: &mut BehaviorData) -> bool {
if let Some(Target { target, .. }) = bdata.agent.target {
if let Some(tgt_pos) = bdata.read_data.positions.get(target) {
let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) {
bdata
.agent_data
.follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos);
return true;
if let Some(stay_pos) = bdata.agent.stay_pos {
let distance_from_stay = stay_pos.0.distance_squared(bdata.agent_data.pos.0);
bdata.controller.push_action(ControlAction::Sit);
if distance_from_stay > (MAX_STAY_DISTANCE).powi(2) {
bdata.agent_data.follow(
bdata.agent,
bdata.controller,
bdata.read_data,
&stay_pos,
);
return true;
}
} else {
bdata.controller.push_action(ControlAction::Stand);
let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) {
bdata.agent_data.follow(
bdata.agent,
bdata.controller,
bdata.read_data,
tgt_pos,
);
return true;
}
}
}
}
@ -431,8 +448,8 @@ fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
} else {
false
};
if owner_recently_attacked {
let stay = bdata.agent.stay_pos.is_some();
if owner_recently_attacked && !stay {
bdata.agent_data.attack_target_attacker(
bdata.agent,
bdata.read_data,

8
server/src/sys/pets.rs Normal file → Executable file
View File

@ -1,5 +1,5 @@
use common::{
comp::{Alignment, Pet, PhysicsState, Pos},
comp::{Agent, Alignment, Pet, PhysicsState, Pos},
terrain::TerrainGrid,
uid::IdMaps,
};
@ -16,6 +16,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Pos>,
ReadStorage<'a, Alignment>,
ReadStorage<'a, Pet>,
ReadStorage<'a, Agent>,
ReadStorage<'a, PhysicsState>,
Read<'a, IdMaps>,
);
@ -26,7 +27,7 @@ impl<'a> System<'a> for Sys {
fn run(
_job: &mut Job<Self>,
(entities, terrain, mut positions, alignments, pets, physics, id_maps): Self::SystemData,
(entities, terrain, mut positions, alignments, pets, agn, physics, id_maps): Self::SystemData,
) {
const LOST_PET_DISTANCE_THRESHOLD: f32 = 200.0;
@ -57,7 +58,8 @@ impl<'a> System<'a> for Sys {
.collect();
for (pet_entity, owner_pos) in lost_pets.iter() {
if let Some(mut pet_pos) = positions.get_mut(*pet_entity) {
let stay = agn.get(*pet_entity).and_then(|x| x.stay_pos).is_some();
if let Some(mut pet_pos) = positions.get_mut(*pet_entity) && !stay{
// Move the pets to their owner's position
// TODO: Create a teleportation event to handle this instead of
// processing the entity position move here

View File

@ -81,6 +81,8 @@ pub enum GameInput {
ToggleLantern,
#[strum(serialize = "gameinput-mount")]
Mount,
#[strum(serialize = "gameinput-stayfollow")]
StayFollow,
#[strum(serialize = "gameinput-chat")]
Chat,
#[strum(serialize = "gameinput-command")]

39
voxygen/src/hud/mod.rs Normal file → Executable file
View File

@ -1505,6 +1505,7 @@ impl Hud {
let is_mounts = ecs.read_storage::<Is<Mount>>();
let is_riders = ecs.read_storage::<Is<Rider>>();
let stances = ecs.read_storage::<comp::Stance>();
let char_activities = ecs.read_storage::<comp::CharacterActivity>();
let time = ecs.read_resource::<Time>();
// Check if there was a persistence load error of the skillset, and if so
@ -2254,7 +2255,6 @@ impl Hud {
}
let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars
for (
entity,
@ -2272,6 +2272,7 @@ impl Hud {
dist_sqr,
alignment,
is_mount,
character_activity,
) in (
&entities,
&pos,
@ -2286,6 +2287,7 @@ impl Hud {
&mut hp_floater_lists,
&uids,
&inventories,
char_activities.maybe(),
poises.maybe(),
(
alignments.maybe(),
@ -2314,6 +2316,7 @@ impl Hud {
hpfl,
uid,
inventory,
character_activity,
poise,
(alignment, is_mount, is_rider, stance),
)| {
@ -2322,6 +2325,7 @@ impl Hud {
let in_group = client.group_members().contains_key(uid);
let is_me = entity == me;
let dist_sqr = pos.distance_squared(player_pos);
// Determine whether to display nametag and healthbar based on whether the
// entity is mounted, has been damaged, is targeted/selected, or is in your
// group
@ -2375,8 +2379,22 @@ impl Hud {
};
(info.is_some() || bubble.is_some()).then_some({
(
entity, pos, info, bubble, stats, skill_set, health, buffs, scale,
body, hpfl, in_group, dist_sqr, alignment, is_mount,
entity,
pos,
info,
bubble,
stats,
skill_set,
health,
buffs,
scale,
body,
hpfl,
in_group,
dist_sqr,
alignment,
is_mount,
character_activity,
)
})
},
@ -2429,8 +2447,21 @@ impl Hud {
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(),
));
}
options
},

View File

@ -13,14 +13,14 @@ use vek::*;
use client::{self, Client};
use common::{
comp,
comp::{
self,
dialogue::Subject,
inventory::slot::{EquipSlot, Slot},
invite::InviteKind,
item::{tool::ToolKind, ItemDesc},
ChatType, Content, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats,
UtteranceKind, Vel,
CharacterActivity, ChatType, Content, InputKind, InventoryUpdateEvent, Pos, PresenceKind,
Stats, UtteranceKind, Vel,
},
consts::MAX_MOUNT_RANGE,
event::UpdateCharacterMetadata,
@ -32,6 +32,7 @@ use common::{
trade::TradeResult,
util::{Dir, Plane},
vol::ReadVol,
CachedSpatialGrid,
};
use common_base::{prof_span, span};
use common_net::{msg::server::InviteAnswer, sync::WorldSyncExt};
@ -938,6 +939,45 @@ impl PlayState for SessionState {
}
}
},
GameInput::StayFollow if state => {
let mut client = self.client.borrow_mut();
let player_pos = client
.state()
.read_storage::<Pos>()
.get(client.entity())
.copied();
let mut close_pet = None;
if let Some(player_pos) = player_pos {
let positions = client.state().read_storage::<Pos>();
close_pet = client.state().ecs().read_resource::<CachedSpatialGrid>().0
.in_circle_aabr(player_pos.0.xy(), MAX_MOUNT_RANGE)
.filter(|e|
*e != client.entity()
)
.filter(|e|
matches!(client.state().ecs().read_storage::<comp::Alignment>().get(*e),
Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid())
)
.filter(|e|
client.state().ecs().read_storage::<Is<Mount>>().get(*e).is_none()
)
.min_by_key(|e| {
OrderedFloat(positions
.get(*e)
.map_or(MAX_MOUNT_RANGE * MAX_MOUNT_RANGE, |x| {
player_pos.0.distance_squared(x.0)
}
))
});
}
if let Some(pet_entity) = close_pet && client.state().read_storage::<Is<Mount>>().get(pet_entity).is_none() {
let is_staying = client.state()
.read_component_copied::<CharacterActivity>(pet_entity)
.map_or(false, |activity| activity.is_pet_staying);
client.set_pet_stay(pet_entity, !is_staying);
}
},
GameInput::Interact => {
if state {
let mut client = self.client.borrow_mut();

View File

@ -148,6 +148,7 @@ impl ControlSettings {
GameInput::Sneak => Some(KeyMouse::Key(VirtualKeyCode::LShift)),
GameInput::ToggleLantern => Some(KeyMouse::Key(VirtualKeyCode::G)),
GameInput::Mount => Some(KeyMouse::Key(VirtualKeyCode::F)),
GameInput::StayFollow => Some(KeyMouse::Key(VirtualKeyCode::V)),
GameInput::Map => Some(KeyMouse::Key(VirtualKeyCode::M)),
GameInput::Bag => Some(KeyMouse::Key(VirtualKeyCode::B)),
GameInput::Trade => Some(KeyMouse::Key(VirtualKeyCode::T)),

View File

@ -320,6 +320,7 @@ pub mod con_settings {
pub sneak: Button,
pub toggle_lantern: Button,
pub mount: Button,
pub stayfollow: Button,
pub map: Button,
pub bag: Button,
pub quest_log: Button,
@ -424,6 +425,7 @@ pub mod con_settings {
sneak: Button::Simple(GilButton::LeftThumb),
toggle_lantern: Button::Simple(GilButton::Unknown),
mount: Button::Simple(GilButton::South),
stayfollow: Button::Simple(GilButton::Unknown),
map: Button::Simple(GilButton::Unknown),
bag: Button::Simple(GilButton::East),
quest_log: Button::Simple(GilButton::Unknown),