mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added basic tethering
This commit is contained in:
parent
c422616f70
commit
9aa757cd09
@ -689,6 +689,11 @@ impl ServerChatCommand {
|
||||
Optional,
|
||||
),
|
||||
Float("destination_degrees_ccw_of_east", 90.0, Optional),
|
||||
Boolean(
|
||||
"Whether the ship should be tethered to the target (or its mount)",
|
||||
"false".to_string(),
|
||||
Optional,
|
||||
),
|
||||
],
|
||||
"Spawns a ship",
|
||||
Some(Admin),
|
||||
@ -723,6 +728,7 @@ impl ServerChatCommand {
|
||||
Integer("amount", 1, Optional),
|
||||
Boolean("ai", "true".to_string(), Optional),
|
||||
Float("scale", 1.0, Optional),
|
||||
Boolean("tethered", "false".to_string(), Optional),
|
||||
],
|
||||
"Spawn a test entity",
|
||||
Some(Admin),
|
||||
|
@ -1214,6 +1214,8 @@ impl Body {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn tether_offset(&self) -> Vec3<f32> { Vec3::new(0.0, self.dimensions().y * 0.5, 0.0) }
|
||||
|
||||
pub fn localize(&self) -> Content {
|
||||
match self {
|
||||
Self::BipedLarge(biped_large) => biped_large.localize(),
|
||||
|
@ -57,6 +57,7 @@ pub mod spiral;
|
||||
pub mod states;
|
||||
pub mod store;
|
||||
pub mod terrain;
|
||||
pub mod tether;
|
||||
pub mod time;
|
||||
pub mod trade;
|
||||
pub mod util;
|
||||
|
@ -2,7 +2,8 @@ use crate::{
|
||||
comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
|
||||
link::{Is, Link, LinkHandle, Role},
|
||||
terrain::{Block, TerrainGrid},
|
||||
uid::{IdMaps, Uid},
|
||||
tether,
|
||||
uid::{Uid, UidAllocator},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use hashbrown::HashSet;
|
||||
@ -45,6 +46,7 @@ impl Link for Mounting {
|
||||
WriteStorage<'a, Is<Mount>>,
|
||||
WriteStorage<'a, Is<Rider>>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
ReadStorage<'a, Is<tether::Follower>>,
|
||||
);
|
||||
type DeleteData<'a> = (
|
||||
Read<'a, IdMaps>,
|
||||
@ -67,7 +69,7 @@ impl Link for Mounting {
|
||||
|
||||
fn create(
|
||||
this: &LinkHandle<Self>,
|
||||
(id_maps, is_mounts, is_riders, is_volume_rider): &mut Self::CreateData<'_>,
|
||||
(id_maps, is_mounts, is_riders, is_volume_rider, is_followers): &mut Self::CreateData<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let entity = |uid: Uid| id_maps.uid_entity(uid);
|
||||
|
||||
@ -79,7 +81,7 @@ impl Link for Mounting {
|
||||
// relationship
|
||||
if !is_mounts.contains(mount)
|
||||
&& !is_riders.contains(rider)
|
||||
&& !is_riders.contains(rider)
|
||||
&& !is_followers.contains(rider)
|
||||
// TODO: Does this definitely prevent mount cycles?
|
||||
&& (!is_mounts.contains(rider) || !is_riders.contains(mount))
|
||||
&& !is_volume_rider.contains(rider)
|
||||
|
140
common/src/tether.rs
Normal file
140
common/src/tether.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use crate::{
|
||||
comp,
|
||||
link::{Is, Link, LinkHandle, Role},
|
||||
mounting::{Mount, Rider, VolumeRider},
|
||||
uid::{Uid, UidAllocator},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{
|
||||
saveload::MarkerAllocator, storage::GenericWriteStorage, Component, DenseVecStorage, Entities,
|
||||
Entity, Read, ReadExpect, ReadStorage, Write, WriteStorage,
|
||||
};
|
||||
use vek::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Leader;
|
||||
|
||||
impl Role for Leader {
|
||||
type Link = Tethered;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Follower;
|
||||
|
||||
impl Role for Follower {
|
||||
type Link = Tethered;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Tethered {
|
||||
pub leader: Uid,
|
||||
pub follower: Uid,
|
||||
pub tether_length: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TetherError {
|
||||
NoSuchEntity,
|
||||
NotTetherable,
|
||||
}
|
||||
|
||||
impl Link for Tethered {
|
||||
type CreateData<'a> = (
|
||||
Read<'a, UidAllocator>,
|
||||
WriteStorage<'a, Is<Leader>>,
|
||||
WriteStorage<'a, Is<Follower>>,
|
||||
ReadStorage<'a, Is<Rider>>,
|
||||
ReadStorage<'a, Is<Mount>>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
);
|
||||
type DeleteData<'a> = (
|
||||
Read<'a, UidAllocator>,
|
||||
WriteStorage<'a, Is<Leader>>,
|
||||
WriteStorage<'a, Is<Follower>>,
|
||||
WriteStorage<'a, comp::Pos>,
|
||||
WriteStorage<'a, comp::ForceUpdate>,
|
||||
);
|
||||
type Error = TetherError;
|
||||
type PersistData<'a> = (
|
||||
Read<'a, UidAllocator>,
|
||||
Entities<'a>,
|
||||
ReadStorage<'a, comp::Health>,
|
||||
ReadStorage<'a, comp::Body>,
|
||||
ReadStorage<'a, Is<Leader>>,
|
||||
ReadStorage<'a, Is<Follower>>,
|
||||
ReadStorage<'a, comp::CharacterState>,
|
||||
);
|
||||
|
||||
fn create(
|
||||
this: &LinkHandle<Self>,
|
||||
(
|
||||
uid_allocator,
|
||||
is_leaders,
|
||||
is_followers,
|
||||
is_riders,
|
||||
is_mounts,
|
||||
is_volume_rider,
|
||||
): &mut Self::CreateData<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
if this.leader == this.follower {
|
||||
// Forbid self-tethering
|
||||
Err(TetherError::NotTetherable)
|
||||
} else if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) {
|
||||
// Ensure that neither leader or follower are already part of a conflicting
|
||||
// relationship
|
||||
if !is_riders.contains(follower)
|
||||
&& !is_volume_rider.contains(follower)
|
||||
&& !is_followers.contains(follower)
|
||||
// TODO: Does this definitely prevent tether cycles?
|
||||
&& (!is_leaders.contains(follower) || !is_followers.contains(leader))
|
||||
{
|
||||
let _ = is_leaders.insert(leader, this.make_role());
|
||||
let _ = is_followers.insert(follower, this.make_role());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TetherError::NotTetherable)
|
||||
}
|
||||
} else {
|
||||
Err(TetherError::NoSuchEntity)
|
||||
}
|
||||
}
|
||||
|
||||
fn persist(
|
||||
this: &LinkHandle<Self>,
|
||||
(uid_allocator, entities, healths, bodies, is_leaders, is_followers, character_states): &mut Self::PersistData<'_>,
|
||||
) -> bool {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) {
|
||||
let is_alive = |entity| {
|
||||
entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead)
|
||||
};
|
||||
|
||||
// Ensure that both entities are alive and that they continue to be linked
|
||||
is_alive(leader)
|
||||
&& is_alive(follower)
|
||||
&& is_leaders.get(leader).is_some()
|
||||
&& is_followers.get(follower).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(
|
||||
this: &LinkHandle<Self>,
|
||||
(uid_allocator, is_leaders, is_followers, positions, force_update): &mut Self::DeleteData<
|
||||
'_,
|
||||
>,
|
||||
) {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
let leader = entity(this.leader);
|
||||
let follower = entity(this.follower);
|
||||
|
||||
// Delete link components
|
||||
leader.map(|leader| is_leaders.remove(leader));
|
||||
follower.map(|follower| is_followers.remove(follower));
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ use common::{
|
||||
shared_server_config::ServerConstants,
|
||||
slowjob::SlowJobPool,
|
||||
terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid},
|
||||
tether,
|
||||
time::DayPeriod,
|
||||
trade::Trades,
|
||||
vol::{ReadVol, WriteVol},
|
||||
@ -202,6 +203,8 @@ impl State {
|
||||
ecs.register::<Is<Mount>>();
|
||||
ecs.register::<Is<Rider>>();
|
||||
ecs.register::<Is<VolumeRider>>();
|
||||
ecs.register::<Is<tether::Leader>>();
|
||||
ecs.register::<Is<tether::Follower>>();
|
||||
ecs.register::<comp::Mass>();
|
||||
ecs.register::<comp::Density>();
|
||||
ecs.register::<comp::Collider>();
|
||||
|
@ -13,6 +13,7 @@ pub mod phys;
|
||||
pub mod projectile;
|
||||
mod shockwave;
|
||||
mod stats;
|
||||
mod tether;
|
||||
|
||||
// External
|
||||
use common_ecs::{dispatch, System};
|
||||
@ -21,6 +22,7 @@ use specs::DispatcherBuilder;
|
||||
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
//TODO: don't run interpolation on server
|
||||
dispatch::<interpolation::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<tether::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<mount::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<controller::Sys>(dispatch_builder, &[&mount::Sys::sys_name()]);
|
||||
dispatch::<character_behavior::Sys>(dispatch_builder, &[&controller::Sys::sys_name()]);
|
||||
|
106
common/systems/src/tether.rs
Normal file
106
common/systems/src/tether.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use common::{
|
||||
comp::{Body, Collider, InputKind, Mass, Ori, Pos, Scale, Vel},
|
||||
link::Is,
|
||||
resources::DeltaTime,
|
||||
tether::{Follower, Leader},
|
||||
uid::UidAllocator,
|
||||
util::Dir,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{
|
||||
saveload::{Marker, MarkerAllocator},
|
||||
Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage,
|
||||
};
|
||||
use tracing::error;
|
||||
use vek::*;
|
||||
|
||||
/// This system is responsible for controlling mounts
|
||||
#[derive(Default)]
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Read<'a, UidAllocator>,
|
||||
Entities<'a>,
|
||||
Read<'a, DeltaTime>,
|
||||
ReadStorage<'a, Is<Leader>>,
|
||||
ReadStorage<'a, Is<Follower>>,
|
||||
WriteStorage<'a, Pos>,
|
||||
WriteStorage<'a, Vel>,
|
||||
WriteStorage<'a, Ori>,
|
||||
ReadStorage<'a, Body>,
|
||||
ReadStorage<'a, Scale>,
|
||||
ReadStorage<'a, Collider>,
|
||||
ReadStorage<'a, Mass>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "tether";
|
||||
const ORIGIN: Origin = Origin::Common;
|
||||
const PHASE: Phase = Phase::Create;
|
||||
|
||||
fn run(
|
||||
_job: &mut Job<Self>,
|
||||
(
|
||||
uid_allocator,
|
||||
entities,
|
||||
dt,
|
||||
is_leaders,
|
||||
is_followers,
|
||||
mut positions,
|
||||
mut velocities,
|
||||
mut orientations,
|
||||
bodies,
|
||||
scales,
|
||||
colliders,
|
||||
masses,
|
||||
): Self::SystemData,
|
||||
) {
|
||||
for (follower, is_follower, follower_body) in
|
||||
(&entities, &is_followers, bodies.maybe()).join()
|
||||
{
|
||||
let Some(leader) = uid_allocator
|
||||
.retrieve_entity_internal(is_follower.leader.id())
|
||||
else { continue };
|
||||
|
||||
let (Some(leader_pos), Some(follower_pos)) = (
|
||||
positions.get(leader).copied(),
|
||||
positions.get(follower).copied(),
|
||||
) else { continue };
|
||||
|
||||
let (Some(leader_mass), Some(follower_mass)) = (
|
||||
masses.get(leader).copied(),
|
||||
masses.get(follower).copied(),
|
||||
) else { continue };
|
||||
|
||||
if velocities.contains(follower) && velocities.contains(leader) {
|
||||
let tether_offset = orientations
|
||||
.get(follower)
|
||||
.map(|ori| {
|
||||
ori.to_quat() * follower_body.map(|b| b.tether_offset()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tether_pos = follower_pos.0 + tether_offset;
|
||||
let pull_factor =
|
||||
(leader_pos.0.distance(tether_pos) - is_follower.tether_length).clamp(0.0, 1.0);
|
||||
let strength = pull_factor * 30000.0;
|
||||
let pull_dir = (leader_pos.0 - follower_pos.0)
|
||||
.try_normalized()
|
||||
.unwrap_or_default();
|
||||
let impulse = pull_dir * strength * dt.0;
|
||||
|
||||
// Can't fail
|
||||
velocities.get_mut(follower).unwrap().0 += impulse / follower_mass.0;
|
||||
velocities.get_mut(leader).unwrap().0 -= impulse / leader_mass.0;
|
||||
|
||||
if let Some(follower_ori) = orientations.get_mut(follower) {
|
||||
let turn_strength = pull_factor
|
||||
* follower_body
|
||||
.map(|b| b.tether_offset().magnitude())
|
||||
.unwrap_or(0.0)
|
||||
* 4.0;
|
||||
let target_ori = follower_ori.yawed_towards(Dir::new(pull_dir));
|
||||
*follower_ori = follower_ori.slerped_towards(target_ori, turn_strength * dt.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -46,7 +46,8 @@ use common::{
|
||||
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale},
|
||||
rtsim::{Actor, Role},
|
||||
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
|
||||
uid::Uid,
|
||||
tether::Tethered,
|
||||
uid::{IdMaps, Uid},
|
||||
vol::ReadVol,
|
||||
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
|
||||
};
|
||||
@ -1558,8 +1559,15 @@ fn handle_spawn(
|
||||
args: Vec<String>,
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) {
|
||||
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai, opt_scale) => {
|
||||
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32, bool) {
|
||||
(
|
||||
Some(opt_align),
|
||||
Some(npc::NpcBody(id, mut body)),
|
||||
opt_amount,
|
||||
opt_ai,
|
||||
opt_scale,
|
||||
opt_tethered,
|
||||
) => {
|
||||
let uid = uid(server, target, "target")?;
|
||||
let alignment = parse_alignment(uid, &opt_align)?;
|
||||
let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50);
|
||||
@ -1608,6 +1616,28 @@ fn handle_spawn(
|
||||
|
||||
let new_entity = entity_base.build();
|
||||
|
||||
if opt_tethered == Some(true) {
|
||||
let tether_leader = server
|
||||
.state
|
||||
.read_component_cloned::<Is<Rider>>(target)
|
||||
.map(|is_rider| is_rider.mount)
|
||||
.or_else(|| server.state.ecs().uid_from_entity(target));
|
||||
let tether_follower = server.state.ecs().uid_from_entity(new_entity);
|
||||
|
||||
if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
|
||||
server
|
||||
.state
|
||||
.link(Tethered {
|
||||
leader,
|
||||
follower,
|
||||
tether_length: 4.0,
|
||||
})
|
||||
.map_err(|_| "Failed to tether entities")?;
|
||||
} else {
|
||||
return Err("Tether members don't have Uids.".into());
|
||||
}
|
||||
}
|
||||
|
||||
// Add to group system if a pet
|
||||
if matches!(alignment, comp::Alignment::Owned { .. }) {
|
||||
let server_eventbus =
|
||||
@ -1748,7 +1778,7 @@ fn handle_spawn_ship(
|
||||
args: Vec<String>,
|
||||
_action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
let (body_name, angle) = parse_cmd_args!(args, String, f32);
|
||||
let (body_name, angle, tethered) = parse_cmd_args!(args, String, f32, bool);
|
||||
let mut pos = position(server, target, "target")?;
|
||||
pos.0.z += 2.0;
|
||||
const DESTINATION_RADIUS: f32 = 2000.0;
|
||||
@ -1767,16 +1797,40 @@ fn handle_spawn_ship(
|
||||
let mut builder = server
|
||||
.state
|
||||
.create_ship(pos, ori, ship, |ship| ship.make_collider());
|
||||
if let Some(pos) = destination {
|
||||
let (kp, ki, kd) =
|
||||
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
|
||||
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
|
||||
let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
|
||||
.with_destination(pos)
|
||||
.with_position_pid_controller(comp::PidController::new(kp, ki, kd, pos, 0.0, pure_z));
|
||||
builder = builder.with(agent);
|
||||
|
||||
// if let Some(pos) = destination {
|
||||
// let (kp, ki, kd) =
|
||||
// comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.
|
||||
// 0, 0.0, 0.0)); fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp -
|
||||
// pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
|
||||
// .with_destination(pos)
|
||||
// .with_position_pid_controller(comp::PidController::new(kp, ki, kd,
|
||||
// pos, 0.0, pure_z)); builder = builder.with(agent);
|
||||
// }
|
||||
|
||||
let new_entity = builder.build();
|
||||
|
||||
if tethered == Some(true) {
|
||||
let tether_leader = server
|
||||
.state
|
||||
.read_component_cloned::<Is<Rider>>(target)
|
||||
.map(|is_rider| is_rider.mount)
|
||||
.or_else(|| server.state.ecs().uid_from_entity(target));
|
||||
let tether_follower = server.state.ecs().uid_from_entity(new_entity);
|
||||
|
||||
if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
|
||||
server
|
||||
.state
|
||||
.link(Tethered {
|
||||
leader,
|
||||
follower,
|
||||
tether_length: 6.0,
|
||||
})
|
||||
.map_err(|_| "Failed to tether entities")?;
|
||||
} else {
|
||||
return Err("Tether members don't have Uids.".into());
|
||||
}
|
||||
}
|
||||
builder.build();
|
||||
|
||||
server.notify_client(
|
||||
client,
|
||||
|
Loading…
Reference in New Issue
Block a user