intercat with sprites on ships

This commit is contained in:
Isse 2023-04-17 15:38:41 +02:00
parent 7fbe2cd5ec
commit 6674cce2cc
46 changed files with 796 additions and 441 deletions

View File

@ -19,6 +19,9 @@
custom_indices: {
1: Air(ChairSingle, 4),
2: Air(Helm, 0),
3: Air(Door, 4),
8: Air(Door, 0),
},
),
AirBalloon: (

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/furniture/helm.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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: [

View File

@ -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<i32>, 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<i32>) -> 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<Vec3<i32>>,
sprite_pos: Option<VolumePos>,
) -> bool {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
@ -1288,7 +1288,7 @@ impl Client {
material: InvSlotId,
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Option<Vec3<i32>>,
sprite_pos: Option<VolumePos>,
) {
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<i32>,
sprite_pos: VolumePos,
) -> bool {
let is_repairable = {
let inventories = self.inventories();
@ -1448,6 +1448,12 @@ impl Client {
.read_storage::<Is<Rider>>()
.get(self.entity())
.is_some()
|| self
.state
.ecs()
.read_storage::<Is<VolumeRider>>()
.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::<CharacterState>()
.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::<CharacterState>()
.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<i32>) {
self.control_action(ControlAction::MountSprite(pos));
}
pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
let auxiliary_key = self
.inventories()

View File

@ -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<Mount>;
pub type IsRider = Is<Rider>;
pub type IsVolumeRider = Is<VolumeRider>;
}
// 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;
}

View File

@ -470,7 +470,6 @@ impl From<&CharacterState> for CharacterAbilityType {
| CharacterState::SpriteSummon(_)
| CharacterState::UseItem(_)
| CharacterState::SpriteInteract(_)
| CharacterState::MountSprite(_)
| CharacterState::Skate(_)
| CharacterState::Wallrun(_) => Self::Other,
}

View File

@ -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,

View File

@ -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<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
}
@ -57,7 +58,7 @@ pub enum InventoryManip {
Sort,
CraftRecipe {
craft_event: CraftEvent,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
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<i32>),
Dance,
Sneak,
Stand,

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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<Mount>>,
WriteStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
);
type DeleteData<'a> = (
Read<'a, UidAllocator>,
@ -59,7 +67,7 @@ impl Link for Mounting {
fn create(
this: &LinkHandle<Self>,
(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<i32>,
}
impl VolumePos {
pub fn terrain(block_pos: Vec3<i32>) -> Self {
Self {
kind: Volume::Terrain,
pos: block_pos,
}
}
pub fn entity(block_pos: Vec3<i32>, 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<comp::Pos, impl std::ops::Deref<Target = MaskedStorage<comp::Pos>>>,
orientations: &Storage<comp::Ori, impl std::ops::Deref<Target = MaskedStorage<comp::Ori>>>,
colliders: &ReadStorage<comp::Collider>,
) -> Option<(Mat4<f32>, 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::<f32>::translation_3d(local_translation);
Some((trans, block))
})
},
}
}
pub fn get_block(
&self,
terrain: &TerrainGrid,
uid_allocator: &UidAllocator,
colliders: &ReadStorage<comp::Collider>,
) -> Option<Block> {
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<Vec3<i32>>,
}
impl Component for VolumeRiders {
type Storage = DenseVecStorage<Self>;
}
#[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<VolumeRider>>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<Mount>>,
ReadExpect<'a, TerrainGrid>,
Read<'a, UidAllocator>,
ReadStorage<'a, comp::Collider>,
);
type DeleteData<'a> = (
Write<'a, VolumeRiders>,
WriteStorage<'a, VolumeRiders>,
WriteStorage<'a, Is<VolumeRider>>,
Read<'a, UidAllocator>,
);
type Error = MountingError;
type PersistData<'a> = (
Entities<'a>,
ReadStorage<'a, comp::Health>,
Read<'a, VolumeRiders>,
ReadStorage<'a, VolumeRiders>,
ReadStorage<'a, Is<VolumeRider>>,
ReadExpect<'a, TerrainGrid>,
Read<'a, UidAllocator>,
ReadStorage<'a, comp::Collider>,
);
fn create(
this: &LinkHandle<Self>,
(
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<Self>,
(
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<Self>,
(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);
}
}
}

View File

@ -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<i32>,
) -> 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<Rider>>,
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
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<Rider>>,
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
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,
}
}

View File

@ -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<i32>) -> 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

View File

@ -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<i32>) -> 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);

View File

@ -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<i32>) -> 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);

View File

@ -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;

View File

@ -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<f32>,
pub mount_dir: Vec3<f32>,
/// Position sprite is located at
pub sprite_pos: Vec3<i32>,
}
#[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
}
}

View File

@ -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<i32>) -> 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

View File

@ -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<i32>) -> 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());

View File

@ -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<i32>,
ori: u8,
) -> impl ExactSizeIterator<Item = (Vec3<f32>, Vec3<f32>)> {
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<i32>) {
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;

View File

@ -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<i32>) -> 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);

View File

@ -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<f32>, Vec3<f32>)> {
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 {

View File

@ -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<f32>, Vec3<f32>)] {
const UNIT_Y: Vec3<f32> = Vec3 {
x: 0.0,
y: -1.0,
z: 0.0,
};
pub fn mount_offset(&self) -> Option<(Vec3<f32>, Vec3<f32>)> {
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,
)
}
}

