Merge branch 'aweinstock/airship-mvp-rebased' into 'master'

Airships

See merge request veloren/veloren!1888
This commit is contained in:
Marcel 2021-03-15 11:37:12 +00:00
commit 5b21ee7200
51 changed files with 2202 additions and 841 deletions

View File

@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Gave weapons critical strike {chance, multiplier} stats
- A system to add glow and reflection effects to figures (i.e: characters, armour, weapons, etc.)
- Merchants will trade wares with players
- Airships that can be mounted and flown, and also walked on (`/airship` admin command)
### Changed

1
Cargo.lock generated
View File

@ -5619,6 +5619,7 @@ dependencies = [
"hashbrown",
"serde",
"specs",
"specs-idvs",
"sum_type",
"tracing",
"vek 0.14.1",

View File

@ -969,6 +969,15 @@
),
species: ()
),
ship: (
body: (
keyword: "ship",
names_0: [
"Boaty McBoatface",
],
),
species: (),
),
biped_small: (
body: (
keyword: "biped_small",

View File

@ -0,0 +1,23 @@
({
DefaultAirship: (
bone0: (
//offset: (3.0, 7.0, 1.0),
//offset: (-20.75, -34.75, 1.25),
//offset: (0.0, 0.0, 0.0),
offset: (-17.5, -35.0, 1.0),
//phys_offset: (0.25, 0.25, 0.25),
phys_offset: (0.0, 0.0, 0.0),
central: ("Human_Airship"),
),
bone1: (
offset: (0.0, 40.0, -8.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("propeller-l"),
),
bone2: (
offset: (0.0, 0.0, -4.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("propeller-r"),
),
),
})

BIN
assets/server/voxel/Human_Airship.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/server/voxel/airship.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/server/voxel/propeller-l.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/server/voxel/propeller-r.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -33,6 +33,7 @@ use common::{
grid::Grid,
outcome::Outcome,
recipe::RecipeBook,
resources::PlayerEntity,
terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize},
trade::{PendingTrade, TradeAction, TradeId, TradeResult},
uid::{Uid, UidAllocator},
@ -281,6 +282,7 @@ impl Client {
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
*state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
state.ecs_mut().insert(material_stats);
state.ecs_mut().insert(ability_map);
@ -1458,6 +1460,7 @@ impl Client {
ServerGeneral::SetPlayerEntity(uid) => {
if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) {
self.entity = entity;
*self.state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
} else {
return Err(Error::Other("Failed to find entity from uid.".to_owned()));
}

View File

@ -15,6 +15,7 @@ default = ["simd"]
[dependencies]
common-base = { package = "veloren-common-base", path = "base" }
#inline_tweak = "1.0.2"
# Serde
serde = { version = "1.0.110", features = ["derive", "rc"] }

View File

@ -12,6 +12,7 @@ default = ["simd"]
[dependencies]
common = {package = "veloren-common", path = "../../common"}
#inline_tweak = "1.0.2"
sum_type = "0.2.0"
vek = { version = "=0.14.1", features = ["serde"] }
@ -25,6 +26,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253
# ECS
specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "storage-event-control"], rev = "5a9b71035007be0e3574f35184acac1cd4530496" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", rev = "b65fb220e94f5d3c9bc30074a076149763795556" }
# Serde
serde = { version = "1.0.110", features = ["derive"] }
serde = { version = "1.0.110", features = ["derive"] }

View File

@ -100,9 +100,9 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_interp_insert(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_interp_insert(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_interp_insert(comp, entity, world),
EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_insert(comp, entity, world),
}
@ -132,9 +132,9 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_interp_modify(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_interp_modify(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_interp_modify(comp, entity, world),
EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_modify(comp, entity, world),
}
@ -168,9 +168,9 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world)
},
EcsCompPhantom::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(entity, world),
EcsCompPhantom::Pos(_) => sync::handle_interp_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_interp_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_interp_remove::<comp::Ori>(entity, world),
EcsCompPhantom::Shockwave(_) => sync::handle_remove::<comp::Shockwave>(entity, world),
EcsCompPhantom::BeamSegment(_) => sync::handle_remove::<comp::Ori>(entity, world),
}

View File

@ -0,0 +1,155 @@
// impls of `InterpolatableComponent` on things defined in `common`, since
// `common_net` is downstream of `common`, and an `InterpolationSystem` that
// applies them
use super::InterpolatableComponent;
use common::comp::{Ori, Pos, Vel};
use specs::Component;
use specs_idvs::IdvStorage;
use tracing::warn;
use vek::ops::{Lerp, Slerp};
#[derive(Debug, Default)]
pub struct InterpBuffer<T> {
pub buf: [(f64, T); 4],
pub i: usize,
}
impl<T> InterpBuffer<T> {
fn push(&mut self, time: f64, x: T) {
let InterpBuffer {
ref mut buf,
ref mut i,
} = self;
*i += 1;
*i %= buf.len();
buf[*i] = (time, x);
}
}
impl<T: 'static + Send + Sync> Component for InterpBuffer<T> {
type Storage = IdvStorage<Self>;
}
// 0 is pure physics, 1 is pure extrapolation
const PHYSICS_VS_EXTRAPOLATION_FACTOR: f32 = 0.2;
const POSITION_INTERP_SANITY: Option<f32> = None;
const VELOCITY_INTERP_SANITY: Option<f32> = None;
const ENABLE_POSITION_HERMITE: bool = false;
impl InterpolatableComponent for Pos {
type InterpData = InterpBuffer<Pos>;
type ReadData = InterpBuffer<Vel>;
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) {
interp_data.push(time, *self);
}
fn interpolate(self, interp_data: &Self::InterpData, t2: f64, vel: &InterpBuffer<Vel>) -> Self {
// lerp to test interface, do hermite spline later
let InterpBuffer { ref buf, ref i } = interp_data;
let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()];
let (t1, p1) = buf[i % buf.len()];
if (t1 - t0).abs() < f64::EPSILON {
return self;
}
if POSITION_INTERP_SANITY
.map_or(false, |limit| p0.0.distance_squared(p1.0) > limit.powf(2.0))
{
warn!("position delta exceeded sanity check, clamping");
return p1;
}
let (t0prime, m0) = vel.buf[(i + vel.buf.len() - 1) % vel.buf.len()];
let (t1prime, m1) = vel.buf[i % vel.buf.len()];
let mut out;
let t = (t2 - t0) / (t1 - t0);
if ENABLE_POSITION_HERMITE
&& ((t0 - t0prime).abs() < f64::EPSILON && (t1 - t1prime).abs() < f64::EPSILON)
{
let h00 = |t: f64| (2.0 * t.powf(3.0) - 3.0 * t.powf(2.0) + 1.0) as f32;
let h10 = |t: f64| (t.powf(3.0) - 2.0 * t.powf(2.0) + t) as f32;
let h01 = |t: f64| (-2.0 * t.powf(3.0) + 3.0 * t.powf(2.0)) as f32;
let h11 = |t: f64| (t.powf(3.0) - t.powf(2.0)) as f32;
let dt = (t1 - t0) as f32;
out = h00(t) * p0.0 + h10(t) * dt * m0.0 + h01(t) * p1.0 + h11(t) * dt * m1.0;
} else {
if ENABLE_POSITION_HERMITE {
warn!(
"timestamps for pos and vel don't match ({:?}, {:?}), falling back to lerp",
interp_data, vel
);
}
out = Lerp::lerp_unclamped(p0.0, p1.0, t as f32);
}
if out.map(|x| x.is_nan()).reduce_or() {
warn!("interpolation output is nan: {}, {}, {:?}", t2, t, buf);
out = p1.0;
}
Pos(Lerp::lerp(self.0, out, PHYSICS_VS_EXTRAPOLATION_FACTOR))
}
}
impl InterpolatableComponent for Vel {
type InterpData = InterpBuffer<Vel>;
type ReadData = ();
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) {
interp_data.push(time, *self);
}
fn interpolate(self, interp_data: &Self::InterpData, t2: f64, _: &()) -> Self {
let InterpBuffer { ref buf, ref i } = interp_data;
let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()];
let (t1, p1) = buf[i % buf.len()];
if (t1 - t0).abs() < f64::EPSILON {
return self;
}
if VELOCITY_INTERP_SANITY
.map_or(false, |limit| p0.0.distance_squared(p1.0) > limit.powf(2.0))
{
warn!("velocity delta exceeded sanity check, clamping");
return p1;
}
let lerp_factor = 1.0 + ((t2 - t1) / (t1 - t0)) as f32;
let mut out = Lerp::lerp_unclamped(p0.0, p1.0, lerp_factor);
if out.map(|x| x.is_nan()).reduce_or() {
warn!(
"interpolation output is nan: {}, {}, {:?}",
t2, lerp_factor, buf
);
out = p1.0;
}
Vel(Lerp::lerp(self.0, out, PHYSICS_VS_EXTRAPOLATION_FACTOR))
}
}
impl InterpolatableComponent for Ori {
type InterpData = InterpBuffer<Ori>;
type ReadData = ();
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) {
interp_data.push(time, *self);
}
fn interpolate(self, interp_data: &Self::InterpData, t2: f64, _: &()) -> Self {
let InterpBuffer { ref buf, ref i } = interp_data;
let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()];
let (t1, p1) = buf[i % buf.len()];
if (t1 - t0).abs() < f64::EPSILON {
return self;
}
let lerp_factor = 1.0 + ((t2 - t1) / (t1 - t0)) as f32;
let mut out = Slerp::slerp_unclamped(p0.to_quat(), p1.to_quat(), lerp_factor);
if out.into_vec4().map(|x| x.is_nan()).reduce_or() {
warn!(
"interpolation output is nan: {}, {}, {:?}",
t2, lerp_factor, buf
);
out = p1.to_quat();
}
Ori::new(Slerp::slerp(self.to_quat(), out, PHYSICS_VS_EXTRAPOLATION_FACTOR).normalized())
}
}

