Add stay/follow option for pets

This commit is contained in:
TelepathicWalrus 2023-04-30 16:45:21 +01:00 committed by Maxicarlos08
parent c6e9d3a202
commit e611d695b1
No known key found for this signature in database
20 changed files with 158 additions and 16 deletions

View File

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

View File

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

View File

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

View File

@ -46,6 +46,7 @@ macro_rules! synced_components {
beam_segment: BeamSegment, beam_segment: BeamSegment,
alignment: Alignment, alignment: Alignment,
stance: Stance, stance: Stance,
pet_state: PetStates,
// TODO: change this to `SyncFrom::ClientEntity` and sync the bare minimum // TODO: change this to `SyncFrom::ClientEntity` and sync the bare minimum
// from other entities (e.g. just keys needed to show appearance // from other entities (e.g. just keys needed to show appearance
// based on their loadout). Also, it looks like this actually has // based on their loadout). Also, it looks like this actually has
@ -75,7 +76,7 @@ macro_rules! reexport_comps {
pub use common::comp::*; pub use common::comp::*;
use common::link::Is; use common::link::Is;
use common::mounting::{Mount, Rider, VolumeRider}; use common::mounting::{Mount, Rider, VolumeRider};
use common::comp::pet::PetState;
// We alias these because the identifier used for the // We alias these because the identifier used for the
// component's type is reused as an enum variant name // component's type is reused as an enum variant name
// in the macro's that we pass to `synced_components!`. // in the macro's that we pass to `synced_components!`.
@ -85,6 +86,7 @@ macro_rules! reexport_comps {
pub type IsMount = Is<Mount>; pub type IsMount = Is<Mount>;
pub type IsRider = Is<Rider>; pub type IsRider = Is<Rider>;
pub type IsVolumeRider = Is<VolumeRider>; pub type IsVolumeRider = Is<VolumeRider>;
pub type PetStates = PetState;
} }
// Re-export all the component types. So that uses of `synced_components!` outside this // Re-export all the component types. So that uses of `synced_components!` outside this
@ -105,7 +107,11 @@ synced_components!(reexport_comps);
use crate::sync::{NetSync, SyncFrom}; use crate::sync::{NetSync, SyncFrom};
// These are synced from any entity within range. // These are synced from any entity within range.
impl NetSync for PetStates {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Body { impl NetSync for Body {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;

View File

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

View File

@ -1,7 +1,8 @@
use crate::comp::{body::Body, phys::Mass, quadruped_medium, quadruped_small}; use crate::comp::{body::Body, phys::Mass, quadruped_medium, quadruped_small};
use crossbeam_utils::atomic::AtomicCell; use crossbeam_utils::atomic::AtomicCell;
use specs::Component; use specs::{Component, DerefFlaggedStorage};
use std::{num::NonZeroU64, sync::Arc}; use std::{num::NonZeroU64, sync::Arc};
use serde::{Deserialize, Serialize};
pub type PetId = AtomicCell<Option<NonZeroU64>>; pub type PetId = AtomicCell<Option<NonZeroU64>>;
@ -107,3 +108,33 @@ impl Component for Pet {
// isn't worth using `DenseVecStorage` here. // isn't worth using `DenseVecStorage` here.
type Storage = specs::VecStorage<Self>; type Storage = specs::VecStorage<Self>;
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum StayFollow{
Stay,
Follow,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PetState{
pub stay: bool,
}
impl Default for PetState {
fn default() -> Self {
Self {
stay: false,
}
}
}
impl PetState {
pub fn get_state(&self) -> bool{
self.stay
}
}
impl Component for PetState {
type Storage = DerefFlaggedStorage<Self>;
}

View File

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

View File

@ -203,6 +203,7 @@ impl State {
ecs.register::<Is<Mount>>(); ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>(); ecs.register::<Is<Rider>>();
ecs.register::<Is<VolumeRider>>(); ecs.register::<Is<VolumeRider>>();
ecs.register::<comp::pet::PetState>();
ecs.register::<comp::Mass>(); ecs.register::<comp::Mass>();
ecs.register::<comp::Density>(); ecs.register::<comp::Density>();
ecs.register::<comp::Collider>(); ecs.register::<comp::Collider>();

View File

@ -63,6 +63,13 @@ impl<'a> System<'a> for Sys {
} }
} }
}, },
ControlEvent::ToggleStay(pet_uid) => {
if let Some(pet_entity) = read_data
.uid_allocator
.retrieve_entity_internal(pet_uid.id()){
server_emitter.emit(ServerEvent::ToggleStay(pet_entity));
}
}
ControlEvent::RemoveBuff(buff_id) => { ControlEvent::RemoveBuff(buff_id) => {
server_emitter.emit(ServerEvent::Buff { server_emitter.emit(ServerEvent::Buff {
entity, entity,

View File

@ -14,7 +14,7 @@ use common::{
}, },
ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory, ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory,
LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, Scale, SkillSet, Stance, LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, Scale, SkillSet, Stance,
Stats, Vel, Stats, Vel, pet::PetState,
}, },
consts::GRAVITY, consts::GRAVITY,
link::Is, link::Is,
@ -52,6 +52,7 @@ pub struct AgentData<'a> {
pub light_emitter: Option<&'a LightEmitter>, pub light_emitter: Option<&'a LightEmitter>,
pub glider_equipped: bool, pub glider_equipped: bool,
pub is_gliding: bool, pub is_gliding: bool,
pub is_stay: bool,
pub health: Option<&'a Health>, pub health: Option<&'a Health>,
pub char_state: &'a CharacterState, pub char_state: &'a CharacterState,
pub active_abilities: &'a ActiveAbilities, pub active_abilities: &'a ActiveAbilities,
@ -347,6 +348,7 @@ pub struct ReadData<'a> {
pub is_mounts: ReadStorage<'a, Is<Mount>>, pub is_mounts: ReadStorage<'a, Is<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>, pub is_riders: ReadStorage<'a, Is<Rider>>,
pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>, pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
pub pet_states: ReadStorage<'a, PetState>,
pub time_of_day: Read<'a, TimeOfDay>, pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>, pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]

View File

@ -10,7 +10,7 @@ use common::{
inventory::slot::EquipSlot, inventory::slot::EquipSlot,
item::{flatten_counted_items, MaterialStatManifest}, item::{flatten_counted_items, MaterialStatManifest},
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
pet::is_mountable, pet::{is_mountable, PetState},
tool::{AbilityMap, ToolKind}, tool::{AbilityMap, ToolKind},
Inventory, LootOwner, Pos, SkillGroupKind, Inventory, LootOwner, Pos, SkillGroupKind,
}, },
@ -202,6 +202,18 @@ pub fn handle_unmount(server: &mut Server, rider: EcsEntity) {
state.ecs().write_storage::<Is<VolumeRider>>().remove(rider); state.ecs().write_storage::<Is<VolumeRider>>().remove(rider);
} }
pub fn handle_toggle_stay(server: &mut Server, pet: EcsEntity){
let state = server.state_mut();
if state.ecs()
.read_storage::<PetState>()
.get(pet)
.map_or(false, |s| s.stay){
let _ = state.ecs().write_storage::<PetState>().insert(pet, PetState { stay: false });
} else {
let _ = state.ecs().write_storage::<PetState>().insert(pet, PetState { stay: true });
}
}
fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool { fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool {
match (player_position, mount_position) { match (player_position, mount_position) {
(Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_MOUNT_RANGE.powi(2), (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 information::handle_site_info;
use interaction::{ use interaction::{
handle_create_sprite, handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, handle_create_sprite, handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction,
handle_sound, handle_unmount, handle_sound, handle_unmount, handle_toggle_stay,
}; };
use inventory_manip::handle_inventory; use inventory_manip::handle_inventory;
use invite::{handle_invite, handle_invite_response}; use invite::{handle_invite, handle_invite_response};
@ -144,6 +144,7 @@ impl Server {
handle_mount_volume(self, mounter, volume) handle_mount_volume(self, mounter, volume)
}, },
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::ToggleStay(pet) => handle_toggle_stay(self, pet),
ServerEvent::Possess(possessor_uid, possesse_uid) => { ServerEvent::Possess(possessor_uid, possesse_uid) => {
handle_possess(self, possessor_uid, possesse_uid) handle_possess(self, possessor_uid, possesse_uid)
}, },

View File

@ -71,6 +71,7 @@ impl<'a> System<'a> for Sys {
!&read_data.is_mounts, !&read_data.is_mounts,
read_data.is_riders.maybe(), read_data.is_riders.maybe(),
read_data.is_volume_riders.maybe(), read_data.is_volume_riders.maybe(),
read_data.pet_states.maybe(),
) )
.par_join() .par_join()
.for_each_init( .for_each_init(
@ -96,6 +97,7 @@ impl<'a> System<'a> for Sys {
_, _,
is_rider, is_rider,
is_volume_rider, is_volume_rider,
pet_state,
)| { )| {
let mut event_emitter = event_bus.emitter(); let mut event_emitter = event_bus.emitter();
let mut rng = thread_rng(); let mut rng = thread_rng();
@ -158,6 +160,8 @@ impl<'a> System<'a> for Sys {
.map_or(false, |item| { .map_or(false, |item| {
matches!(&*item.kind(), comp::item::ItemKind::Glider) matches!(&*item.kind(), comp::item::ItemKind::Glider)
}); });
let is_stay = pet_state
.map_or(false, |s| s.stay);
let is_gliding = matches!( let is_gliding = matches!(
read_data.char_states.get(entity), read_data.char_states.get(entity),
@ -229,6 +233,7 @@ impl<'a> System<'a> for Sys {
light_emitter, light_emitter,
glider_equipped, glider_equipped,
is_gliding, is_gliding,
is_stay,
health: read_data.healths.get(entity), health: read_data.healths.get(entity),
char_state, char_state,
active_abilities, active_abilities,

View File

@ -407,8 +407,8 @@ fn follow_if_far_away(bdata: &mut BehaviorData) -> bool {
if let Some(Target { target, .. }) = bdata.agent.target { if let Some(Target { target, .. }) = bdata.agent.target {
if let Some(tgt_pos) = bdata.read_data.positions.get(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); let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
let stay = bdata.agent_data.is_stay;
if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) { if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) && !stay{
bdata bdata
.agent_data .agent_data
.follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos); .follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos);
@ -431,8 +431,8 @@ fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
} else { } else {
false false
}; };
let stay = bdata.agent_data.is_stay;
if owner_recently_attacked { if owner_recently_attacked && !stay{
bdata.agent_data.attack_target_attacker( bdata.agent_data.attack_target_attacker(
bdata.agent, bdata.agent,
bdata.read_data, bdata.read_data,

View File

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

View File

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

View File

@ -1504,6 +1504,7 @@ impl Hud {
let alignments = ecs.read_storage::<comp::Alignment>(); let alignments = ecs.read_storage::<comp::Alignment>();
let is_mounts = ecs.read_storage::<Is<Mount>>(); let is_mounts = ecs.read_storage::<Is<Mount>>();
let is_riders = ecs.read_storage::<Is<Rider>>(); let is_riders = ecs.read_storage::<Is<Rider>>();
let is_stay = ecs.read_storage::<comp::pet::PetState>();
let stances = ecs.read_storage::<comp::Stance>(); let stances = ecs.read_storage::<comp::Stance>();
let time = ecs.read_resource::<Time>(); let time = ecs.read_resource::<Time>();
@ -2254,7 +2255,6 @@ impl Hud {
} }
let speech_bubbles = &self.speech_bubbles; let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars // Render overhead name tags and health bars
for ( for (
entity, entity,
@ -2317,11 +2317,13 @@ impl Hud {
poise, poise,
(alignment, is_mount, is_rider, stance), (alignment, is_mount, is_rider, stance),
)| { )| {
// Use interpolated position if available // Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos); let pos = interpolated.map_or(pos.0, |i| i.pos);
let in_group = client.group_members().contains_key(uid); let in_group = client.group_members().contains_key(uid);
let is_me = entity == me; let is_me = entity == me;
let dist_sqr = pos.distance_squared(player_pos); let dist_sqr = pos.distance_squared(player_pos);
// Determine whether to display nametag and healthbar based on whether the // Determine whether to display nametag and healthbar based on whether the
// entity is mounted, has been damaged, is targeted/selected, or is in your // entity is mounted, has been damaged, is targeted/selected, or is in your
// group // group
@ -2429,8 +2431,33 @@ impl Hud {
options.push(( options.push((
GameInput::Mount, GameInput::Mount,
i18n.get_msg("hud-mount").to_string(), i18n.get_msg("hud-mount").to_string(),
)) ));
} }
}
let p = entity;
let s = is_stay.get(p)
.map(|st| st.stay);
match s {
Some(false) => {
options.push((
GameInput::StayFollow,
i18n.get_msg("hud-stay").to_string(),
))
},
Some(true) => {
options.push((
GameInput::StayFollow,
i18n.get_msg("hud-follow").to_string(),
))
},
None => {
options.push((
GameInput::StayFollow,
i18n.get_msg("hud-stay").to_string(),
))
},
} }
options options
}, },

View File

@ -938,6 +938,38 @@ 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();
if let Some(player_pos) = player_pos {
// Find closest mountable entity
let closest_pet = (
&client.state().ecs().entities(),
&client.state().ecs().read_storage::<Pos>(),
// TODO: More cleverly filter by things that can actually be mounted
client.state().ecs().read_storage::<comp::Alignment>().maybe(),
)
.join()
.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)| {
*dist_sqr < MAX_MOUNT_RANGE.powi(2)
})
.min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr));
if let Some((pet_entity, _)) = closest_pet
{
client.toggle_stay(pet_entity);
}
}
},
GameInput::Interact => { GameInput::Interact => {
if state { if state {
let mut client = self.client.borrow_mut(); let mut client = self.client.borrow_mut();

View File

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

View File

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