View File

@ -149,6 +149,18 @@ impl FindDist<Vec3<f32>> for Cylinder {
}
}
impl FindDist<Cylinder> for Vec3<f32> {
#[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::*;

View File

@ -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::<comp::Scale>();
ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>();
ecs.register::<Is<VolumeRider>>();
ecs.register::<comp::Mass>();
ecs.register::<comp::Density>();
ecs.register::<comp::Collider>();
@ -260,6 +261,7 @@ impl State {
ecs.register::<comp::invite::Invite>();
ecs.register::<comp::invite::PendingInvites>();
ecs.register::<comp::Beam>();
ecs.register::<VolumeRiders>();
// 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")]

View File

@ -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<Rider>>,
is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
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),
};

View File

@ -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<ServerEvent>>,
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,

View File

@ -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<Mount>>,
ReadStorage<'a, Is<VolumeRider>>,
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<Self>,
(
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 => {},
}
}
}
}
}
}

View File

@ -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<Rider>>,
is_volume_ridings: ReadStorage<'a, Is<VolumeRider>>,
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

View File

@ -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);
}

View File

@ -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<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>,
pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")]

View File

@ -229,6 +229,7 @@ fn position_mut<T>(
descriptor: &str,
f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
) -> CmdResult<T> {
// TODO: Handle volume mount
let entity = server
.state
.ecs()

View File

@ -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::<Is<Rider>>().get(rider).is_none() {
let not_mounting_yet = state.ecs().read_storage::<Is<Mount>>().get(mount).is_none();
let within_range = {
let positions = state.ecs().read_storage::<Pos>();
within_mounting_range(positions.get(rider), positions.get(mount))
};
let within_range = || {
let positions = state.ecs().read_storage::<Pos>();
within_mounting_range(positions.get(rider), positions.get(mount))
};
let healths = state.ecs().read_storage::<comp::Health>();
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::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(rider).copied(), uids.get(mount).copied())
{
let is_pet = matches!(
state
.ecs()
.read_storage::<comp::Alignment>()
.get(mount),
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
);
let can_ride = state
if within_range {
let uids = state.ecs().read_storage::<Uid>();
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::<comp::Alignment>()
.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::<Pos>();
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::<Uid>().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::<Is<Rider>>().remove(rider);
state.ecs().write_storage::<Is<VolumeRider>>().remove(rider);
}
fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool {

View File

@ -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::<AbilityMap>();
let msm = state.ecs().read_resource::<MaterialStatManifest>();
let get_craft_sprite = |state, sprite_pos: Option<Vec3<i32>>| {
let get_craft_sprite = |state, sprite_pos: Option<VolumePos>| {
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())
};

View File

@ -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)

View File

@ -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::<Mounting>(self);
maintain_link::<VolumeMounting>(self);
}
fn delete_entity_recorded(

View File

@ -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);

View File

@ -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<Rider>>,
is_volume_rider: &ReadStorage<'_, Is<VolumeRider>>,
force_updates: &ReadStorage<'_, ForceUpdate>,
skill_set: &mut Option<Cow<'_, SkillSet>>,
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<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
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,

View File

@ -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<String>,
pub craft_sprite: Option<(Vec3<i32>, 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?

View File

@ -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<i32>, SpriteKind)>,
craft_sprite: Option<(VolumePos, SpriteKind)>,
amount: u32,
},
SalvageItem {
slot: InvSlotId,
salvage_pos: Vec3<i32>,
salvage_pos: VolumePos,
},
CraftModularWeapon {
primary_slot: InvSlotId,
secondary_slot: InvSlotId,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
CraftModularWeaponComponent {
toolkind: ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
RepairItem {
item: Slot,
sprite_pos: Vec3<i32>,
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<i32>, 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<Vec3<i32>, CollectFailedData>,
failed_block_pickups: HashMap<VolumePos, CollectFailedData>,
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
new_loot_messages: VecDeque<LootMessage>,
new_messages: VecDeque<comp::ChatMsg>,
@ -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<i32>, 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 {

View File

@ -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::<LightEmitter>().maybe(),
(
ecs.read_storage::<Is<Rider>>().maybe(),
ecs.read_storage::<Is<VolumeRider>>().maybe(),
ecs.read_storage::<Collider>().maybe(),
ecs.read_storage::<Stance>().maybe(),
ecs.read_storage::<SkillSet>().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,

View File

@ -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<Vec3<f32>>),
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)),
_ => {},
},
}

View File

@ -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<Vec3<f32>>),
Mount,
}
#[derive(Clone, Debug)]
pub enum Interactable {
Block(Block, Vec3<i32>, BlockInteraction),
Block(Block, VolumePos, BlockInteraction),
Entity(specs::Entity),
}
@ -52,28 +52,34 @@ impl Interactable {
fn from_block_pos(
terrain: &TerrainGrid,
pos: Vec3<i32>,
uid_allocator: &UidAllocator,
colliders: &ReadStorage<Collider>,
volume_pos: VolumePos,
interaction: Interaction,
) -> Option<Self> {
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::<Uid>(),
&ecs.read_storage::<comp::Body>(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Ori>(),
&ecs.read_storage::<comp::Collider>(),
);
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::<Is<VolumeRider>>();
// 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_::<f32>() + 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::<UidAllocator>(),
&ecs.read_storage(),
block_pos,
interaction.clone(),
)
})
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
}

View File

@ -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(_) => {},