View File

@ -1,5 +1,6 @@
// Note: Currently only one-way sync is supported until a usecase for two-way
// sync arises
pub mod interpolation;
mod packet;
mod sync_ext;
mod track;
@ -7,8 +8,9 @@ mod track;
// Reexports
pub use common::uid::{Uid, UidAllocator};
pub use packet::{
handle_insert, handle_modify, handle_remove, CompPacket, CompSyncPackage, EntityPackage,
EntitySyncPackage, StatePackage,
handle_insert, handle_interp_insert, handle_interp_modify, handle_interp_remove, handle_modify,
handle_remove, CompPacket, CompSyncPackage, EntityPackage, EntitySyncPackage,
InterpolatableComponent, StatePackage,
};
pub use sync_ext::WorldSyncExt;
pub use track::UpdateTracker;

View File

@ -1,5 +1,5 @@
use super::track::UpdateTracker;
use common::uid::Uid;
use common::{resources::Time, uid::Uid};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use specs::{Component, Entity, Join, ReadStorage, World, WorldExt};
use std::{
@ -9,6 +9,10 @@ use std::{
};
use tracing::error;
// TODO: apply_{insert,modify,remove} all take the entity and call
// `write_storage` once per entity per component, instead of once per update
// batch(e.g. in a system-like memory access pattern); if sync ends up being a
// bottleneck, try optimizing this
/// Implemented by type that carries component data for insertion and
/// modification The assocatied `Phantom` type only carries information about
/// which component type is of interest and is used to transmit deletion events
@ -42,6 +46,44 @@ pub fn handle_remove<C: Component>(entity: Entity, world: &World) {
world.write_storage::<C>().remove(entity);
}
pub trait InterpolatableComponent: Component {
type InterpData: Component + Default;
type ReadData;
fn update_component(&self, data: &mut Self::InterpData, time: f64);
fn interpolate(self, data: &Self::InterpData, time: f64, read_data: &Self::ReadData) -> Self;
}
pub fn handle_interp_insert<C: InterpolatableComponent>(comp: C, entity: Entity, world: &World) {
let mut interp_data = C::InterpData::default();
let time = world.read_resource::<Time>().0;
comp.update_component(&mut interp_data, time);
handle_insert(comp, entity, world);
handle_insert(interp_data, entity, world);
}
pub fn handle_interp_modify<C: InterpolatableComponent + Debug>(
comp: C,
entity: Entity,
world: &World,
) {
if let Some(mut interp_data) = world.write_storage::<C::InterpData>().get_mut(entity) {
let time = world.read_resource::<Time>().0;
comp.update_component(&mut interp_data, time);
handle_modify(comp, entity, world);
} else {
error!(
?comp,
"Error modifying interpolation data for synced component, it doesn't seem to exist"
);
}
}
pub fn handle_interp_remove<C: InterpolatableComponent>(entity: Entity, world: &World) {
handle_remove::<C>(entity, world);
handle_remove::<C::InterpData>(entity, world);
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum CompUpdateKind<P: CompPacket> {
Inserted(P),

View File

@ -36,6 +36,7 @@ impl ChatCommandData {
#[derive(Copy, Clone)]
pub enum ChatCommand {
Adminify,
Airship,
Alias,
Ban,
Build,
@ -89,6 +90,7 @@ pub enum ChatCommand {
// Thank you for keeping this sorted alphabetically :-)
pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Adminify,
ChatCommand::Airship,
ChatCommand::Alias,
ChatCommand::Ban,
ChatCommand::Build,
@ -222,6 +224,11 @@ impl ChatCommand {
"Temporarily gives a player admin permissions or removes them",
Admin,
),
ChatCommand::Airship => cmd(
vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)],
"Spawns an airship",
Admin,
),
ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin),
ChatCommand::Ban => cmd(
vec![Any("username", Required), Message(Optional)],
@ -449,6 +456,7 @@ impl ChatCommand {
pub fn keyword(&self) -> &'static str {
match self {
ChatCommand::Adminify => "adminify",
ChatCommand::Airship => "airship",
ChatCommand::Alias => "alias",
ChatCommand::Ban => "ban",
ChatCommand::Build => "build",

View File

@ -168,6 +168,7 @@ impl<'a> From<&'a Body> for Psyche {
Body::Golem(_) => 1.0,
Body::Theropod(_) => 1.0,
Body::Dragon(_) => 1.0,
Body::Ship(_) => 1.0,
},
}
}
@ -221,6 +222,15 @@ impl Agent {
self
}
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
can_speak: true,
psyche: Psyche { aggro: 1.0 },
rtsim_controller: RtSimController::with_destination(pos),
..Default::default()
}
}
pub fn new(
patrol_origin: Option<Vec3<f32>>,
can_speak: bool,

View File

@ -11,6 +11,7 @@ pub mod object;
pub mod quadruped_low;
pub mod quadruped_medium;
pub mod quadruped_small;
pub mod ship;
pub mod theropod;
use crate::{
@ -44,6 +45,7 @@ make_case_elim!(
Golem(body: golem::Body) = 11,
Theropod(body: theropod::Body) = 12,
QuadrupedLow(body: quadruped_low::Body) = 13,
Ship(body: ship::Body) = 14,
}
);
@ -78,6 +80,7 @@ pub struct AllBodies<BodyMeta, SpeciesMeta> {
pub golem: BodyData<BodyMeta, golem::AllSpecies<SpeciesMeta>>,
pub theropod: BodyData<BodyMeta, theropod::AllSpecies<SpeciesMeta>>,
pub quadruped_low: BodyData<BodyMeta, quadruped_low::AllSpecies<SpeciesMeta>>,
pub ship: BodyData<BodyMeta, ()>,
}
/// Can only retrieve body metadata by direct index.
@ -124,6 +127,7 @@ impl<'a, BodyMeta, SpeciesMeta> core::ops::Index<&'a Body> for AllBodies<BodyMet
Body::Golem(_) => &self.golem.body,
Body::Theropod(_) => &self.theropod.body,
Body::QuadrupedLow(_) => &self.quadruped_low.body,
Body::Ship(_) => &self.ship.body,
}
}
}
@ -218,6 +222,7 @@ impl Body {
Body::Golem(_) => 2.5,
Body::BipedSmall(_) => 0.75,
Body::Object(_) => 0.4,
Body::Ship(_) => 1.0,
}
}
@ -294,6 +299,7 @@ impl Body {
object::Body::Crossbow => 1.7,
_ => 1.0,
},
Body::Ship(_) => 1.0,
}
}
@ -416,6 +422,7 @@ impl Body {
quadruped_low::Species::Deadwood => 600,
_ => 200,
},
Body::Ship(_) => 10000,
}
}
@ -508,12 +515,23 @@ impl Body {
quadruped_low::Species::Deadwood => 30,
_ => 20,
},
Body::Ship(_) => 500,
}
}
pub fn flying_height(&self) -> f32 {
match self {
Body::BirdSmall(_) => 30.0,
Body::BirdMedium(_) => 40.0,
Body::Dragon(_) => 60.0,
Body::Ship(ship::Body::DefaultAirship) => 60.0,
_ => 0.0,
}
}
pub fn immune_to(&self, buff: BuffKind) -> bool {
match buff {
BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_)),
BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_) | Body::Ship(_)),
_ => false,
}
}
@ -521,7 +539,13 @@ impl Body {
/// Returns a multiplier representing increased difficulty not accounted for
/// due to AI or not using an actual weapon
// TODO: Match on species
pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) = self { 0.0 } else { 1.0 } }
pub fn combat_multiplier(&self) -> f32 {
if let Body::Object(_) | Body::Ship(_) = self {
0.0
} else {
1.0
}
}
pub fn base_poise(&self) -> u32 {
match self {
@ -549,6 +573,13 @@ impl Body {
Body::Humanoid(_) | Body::BipedSmall(_) | Body::BipedLarge(_)
)
}
pub fn mounting_offset(&self) -> Vec3<f32> {
match self {
Body::Ship(ship::Body::DefaultAirship) => Vec3::from([0.0, 0.0, 10.0]),
_ => Vec3::unit_z(),
}
}
}
impl Component for Body {

View File

@ -0,0 +1,126 @@
use crate::make_case_elim;
use serde::{Deserialize, Serialize};
make_case_elim!(
body,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Body {
DefaultAirship = 0,
}
);
impl From<Body> for super::Body {
fn from(body: Body) -> Self { super::Body::Ship(body) }
}
impl Body {
pub fn manifest_entry(&self) -> &'static str {
match self {
Body::DefaultAirship => "Human_Airship",
}
}
}
/// Terrain is 11.0 scale relative to small-scale voxels, and all figures get
/// multiplied by 0.8 in rendering. For now, have a constant in `comp::Scale`
/// that compensates for both of these, but there might be a more elegant way
/// (e.g. using `Scale(0.8)` for everything else and not having a magic number
/// in figure rendering, and multiplying terrain models by 11.0 in animation).
pub const AIRSHIP_SCALE: f32 = 11.0 / 0.8;
/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to
/// avoid having to refactor all of that to `common` for using voxels as
/// collider geometry
pub mod figuredata {
use crate::{
assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron},
figure::cell::Cell,
terrain::{
block::{Block, BlockKind},
sprite::SpriteKind,
},
volumes::dyna::{ColumnAccess, Dyna},
};
use hashbrown::HashMap;
use lazy_static::lazy_static;
use serde::Deserialize;
use vek::Vec3;
#[derive(Deserialize)]
pub struct VoxSimple(pub String);
#[derive(Deserialize)]
pub struct ShipCentralSpec(pub HashMap<super::Body, SidedShipCentralVoxSpec>);
#[derive(Deserialize)]
pub struct SidedShipCentralVoxSpec {
pub bone0: ShipCentralSubSpec,
pub bone1: ShipCentralSubSpec,
pub bone2: ShipCentralSubSpec,
}
#[derive(Deserialize)]
pub struct ShipCentralSubSpec {
pub offset: [f32; 3],
pub phys_offset: [f32; 3],
pub central: VoxSimple,
}
/// manual instead of through `make_vox_spec!` so that it can be in `common`
#[derive(Clone)]
pub struct ShipSpec {
pub central: AssetHandle<Ron<ShipCentralSpec>>,
pub colliders: HashMap<String, VoxelCollider>,
}
#[derive(Clone)]
pub struct VoxelCollider {
pub dyna: Dyna<Block, (), ColumnAccess>,
pub translation: Vec3<f32>,
}
impl assets::Compound for ShipSpec {
fn load<S: assets::source::Source>(
cache: &assets::AssetCache<S>,
_: &str,
) -> Result<Self, assets::Error> {
let manifest: AssetHandle<Ron<ShipCentralSpec>> =
AssetExt::load("server.manifests.ship_manifest")?;
let mut colliders = HashMap::new();
for (_, spec) in (manifest.read().0).0.iter() {
for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() {
// TODO: Currently both client and server load models and manifests from
// "server.voxel.". In order to support CSG procedural airships, we probably
// need to load them in the server and sync them as an ECS resource.
let vox =
cache.load::<DotVoxAsset>(&["server.voxel.", &bone.central.0].concat())?;
let dyna = Dyna::<Cell, (), ColumnAccess>::from_vox(&vox.read().0, false);
let dyna = dyna.map_into(|cell| {
if let Some(rgb) = cell.get_color() {
Block::new(BlockKind::Misc, rgb)
} else {
Block::air(SpriteKind::Empty)
}
});
let collider = VoxelCollider {
dyna,
translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset),
};
colliders.insert(bone.central.0.clone(), collider);
}
}
Ok(ShipSpec {
central: manifest,
colliders,
})
}
}
lazy_static! {
// TODO: Load this from the ECS as a resource, and maybe make it more general than ships
// (although figuring out how to keep the figure bones in sync with the terrain offsets seems
// like a hard problem if they're not the same manifest)
pub static ref VOXEL_COLLIDER_MANIFEST: AssetHandle<ShipSpec> = AssetExt::load_expect("server.manifests.ship_manifest");
}
}

