From 6674cce2ccf856786382d2ac40d3c37e87c3aa12 Mon Sep 17 00:00:00 2001 From: Isse Date: Mon, 17 Apr 2023 15:38:41 +0200 Subject: [PATCH] intercat with sprites on ships --- assets/common/manifests/ship_manifest.ron | 3 + .../common/voxel/airship_human/structure.vox | 4 +- .../voxygen/voxel/sprite/furniture/helm.vox | 3 + assets/voxygen/voxel/sprite_manifest.ron | 11 + client/src/lib.rs | 46 ++- common/net/src/synced_components.rs | 8 +- common/src/comp/ability.rs | 1 - common/src/comp/character_state.rs | 9 - common/src/comp/controller.rs | 7 +- common/src/comp/phys.rs | 10 +- common/src/consts.rs | 1 + common/src/event.rs | 2 + common/src/mounting.rs | 277 +++++++++++++++++- common/src/states/behavior.rs | 16 +- common/src/states/dance.rs | 7 - common/src/states/glide_wield.rs | 7 - common/src/states/idle.rs | 7 - common/src/states/mod.rs | 1 - common/src/states/mount_sprite.rs | 60 ---- common/src/states/skate.rs | 7 - common/src/states/talk.rs | 8 - common/src/states/utils.rs | 49 +--- common/src/states/wielding.rs | 7 - common/src/terrain/block.rs | 11 +- common/src/terrain/sprite.rs | 55 ++-- common/src/util/find_dist.rs | 12 + common/state/src/state.rs | 5 +- common/systems/src/character_behavior.rs | 4 +- common/systems/src/controller.rs | 18 +- common/systems/src/mount.rs | 96 +++++- common/systems/src/phys.rs | 38 ++- common/systems/src/stats.rs | 2 +- server/agent/src/data.rs | 3 +- server/src/cmd.rs | 1 + server/src/events/interaction.rs | 101 ++++--- server/src/events/inventory_manip.rs | 13 +- server/src/events/mod.rs | 7 +- server/src/state_ext.rs | 3 +- server/src/sys/agent.rs | 13 + server/src/sys/msg/in_game.rs | 7 +- voxygen/src/hud/crafting.rs | 3 +- voxygen/src/hud/mod.rs | 64 ++-- voxygen/src/scene/figure/mod.rs | 25 +- voxygen/src/scene/terrain/watcher.rs | 25 +- voxygen/src/session/interactable.rs | 154 ++++++---- voxygen/src/session/mod.rs | 26 +- 46 files changed, 796 insertions(+), 441 deletions(-) create mode 100644 assets/voxygen/voxel/sprite/furniture/helm.vox delete mode 100644 common/src/states/mount_sprite.rs diff --git a/assets/common/manifests/ship_manifest.ron b/assets/common/manifests/ship_manifest.ron index 3a50799ba5..a35bb7a264 100644 --- a/assets/common/manifests/ship_manifest.ron +++ b/assets/common/manifests/ship_manifest.ron @@ -19,6 +19,9 @@ custom_indices: { 1: Air(ChairSingle, 4), + 2: Air(Helm, 0), + 3: Air(Door, 4), + 8: Air(Door, 0), }, ), AirBalloon: ( diff --git a/assets/common/voxel/airship_human/structure.vox b/assets/common/voxel/airship_human/structure.vox index f567305bd4..31d1adcf51 100644 --- a/assets/common/voxel/airship_human/structure.vox +++ b/assets/common/voxel/airship_human/structure.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cee69dda42513edc342129fe9a3d03a120f71b35000f0c8ce602888b5ac33dd5 -size 99391 +oid sha256:45dcf85530d5a788a160f78a5c6ffe47cb468e96398d4d3479e120b805b9d914 +size 99675 diff --git a/assets/voxygen/voxel/sprite/furniture/helm.vox b/assets/voxygen/voxel/sprite/furniture/helm.vox new file mode 100644 index 0000000000..da1303d303 --- /dev/null +++ b/assets/voxygen/voxel/sprite/furniture/helm.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f2d8ab37e37d048f12759106ee0cd8c20afabf51c78c6a983b316efba50bb4a +size 2136 diff --git a/assets/voxygen/voxel/sprite_manifest.ron b/assets/voxygen/voxel/sprite_manifest.ron index 47a24f4952..34cd801de1 100644 --- a/assets/voxygen/voxel/sprite_manifest.ron +++ b/assets/voxygen/voxel/sprite_manifest.ron @@ -1924,6 +1924,17 @@ ChairDouble: Some(( ], wind_sway: 0.0, )), +// Helm +Helm: Some(( + variations: [ + ( + model: "voxygen.voxel.sprite.furniture.helm", + offset: (-5.5, -5.5, 0.0), + lod_axes: (1.0, 1.0, 1.0), + ), + ], + wind_sway: 0.0, +)), // CoatRack CoatRack: Some(( variations: [ diff --git a/client/src/lib.rs b/client/src/lib.rs index ef7f87c9ca..0c0c3de1b7 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -37,7 +37,7 @@ use common::{ grid::Grid, link::Is, lod, - mounting::Rider, + mounting::{Rider, VolumePos, VolumeRider}, outcome::Outcome, recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook}, resources::{GameMode, PlayerEntity, Time, TimeOfDay}, @@ -1185,7 +1185,7 @@ impl Client { &mut self, recipe: &str, slots: Vec<(u32, InvSlotId)>, - craft_sprite: Option<(Vec3, SpriteKind)>, + craft_sprite: Option<(VolumePos, SpriteKind)>, amount: u32, ) -> bool { let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount); @@ -1217,7 +1217,7 @@ impl Client { /// Salvage the item in the given inventory slot. `salvage_pos` should be /// the location of a relevant crafting station within range of the player. - pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3) -> bool { + pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool { let is_salvageable = self.can_salvage_item(slot); if is_salvageable { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent( @@ -1239,7 +1239,7 @@ impl Client { &mut self, primary_component: InvSlotId, secondary_component: InvSlotId, - sprite_pos: Option>, + sprite_pos: Option, ) -> bool { let inventories = self.inventories(); let inventory = inventories.get(self.entity()); @@ -1288,7 +1288,7 @@ impl Client { material: InvSlotId, modifier: Option, slots: Vec<(u32, InvSlotId)>, - sprite_pos: Option>, + sprite_pos: Option, ) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent( InventoryEvent::CraftRecipe { @@ -1309,7 +1309,7 @@ impl Client { &mut self, item: Slot, slots: Vec<(u32, InvSlotId)>, - sprite_pos: Vec3, + sprite_pos: VolumePos, ) -> bool { let is_repairable = { let inventories = self.inventories(); @@ -1448,6 +1448,12 @@ impl Client { .read_storage::>() .get(self.entity()) .is_some() + || self + .state + .ecs() + .read_storage::>() + .get(self.entity()) + .is_some() } pub fn is_lantern_enabled(&self) -> bool { @@ -1464,6 +1470,12 @@ impl Client { } } + pub fn mount_volume(&mut self, volume_pos: VolumePos) { + self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume( + volume_pos, + ))); + } + pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); } pub fn respawn(&mut self) { @@ -1530,7 +1542,7 @@ impl Client { .ecs() .read_storage::() .get(self.entity()) - .map(|cs| matches!(cs, CharacterState::Sit | CharacterState::MountSprite(_))); + .map(|cs| matches!(cs, CharacterState::Sit)); match is_sitting { Some(true) => self.control_action(ControlAction::Stand), @@ -1539,22 +1551,6 @@ impl Client { } } - pub fn stand_if_mounted(&mut self) -> bool { - let is_sitting = self - .state - .ecs() - .read_storage::() - .get(self.entity()) - .map(|cs| matches!(cs, CharacterState::MountSprite(_))); - - match is_sitting { - Some(true) => self.control_action(ControlAction::Stand), - Some(false) => {}, - None => warn!("Can't stand, client entity doesn't have a `CharacterState`"), - } - is_sitting.unwrap_or(false) - } - pub fn toggle_dance(&mut self) { let is_dancing = self .state @@ -1738,10 +1734,6 @@ impl Client { ))); } - pub fn mount_sprite(&mut self, pos: Vec3) { - self.control_action(ControlAction::MountSprite(pos)); - } - pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) { let auxiliary_key = self .inventories() diff --git a/common/net/src/synced_components.rs b/common/net/src/synced_components.rs index 71bcf20610..63b5fc1e5e 100644 --- a/common/net/src/synced_components.rs +++ b/common/net/src/synced_components.rs @@ -34,6 +34,7 @@ macro_rules! synced_components { group: Group, is_mount: IsMount, is_rider: IsRider, + is_volume_rider: IsVolumeRider, mass: Mass, density: Density, collider: Collider, @@ -72,7 +73,7 @@ macro_rules! reexport_comps { mod inner { pub use common::comp::*; use common::link::Is; - use common::mounting::{Mount, Rider}; + 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 @@ -82,6 +83,7 @@ macro_rules! reexport_comps { // we can't just re-export all the types directly from `common::comp`. pub type IsMount = Is; pub type IsRider = Is; + pub type IsVolumeRider = Is; } // Re-export all the component types. So that uses of `synced_components!` outside this @@ -178,6 +180,10 @@ impl NetSync for IsRider { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } +impl NetSync for IsVolumeRider { + const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; +} + impl NetSync for Mass { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 38675ba8b5..3ff759d6d4 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -470,7 +470,6 @@ impl From<&CharacterState> for CharacterAbilityType { | CharacterState::SpriteSummon(_) | CharacterState::UseItem(_) | CharacterState::SpriteInteract(_) - | CharacterState::MountSprite(_) | CharacterState::Skate(_) | CharacterState::Wallrun(_) => Self::Other, } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 2d78018a50..e914d4b32b 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -130,8 +130,6 @@ pub enum CharacterState { /// Handles logic for interacting with a sprite, e.g. using a chest or /// picking a plant SpriteInteract(sprite_interact::Data), - // Mounted to a sprite - MountSprite(mount_sprite::Data), /// Runs on the wall Wallrun(wallrun::Data), /// Ice skating or skiing @@ -472,7 +470,6 @@ impl CharacterState { CharacterState::SpriteSummon(data) => data.behavior(j, output_events), CharacterState::UseItem(data) => data.behavior(j, output_events), CharacterState::SpriteInteract(data) => data.behavior(j, output_events), - CharacterState::MountSprite(data) => data.behavior(j, output_events), CharacterState::Skate(data) => data.behavior(j, output_events), CharacterState::Music(data) => data.behavior(j, output_events), CharacterState::FinisherMelee(data) => data.behavior(j, output_events), @@ -527,7 +524,6 @@ impl CharacterState { CharacterState::SpriteSummon(data) => data.handle_event(j, output_events, action), CharacterState::UseItem(data) => data.handle_event(j, output_events, action), CharacterState::SpriteInteract(data) => data.handle_event(j, output_events, action), - CharacterState::MountSprite(data) => data.handle_event(j, output_events, action), CharacterState::Skate(data) => data.handle_event(j, output_events, action), CharacterState::Music(data) => data.handle_event(j, output_events, action), CharacterState::FinisherMelee(data) => data.handle_event(j, output_events, action), @@ -582,7 +578,6 @@ impl CharacterState { CharacterState::SpriteSummon(data) => Some(data.static_data.ability_info), CharacterState::UseItem(_) => None, CharacterState::SpriteInteract(_) => None, - CharacterState::MountSprite(_) => None, CharacterState::FinisherMelee(data) => Some(data.static_data.ability_info), CharacterState::Music(data) => Some(data.static_data.ability_info), CharacterState::DiveMelee(data) => Some(data.static_data.ability_info), @@ -628,7 +623,6 @@ impl CharacterState { CharacterState::SpriteSummon(data) => Some(data.stage_section), CharacterState::UseItem(data) => Some(data.stage_section), CharacterState::SpriteInteract(data) => Some(data.stage_section), - CharacterState::MountSprite(_) => None, CharacterState::FinisherMelee(data) => Some(data.stage_section), CharacterState::Music(data) => Some(data.stage_section), CharacterState::DiveMelee(data) => Some(data.stage_section), @@ -800,7 +794,6 @@ impl CharacterState { recover: Some(data.static_data.recover_duration), ..Default::default() }), - CharacterState::MountSprite(_) => None, CharacterState::FinisherMelee(data) => Some(DurationsInfo { buildup: Some(data.static_data.buildup_duration), action: Some(data.static_data.swing_duration), @@ -869,7 +862,6 @@ impl CharacterState { CharacterState::SpriteSummon(data) => Some(data.timer), CharacterState::UseItem(data) => Some(data.timer), CharacterState::SpriteInteract(data) => Some(data.timer), - CharacterState::MountSprite(_) => None, CharacterState::FinisherMelee(data) => Some(data.timer), CharacterState::Music(data) => Some(data.timer), CharacterState::DiveMelee(data) => Some(data.timer), @@ -889,7 +881,6 @@ impl CharacterState { CharacterState::GlideWield(_) => None, CharacterState::Stunned(_) => None, CharacterState::Sit => None, - CharacterState::MountSprite(_) => None, CharacterState::Dance => None, CharacterState::BasicBlock(_) => None, CharacterState::Roll(_) => None, diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 6210401ef7..112aff9f76 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -9,6 +9,7 @@ use crate::{ invite::{InviteKind, InviteResponse}, BuffKind, }, + mounting::VolumePos, trade::{TradeAction, TradeId}, uid::Uid, util::Dir, @@ -28,7 +29,7 @@ pub enum InventoryEvent { Sort, CraftRecipe { craft_event: CraftEvent, - craft_sprite: Option>, + craft_sprite: Option, }, } @@ -57,7 +58,7 @@ pub enum InventoryManip { Sort, CraftRecipe { craft_event: CraftEvent, - craft_sprite: Option>, + craft_sprite: Option, }, SwapEquippedWeapons, } @@ -143,6 +144,7 @@ pub enum ControlEvent { InviteResponse(InviteResponse), PerformTradeAction(TradeId, TradeAction), Mount(Uid), + MountVolume(VolumePos), Unmount, InventoryEvent(InventoryEvent), GroupManip(GroupManip), @@ -165,7 +167,6 @@ pub enum ControlAction { GlideWield, Unwield, Sit, - MountSprite(Vec3), Dance, Sneak, Stand, diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index ebe0dc18eb..31282be1f9 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -1,4 +1,4 @@ -use super::{Fluid, Ori}; +use super::{ship::figuredata::ShipSpec, Fluid, Ori}; use crate::{ comp::{body::ship::figuredata::VoxelCollider, inventory::item::armor::Friction}, consts::WATER_DENSITY, @@ -127,6 +127,14 @@ pub enum Collider { impl Collider { pub fn is_voxel(&self) -> bool { matches!(self, Collider::Voxel { .. } | Collider::Volume(_)) } + pub fn get_vol<'a>(&'a self, ship_spec: &'a ShipSpec) -> Option<&'a VoxelCollider> { + match self { + Collider::Voxel { id } => ship_spec.colliders.get(id), + Collider::Volume(vol) => Some(&**vol), + _ => None, + } + } + pub fn bounding_radius(&self) -> f32 { match self { Collider::Voxel { .. } | Collider::Volume(_) => 1.0, diff --git a/common/src/consts.rs b/common/src/consts.rs index 88895900f5..41c4171a8e 100644 --- a/common/src/consts.rs +++ b/common/src/consts.rs @@ -1,6 +1,7 @@ // 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 = 5.0; +pub const MAX_SPRITE_MOUNT_RANGE: f32 = 1.5; pub const MAX_TRADE_RANGE: f32 = 20.0; pub const GRAVITY: f32 = 25.0; diff --git a/common/src/event.rs b/common/src/event.rs index 2cdfed9c49..ed44b7b31f 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -8,6 +8,7 @@ use crate::{ DisconnectReason, Ori, Pos, }, lottery::LootSpec, + mounting::VolumePos, outcome::Outcome, rtsim::{RtSimEntity, RtSimVehicle}, terrain::SpriteKind, @@ -195,6 +196,7 @@ pub enum ServerEvent { InitiateInvite(EcsEntity, Uid, InviteKind), ProcessTradeAction(EcsEntity, TradeId, TradeAction), Mount(EcsEntity, EcsEntity), + MountVolume(EcsEntity, VolumePos), Unmount(EcsEntity), Possess(Uid, Uid), /// Inserts default components for a character when loading into the game diff --git a/common/src/mounting.rs b/common/src/mounting.rs index cd45231c6a..0a087fa693 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -1,11 +1,18 @@ use crate::{ - comp, + comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, link::{Is, Link, LinkHandle, Role}, - terrain::TerrainGrid, + terrain::{Block, TerrainGrid}, uid::{Uid, UidAllocator}, + vol::ReadVol, }; +use hashbrown::HashSet; use serde::{Deserialize, Serialize}; -use specs::{saveload::MarkerAllocator, Entities, Read, ReadExpect, ReadStorage, WriteStorage}; +use specs::{ + saveload::MarkerAllocator, + storage::{GenericWriteStorage, MaskedStorage}, + Component, DenseVecStorage, Entities, Read, ReadExpect, ReadStorage, Storage, Write, + WriteStorage, +}; use vek::*; #[derive(Serialize, Deserialize, Debug)] @@ -39,6 +46,7 @@ impl Link for Mounting { Read<'a, UidAllocator>, WriteStorage<'a, Is>, WriteStorage<'a, Is>, + ReadStorage<'a, Is>, ); type DeleteData<'a> = ( Read<'a, UidAllocator>, @@ -59,7 +67,7 @@ impl Link for Mounting { fn create( this: &LinkHandle, - (uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>, + (uid_allocator, mut is_mounts, mut is_riders, is_volume_rider): Self::CreateData<'_>, ) -> Result<(), Self::Error> { let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); @@ -67,8 +75,11 @@ 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.get(entity).is_none() && is_riders.get(entity).is_none(); + 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 @@ -145,3 +156,257 @@ impl Link for Mounting { }); } } + +#[derive(Serialize, Deserialize, Debug)] +pub struct VolumeRider; + +impl Role for VolumeRider { + type Link = VolumeMounting; +} + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum Volume { + Terrain, + Entity(Uid), +} + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct VolumePos { + pub kind: Volume, + pub pos: Vec3, +} + +impl VolumePos { + pub fn terrain(block_pos: Vec3) -> Self { + Self { + kind: Volume::Terrain, + pos: block_pos, + } + } + + pub fn entity(block_pos: Vec3, uid: Uid) -> Self { + Self { + kind: Volume::Entity(uid), + pos: block_pos, + } + } +} + +impl VolumePos { + pub fn get_block_and_transform( + &self, + terrain: &TerrainGrid, + uid_allocator: &UidAllocator, + positions: &Storage>>, + orientations: &Storage>>, + colliders: &ReadStorage, + ) -> Option<(Mat4, Block)> { + match self.kind { + Volume::Terrain => Some(( + Mat4::translation_3d(self.pos.as_()), + *terrain.get(self.pos).ok()?, + )), + Volume::Entity(uid) => { + uid_allocator + .retrieve_entity_internal(uid.0) + .and_then(|entity| { + let collider = colliders.get(entity)?; + let pos = positions.get(entity)?; + let ori = orientations.get(entity)?; + + let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); + let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?; + + let block = *voxel_collider.volume().get(self.pos).ok()?; + + let local_translation = voxel_collider.translation + self.pos.as_(); + + let trans = Mat4::from(ori.to_quat()).translated_3d(pos.0) + * Mat4::::translation_3d(local_translation); + + Some((trans, block)) + }) + }, + } + } + + pub fn get_block( + &self, + terrain: &TerrainGrid, + uid_allocator: &UidAllocator, + colliders: &ReadStorage, + ) -> Option { + match self.kind { + Volume::Terrain => Some(*terrain.get(self.pos).ok()?), + Volume::Entity(uid) => { + uid_allocator + .retrieve_entity_internal(uid.0) + .and_then(|entity| { + let collider = colliders.get(entity)?; + + let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); + let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?; + + let block = *voxel_collider.volume().get(self.pos).ok()?; + + Some(block) + }) + }, + } + } +} + +#[derive(Default)] +pub struct VolumeRiders { + riders: HashSet>, +} + +impl Component for VolumeRiders { + type Storage = DenseVecStorage; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct VolumeMounting { + pub pos: VolumePos, + pub block: Block, + pub rider: Uid, +} + +impl Link for VolumeMounting { + type CreateData<'a> = ( + Write<'a, VolumeRiders>, + WriteStorage<'a, VolumeRiders>, + WriteStorage<'a, Is>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + ReadExpect<'a, TerrainGrid>, + Read<'a, UidAllocator>, + ReadStorage<'a, comp::Collider>, + ); + type DeleteData<'a> = ( + Write<'a, VolumeRiders>, + WriteStorage<'a, VolumeRiders>, + WriteStorage<'a, Is>, + Read<'a, UidAllocator>, + ); + type Error = MountingError; + type PersistData<'a> = ( + Entities<'a>, + ReadStorage<'a, comp::Health>, + Read<'a, VolumeRiders>, + ReadStorage<'a, VolumeRiders>, + ReadStorage<'a, Is>, + ReadExpect<'a, TerrainGrid>, + Read<'a, UidAllocator>, + ReadStorage<'a, comp::Collider>, + ); + + fn create( + this: &LinkHandle, + ( + mut terrain_riders, + mut volume_riders, + mut is_volume_riders, + is_riders, + is_mounts, + terrain_grid, + uid_allocator, + colliders, + ): Self::CreateData<'_>, + ) -> Result<(), Self::Error> { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + let riders = match this.pos.kind { + Volume::Terrain => &mut *terrain_riders, + Volume::Entity(uid) => entity(uid) + .and_then(|entity| volume_riders.get_mut_or_default(entity)) + .ok_or(MountingError::NoSuchEntity)?, + }; + let rider = entity(this.rider).ok_or(MountingError::NoSuchEntity)?; + + if !riders.riders.contains(&this.pos.pos) + && !is_volume_riders.contains(rider) + && !is_volume_riders.contains(rider) + && !is_riders.contains(rider) + && !is_mounts.contains(rider) + { + let block = this + .pos + .get_block(&terrain_grid, &uid_allocator, &colliders) + .ok_or(MountingError::NoSuchEntity)?; + + if block == this.block { + let _ = is_volume_riders.insert(rider, this.make_role()); + riders.riders.insert(this.pos.pos); + Ok(()) + } else { + Err(MountingError::NotMountable) + } + } else { + Err(MountingError::NotMountable) + } + } + + fn persist( + this: &LinkHandle, + ( + entities, + healths, + terrain_riders, + volume_riders, + is_volume_riders, + terrain_grid, + uid_allocator, + colliders, + ): Self::PersistData<'_>, + ) -> bool { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + let is_alive = + |entity| entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead); + let riders = match this.pos.kind { + Volume::Terrain => &*terrain_riders, + Volume::Entity(uid) => { + let Some(riders) = entity(uid) + .filter(|entity| is_alive(*entity)) + .and_then(|entity| volume_riders.get(entity)) else { + return false; + }; + riders + }, + }; + + let rider_exists = entity(this.rider).map_or(false, |rider| { + is_volume_riders.contains(rider) && is_alive(rider) + }); + let mount_spot_exists = riders.riders.contains(&this.pos.pos); + + let block_exists = this + .pos + .get_block(&terrain_grid, &uid_allocator, &colliders) + .map_or(false, |block| block == this.block); + + rider_exists && mount_spot_exists && block_exists + } + + fn delete( + this: &LinkHandle, + (mut terrain_riders, mut volume_riders, mut is_rider, uid_allocator): Self::DeleteData<'_>, + ) { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + let riders = match this.pos.kind { + Volume::Terrain => Some(&mut *terrain_riders), + Volume::Entity(uid) => { + entity(uid).and_then(|entity| volume_riders.get_mut_or_default(entity)) + }, + }; + + if let Some(riders) = riders { + riders.riders.remove(&this.pos.pos); + } + + if let Some(entity) = entity(this.rider) { + is_rider.remove(entity); + } + } +} diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 1ae00b204d..8bc65b4030 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -9,7 +9,7 @@ use crate::{ Stats, Vel, }, link::Is, - mounting::Rider, + mounting::{Rider, VolumeRider}, resources::{DeltaTime, Time}, terrain::TerrainGrid, uid::Uid, @@ -59,14 +59,6 @@ pub trait CharacterBehavior { fn talk(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { StateUpdate::from(data) } - fn mount_sprite( - &self, - data: &JoinData, - _output_events: &mut OutputEvents, - _pos: Vec3, - ) -> StateUpdate { - StateUpdate::from(data) - } // start_input has custom implementation in the following character states that // may also need to be modified when changes are made here: ComboMelee2 fn start_input( @@ -105,7 +97,7 @@ pub trait CharacterBehavior { ControlAction::Sit => self.sit(data, output_events), ControlAction::Dance => self.dance(data, output_events), ControlAction::Sneak => { - if data.mount_data.is_none() { + if data.mount_data.is_none() && data.volume_mount_data.is_none() { self.sneak(data, output_events) } else { self.stand(data, output_events) @@ -119,7 +111,6 @@ pub trait CharacterBehavior { select_pos, } => self.start_input(data, input, target_entity, select_pos), ControlAction::CancelInput(input) => self.cancel_input(data, input), - ControlAction::MountSprite(pos) => self.mount_sprite(data, output_events, pos), } } } @@ -156,6 +147,7 @@ pub struct JoinData<'a> { pub alignment: Option<&'a comp::Alignment>, pub terrain: &'a TerrainGrid, pub mount_data: Option<&'a Is>, + pub volume_mount_data: Option<&'a Is>, pub stance: Option<&'a Stance>, } @@ -185,6 +177,7 @@ pub struct JoinStruct<'a> { pub alignment: Option<&'a comp::Alignment>, pub terrain: &'a TerrainGrid, pub mount_data: Option<&'a Is>, + pub volume_mount_data: Option<&'a Is>, pub stance: Option<&'a Stance>, } @@ -228,6 +221,7 @@ impl<'a> JoinData<'a> { terrain: j.terrain, active_abilities: j.active_abilities, mount_data: j.mount_data, + volume_mount_data: j.volume_mount_data, stance: j.stance, } } diff --git a/common/src/states/dance.rs b/common/src/states/dance.rs index 938c5897f8..c151f794fd 100644 --- a/common/src/states/dance.rs +++ b/common/src/states/dance.rs @@ -7,7 +7,6 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] pub struct Data; @@ -51,12 +50,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - attempt_mount_sprite(data, &mut update, pos); - 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/glide_wield.rs b/common/src/states/glide_wield.rs index 2da4df6e38..a47c151084 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -12,7 +12,6 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Data { @@ -111,12 +110,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - attempt_mount_sprite(data, &mut update, pos); - update - } - fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); attempt_dance(data, &mut update); diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs index faf3a6b95e..c89263f2af 100644 --- a/common/src/states/idle.rs +++ b/common/src/states/idle.rs @@ -8,7 +8,6 @@ use crate::{ states::behavior::{CharacterBehavior, JoinData}, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Data { @@ -87,12 +86,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - attempt_mount_sprite(data, &mut update, pos); - update - } - fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); attempt_dance(data, &mut update); diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 2cfe806c0b..4a49159f19 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -22,7 +22,6 @@ pub mod glide_wield; pub mod idle; pub mod leap_melee; pub mod leap_shockwave; -pub mod mount_sprite; pub mod music; pub mod rapid_melee; pub mod repeater_ranged; diff --git a/common/src/states/mount_sprite.rs b/common/src/states/mount_sprite.rs deleted file mode 100644 index bbf885423a..0000000000 --- a/common/src/states/mount_sprite.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde::{Deserialize, Serialize}; -use vek::Vec3; - -use crate::{ - comp::{character_state::OutputEvents, CharacterState, StateUpdate}, - util::Dir, -}; - -use super::{ - behavior::{CharacterBehavior, JoinData}, - idle, - utils::{end_ability, handle_orientation, handle_wield}, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct StaticData { - pub mount_pos: Vec3, - pub mount_dir: Vec3, - /// Position sprite is located at - pub sprite_pos: Vec3, -} - -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct Data { - /// Struct containing data that does not change over the course of the - /// character state - pub static_data: StaticData, -} - -impl CharacterBehavior for Data { - fn behavior(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { - let mut update = StateUpdate::from(data); - update.pos.0 = self.static_data.mount_pos; - update.vel.0 = Vec3::zero(); - - handle_orientation( - data, - &mut update, - 1.0, - Some(Dir::new(self.static_data.mount_dir)), - ); - - handle_wield(data, &mut update); - - // 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 stand(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { - let mut update = StateUpdate::from(data); - - end_ability(data, &mut update); - - update - } -} diff --git a/common/src/states/skate.rs b/common/src/states/skate.rs index 9db55ec8e3..8d877675ad 100644 --- a/common/src/states/skate.rs +++ b/common/src/states/skate.rs @@ -10,7 +10,6 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Data { @@ -109,12 +108,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - attempt_mount_sprite(data, &mut update, pos); - 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/talk.rs b/common/src/states/talk.rs index dd665c20ff..bab5b59f4d 100644 --- a/common/src/states/talk.rs +++ b/common/src/states/talk.rs @@ -7,7 +7,6 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; const TURN_RATE: f32 = 40.0; @@ -48,13 +47,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - update.character = CharacterState::Idle(idle::Data::default()); - attempt_mount_sprite(data, &mut update, pos); - update - } - fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle(idle::Data::default()); diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ae240e4fdf..efe78b207b 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -21,13 +21,12 @@ use crate::{ event::{LocalEvent, ServerEvent}, outcome::Outcome, states::{behavior::JoinData, utils::CharacterState::Idle, *}, - terrain::{SpriteKind, TerrainGrid, UnlockKind}, + terrain::{TerrainGrid, UnlockKind}, util::Dir, vol::ReadVol, }; use core::hash::BuildHasherDefault; use fxhash::FxHasher64; -use ordered_float::OrderedFloat; use serde::{Deserialize, Serialize}; use std::{ f32::consts::PI, @@ -785,52 +784,6 @@ pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) { } } -pub fn sprite_mount_points( - sprite_kind: SpriteKind, - pos: Vec3, - ori: u8, -) -> impl ExactSizeIterator, Vec3)> { - let mat = Mat4::identity() - .rotated_z(std::f32::consts::PI * 0.25 * ori as f32) - .translated_3d(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)); - sprite_kind - .mount_offsets() - .iter() - .map(move |(pos, dir)| ((mat * pos.with_w(1.0)).xyz(), (mat * dir.with_w(0.0)).xyz())) -} - -const SPRITE_MOUNT_RANGE: f32 = 2.0; -pub const SPRITE_MOUNT_RANGE_SQR: f32 = SPRITE_MOUNT_RANGE * SPRITE_MOUNT_RANGE; - -pub fn attempt_mount_sprite(data: &JoinData<'_>, update: &mut StateUpdate, pos: Vec3) { - if let Some((kind, ori)) = data - .terrain - .get(pos) - .ok() - .and_then(|block| block.get_sprite().zip(block.get_ori())) - { - if let Some((mount_pos, mount_dir)) = sprite_mount_points(kind, pos, ori) - .min_by_key(|(pos, _)| OrderedFloat(data.pos.0.distance_squared(*pos))) - { - if reach_block( - data.pos.0, - pos, - SPRITE_MOUNT_RANGE_SQR, - data.body, - data.terrain, - ) { - update.character = CharacterState::MountSprite(mount_sprite::Data { - static_data: mount_sprite::StaticData { - mount_pos, - mount_dir, - sprite_pos: pos, - }, - }); - } - } - } -} - pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) { if data.physics.on_ground.is_some() && data.body.is_humanoid() { update.character = CharacterState::Dance; diff --git a/common/src/states/wielding.rs b/common/src/states/wielding.rs index 7c8c161ce6..c8ad0b16f5 100644 --- a/common/src/states/wielding.rs +++ b/common/src/states/wielding.rs @@ -11,7 +11,6 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use vek::Vec3; #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Data { @@ -94,12 +93,6 @@ impl CharacterBehavior for Data { update } - fn mount_sprite(&self, data: &JoinData, _: &mut OutputEvents, pos: Vec3) -> StateUpdate { - let mut update = StateUpdate::from(data); - attempt_mount_sprite(data, &mut update, pos); - update - } - fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); attempt_dance(data, &mut update); diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 2bfb9daf6b..cacae32e9b 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -428,7 +428,16 @@ impl Block { } #[inline] - pub fn is_mountable(&self) -> bool { self.get_sprite().map_or(false, |s| s.is_mountable()) } + pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() } + + /// Get the position and direction to mount this block if any. + pub fn mount_offset(&self) -> Option<(Vec3, Vec3)> { + self.get_sprite().and_then(|sprite| sprite.mount_offset()) + } + + pub fn is_controller(&self) -> bool { + self.get_sprite().map_or(false, |sprite| sprite.is_controller()) + } #[inline] pub fn is_bonkable(&self) -> bool { diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index cb692073fc..0fc2f4bf57 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -242,6 +242,7 @@ make_case_elim!( KeyDoor = 0xD8, CommonLockedChest = 0xD9, RepairBench = 0xDA, + Helm = 0xDB, } ); @@ -475,47 +476,40 @@ impl SpriteKind { matches!(self.collectible_id(), Some(Some(LootSpec::LootTable(_)))) } - /// Is the sprite a container that will emit a mystery item? + /// Get the position and direction to mount this sprite if any. #[inline] - pub fn mount_offsets(&self) -> &'static [(Vec3, Vec3)] { - const UNIT_Y: Vec3 = Vec3 { - x: 0.0, - y: -1.0, - z: 0.0, - }; + pub fn mount_offset(&self) -> Option<(Vec3, Vec3)> { match self { - SpriteKind::ChairSingle => &[( + SpriteKind::ChairSingle | SpriteKind::ChairDouble => Some(( Vec3 { x: 0.0, y: 0.0, z: 0.5, }, - UNIT_Y, - )], - SpriteKind::ChairDouble => &[ - ( - Vec3 { - x: -0.5, - y: 0.0, - z: 0.6, - }, - UNIT_Y, - ), - ( - Vec3 { - x: 0.5, - y: -0.1, - z: 0.6, - }, - UNIT_Y, - ), - ], - _ => &[], + -Vec3::unit_y(), + )), + SpriteKind::Helm => Some(( + Vec3 { + x: 0.0, + y: -0.6, + z: 0.2, + }, + Vec3::unit_y(), + )), + _ => None, } } #[inline] - pub fn is_mountable(&self) -> bool { !self.mount_offsets().is_empty() } + pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() } + + #[inline] + pub fn is_controller(&self) -> bool { + match self { + SpriteKind::Helm => true, + _ => false, + } + } /// Which tool (if any) is needed to collect this sprite? #[inline] @@ -646,6 +640,7 @@ impl SpriteKind { | SpriteKind::Grave | SpriteKind::Gravestone | SpriteKind::MagicalBarrier + | SpriteKind::Helm, ) } } diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs index d2a924277a..d27202c360 100644 --- a/common/src/util/find_dist.rs +++ b/common/src/util/find_dist.rs @@ -149,6 +149,18 @@ impl FindDist> for Cylinder { } } +impl FindDist for Vec3 { + #[inline] + fn approx_in_range(self, other: Cylinder, range: f32) -> bool { + other.approx_in_range(self, range) + } + + #[inline] + fn min_distance(self, other: Cylinder) -> f32 { + other.min_distance(self) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 2c6fa1405b..5293f7b548 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -9,7 +9,7 @@ use common::{ comp, event::{EventBus, LocalEvent, ServerEvent}, link::Is, - mounting::{Mount, Rider}, + mounting::{Mount, Rider, VolumeRider, VolumeRiders}, outcome::Outcome, region::RegionMap, resources::{ @@ -201,6 +201,7 @@ impl State { ecs.register::(); ecs.register::>(); ecs.register::>(); + ecs.register::>(); ecs.register::(); ecs.register::(); ecs.register::(); @@ -260,6 +261,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); @@ -294,6 +296,7 @@ impl State { ecs.insert(PhysicsMetrics::default()); ecs.insert(Trades::default()); ecs.insert(PlayerPhysicsSettings::default()); + ecs.insert(VolumeRiders::default()); // Load plugins from asset directory #[cfg(feature = "plugins")] diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 39ffd3e13a..59099305b7 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -14,7 +14,7 @@ use common::{ }, event::{EventBus, LocalEvent, ServerEvent}, link::Is, - mounting::Rider, + mounting::{Rider, VolumeRider}, outcome::Outcome, resources::{DeltaTime, Time}, states::{ @@ -43,6 +43,7 @@ pub struct ReadData<'a> { beams: ReadStorage<'a, Beam>, uids: ReadStorage<'a, Uid>, is_riders: ReadStorage<'a, Is>, + is_volume_riders: ReadStorage<'a, Is>, stats: ReadStorage<'a, Stats>, skill_sets: ReadStorage<'a, SkillSet>, active_abilities: ReadStorage<'a, ActiveAbilities>, @@ -207,6 +208,7 @@ impl<'a> System<'a> for Sys { alignment: read_data.alignments.get(entity), terrain: &read_data.terrain, mount_data: read_data.is_riders.get(entity), + volume_mount_data: read_data.is_volume_riders.get(entity), stance: read_data.stances.get(entity), }; diff --git a/common/systems/src/controller.rs b/common/systems/src/controller.rs index 46ce5973e2..434fc7df8c 100644 --- a/common/systems/src/controller.rs +++ b/common/systems/src/controller.rs @@ -2,16 +2,17 @@ use common::{ comp::{ ability::Stance, agent::{Sound, SoundKind}, - Body, BuffChange, ControlEvent, Controller, Pos, Scale, + Body, BuffChange, Collider, ControlEvent, Controller, Pos, Scale, }, event::{EventBus, ServerEvent}, + terrain::TerrainGrid, uid::UidAllocator, }; use common_ecs::{Job, Origin, Phase, System}; use specs::{ saveload::{Marker, MarkerAllocator}, shred::ResourceId, - Entities, Join, Read, ReadStorage, SystemData, World, WriteStorage, + Entities, Join, Read, ReadExpect, ReadStorage, SystemData, World, WriteStorage, }; use vek::*; @@ -20,9 +21,11 @@ pub struct ReadData<'a> { entities: Entities<'a>, uid_allocator: Read<'a, UidAllocator>, server_bus: Read<'a, EventBus>, + terrain_grid: ReadExpect<'a, TerrainGrid>, positions: ReadStorage<'a, Pos>, bodies: ReadStorage<'a, Body>, scales: ReadStorage<'a, Scale>, + colliders: ReadStorage<'a, Collider>, } #[derive(Default)] @@ -53,6 +56,17 @@ impl<'a> System<'a> for Sys { server_emitter.emit(ServerEvent::Mount(entity, mountee_entity)); } }, + ControlEvent::MountVolume(volume) => { + if let Some(block) = volume.get_block( + &read_data.terrain_grid, + &read_data.uid_allocator, + &read_data.colliders, + ) { + if block.is_mountable() { + server_emitter.emit(ServerEvent::MountVolume(entity, volume)); + } + } + }, ControlEvent::RemoveBuff(buff_id) => { server_emitter.emit(ServerEvent::Buff { entity, diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index b8d70b37b9..eaa060641a 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,14 +1,16 @@ use common::{ - comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel}, + comp::{Body, Collider, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel}, link::Is, - mounting::Mount, + mounting::{Mount, VolumeRider}, + terrain::TerrainGrid, uid::UidAllocator, }; use common_ecs::{Job, Origin, Phase, System}; use specs::{ saveload::{Marker, MarkerAllocator}, - Entities, Join, Read, ReadStorage, WriteStorage, + Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage, }; +use tracing::error; use vek::*; /// This system is responsible for controlling mounts @@ -17,14 +19,17 @@ pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( Read<'a, UidAllocator>, + ReadExpect<'a, TerrainGrid>, Entities<'a>, WriteStorage<'a, Controller>, ReadStorage<'a, Is>, + ReadStorage<'a, Is>, WriteStorage<'a, Pos>, WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, ReadStorage<'a, Body>, ReadStorage<'a, Scale>, + ReadStorage<'a, Collider>, ); const NAME: &'static str = "mount"; @@ -35,14 +40,17 @@ impl<'a> System<'a> for Sys { _job: &mut Job, ( uid_allocator, + terrain, entities, mut controllers, is_mounts, + is_volume_riders, mut positions, mut velocities, mut orientations, bodies, scales, + colliders, ): Self::SystemData, ) { // For each mount... @@ -84,5 +92,87 @@ impl<'a> System<'a> for Sys { controller.actions = actions; } } + + // For each volume rider. + for (entity, is_volume_rider) in (&entities, &is_volume_riders).join() { + if let Some((mut mat, _)) = is_volume_rider.pos.get_block_and_transform( + &terrain, + &uid_allocator, + &positions, + &orientations, + &colliders, + ) { + let Some((mount_offset, mount_dir)) = is_volume_rider.block.mount_offset() else { + error!("Mounted on unmountable block"); + continue; + }; + + if let Some(ori) = is_volume_rider.block.get_ori() { + mat *= Mat4::identity() + .translated_3d(mount_offset) + .rotated_z(std::f32::consts::PI * 0.25 * ori as f32) + .translated_3d(Vec3::new(0.5, 0.5, 0.0)); + } else { + mat *= Mat4::identity().translated_3d(mount_offset + Vec3::new(0.5, 0.5, 0.0)); + } + + if let Some(pos) = positions.get_mut(entity) { + pos.0 = mat.mul_point(Vec3::zero()); + } + if let Some(ori) = orientations.get_mut(entity) { + *ori = Ori::from_unnormalized_vec(mat.mul_direction(mount_dir)) + .unwrap_or_default(); + } + } + let v = match is_volume_rider.pos.kind { + common::mounting::Volume::Terrain => Vec3::zero(), + common::mounting::Volume::Entity(uid) => { + if let Some(v) = uid_allocator + .retrieve_entity_internal(uid.into()) + .and_then(|e| velocities.get(e)) + { + v.0 + } else { + Vec3::zero() + } + }, + }; + if let Some(vel) = velocities.get_mut(entity) { + vel.0 = v; + } + + let inputs = controllers.get_mut(entity).map(|c| { + let actions: Vec<_> = c + .actions + .drain_filter(|action| match action { + ControlAction::StartInput { input: i, .. } + | ControlAction::CancelInput(i) => { + matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll) + }, + _ => false, + }) + .collect(); + let inputs = c.inputs.clone(); + + (actions, inputs) + }); + + if is_volume_rider.block.is_controller() { + if let Some((actions, inputs)) = inputs { + match is_volume_rider.pos.kind { + common::mounting::Volume::Entity(uid) => { + if let Some(controller) = uid_allocator + .retrieve_entity_internal(uid.into()) + .and_then(|e| controllers.get_mut(e)) + { + controller.inputs = inputs; + controller.actions = actions; + } + }, + common::mounting::Volume::Terrain => {}, + } + } + } + } } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 21bb4f5f3f..4e13468bdf 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -9,7 +9,7 @@ use common::{ consts::{AIR_DENSITY, FRIC_GROUND, GRAVITY}, event::{EventBus, ServerEvent}, link::Is, - mounting::Rider, + mounting::{Rider, VolumeRider}, outcome::Outcome, resources::DeltaTime, states, @@ -122,6 +122,7 @@ pub struct PhysicsRead<'a> { masses: ReadStorage<'a, Mass>, colliders: ReadStorage<'a, Collider>, is_ridings: ReadStorage<'a, Is>, + is_volume_ridings: ReadStorage<'a, Is>, projectiles: ReadStorage<'a, Projectile>, char_states: ReadStorage<'a, CharacterState>, bodies: ReadStorage<'a, Body>, @@ -335,6 +336,7 @@ impl<'a> PhysicsData<'a> { &read.masses, &read.colliders, read.is_ridings.maybe(), + read.is_volume_ridings.maybe(), read.stickies.maybe(), read.immovables.maybe(), &mut write.physics_states, @@ -359,6 +361,7 @@ impl<'a> PhysicsData<'a> { mass, collider, is_riding, + is_volume_riding, sticky, immovable, physics, @@ -366,8 +369,7 @@ impl<'a> PhysicsData<'a> { char_state_maybe, )| { let is_sticky = sticky.is_some(); - let is_immovable = immovable.is_some() - || matches!(char_state_maybe, Some(CharacterState::MountSprite(_))); + let is_immovable = immovable.is_some(); let is_mid_air = physics.on_surface().is_none(); let mut entity_entity_collision_checks = 0; let mut entity_entity_collisions = 0; @@ -492,7 +494,9 @@ impl<'a> PhysicsData<'a> { mass: *mass_other, }, vel, - is_riding.is_some() || other_is_riding_maybe.is_some(), + is_riding.is_some() + || is_volume_riding.is_some() + || other_is_riding_maybe.is_some(), ); } }, @@ -547,11 +551,7 @@ impl<'a> PhysicsData<'a> { ) .join() { - let vol = match collider { - Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id), - Collider::Volume(vol) => Some(&**vol), - _ => None, - }; + let vol = collider.get_vol(&voxel_colliders_manifest); if let Some(vol) = vol { let sphere = voxel_collider_bounding_sphere(vol, pos, ori); @@ -588,6 +588,7 @@ impl<'a> PhysicsData<'a> { !&write.pos_vel_ori_defers, // This is the one we are adding write.previous_phys_cache.mask(), !&read.is_ridings, + !&read.is_volume_ridings, ) .join() .map(|t| (t.0, *t.2, *t.3, *t.4)) @@ -620,9 +621,9 @@ impl<'a> PhysicsData<'a> { &read.densities, read.scales.maybe(), !&read.is_ridings, + !&read.is_volume_ridings, ) .par_join() - .filter(|tuple| !matches!(tuple.4, Some(CharacterState::MountSprite(_)))) .for_each_init( || { prof_span!(guard, "velocity update rayon job"); @@ -640,6 +641,7 @@ impl<'a> PhysicsData<'a> { density, scale, _, + _, )| { let in_loaded_chunk = read .terrain @@ -751,12 +753,10 @@ impl<'a> PhysicsData<'a> { &mut write.pos_vel_ori_defers, previous_phys_cache, !&read.is_ridings, + !&read.is_volume_ridings, ) .par_join() - .filter(|tuple| { - tuple.3.is_voxel() == terrain_like_entities - && !matches!(tuple.8, Some(CharacterState::MountSprite(_))) - }) + .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities) .map_init( || { prof_span!(guard, "physics e<>t rayon job"); @@ -777,6 +777,7 @@ impl<'a> PhysicsData<'a> { pos_vel_ori_defer, previous_cache, _, + _, )| { let mut land_on_ground = None; let mut outcomes = Vec::new(); @@ -1066,13 +1067,8 @@ impl<'a> PhysicsData<'a> { return; } - let voxel_collider = match collider_other { - Collider::Voxel { id } => { - voxel_colliders_manifest.colliders.get(id) - }, - Collider::Volume(vol) => Some(&**vol), - _ => None, - }; + let voxel_collider = + collider_other.get_vol(&voxel_colliders_manifest); // use bounding cylinder regardless of our collider // TODO: extract point-terrain collision above to its own diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 6b55c3a193..c5cd629fe9 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -145,7 +145,7 @@ impl<'a> System<'a> for Sys { { match character_state { // Sitting accelerates recharging energy the most - CharacterState::Sit | CharacterState::MountSprite(_) => { + CharacterState::Sit => { if energy.needs_regen() { energy.regen(SIT_ENERGY_REGEN_ACCEL, dt); } diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 6db84f7092..1da7fee2be 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -10,7 +10,7 @@ use common::{ SkillSet, Stance, Stats, Vel, }, link::Is, - mounting::{Mount, Rider}, + mounting::{Mount, Rider, VolumeRider}, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{Actor, RtSimEntity}, @@ -236,6 +236,7 @@ pub struct ReadData<'a> { pub bodies: ReadStorage<'a, Body>, pub is_mounts: ReadStorage<'a, Is>, pub is_riders: ReadStorage<'a, Is>, + pub is_volume_riders: ReadStorage<'a, Is>, pub time_of_day: Read<'a, TimeOfDay>, pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 4c5d2406b9..5dc7b0f0a8 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -229,6 +229,7 @@ fn position_mut( descriptor: &str, f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T, ) -> CmdResult { + // TODO: Handle volume mount let entity = server .state .ecs() diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 68c8f1e0f2..e8a0dbc0a9 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -14,10 +14,10 @@ use common::{ tool::{AbilityMap, ToolKind}, Inventory, LootOwner, Pos, SkillGroupKind, }, - consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, + consts::{MAX_MOUNT_RANGE, MAX_SPRITE_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, event::EventBus, link::Is, - mounting::{Mount, Mounting, Rider}, + mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider}, outcome::Outcome, terrain::{Block, SpriteKind}, uid::Uid, @@ -103,53 +103,78 @@ pub fn handle_npc_interaction( pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { let state = server.state_mut(); - if state.ecs().read_storage::>().get(rider).is_none() { - let not_mounting_yet = state.ecs().read_storage::>().get(mount).is_none(); + let within_range = { + let positions = state.ecs().read_storage::(); + within_mounting_range(positions.get(rider), positions.get(mount)) + }; - let within_range = || { - let positions = state.ecs().read_storage::(); - within_mounting_range(positions.get(rider), positions.get(mount)) - }; - let healths = state.ecs().read_storage::(); - let alive = |e| healths.get(e).map_or(true, |h| !h.is_dead); - - if not_mounting_yet && within_range() && alive(rider) && alive(mount) { - let uids = state.ecs().read_storage::(); - 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 can_ride = state + if within_range { + let uids = state.ecs().read_storage::(); + 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) - .map_or(false, |mount_body| { - is_mountable(mount_body, state.ecs().read_storage().get(rider)) - }); + .read_storage::() + .get(mount), + Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, + ); - if is_pet && can_ride { - drop(uids); - drop(healths); - let _ = state.link(Mounting { - mount: mount_uid, - rider: rider_uid, - }); - } + let can_ride = state + .ecs() + .read_storage() + .get(mount) + .map_or(false, |mount_body| { + is_mountable(mount_body, state.ecs().read_storage().get(rider)) + }); + + if is_pet && can_ride { + drop(uids); + let _ = state.link(Mounting { + mount: mount_uid, + rider: rider_uid, + }); } } } } +pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: VolumePos) { + let state = server.state_mut(); + + let block_transform = volume_pos.get_block_and_transform( + &state.ecs().read_resource(), + &state.ecs().read_resource(), + &state.ecs().read_storage(), + &state.ecs().read_storage(), + &state.ecs().read_storage(), + ); + + if let Some((transform, block)) = block_transform + && let Some(mount_offset) = block.mount_offset() { + let mount_pos = (Mat4::from(transform) * mount_offset.0.with_w(1.0)).xyz(); + let within_range = { + let positions = state.ecs().read_storage::(); + positions.get(rider).map_or(false, |pos| pos.0.distance_squared(mount_pos) < MAX_SPRITE_MOUNT_RANGE * MAX_SPRITE_MOUNT_RANGE) + }; + + let maybe_uid = state.ecs().read_storage::().get(rider).copied(); + + if let Some(rider) = maybe_uid && within_range { + let _ = state.link(VolumeMounting { + pos: volume_pos, + block, + rider, + }); + } + } +} + pub fn handle_unmount(server: &mut Server, rider: EcsEntity) { let state = server.state_mut(); state.ecs().write_storage::>().remove(rider); + state.ecs().write_storage::>().remove(rider); } fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool { diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index f86f23d31f..047390c6b3 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -21,7 +21,7 @@ use common::{ trade::Trades, uid::Uid, util::find_dist::{self, FindDist}, - vol::ReadVol, + vol::ReadVol, mounting::VolumePos, }; use common_net::sync::WorldSyncExt; use common_state::State; @@ -745,27 +745,24 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv let ability_map = &state.ecs().read_resource::(); let msm = state.ecs().read_resource::(); - let get_craft_sprite = |state, sprite_pos: Option>| { + let get_craft_sprite = |state, sprite_pos: Option| { sprite_pos .filter(|pos| { let entity_cylinder = get_cylinder(state, entity); let in_range = within_pickup_range(entity_cylinder, || { - Some(find_dist::Cube { - min: pos.as_(), - side_length: 1.0, - }) + pos.get_block_and_transform(&state.terrain(), &state.ecs().read_resource(), &state.read_storage(), &state.read_storage(), &state.read_storage()).map(|(transform, _)| transform.mul_point(Vec3::broadcast(0.5))) }); if !in_range { debug!( ?entity_cylinder, "Failed to craft recipe as not within range of required sprite, \ - sprite pos: {}", + sprite pos: {:?}", pos ); } in_range }) - .and_then(|pos| state.terrain().get(pos).ok().copied()) + .and_then(|pos| pos.get_block(&state.terrain(), &state.ecs().read_resource(), &state.read_storage())) .and_then(|block| block.get_sprite()) }; diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 6524da88f2..b8cbec958e 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -1,5 +1,7 @@ use crate::{ - events::interaction::handle_tame_pet, persistence::PersistedComponents, state_ext::StateExt, + events::interaction::{handle_mount_volume, handle_tame_pet}, + persistence::PersistedComponents, + state_ext::StateExt, Server, }; use common::event::{EventBus, ServerEvent, ServerEventDiscriminants}; @@ -136,6 +138,9 @@ impl Server { handle_process_trade_action(self, entity, trade_id, action); }, ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee), + ServerEvent::MountVolume(mounter, volume) => { + handle_mount_volume(self, mounter, volume) + }, ServerEvent::Unmount(mounter) => handle_unmount(self, mounter), ServerEvent::Possess(possessor_uid, possesse_uid) => { handle_possess(self, possessor_uid, possesse_uid) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 0ea3374acf..8cce24a2c3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -23,7 +23,7 @@ use common::{ }, effect::Effect, link::{Link, LinkHandle}, - mounting::Mounting, + mounting::{Mounting, VolumeMounting}, resources::{Secs, Time, TimeOfDay}, rtsim::{Actor, RtSimEntity}, slowjob::SlowJobPool, @@ -1099,6 +1099,7 @@ impl StateExt for State { } maintain_link::(self); + maintain_link::(self); } fn delete_entity_recorded( diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index b7ccafc870..3373897dd3 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -12,6 +12,7 @@ use common::{ Controller, Health, InputKind, Scale, }, event::{EventBus, ServerEvent}, + mounting::Volume, path::TraversalConfig, }; use common_base::prof_span; @@ -69,6 +70,7 @@ impl<'a> System<'a> for Sys { read_data.rtsim_entities.maybe(), !&read_data.is_mounts, read_data.is_riders.maybe(), + read_data.is_volume_riders.maybe(), ) .par_join() .for_each_init( @@ -93,6 +95,7 @@ impl<'a> System<'a> for Sys { rtsim_entity, _, is_rider, + is_volume_rider, )| { let mut event_emitter = event_bus.emitter(); let mut rng = thread_rng(); @@ -104,6 +107,16 @@ impl<'a> System<'a> for Sys { .uid_allocator .retrieve_entity_internal(is_rider.mount.into()) }) + .or_else(|| { + is_volume_rider.and_then(|is_volume_rider| { + match is_volume_rider.pos.kind { + Volume::Terrain => None, + Volume::Entity(uid) => { + read_data.uid_allocator.retrieve_entity_internal(uid.into()) + }, + } + }) + }) .unwrap_or(entity); let moving_body = read_data.bodies.get(moving_entity); diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 4e216483d8..e87d919429 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -8,7 +8,7 @@ use common::{ }, event::{EventBus, ServerEvent}, link::Is, - mounting::Rider, + mounting::{Rider, VolumeRider}, resources::{PlayerPhysicsSetting, PlayerPhysicsSettings}, slowjob::SlowJobPool, terrain::TerrainGrid, @@ -52,6 +52,7 @@ impl Sys { terrain: &ReadExpect<'_, TerrainGrid>, can_build: &ReadStorage<'_, CanBuild>, is_rider: &ReadStorage<'_, Is>, + is_volume_rider: &ReadStorage<'_, Is>, force_updates: &ReadStorage<'_, ForceUpdate>, skill_set: &mut Option>, healths: &ReadStorage<'_, Health>, @@ -126,6 +127,7 @@ impl Sys { && force_updates.get(entity).map_or(true, |force_update| force_update.counter() == force_counter) && healths.get(entity).map_or(true, |h| !h.is_dead) && is_rider.get(entity).is_none() + && is_volume_rider.get(entity).is_none() && player_physics_setting .as_ref() .map_or(true, |s| s.client_authoritative()) @@ -322,6 +324,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, CanBuild>, ReadStorage<'a, ForceUpdate>, ReadStorage<'a, Is>, + ReadStorage<'a, Is>, WriteStorage<'a, SkillSet>, ReadStorage<'a, Health>, Write<'a, BlockChange>, @@ -353,6 +356,7 @@ impl<'a> System<'a> for Sys { can_build, force_updates, is_rider, + is_volume_rider, mut skill_sets, healths, mut block_changes, @@ -430,6 +434,7 @@ impl<'a> System<'a> for Sys { &terrain, &can_build, &is_rider, + &is_volume_rider, &force_updates, &mut skill_set, &healths, diff --git a/voxygen/src/hud/crafting.rs b/voxygen/src/hud/crafting.rs index 7ae95e7a39..c631fc9cbe 100644 --- a/voxygen/src/hud/crafting.rs +++ b/voxygen/src/hud/crafting.rs @@ -25,6 +25,7 @@ use common::{ slot::{InvSlotId, Slot}, Inventory, }, + mounting::VolumePos, recipe::{ComponentKey, Recipe, RecipeInput}, terrain::SpriteKind, }; @@ -123,7 +124,7 @@ pub enum Event { pub struct CraftingShow { pub crafting_tab: CraftingTab, pub crafting_search_key: Option, - pub craft_sprite: Option<(Vec3, SpriteKind)>, + pub craft_sprite: Option<(VolumePos, SpriteKind)>, pub salvage: bool, pub initialize_repair: bool, // TODO: Maybe try to do something that doesn't need to allocate? diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 3338446461..d90e170f07 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, + mounting::{Mount, VolumePos}, outcome::Outcome, resources::{Secs, Time}, slowjob::SlowJobPool, @@ -710,27 +710,27 @@ pub enum Event { CraftRecipe { recipe_name: String, - craft_sprite: Option<(Vec3, SpriteKind)>, + craft_sprite: Option<(VolumePos, SpriteKind)>, amount: u32, }, SalvageItem { slot: InvSlotId, - salvage_pos: Vec3, + salvage_pos: VolumePos, }, CraftModularWeapon { primary_slot: InvSlotId, secondary_slot: InvSlotId, - craft_sprite: Option>, + craft_sprite: Option, }, CraftModularWeaponComponent { toolkind: ToolKind, material: InvSlotId, modifier: Option, - craft_sprite: Option>, + craft_sprite: Option, }, RepairItem { item: Slot, - sprite_pos: Vec3, + sprite_pos: VolumePos, }, InviteMember(Uid), AcceptInvite, @@ -995,7 +995,7 @@ impl Show { pub fn open_crafting_tab( &mut self, tab: CraftingTab, - craft_sprite: Option<(Vec3, SpriteKind)>, + craft_sprite: Option<(VolumePos, SpriteKind)>, ) { self.selected_crafting_tab(tab); self.crafting(true); @@ -1289,7 +1289,7 @@ pub struct Hud { item_imgs: ItemImgs, fonts: Fonts, rot_imgs: ImgsRot, - failed_block_pickups: HashMap, CollectFailedData>, + failed_block_pickups: HashMap, failed_entity_pickups: HashMap, new_loot_messages: VecDeque, new_messages: VecDeque, @@ -2040,7 +2040,8 @@ impl Hud { } // Render overtime for an interactable block - if let Some(Interactable::Block(block, pos, interaction)) = interactable { + if let Some(Interactable::Block(block, pos, interaction)) = interactable + && let Some((mat, _)) = pos.get_block_and_transform(&ecs.read_resource(), &ecs.read_resource(), &ecs.read_storage(), &ecs.read_storage(), &ecs.read_storage()) { let overitem_id = overitem_walker.next( &mut self.ids.overitems, &mut ui_widgets.widget_id_generator(), @@ -2050,7 +2051,8 @@ impl Hud { active: true, pickup_failed_pulse: self.failed_block_pickups.get(pos).cloned(), }; - let pos = pos.map(|e| e as f32 + 0.5); + + let pos = mat.mul_point(Vec3::broadcast(0.5)); let over_pos = pos + Vec3::unit_z() * 0.7; let interaction_text = || match interaction { @@ -2107,10 +2109,9 @@ impl Hud { } } }, - BlockInteraction::Mount(_) => vec![( - Some(GameInput::Interact), - i18n.get_msg("hud-sit").to_string(), - )], + BlockInteraction::Mount => { + vec![(Some(GameInput::Mount), i18n.get_msg("hud-sit").to_string())] + }, }; // This is only done once per frame, so it's not a performance issue @@ -4349,7 +4350,7 @@ impl Hud { } } - pub fn add_failed_block_pickup(&mut self, pos: Vec3, reason: HudCollectFailedReason) { + pub fn add_failed_block_pickup(&mut self, pos: VolumePos, reason: HudCollectFailedReason) { self.failed_block_pickups .insert(pos, CollectFailedData::new(self.pulse, reason)); } @@ -4730,16 +4731,31 @@ impl Hud { .handle_event(conrod_core::event::Input::Text("\t".to_string())); } - // Stop selecting a sprite to perform crafting with when out of range + // Stop selecting a sprite to perform crafting with when out of range or sprite + // has been removed self.show.crafting_fields.craft_sprite = - self.show.crafting_fields.craft_sprite.filter(|(pos, _)| { - self.show.crafting - && if let Some(player_pos) = client.position() { - pos.map(|e| e as f32 + 0.5).distance(player_pos) < MAX_PICKUP_RANGE - } else { - false - } - }); + self.show + .crafting_fields + .craft_sprite + .filter(|(pos, sprite)| { + self.show.crafting + && if let Some(player_pos) = client.position() { + pos.get_block_and_transform( + &client.state().terrain(), + &client.state().ecs().read_resource(), + &client.state().read_storage(), + &client.state().read_storage(), + &client.state().read_storage(), + ) + .map_or(false, |(mat, block)| { + block.get_sprite() == Some(*sprite) + && mat.mul_point(Vec3::broadcast(0.5)).distance(player_pos) + < MAX_PICKUP_RANGE + }) + } else { + false + } + }); // Optimization: skip maintaining UI when it's off. if !self.show.ui { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 673ec5230e..48841e4a29 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -43,7 +43,7 @@ use common::{ Scale, SkillSet, Stance, Vel, }, link::Is, - mounting::Rider, + mounting::{Rider, VolumeRider}, resources::{DeltaTime, Time}, states::{equipping, idle, utils::StageSection, wielding}, terrain::{Block, TerrainChunk, TerrainGrid}, @@ -830,7 +830,7 @@ impl FigureMgr { inventory, item, light_emitter, - (is_rider, collider, stance, skillset), + (is_rider, is_volume_rider, collider, stance, skillset), ), ) in ( &ecs.entities(), @@ -850,6 +850,7 @@ impl FigureMgr { ecs.read_storage::().maybe(), ( ecs.read_storage::>().maybe(), + ecs.read_storage::>().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), @@ -1029,11 +1030,15 @@ impl FigureMgr { // If a mount exists, get its animated mounting transform and its position let mount_transform_pos = (|| -> Option<_> { - 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)) + if let Some(is_rider) = is_rider { + 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)) + } else { + None + } })(); let body = *body; @@ -1111,7 +1116,7 @@ impl FigureMgr { physics.on_ground.is_some(), rel_vel.magnitude_squared() > 0.01, // Moving physics.in_liquid().is_some(), // In water - is_rider.is_some(), + is_rider.is_some() || is_volume_rider.is_some(), physics.skating_active, ) { // Standing or Skating @@ -2059,7 +2064,7 @@ impl FigureMgr { skeleton_attr, ) }, - CharacterState::Sit { .. } | CharacterState::MountSprite(_) => { + CharacterState::Sit { .. } => { anim::character::SitAnimation::update_skeleton( &target_base, (active_tool_kind, second_tool_kind, time), @@ -6099,7 +6104,7 @@ impl FigureMgr { pos.0.into(), ori.into_vec4().into(), vk, - Arc::clone(vol), + Arc::clone(&vol), tick, &slow_jobs, terrain, diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index 4b7fa956b4..659ffade8b 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -1,20 +1,17 @@ use crate::hud::CraftingTab; -use common::{ - states::utils::sprite_mount_points, - terrain::{Block, BlockKind, SpriteKind}, -}; +use common::terrain::{Block, BlockKind, SpriteKind}; use common_base::span; use rand::prelude::*; use rand_chacha::ChaCha8Rng; use vek::*; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Interaction { /// This covers mining, unlocking, and regular collectable things (e.g. /// twigs). Collect, Craft(CraftingTab), - Mount(Vec>), + Mount, } pub enum FireplaceType { @@ -175,21 +172,7 @@ impl BlocksOfInterest { Some(SpriteKind::RepairBench) => { interactables.push((pos, Interaction::Craft(CraftingTab::All))) }, - Some( - sprite_kind @ SpriteKind::ChairSingle - | sprite_kind @ SpriteKind::ChairDouble, - ) => interactables.push(( - pos, - Interaction::Mount( - sprite_mount_points( - sprite_kind, - Vec3::zero(), - block.get_ori().unwrap_or(0), - ) - .map(|(pos, _)| pos) - .collect(), - ), - )), + _ if block.is_mountable() => interactables.push((pos, Interaction::Mount)), _ => {}, }, } diff --git a/voxygen/src/session/interactable.rs b/voxygen/src/session/interactable.rs index fbd10ec991..88050a61b9 100644 --- a/voxygen/src/session/interactable.rs +++ b/voxygen/src/session/interactable.rs @@ -1,5 +1,5 @@ use ordered_float::OrderedFloat; -use specs::{Join, WorldExt}; +use specs::{Join, ReadStorage, WorldExt}; use vek::*; use super::{ @@ -9,12 +9,12 @@ use super::{ use client::Client; use common::{ comp, - comp::{tool::ToolKind, CharacterState}, - consts::MAX_PICKUP_RANGE, + comp::{ship::figuredata::VOXEL_COLLIDER_MANIFEST, tool::ToolKind, Collider}, + consts::{MAX_PICKUP_RANGE, MAX_SPRITE_MOUNT_RANGE}, link::Is, - mounting::Mount, - states::utils::SPRITE_MOUNT_RANGE_SQR, + mounting::{Mount, VolumePos, VolumeRider}, terrain::{Block, TerrainGrid, UnlockKind}, + uid::{Uid, UidAllocator}, util::find_dist::{Cube, Cylinder, FindDist}, vol::ReadVol, }; @@ -33,12 +33,12 @@ pub enum BlockInteraction { // TODO: mining blocks don't use the interaction key, so it might not be the best abstraction // to have them here, will see how things turn out Mine(ToolKind), - Mount(Vec>), + Mount, } #[derive(Clone, Debug)] pub enum Interactable { - Block(Block, Vec3, BlockInteraction), + Block(Block, VolumePos, BlockInteraction), Entity(specs::Entity), } @@ -52,28 +52,34 @@ impl Interactable { fn from_block_pos( terrain: &TerrainGrid, - pos: Vec3, + uid_allocator: &UidAllocator, + colliders: &ReadStorage, + volume_pos: VolumePos, interaction: Interaction, ) -> Option { - let Ok(&block) = terrain.get(pos) else { return None }; + let Some(block) = volume_pos.get_block(terrain, uid_allocator, colliders) else { return None }; let block_interaction = match interaction { Interaction::Collect => { // Check if this is an unlockable sprite - let unlock = block.get_sprite().and_then(|sprite| { - let Some(chunk) = terrain.pos_chunk(pos) else { return None }; - let sprite_chunk_pos = TerrainGrid::chunk_offs(pos); - let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos); - let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned()); - // HACK: No other way to distinguish between things that should be unlockable - // and regular sprites with the current unlock_condition method so we hack - // around that by saying that it is a regular collectible sprite if - // `unlock_condition` returns UnlockKind::Free and the cfg was `None`. - if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) { - Some(unlock_condition) - } else { - None - } - }); + let unlock = match volume_pos.kind { + common::mounting::Volume::Terrain => block.get_sprite().and_then(|sprite| { + let Some(chunk) = terrain.pos_chunk(volume_pos.pos) else { return None }; + let sprite_chunk_pos = TerrainGrid::chunk_offs(volume_pos.pos); + let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos); + let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned()); + // HACK: No other way to distinguish between things that should be + // unlockable and regular sprites with the current + // unlock_condition method so we hack around that by + // saying that it is a regular collectible sprite if + // `unlock_condition` returns UnlockKind::Free and the cfg was `None`. + if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) { + Some(unlock_condition) + } else { + None + } + }), + common::mounting::Volume::Entity(_) => None, + }; if let Some(unlock) = unlock { BlockInteraction::Unlock(unlock) @@ -84,9 +90,9 @@ impl Interactable { } }, Interaction::Craft(tab) => BlockInteraction::Craft(tab), - Interaction::Mount(points) => BlockInteraction::Mount(points), + Interaction::Mount => BlockInteraction::Mount, }; - Some(Self::Block(block, pos, block_interaction)) + Some(Self::Block(block, volume_pos, block_interaction)) } } @@ -130,7 +136,11 @@ pub(super) fn select_interactable( collect_target.and_then(|t| { if Some(t.distance) == nearest_dist { terrain.get(t.position_int()).ok().map(|&b| { - Interactable::Block(b, t.position_int(), BlockInteraction::Collect) + Interactable::Block( + b, + VolumePos::terrain(t.position_int()), + BlockInteraction::Collect, + ) }) } else { None @@ -149,7 +159,7 @@ pub(super) fn select_interactable( if let Some(mine_tool) = b.mine_tool() && b.is_air() { Some(Interactable::Block( b, - t.position_int(), + VolumePos::terrain(t.position_int()), BlockInteraction::Mine(mine_tool), )) } else { @@ -232,6 +242,49 @@ pub(super) fn select_interactable( }); let scene_terrain = scene.terrain(); + let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); + + let volumes_data = ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ); + + let volumes = volumes_data + .join() + .filter_map(|(entity, uid, body, pos, ori, collider)| { + let vol = collider.get_vol(&voxel_colliders_manifest)?; + + let mat = Mat4::from(ori.to_quat()).translated_3d(pos.0) + * Mat4::translation_3d(vol.translation); + + let p = mat.inverted().mul_point(player_pos); + let aabb = Aabb { + min: Vec3::zero(), + max: vol.volume().sz.as_(), + }; + if aabb.contains_point(p) || aabb.distance_to_point(p) < search_dist { + scene + .figure_mgr() + .get_blocks_of_interest(entity, body, Some(collider)) + .map(move |(blocks_of_interest, _)| { + blocks_of_interest.interactables.iter().map( + move |(block_offset, interaction)| { + let wpos = mat.mul_point(block_offset.as_() + 0.5); + (wpos, VolumePos::entity(*block_offset, *uid), interaction) + }, + ) + }) + } else { + None + } + }) + .flatten(); + + let is_volume_rider = ecs.read_storage::>(); // Find closest interactable block // TODO: consider doing this one first? let closest_interactable_block_pos = Spiral2d::new() @@ -252,39 +305,36 @@ pub(super) fn select_interactable( .interactables .iter() .map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction)) - .filter_map(|(pos, interaction)| { - if matches!(player_char_state, Some(CharacterState::MountSprite(_))) && matches!(interaction, Interaction::Mount(_) | Interaction::Collect) { - None - } else if let Interaction::Mount(mount_points) = interaction { - mount_points.iter() - .map(|p| (p + pos.as_()).distance_squared(player_pos)) - .filter(|dist| *dist < SPRITE_MOUNT_RANGE_SQR) - .min_by_key(|dist| OrderedFloat(*dist)) - .map(|dist| (pos, dist)) - .zip(Some(interaction)) - } else { - Some(((pos, (pos.as_() + 0.5).distance_squared(player_pos)), interaction)) - } + .map(|(pos, interaction)| { + (pos.as_::() + 0.5, VolumePos::terrain(pos), interaction) }) }) - .map(|((block_pos, dis), interaction)| ( - block_pos, - dis, - interaction, - )) - .min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr)) - .map(|(block_pos, _, interaction)| (block_pos, interaction)); + .chain(volumes) + .filter(|(wpos, volume_pos, interaction)| { + match interaction { + Interaction::Mount => !is_volume_rider.contains(player_entity) && wpos.distance_squared(player_pos) < MAX_SPRITE_MOUNT_RANGE * MAX_SPRITE_MOUNT_RANGE && + is_volume_rider.join().find(|is_volume_rider| is_volume_rider.pos == *volume_pos).is_none(), + _ => true, + } + }) + .min_by_key(|(wpos, _, _)| OrderedFloat(wpos.distance_squared(player_pos))); // Return the closest of the 2 closest closest_interactable_block_pos - .filter(|(block_pos, _)| { + .filter(|(wpos, _, _)| { player_cylinder.min_distance(Cube { - min: block_pos.as_(), + min: *wpos, side_length: 1.0, }) < search_dist }) - .and_then(|(block_pos, interaction)| { - Interactable::from_block_pos(&terrain, block_pos, interaction.clone()) + .and_then(|(_, block_pos, interaction)| { + Interactable::from_block_pos( + &terrain, + &ecs.read_resource::(), + &ecs.read_storage(), + block_pos, + interaction.clone(), + ) }) .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 0c30f89370..52d44c7756 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -25,7 +25,7 @@ use common::{ consts::MAX_MOUNT_RANGE, event::UpdateCharacterMetadata, link::Is, - mounting::Mount, + mounting::{Mount, VolumePos}, outcome::Outcome, recipe, terrain::{Block, BlockKind}, @@ -350,7 +350,8 @@ impl SessionState { match inv_event { InventoryUpdateEvent::BlockCollectFailed { pos, reason } => { self.hud.add_failed_block_pickup( - pos, + // TODO: Support volumes. + VolumePos::terrain(pos), HudCollectFailedReason::from_server_reason( &reason, client.state().ecs(), @@ -887,14 +888,12 @@ impl PlayState for SessionState { let mut client = self.client.borrow_mut(); if client.is_riding() { client.unmount(); - } else if client.stand_if_mounted() { } else { if let Some(interactable) = &self.interactable { match interactable { Interactable::Block(_, pos, interaction) => { - if matches!(interaction, BlockInteraction::Mount(_)) - { - client.mount_sprite(*pos) + if matches!(interaction, BlockInteraction::Mount) { + client.mount_volume(*pos) } }, Interactable::Entity(entity) => client.mount(*entity), @@ -941,18 +940,25 @@ impl PlayState for SessionState { BlockInteraction::Collect | BlockInteraction::Unlock(_) => { if block.is_collectible() { - client.collect_block(*pos); + match pos.kind { + common::mounting::Volume::Terrain => { + client.collect_block(pos.pos); + } + common::mounting::Volume::Entity(_) => todo!(), + } } }, BlockInteraction::Craft(tab) => { self.hud.show.open_crafting_tab( *tab, - block.get_sprite().map(|s| (*pos, s)), + block + .get_sprite() + .map(|s| (*pos, s)), ) }, - BlockInteraction::Mount(_) => { + BlockInteraction::Mount => { if block.is_mountable() { - client.mount_sprite(*pos); + client.mount_volume(*pos) } }, BlockInteraction::Mine(_) => {},