View File

@ -48,8 +48,8 @@ pub use self::{
beam::{Beam, BeamSegment},
body::{
biped_large, biped_small, bird_medium, bird_small, dragon, fish_medium, fish_small, golem,
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies,
Body, BodyData,
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, ship, theropod,
AllBodies, Body, BodyData,
},
buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs,

View File

@ -33,6 +33,7 @@ pub struct PreviousPhysCache {
pub collision_boundary: f32,
pub scale: f32,
pub scaled_radius: f32,
pub ori: Quaternion<f32>,
}
impl Component for PreviousPhysCache {
@ -55,9 +56,12 @@ impl Component for Mass {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
// Mass
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
// Collider
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Collider {
// TODO: pass the map from ids -> voxel data to get_radius and get_z_limits to compute a
// bounding cylinder
Voxel { id: String },
Box { radius: f32, z_min: f32, z_max: f32 },
Point,
}
@ -65,6 +69,7 @@ pub enum Collider {
impl Collider {
pub fn get_radius(&self) -> f32 {
match self {
Collider::Voxel { .. } => 1.0,
Collider::Box { radius, .. } => *radius,
Collider::Point => 0.0,
}
@ -72,6 +77,7 @@ impl Collider {
pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) {
match self {
Collider::Voxel { .. } => (0.0, 1.0),
Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier),
Collider::Point => (0.0, 0.0),
}
@ -104,6 +110,7 @@ pub struct PhysicsState {
pub on_wall: Option<Vec3<f32>>,
pub touch_entities: Vec<Uid>,
pub in_liquid: Option<f32>, // Depth
pub ground_vel: Vec3<f32>,
}
impl PhysicsState {
@ -113,6 +120,8 @@ impl PhysicsState {
touch_entities.clear();
*self = Self {
touch_entities,
ground_vel: self.ground_vel, /* Preserved, since it's the velocity of the last
* contact point */
..Self::default()
}
}

View File

@ -20,17 +20,12 @@ pub type SiteId = u64;
pub enum LocalEvent {
/// Applies upward force to entity's `Vel`
Jump(EcsEntity),
Jump(EcsEntity, f32),
/// Applies the `impulse` to `entity`'s `Vel`
ApplyImpulse {
entity: EcsEntity,
impulse: Vec3<f32>,
},
/// Applies leaping force to `entity`'s `Vel` away from `wall_dir` direction
WallLeap {
entity: EcsEntity,
wall_dir: Vec3<f32>,
},
/// Applies `vel` velocity to `entity`
Boost { entity: EcsEntity, vel: Vec3<f32> },
}

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use specs::Entity;
/// A resource that stores the time of day.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
@ -26,3 +27,8 @@ pub enum GameMode {
// To be used later when we no longer start up an entirely new server for singleplayer
Singleplayer,
}
/// A resource that stores the player's entity (on the client), and None on the
/// server
#[derive(Copy, Clone, Default, Debug)]
pub struct PlayerEntity(pub Option<Entity>);

View File

@ -46,4 +46,11 @@ impl Default for RtSimController {
impl RtSimController {
pub fn reset(&mut self) { *self = Self::default(); }
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
travel_to: Some((pos, format!("{:0.1?}", pos))),
speed_factor: 0.25,
}
}
}

View File

@ -9,12 +9,9 @@ use crate::{
util::Dir,
};
use serde::{Deserialize, Serialize};
use vek::{
vec::{Vec2, Vec3},
Lerp,
};
use vek::*;
const HUMANOID_CLIMB_ACCEL: f32 = 5.0;
const HUMANOID_CLIMB_ACCEL: f32 = 24.0;
const CLIMB_SPEED: f32 = 5.0;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
@ -36,7 +33,7 @@ impl CharacterBehavior for Data {
// They've climbed atop something, give them a boost
update
.local_events
.push_front(LocalEvent::Jump(data.entity));
.push_front(LocalEvent::Jump(data.entity, BASE_JUMP_IMPULSE * 0.5));
}
update.character = CharacterState::Idle {};
return update;
@ -76,26 +73,10 @@ impl CharacterBehavior for Data {
};
// Apply Vertical Climbing Movement
if update.vel.0.z <= CLIMB_SPEED {
match climb {
Climb::Down => {
update.vel.0 -=
data.dt.0 * update.vel.0.map(|e| e.abs().powf(1.5) * e.signum() * 1.0);
},
Climb::Up => {
update.vel.0.z = (update.vel.0.z + data.dt.0 * GRAVITY * 1.25).min(CLIMB_SPEED);
},
Climb::Hold => {
// Antigrav
update.vel.0.z =
(update.vel.0.z + data.dt.0 * GRAVITY * 1.075).min(CLIMB_SPEED);
update.vel.0 = Lerp::lerp(
update.vel.0,
Vec3::zero(),
30.0 * data.dt.0 / (1.0 - update.vel.0.z.min(0.0) * 5.0),
);
},
}
match climb {
Climb::Down => update.vel.0.z += data.dt.0 * (GRAVITY - HUMANOID_CLIMB_ACCEL),
Climb::Up => update.vel.0.z += data.dt.0 * (GRAVITY + HUMANOID_CLIMB_ACCEL),
Climb::Hold => update.vel.0.z += data.dt.0 * GRAVITY,
}
update

View File

@ -9,8 +9,7 @@ use vek::Vec2;
// Gravity is 9.81 * 4, so this makes gravity equal to .15
const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90;
const GLIDE_ACCEL: f32 = 12.0;
const GLIDE_SPEED: f32 = 45.0;
const GLIDE_ACCEL: f32 = 5.0;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
pub struct Data;
@ -40,13 +39,7 @@ impl CharacterBehavior for Data {
handle_climb(&data, &mut update);
// Move player according to movement direction vector
update.vel.0 += Vec2::broadcast(data.dt.0)
* data.inputs.move_dir
* if data.vel.0.magnitude_squared() < GLIDE_SPEED.powi(2) {
GLIDE_ACCEL
} else {
0.0
};
update.vel.0 += Vec2::broadcast(data.dt.0) * data.inputs.move_dir * GLIDE_ACCEL;
// Determine orientation vector from movement direction vector
let horiz_vel = Vec2::<f32>::from(update.vel.0);
@ -56,7 +49,7 @@ impl CharacterBehavior for Data {
// Apply Glide antigrav lift
let horiz_speed_sq = horiz_vel.magnitude_squared();
if horiz_speed_sq < GLIDE_SPEED.powi(2) && update.vel.0.z < 0.0 {
if update.vel.0.z < 0.0 {
let lift = (GLIDE_ANTIGRAV + update.vel.0.z.powi(2) * 0.15)
* (horiz_speed_sq * f32::powf(0.075, 2.0)).clamp(0.2, 1.0);

View File

@ -3,7 +3,7 @@ use crate::{
biped_large, biped_small,
inventory::slot::EquipSlot,
item::{Hands, ItemKind, Tool, ToolKind},
quadruped_low, quadruped_medium, quadruped_small,
quadruped_low, quadruped_medium, quadruped_small, ship,
skills::Skill,
theropod, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate,
},
@ -17,10 +17,11 @@ use std::time::Duration;
use vek::*;
pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
const BASE_HUMANOID_AIR_ACCEL: f32 = 8.0;
const BASE_FLIGHT_ACCEL: f32 = 16.0;
const BASE_HUMANOID_AIR_ACCEL: f32 = 2.0;
const BASE_FLIGHT_ACCEL: f32 = 2.0;
const BASE_HUMANOID_WATER_ACCEL: f32 = 150.0;
const BASE_HUMANOID_WATER_SPEED: f32 = 180.0;
pub const BASE_JUMP_IMPULSE: f32 = 16.0;
// const BASE_HUMANOID_CLIMB_ACCEL: f32 = 10.0;
// const ROLL_SPEED: f32 = 17.0;
// const CHARGE_SPEED: f32 = 20.0;
@ -117,6 +118,7 @@ impl Body {
quadruped_low::Species::Basilisk => 120.0,
quadruped_low::Species::Deadwood => 140.0,
},
Body::Ship(_) => 30.0,
}
}
@ -168,14 +170,24 @@ impl Body {
quadruped_low::Species::Lavadrake => 4.0,
_ => 6.0,
},
Body::Ship(_) => 0.175,
}
}
pub fn can_fly(&self) -> bool {
matches!(
self,
Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_)
)
/// Returns flying speed if the body type can fly, otherwise None
pub fn can_fly(&self) -> Option<f32> {
match self {
Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) => Some(1.0),
Body::Ship(ship::Body::DefaultAirship) => Some(1.0),
_ => None,
}
}
pub fn jump_impulse(&self) -> Option<f32> {
match self {
Body::Object(_) | Body::Ship(_) => None,
_ => Some(BASE_JUMP_IMPULSE),
}
}
pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
@ -186,10 +198,18 @@ pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
if let Some(depth) = data.physics.in_liquid {
swim_move(data, update, efficiency, depth);
} else if input_is_pressed(data, InputKind::Fly)
&& !data.physics.on_ground
&& data.body.can_fly()
&& (!data.physics.on_ground || data.body.jump_impulse().is_none())
&& data.body.can_fly().is_some()
{
fly_move(data, update, efficiency);
fly_move(
data,
update,
efficiency
* data
.body
.can_fly()
.expect("can_fly is_some right above this"),
);
} else {
basic_move(data, update, efficiency);
}
@ -289,7 +309,11 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth:
}
* efficiency;
handle_orientation(data, update, if data.physics.on_ground { 9.0 } else { 2.0 });
handle_orientation(
data,
update,
data.body.base_ori_rate() * if data.physics.on_ground { 0.5 } else { 0.1 },
);
// Swim
update.vel.0.z = (update.vel.0.z
@ -306,7 +330,6 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth:
/// Updates components to move entity as if it's flying
fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
// Update velocity (counteract gravity with lift)
// TODO: Do this better
update.vel.0 += Vec3::unit_z() * data.dt.0 * GRAVITY
+ Vec3::new(
data.inputs.move_dir.x,
@ -316,7 +339,7 @@ fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
* BASE_FLIGHT_ACCEL
* efficiency;
handle_orientation(data, update, 1.0);
handle_orientation(data, update, data.body.base_ori_rate());
}
/// Checks if an input related to an attack is held. If one is, moves entity
@ -429,10 +452,12 @@ pub fn handle_jump(data: &JoinData, update: &mut StateUpdate) {
.in_liquid
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& data.body.jump_impulse().is_some()
{
update
.local_events
.push_front(LocalEvent::Jump(data.entity));
update.local_events.push_front(LocalEvent::Jump(
data.entity,
data.body.jump_impulse().unwrap(),
));
}
}

View File

@ -35,11 +35,11 @@ impl Cylinder {
pub fn from_components(
pos: Vec3<f32>,
scale: Option<crate::comp::Scale>,
collider: Option<crate::comp::Collider>,
collider: Option<&crate::comp::Collider>,
char_state: Option<&crate::comp::CharacterState>,
) -> Self {
let scale = scale.map_or(1.0, |s| s.0);
let radius = collider.map_or(0.5, |c| c.get_radius()) * scale;
let radius = collider.as_ref().map_or(0.5, |c| c.get_radius()) * scale;
let z_limit_modifier = char_state
.filter(|char_state| char_state.is_dodge())
.map_or(1.0, |_| 0.5)

View File

@ -44,6 +44,21 @@ impl<V, M, A: Access> Dyna<V, M, A> {
None
}
}
pub fn map_into<W, F: FnMut(V) -> W>(self, f: F) -> Dyna<W, M, A> {
let Dyna {
vox,
meta,
sz,
_phantom,
} = self;
Dyna {
vox: vox.into_iter().map(f).collect(),
meta,
sz,
_phantom,
}
}
}
impl<V, M, A: Access> BaseVol for Dyna<V, M, A> {

View File

@ -153,15 +153,6 @@ impl<'a> System<'a> for Sys {
// Do nothing
continue;
}
// If mounted, character state is controlled by mount
// TODO: Make mounting a state
if let Some(Mounting(_)) = read_data.mountings.get(entity) {
let sit_state = CharacterState::Sit {};
if char_state.get_unchecked() != &sit_state {
*char_state.get_mut_unchecked() = sit_state;
}
continue;
}
// Enter stunned state if poise damage is enough
if let Some(mut poise) = poises.get_mut(entity) {
@ -317,6 +308,17 @@ impl<'a> System<'a> for Sys {
incorporate_update(&mut join_struct, state_update);
}
// Mounted occurs after control actions have been handled
// If mounted, character state is controlled by mount
// TODO: Make mounting a state
if let Some(Mounting(_)) = read_data.mountings.get(entity) {
let sit_state = CharacterState::Sit {};
if join_struct.char_state.get_unchecked() != &sit_state {
*join_struct.char_state.get_mut_unchecked() = sit_state;
}
continue;
}
let j = JoinData::new(
&join_struct,
&read_data.lazy_update,

View File

@ -0,0 +1,63 @@
use common::{
comp::{Ori, Pos, Vel},
resources::{PlayerEntity, Time},
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::sync::InterpolatableComponent;
use specs::{
prelude::ParallelIterator, shred::ResourceId, Entities, ParJoin, Read, ReadStorage, SystemData,
World, WriteStorage,
};
#[derive(SystemData)]
pub struct InterpolationSystemData<'a> {
time: Read<'a, Time>,
player: Read<'a, PlayerEntity>,
entities: Entities<'a>,
pos: WriteStorage<'a, Pos>,
pos_interpdata: ReadStorage<'a, <Pos as InterpolatableComponent>::InterpData>,
vel: WriteStorage<'a, Vel>,
vel_interpdata: ReadStorage<'a, <Vel as InterpolatableComponent>::InterpData>,
ori: WriteStorage<'a, Ori>,
ori_interpdata: ReadStorage<'a, <Ori as InterpolatableComponent>::InterpData>,
}
#[derive(Default)]
pub struct InterpolationSystem;
impl<'a> System<'a> for InterpolationSystem {
type SystemData = InterpolationSystemData<'a>;
const NAME: &'static str = "interpolation";
const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Apply;
fn run(_job: &mut Job<Self>, mut data: InterpolationSystemData<'a>) {
let time = data.time.0;
let player = data.player.0;
(
&data.entities,
&mut data.pos,
&data.pos_interpdata,
&data.vel_interpdata,
)
.par_join()
.filter(|(e, _, _, _)| Some(e) != player.as_ref())
.for_each(|(_, pos, interp, vel)| {
*pos = pos.interpolate(interp, time, vel);
});
(&data.entities, &mut data.vel, &data.vel_interpdata)
.par_join()
.filter(|(e, _, _)| Some(e) != player.as_ref())
.for_each(|(_, vel, interp)| {
*vel = vel.interpolate(interp, time, &());
});
(&data.entities, &mut data.ori, &data.ori_interpdata)
.par_join()
.filter(|(e, _, _)| Some(e) != player.as_ref())
.for_each(|(_, ori, interp)| {
*ori = ori.interpolate(interp, time, &());
});
}
}

View File

@ -5,6 +5,7 @@ mod beam;
mod buff;
pub mod character_behavior;
pub mod controller;
mod interpolation;
pub mod melee;
mod mount;
pub mod phys;

View File

@ -1,11 +1,11 @@
use common::{
comp::{Controller, MountState, Mounting, Ori, Pos, Vel},
comp::{Body, Controller, MountState, Mounting, Ori, Pos, Vel},
uid::UidAllocator,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Join, Read, WriteStorage,
Entities, Join, Read, ReadStorage, WriteStorage,
};
use vek::*;
@ -23,6 +23,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
ReadStorage<'a, Body>,
);
const NAME: &'static str = "mount";
@ -40,21 +41,24 @@ impl<'a> System<'a> for Sys {
mut positions,
mut velocities,
mut orientations,
bodies,
): Self::SystemData,
) {
// Mounted entities.
for (entity, mut mount_states) in (&entities, &mut mount_state.restrict_mut()).join() {
for (entity, mut mount_states, body) in
(&entities, &mut mount_state.restrict_mut(), bodies.maybe()).join()
{
match mount_states.get_unchecked() {
MountState::Unmounted => {},
MountState::MountedBy(mounter_uid) => {
// Note: currently controller events are not passed through since none of them
// are currently relevant to controlling the mounted entity
if let Some((inputs, mounter)) = uid_allocator
if let Some((inputs, queued_inputs, mounter)) = uid_allocator
.retrieve_entity_internal(mounter_uid.id())
.and_then(|mounter| {
controllers
.get(mounter)
.map(|c| (c.inputs.clone(), mounter))
.map(|c| (c.inputs.clone(), c.queued_inputs.clone(), mounter))
})
{
// TODO: consider joining on these? (remember we can use .maybe())
@ -62,13 +66,16 @@ impl<'a> System<'a> for Sys {
let ori = orientations.get(entity).copied();
let vel = velocities.get(entity).copied();
if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) {
let _ = positions.insert(mounter, Pos(pos.0 + Vec3::unit_z() * 1.0));
let mounting_offset =
body.map_or(Vec3::unit_z(), Body::mounting_offset);
let _ = positions.insert(mounter, Pos(pos.0 + mounting_offset));
let _ = orientations.insert(mounter, ori);
let _ = velocities.insert(mounter, vel);
}
if let Some(controller) = controllers.get_mut(entity) {
*controller = Controller {
inputs,
queued_inputs,
..Default::default()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ use common::{
comp,
event::{EventBus, LocalEvent, ServerEvent},
region::RegionMap,
resources::{DeltaTime, GameMode, Time, TimeOfDay},
resources::{DeltaTime, GameMode, PlayerEntity, Time, TimeOfDay},
terrain::{Block, TerrainChunk, TerrainGrid},
time::DayPeriod,
trade::Trades,
@ -14,8 +14,8 @@ use common::{
vol::{ReadVol, WriteVol},
};
use common_base::span;
use common_ecs::{PhysicsMetrics, SysMetrics};
use common_net::sync::WorldSyncExt;
use common_ecs::{run_now, PhysicsMetrics, SysMetrics};
use common_net::sync::{interpolation as sync_interp, WorldSyncExt};
use hashbrown::{HashMap, HashSet};
use rayon::{ThreadPool, ThreadPoolBuilder};
use specs::{
@ -36,7 +36,6 @@ const DAY_CYCLE_FACTOR: f64 = 24.0 * 2.0;
/// this value, the game's physics will begin to produce time lag. Ideally, we'd
/// avoid such a situation.
const MAX_DELTA_TIME: f32 = 1.0;
const HUMANOID_JUMP_ACCEL: f32 = 16.0;
#[derive(Default)]
pub struct BlockChange {
@ -165,6 +164,9 @@ impl State {
// Register client-local components
// TODO: only register on the client
ecs.register::<comp::LightAnimation>();
ecs.register::<sync_interp::InterpBuffer<comp::Pos>>();
ecs.register::<sync_interp::InterpBuffer<comp::Vel>>();
ecs.register::<sync_interp::InterpBuffer<comp::Ori>>();
// Register server-local components
// TODO: only register on the server
@ -194,6 +196,7 @@ impl State {
// Register unsynced resources used by the ECS.
ecs.insert(Time(0.0));
ecs.insert(DeltaTime(0.0));
ecs.insert(PlayerEntity(None));
ecs.insert(TerrainGrid::new().unwrap());
ecs.insert(BlockChange::default());
ecs.insert(TerrainChanges::default());
@ -438,6 +441,7 @@ impl State {
let mut dispatcher = dispatch_builder.build();
drop(guard);
span!(guard, "run systems");
run_now::<crate::interpolation::InterpolationSystem>(&self.ecs);
dispatcher.dispatch(&self.ecs);
drop(guard);
@ -454,11 +458,11 @@ impl State {
let events = self.ecs.read_resource::<EventBus<LocalEvent>>().recv_all();
for event in events {
let mut velocities = self.ecs.write_storage::<comp::Vel>();
let mut controllers = self.ecs.write_storage::<comp::Controller>();
let physics = self.ecs.read_storage::<comp::PhysicsState>();
match event {
LocalEvent::Jump(entity) => {
LocalEvent::Jump(entity, impulse) => {
if let Some(vel) = velocities.get_mut(entity) {
vel.0.z = HUMANOID_JUMP_ACCEL;
vel.0.z = impulse + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z);
}
},
LocalEvent::ApplyImpulse { entity, impulse } => {
@ -466,21 +470,6 @@ impl State {
vel.0 = impulse;
}
},
LocalEvent::WallLeap { entity, wall_dir } => {
if let (Some(vel), Some(_controller)) =
(velocities.get_mut(entity), controllers.get_mut(entity))
{
let hspeed = Vec2::<f32>::from(vel.0).magnitude();
if hspeed > 0.001 && hspeed < 0.5 {
vel.0 += vel.0.normalized()
* Vec3::new(1.0, 1.0, 0.0)
* HUMANOID_JUMP_ACCEL
* 1.5
- wall_dir * 0.03;
vel.0.z = HUMANOID_JUMP_ACCEL * 0.5;
}
}
},
LocalEvent::Boost {
entity,
vel: extra_vel,

View File

@ -77,6 +77,7 @@ type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand
fn get_handler(cmd: &ChatCommand) -> CommandHandler {
match cmd {
ChatCommand::Adminify => handle_adminify,
ChatCommand::Airship => handle_spawn_airship,
ChatCommand::Alias => handle_alias,
ChatCommand::Ban => handle_ban,
ChatCommand::Build => handle_build,
@ -984,6 +985,51 @@ fn handle_spawn_training_dummy(
}
}
fn handle_spawn_airship(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: String,
action: &ChatCommand,
) {
let angle = scan_fmt!(&args, &action.arg_fmt(), f32).ok();
match server.state.read_component_copied::<comp::Pos>(target) {
Some(mut pos) => {
pos.0.z += 50.0;
const DESTINATION_RADIUS: f32 = 2000.0;
let angle = angle.map(|a| a * std::f32::consts::PI / 180.0);
let destination = angle.map(|a| {
pos.0
+ Vec3::new(
DESTINATION_RADIUS * a.cos(),
DESTINATION_RADIUS * a.sin(),
200.0,
)
});
server
.state
.create_ship(pos, comp::ship::Body::DefaultAirship, 1, destination)
.with(comp::Scale(comp::ship::AIRSHIP_SCALE))
.with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0,
flicker: 1.0,
animated: true,
})
.build();
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, "Spawned an airship"),
);
},
None => server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandError, "You have no position!"),
),
}
}
fn handle_spawn_campfire(
server: &mut Server,
client: EcsEntity,

View File

@ -129,7 +129,12 @@ pub fn handle_shoot(
return;
};
let vel = *dir * speed;
let vel = *dir * speed
+ state
.ecs()
.read_storage::<Vel>()
.get(entity)
.map_or(Vec3::zero(), |v| v.0);
// Add an outcome
state

View File

@ -69,7 +69,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
find_dist::Cylinder::from_components(
p.0,
scales.get(entity).copied(),
colliders.get(entity).copied(),
colliders.get(entity),
char_states.get(entity),
)
})

View File

@ -27,7 +27,10 @@ impl Entity {
pub fn get_body(&self) -> comp::Body {
match self.rng(PERM_GENUS).gen::<f32>() {
//we want 50% birds, 50% humans for now
// we want 5% airships, 45% birds, 50% humans
// TODO: uncomment this to re-enable RtSim airships once physics is interpolated well
// in multiplayer.
//x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship),
x if x < 0.50 => {
let species = *(&comp::bird_medium::ALL_SPECIES)
.choose(&mut self.rng(PERM_SPECIES))
@ -53,6 +56,7 @@ impl Entity {
comp::Body::BirdSmall(_) => "Warbler".to_string(),
comp::Body::Dragon(b) => get_npc_name(&npc_names.dragon, b.species).to_string(),
comp::Body::Humanoid(b) => get_npc_name(&npc_names.humanoid, b.species).to_string(),
comp::Body::Ship(_) => "Veloren Air".to_string(),
//TODO: finish match as necessary
_ => unimplemented!(),
}
@ -131,6 +135,7 @@ impl Entity {
.iter()
.filter(|s| match self.get_body() {
comp::Body::Humanoid(_) => s.1.is_settlement() | s.1.is_castle(),
comp::Body::Ship(_) => s.1.is_settlement(),
_ => s.1.is_dungeon(),
})
.filter(|_| thread_rng().gen_range(0i32..4) == 0)

View File

@ -122,7 +122,10 @@ impl<'a> System<'a> for Sys {
comp::Body::Humanoid(_) => comp::Alignment::Npc,
_ => comp::Alignment::Wild,
},
scale: comp::Scale(1.0),
scale: match body {
comp::Body::Ship(_) => comp::Scale(comp::ship::AIRSHIP_SCALE),
_ => comp::Scale(1.0),
},
drop_item: None,
home_chunk: None,
rtsim_entity: Some(RtSimEntity(id)),

View File

@ -41,6 +41,13 @@ pub trait StateExt {
) -> EcsEntityBuilder;
/// Build a static object entity
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
fn create_ship(
&mut self,
pos: comp::Pos,
ship: comp::ship::Body,
level: u16,
destination: Option<Vec3<f32>>,
) -> EcsEntityBuilder;
/// Build a projectile
fn create_projectile(
&mut self,
@ -172,10 +179,15 @@ impl StateExt for State {
))
.unwrap_or_default(),
)
.with(comp::Collider::Box {
radius: body.radius(),
z_min: 0.0,
z_max: body.height(),
.with(match body {
comp::Body::Ship(ship) => comp::Collider::Voxel {
id: ship.manifest_entry().to_string(),
},
_ => comp::Collider::Box {
radius: body.radius(),
z_min: 0.0,
z_max: body.height(),
},
})
.with(comp::Controller::default())
.with(body)
@ -215,6 +227,42 @@ impl StateExt for State {
.with(comp::Gravity(1.0))
}
fn create_ship(
&mut self,
pos: comp::Pos,
ship: comp::ship::Body,
level: u16,
destination: Option<Vec3<f32>>,
) -> EcsEntityBuilder {
let mut builder = self
.ecs_mut()
.create_entity_synced()
.with(pos)
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
.with(comp::Mass(50.0))
.with(comp::Collider::Voxel {
id: ship.manifest_entry().to_string(),
})
.with(comp::Body::Ship(ship))
.with(comp::Gravity(1.0))
.with(comp::Controller::default())
.with(comp::inventory::Inventory::new_empty())
.with(comp::CharacterState::default())
// TODO: some of these are required in order for the character_behavior system to
// recognize a possesed airship; that system should be refactored to use `.maybe()`
.with(comp::Energy::new(ship.into(), level))
.with(comp::Health::new(ship.into(), level))
.with(comp::Stats::new("Airship".to_string()))
.with(comp::Buffs::default())
.with(comp::MountState::Unmounted)
.with(comp::Combo::default());
if let Some(pos) = destination {
builder = builder.with(comp::Agent::with_destination(pos))
}
builder
}
fn create_projectile(
&mut self,
pos: comp::Pos,

View File

@ -34,7 +34,7 @@ use specs::{
Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World,
Write, WriteStorage,
};
use std::f32::consts::PI;
use std::{f32::consts::PI, sync::Arc};
use vek::*;
struct AgentData<'a> {
@ -82,6 +82,7 @@ pub struct ReadData<'a> {
mount_states: ReadStorage<'a, MountState>,
time_of_day: Read<'a, TimeOfDay>,
light_emitter: ReadStorage<'a, LightEmitter>,
world: ReadExpect<'a, Arc<world::World>>,
}
// This is 3.1 to last longer than the last damage timer (3.0 seconds)
@ -213,7 +214,7 @@ impl<'a> System<'a> for Sys {
in_liquid: physics_state.in_liquid.is_some(),
min_tgt_dist: 1.0,
can_climb: body.map(|b| b.can_climb()).unwrap_or(false),
can_fly: body.map(|b| b.can_fly()).unwrap_or(false),
can_fly: body.map(|b| b.can_fly().is_some()).unwrap_or(false),
};
let flees = alignment
@ -619,19 +620,43 @@ impl<'a> AgentData<'a> {
controller.inputs.move_z = bearing.z
+ if self.traversal_config.can_fly {
if read_data
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 60.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
.map_or(true, |b| b.is_some());
let ground_too_close = self
.body
.map(|body| {
let height_approx = self.pos.0.y
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
height_approx < body.flying_height()
|| read_data
.terrain
.ray(
self.pos.0,
self.pos.0 - body.flying_height() * Vec3::unit_z(),
)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
})
.unwrap_or(false);
if obstacle_ahead || ground_too_close {
1.0 //fly up when approaching obstacles
} else {
-0.1

View File

@ -249,9 +249,18 @@ impl<'a> System<'a> for Sys {
{
let mut comp_sync_package = CompSyncPackage::new();
let mut throttle = true;
// Extrapolation depends on receiving several frames indicating that something
// has stopped in order for the extrapolated value to have
// stopped
const SEND_UNCHANGED_PHYSICS_DATA: bool = true;
// TODO: An entity that stopped moving on a tick that it wasn't sent to the
// player will never have its position updated
match last_pos.get(entity).map(|&l| l.0 != pos) {
match last_pos
.get(entity)
.map(|&l| l.0 != pos || SEND_UNCHANGED_PHYSICS_DATA)
{
Some(false) => {},
Some(true) => {
let _ = last_pos.insert(entity, Last(pos));
@ -265,7 +274,10 @@ impl<'a> System<'a> for Sys {
}
if let Some(&vel) = maybe_vel {
match last_vel.get(entity).map(|&l| l.0 != vel) {
match last_vel
.get(entity)
.map(|&l| l.0 != vel || SEND_UNCHANGED_PHYSICS_DATA)
{
Some(false) => {},
Some(true) => {
let _ = last_vel.insert(entity, Last(vel));
@ -286,7 +298,10 @@ impl<'a> System<'a> for Sys {
}
if let Some(&ori) = maybe_ori {
match last_ori.get(entity).map(|&l| l.0 != ori) {
match last_ori
.get(entity)
.map(|&l| l.0 != ori || SEND_UNCHANGED_PHYSICS_DATA)
{
Some(false) => {},
Some(true) => {
let _ = last_ori.insert(entity, Last(ori));

View File

@ -140,7 +140,7 @@ impl<'a> TrackedComps<'a> {
self.mass.get(entity).copied().map(|c| comps.push(c.into()));
self.collider
.get(entity)
.copied()
.cloned()
.map(|c| comps.push(c.into()));
self.sticky
.get(entity)

View File

@ -54,6 +54,7 @@ pub mod object;
pub mod quadruped_low;
pub mod quadruped_medium;
pub mod quadruped_small;
pub mod ship;
pub mod theropod;
pub mod vek;

View File

@ -0,0 +1,33 @@
use super::{
super::{vek::*, Animation},
ShipSkeleton, SkeletonAttr,
};
use common::comp::item::ToolKind;
pub struct IdleAnimation;
impl Animation for IdleAnimation {
type Dependency = (Option<ToolKind>, Option<ToolKind>, f32);
type Skeleton = ShipSkeleton;
#[cfg(feature = "use-dyn-lib")]
const UPDATE_FN: &'static [u8] = b"ship_idle\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "ship_idle")]
#[allow(clippy::approx_constant)] // TODO: Pending review in #587
fn update_skeleton_inner(
skeleton: &Self::Skeleton,
(_active_tool_kind, _second_tool_kind, _global_time): Self::Dependency,
_anim_time: f32,
_rate: &mut f32,
s_a: &SkeletonAttr,
) -> Self::Skeleton {
let mut next = (*skeleton).clone();
next.bone0.position = Vec3::new(s_a.bone0.0, s_a.bone0.1, s_a.bone0.2) / 11.0;
next.bone1.position = Vec3::new(s_a.bone1.0, s_a.bone1.1, s_a.bone1.2) / 11.0;
next
}
}

View File

@ -0,0 +1,68 @@
pub mod idle;
// Reexports
pub use self::idle::IdleAnimation;
use super::{make_bone, vek::*, FigureBoneData, Skeleton};
use common::comp::{self};
use core::convert::TryFrom;
pub type Body = comp::ship::Body;
skeleton_impls!(struct ShipSkeleton {
+ bone0,
+ bone1,
});
impl Skeleton for ShipSkeleton {
type Attr = SkeletonAttr;
type Body = Body;
const BONE_COUNT: usize = 2;
#[cfg(feature = "use-dyn-lib")]
const COMPUTE_FN: &'static [u8] = b"ship_compute_mats\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "ship_compute_mats")]
fn compute_matrices_inner(
&self,
base_mat: Mat4<f32>,
buf: &mut [FigureBoneData; super::MAX_BONE_COUNT],
) -> Vec3<f32> {
let bone0_mat = base_mat * Mat4::<f32>::from(self.bone0);
*(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [
make_bone(bone0_mat * Mat4::scaling_3d(1.0 / 11.0)),
make_bone(Mat4::<f32>::from(self.bone1) * Mat4::scaling_3d(1.0 / 11.0)), /* Decorellated from ori */
];
Vec3::unit_z() * 0.5
}
}
pub struct SkeletonAttr {
bone0: (f32, f32, f32),
bone1: (f32, f32, f32),
}
impl<'a> std::convert::TryFrom<&'a comp::Body> for SkeletonAttr {
type Error = ();
fn try_from(body: &'a comp::Body) -> Result<Self, Self::Error> {
match body {
comp::Body::Ship(body) => Ok(SkeletonAttr::from(body)),
_ => Err(()),
}
}
}
impl Default for SkeletonAttr {
fn default() -> Self {
Self {
bone0: (0.0, 0.0, 0.0),
bone1: (0.0, 0.0, 0.0),
}
}
}
impl<'a> From<&'a Body> for SkeletonAttr {
fn from(_: &'a Body) -> Self { Self::default() }
}

View File

@ -16,13 +16,17 @@ use common::{
quadruped_low::{self, BodyType as QLBodyType, Species as QLSpecies},
quadruped_medium::{self, BodyType as QMBodyType, Species as QMSpecies},
quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies},
ship::{
self,
figuredata::{ShipCentralSubSpec, ShipSpec},
},
theropod::{self, BodyType as TBodyType, Species as TSpecies},
},
figure::{DynaUnionizer, MatSegment, Material, Segment},
};
use hashbrown::HashMap;
use serde::Deserialize;
use std::sync::Arc;
use std::{fmt, hash::Hash, sync::Arc};
use tracing::{error, warn};
use vek::*;
@ -34,6 +38,9 @@ fn load_segment(mesh_name: &str) -> Segment {
}
fn graceful_load_vox(mesh_name: &str) -> AssetHandle<DotVoxAsset> {
let full_specifier: String = ["voxygen.voxel.", mesh_name].concat();
graceful_load_vox_fullspec(&full_specifier)
}
fn graceful_load_vox_fullspec(full_specifier: &str) -> AssetHandle<DotVoxAsset> {
match DotVoxAsset::load(&full_specifier) {
Ok(dot_vox) => dot_vox,
Err(_) => {
@ -45,6 +52,9 @@ fn graceful_load_vox(mesh_name: &str) -> AssetHandle<DotVoxAsset> {
fn graceful_load_segment(mesh_name: &str) -> Segment {
Segment::from(&graceful_load_vox(mesh_name).read().0)
}
fn graceful_load_segment_fullspec(full_specifier: &str) -> Segment {
Segment::from(&graceful_load_vox_fullspec(full_specifier).read().0)
}
fn graceful_load_segment_flipped(mesh_name: &str, flipped: bool) -> Segment {
Segment::from_vox(&graceful_load_vox(mesh_name).read().0, flipped)
}
@ -4171,3 +4181,53 @@ impl ObjectCentralSpec {
(central, Vec3::from(spec.bone1.offset))
}
}
fn mesh_ship_bone<K: fmt::Debug + Eq + Hash, V, F: Fn(&V) -> &ShipCentralSubSpec>(
map: &HashMap<K, V>,
obj: &K,
f: F,
) -> BoneMeshes {
let spec = match map.get(&obj) {
Some(spec) => spec,
None => {
error!("No specification exists for {:?}", obj);
return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5));
},
};
let bone = f(spec);
let central = graceful_load_segment_fullspec(&["server.voxel.", &bone.central.0].concat());
(central, Vec3::from(bone.offset))
}
impl BodySpec for ship::Body {
type Spec = ShipSpec;
#[allow(unused_variables)]
fn load_spec() -> Result<AssetHandle<Self::Spec>, assets::Error> { Self::Spec::load("") }
fn bone_meshes(
FigureKey { body, .. }: &FigureKey<Self>,
spec: &Self::Spec,
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT] {
let map = &(spec.central.read().0).0;
[
Some(mesh_ship_bone(map, body, |spec| &spec.bone0)),
Some(mesh_ship_bone(map, body, |spec| &spec.bone1)),
Some(mesh_ship_bone(map, body, |spec| &spec.bone2)),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -199,6 +199,7 @@ impl Scene {
&camera,
&mut buf,
None,
Vec3::zero(),
);
(model, state)
}),
@ -378,6 +379,7 @@ impl Scene {
&self.camera,
&mut buf,
None,
Vec3::zero(),
);
}
}

View File

@ -1562,7 +1562,7 @@ fn under_cursor(
let player_cylinder = Cylinder::from_components(
player_pos,
scales.get(player_entity).copied(),
colliders.get(player_entity).copied(),
colliders.get(player_entity),
char_states.get(player_entity),
);
let terrain = client.state().terrain();
@ -1643,7 +1643,7 @@ fn under_cursor(
let target_cylinder = Cylinder::from_components(
p,
scales.get(*e).copied(),
colliders.get(*e).copied(),
colliders.get(*e),
char_states.get(*e),
);
@ -1706,7 +1706,7 @@ fn select_interactable(
let player_cylinder = Cylinder::from_components(
player_pos,
scales.get(player_entity).copied(),
colliders.get(player_entity).copied(),
colliders.get(player_entity),
char_states.get(player_entity),
);
@ -1720,7 +1720,7 @@ fn select_interactable(
.join()
.filter(|(e, _, _, _, _)| *e != player_entity)
.map(|(e, p, s, c, cs)| {
let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs);
let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs);
(e, cylinder)
})
// Roughly filter out entities farther than interaction distance