From ff3f3d12713fae9fe5047fa980a8123ac668d482 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Mar 2021 11:48:59 -0500 Subject: [PATCH] Draft of airships (spawn command, visuals, some physics refactoring, no collision yet). --- assets/common/npc_names.ron | 9 + assets/server/manifests/ship_manifest.ron | 16 + assets/server/voxel/Human_Airship.vox | Bin 0 -> 88100 bytes assets/server/voxel/airship.vox | Bin 0 -> 70176 bytes assets/server/voxel/propeller-l.vox | Bin 0 -> 1584 bytes assets/server/voxel/propeller-r.vox | Bin 0 -> 1584 bytes assets/voxygen/voxel/object/Human_Airship.vox | Bin 0 -> 78024 bytes assets/voxygen/voxel/object/airship.vox | 1 + assets/voxygen/voxel/object/propeller-l.vox | 1 + assets/voxygen/voxel/object/propeller-r.vox | 1 + common/src/cmd.rs | 4 + common/src/comp/agent.rs | 1 + common/src/comp/body.rs | 12 +- common/src/comp/body/ship.rs | 69 + common/src/comp/mod.rs | 2 +- common/src/comp/phys.rs | 9 +- common/src/states/utils.rs | 6 +- common/src/util/find_dist.rs | 2 +- common/sys/src/phys.rs | 1230 ++++++++++------- server/src/cmd.rs | 34 + server/src/events/inventory_manip.rs | 2 +- server/src/state_ext.rs | 13 + server/src/sys/sentinel.rs | 2 +- voxygen/anim/src/lib.rs | 1 + voxygen/anim/src/ship/idle.rs | 34 + voxygen/anim/src/ship/mod.rs | 71 + voxygen/src/render/renderer.rs | 2 +- voxygen/src/scene/figure/load.rs | 110 +- voxygen/src/scene/figure/mod.rs | 108 +- voxygen/src/session.rs | 8 +- 30 files changed, 1196 insertions(+), 552 deletions(-) create mode 100644 assets/server/manifests/ship_manifest.ron create mode 100644 assets/server/voxel/Human_Airship.vox create mode 100644 assets/server/voxel/airship.vox create mode 100644 assets/server/voxel/propeller-l.vox create mode 100644 assets/server/voxel/propeller-r.vox create mode 100644 assets/voxygen/voxel/object/Human_Airship.vox create mode 120000 assets/voxygen/voxel/object/airship.vox create mode 120000 assets/voxygen/voxel/object/propeller-l.vox create mode 120000 assets/voxygen/voxel/object/propeller-r.vox create mode 100644 common/src/comp/body/ship.rs create mode 100644 voxygen/anim/src/ship/idle.rs create mode 100644 voxygen/anim/src/ship/mod.rs diff --git a/assets/common/npc_names.ron b/assets/common/npc_names.ron index 49ceb5c023..aeefb77fee 100644 --- a/assets/common/npc_names.ron +++ b/assets/common/npc_names.ron @@ -969,6 +969,15 @@ ), species: () ), + ship: ( + body: ( + keyword: "ship", + names_0: [ + "Boaty McBoatface", + ], + ), + species: (), + ), biped_small: ( body: ( keyword: "biped_small", diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron new file mode 100644 index 0000000000..42555fc051 --- /dev/null +++ b/assets/server/manifests/ship_manifest.ron @@ -0,0 +1,16 @@ +({ + DefaultAirship: ( + bone0: ( + offset: (-20.0, -35.0, 1.0), + central: ("object.Human_Airship"), + ), + bone1: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-l"), + ), + bone2: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-r"), + ), + ), +}) diff --git a/assets/server/voxel/Human_Airship.vox b/assets/server/voxel/Human_Airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..687a8f45b3a6d2c4c65dabcfd9f2d4bb11449d16 GIT binary patch literal 88100 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 literal 0 HcmV?d00001 diff --git a/assets/server/voxel/airship.vox b/assets/server/voxel/airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..10c79d32e7a205c4b2043529e3e8b40a0e8e9d9c GIT binary patch literal 70176 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 QkH&xW*b;qI_aDdZ-@RAK#sB~S literal 0 HcmV?d00001 diff --git a/assets/server/voxel/propeller-l.vox b/assets/server/voxel/propeller-l.vox new file mode 100644 index 0000000000000000000000000000000000000000..2dc7e554e4cc9bf5fb94d4ac40330400197039a4 GIT binary patch literal 1584 zcmc(eT})eL9EbmG$wNQ|f=QB5SJh$*TP zO-w1PWJMLC5>2w`X1bYfrkm*&x`l3`Tj*B0m2Rb5Q#Ot|F^HWwh?7`FVW+TD*eUE3 zb_zR%ox)CGr$k-EDk@u*t;$wqtFl$us%%xZDqEGU8g&!Rq>hMaDHF*eW?~^$q7ap6 zqK()%>ck*+;vi1qB5tCZv=NQih=?s}B3Z;tEW}C_q7qGPye=EB%f{=Xn`|Q{(b?#<8yQxoFxI37$1_Fi5`nddM3;qhQ?tp}Ok)!;IE zyLaKpp%%o)y0JVS#PZY;EYC#IxO@WcKpwn@eej*GhJUsh(rJZ6bjaKjko7#cdj$E% z1x1sBXRZmfh`^B$Mr-~D?o9$6y@F z6p|uXAwQ5`Nb;m$@ngZ-g5cJ5dO$GODfoK7VDl|Oh<#M}{v0B7mYsB<$JJ#NR9ly?pQC*gY*4h$uzfg%|1C5Au2@;0{vu_Jl zKNK_+7a&+wj=_c<80~1rKvf>X^@W(&Q-So*PIR``VBbs42))vV$l+eZPJ}S|UIgid zbJ)0X8R<)dSo>lGhGAe^PBwf#A8P!iXxQ;Io~!j^^4%gVepZSfZ&hLQPBkv^oauSN z?QaD?e=mrID{(PVhxM!N`2DATjGlL6;!6*%-}Pb>CI9>XudA!WM~$=CSu=;0@w4zI zyttVX+s4KZYEnwhP- zhzk>uz)Us~3_-IDH&Tek3nU9!mMqJHi!m`|S&Z?<3onfE?(e_ME0fJjoBZ;=|M&ep z-_!Ry^z1v}n-YGl?cPW*f7O*~V;VwlmwA?J=u3Vm4wY4x$j1XyTyfpy#0Hh*?CTtI$>G zDs&aP3SEV+LRX=y&{bk0Dt(o{N?)a~(pTxL^i}#QeU-jSpV^`g@-%Z6VkI_WCk~KqMX5+`vHH_0F#qFS^;jp)Q7I{WGDr?a2VemeW6&y+dQN&ydQN&yp2@}R zVs{4G}k3=GY2NQz5m)B9^{R@A1 zy;xi8K>BwzvW(95T{v>65s8s@ERRO8JaGie(=pU7pMWQngS^8&_)b?KIMV_ zWcCTjdJa4tg52YR!g0Yf*96*-z?BqaG3Q(n)O46=JMKaBOcp*^c@nJ&LD!_<%|*ef zyR7ZXN6~;ErHOLXra~wf_QF4wkMj9qRNmZ<++IQH8D4uz(6lO$SJ^+VK_=ahB`-#| zXJTPjHts!N0JJ@YNfE4&AIL9c@}yw#W5L>j;MR5KkYF|}_TK53SjD36%w!2VXD6cYwy2~U*`urTfxuY3*rL-TufGD{b~z-|EU|p=RFwv(u?bN^RS8H|NZ>c)z$c@ zZU#FmXVEx%7J11$+?*5KdqZ#~5I`^(M1RQ$Uf#O|Nyd>qoQ;*~htKeIKHr_9BD_;J ziQSD8IC}mZw)N%W&R1nviRNK~_v+vGKkcQjb+G$3hWmk2(+fy`p2CHt8~EhA4Llng z#qP5+X#aE--Pbk{3We}M0MQ8kgl$7`|C@*Z?>^tJ|JvtQxb>YqYD{lFYA?P1xV^0l IKfYf627r)Zg#Z8m literal 0 HcmV?d00001 diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..88e79f59a7a92962a6b023d576accc3a96ed2bcf GIT binary patch literal 78024 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 Y&;F$Em%~44c6t0yn|*KbpWZ+GA6s@|!vFvP literal 0 HcmV?d00001 diff --git a/assets/voxygen/voxel/object/airship.vox b/assets/voxygen/voxel/object/airship.vox new file mode 120000 index 0000000000..3479493953 --- /dev/null +++ b/assets/voxygen/voxel/object/airship.vox @@ -0,0 +1 @@ +../../../server/voxel/airship.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-l.vox b/assets/voxygen/voxel/object/propeller-l.vox new file mode 120000 index 0000000000..a8105d8b1b --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-l.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-l.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-r.vox b/assets/voxygen/voxel/object/propeller-r.vox new file mode 120000 index 0000000000..647f3f66d0 --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-r.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-r.vox \ No newline at end of file diff --git a/common/src/cmd.rs b/common/src/cmd.rs index f6771bdcd3..a74f0cab09 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -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,7 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), + ChatCommand::Airship => cmd(vec![], "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 +452,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", diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 7d6c5a57df..9134e40091 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -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, }, } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index bb331f3a54..1733eec1e1 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -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 { pub golem: BodyData>, pub theropod: BodyData>, pub quadruped_low: BodyData>, + pub ship: BodyData, } /// Can only retrieve body metadata by direct index. @@ -124,6 +127,7 @@ impl<'a, BodyMeta, SpeciesMeta> core::ops::Index<&'a Body> for AllBodies &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,13 @@ impl Body { quadruped_low::Species::Deadwood => 30, _ => 20, }, + Body::Ship(_) => 500, } } 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 +529,7 @@ 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 { diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs new file mode 100644 index 0000000000..ef2e93e113 --- /dev/null +++ b/common/src/comp/body/ship.rs @@ -0,0 +1,69 @@ +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 for super::Body { + fn from(body: Body) -> Self { super::Body::Ship(body) } +} + +impl Body { + pub fn manifest_id(&self) -> &'static str { + match self { + Body::DefaultAirship => "server.manifests.ship_manifest", + } + } +} + +/// 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}, + volumes::dyna::Dyna, + }; + use serde::Deserialize; + use hashbrown::HashMap; + + #[derive(Deserialize)] + pub struct VoxSimple(pub String); + + #[derive(Deserialize)] + pub struct ShipCentralSpec(pub HashMap); + + #[derive(Deserialize)] + pub struct SidedShipCentralVoxSpec { + pub bone0: ShipCentralSubSpec, + pub bone1: ShipCentralSubSpec, + pub bone2: ShipCentralSubSpec, + } + + #[derive(Deserialize)] + pub struct ShipCentralSubSpec { + pub 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>, + } + + impl assets::Compound for ShipSpec { + fn load(_: &assets::AssetCache, _: &str) -> Result { + Ok(ShipSpec { + central: AssetExt::load("server.manifests.ship_manifest")? + }) + } + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index e493a527c9..d97e265919 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -48,7 +48,7 @@ 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, + humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, ship, AllBodies, Body, BodyData, }, buff::{ diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index fdb8d10a05..3e06418792 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -55,9 +55,12 @@ impl Component for Mass { type Storage = DerefFlaggedStorage>; } -// 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 +68,7 @@ pub enum Collider { impl Collider { pub fn get_radius(&self) -> f32 { match self { + Collider::Voxel { .. } => 0.0, Collider::Box { radius, .. } => *radius, Collider::Point => 0.0, } @@ -72,6 +76,7 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { + Collider::Voxel { .. } => (0.0, 0.0), Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Point => (0.0, 0.0), } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index bc43bb7643..ea780a1a38 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -5,7 +5,7 @@ use crate::{ item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, quadruped_small, skills::Skill, - theropod, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, + theropod, ship, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, event::{LocalEvent, ServerEvent}, @@ -117,6 +117,7 @@ impl Body { quadruped_low::Species::Basilisk => 120.0, quadruped_low::Species::Deadwood => 140.0, }, + Body::Ship(_) => 30.0, } } @@ -168,13 +169,14 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, + Body::Ship(_) => 10.0, } } pub fn can_fly(&self) -> bool { matches!( self, - Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) + Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) | Body::Ship(ship::Body::DefaultAirship) ) } diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs index cb02c728fe..fc921bc758 100644 --- a/common/src/util/find_dist.rs +++ b/common/src/util/find_dist.rs @@ -39,7 +39,7 @@ impl Cylinder { 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) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index cb0ee3cd51..8545888c7e 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -8,12 +8,16 @@ use common::{ resources::DeltaTime, terrain::{Block, TerrainGrid}, uid::Uid, - vol::ReadVol, + vol::{BaseVol, ReadVol}, }; use common_base::{prof_span, span}; use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System}; +use hashbrown::HashMap; use rayon::iter::ParallelIterator; -use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; +use specs::{ + shred::{World, ResourceId}, + Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage, SystemData, +}; use std::ops::Range; use vek::*; @@ -62,112 +66,78 @@ fn calc_z_limit( #[derive(Default)] pub struct Sys; -impl<'a> System<'a> for Sys { - #[allow(clippy::type_complexity)] - type SystemData = ( - Entities<'a>, - ReadStorage<'a, Uid>, - ReadExpect<'a, TerrainGrid>, - Read<'a, DeltaTime>, - WriteExpect<'a, PhysicsMetrics>, - Read<'a, EventBus>, - ReadStorage<'a, Scale>, - ReadStorage<'a, Sticky>, - ReadStorage<'a, Mass>, - ReadStorage<'a, Collider>, - ReadStorage<'a, Gravity>, - WriteStorage<'a, PhysicsState>, - WriteStorage<'a, Pos>, - WriteStorage<'a, Vel>, - WriteStorage<'a, Ori>, - WriteStorage<'a, PreviousPhysCache>, - ReadStorage<'a, Mounting>, - ReadStorage<'a, Projectile>, - ReadStorage<'a, BeamSegment>, - ReadStorage<'a, Shockwave>, - ReadStorage<'a, CharacterState>, - ); +#[derive(SystemData)] +pub struct PhysicsSystemDataRead<'a> { + entities: Entities<'a>, + uids: ReadStorage<'a, Uid>, + terrain: ReadExpect<'a, TerrainGrid>, + dt: Read<'a, DeltaTime>, + event_bus: Read<'a, EventBus>, + scales: ReadStorage<'a, Scale>, + stickies: ReadStorage<'a, Sticky>, + masses: ReadStorage<'a, Mass>, + colliders: ReadStorage<'a, Collider>, + gravities: ReadStorage<'a, Gravity>, + mountings: ReadStorage<'a, Mounting>, + projectiles: ReadStorage<'a, Projectile>, + beams: ReadStorage<'a, BeamSegment>, + shockwaves: ReadStorage<'a, Shockwave>, + char_states: ReadStorage<'a, CharacterState>, +} - const NAME: &'static str = "phys"; - const ORIGIN: Origin = Origin::Common; - const PHASE: Phase = Phase::Create; +#[derive(SystemData)] +pub struct PhysicsSystemDataWrite<'a> { + physics_metrics: WriteExpect<'a, PhysicsMetrics>, + physics_states: WriteStorage<'a, PhysicsState>, + positions: WriteStorage<'a, Pos>, + velocities: WriteStorage<'a, Vel>, + orientations: WriteStorage<'a, Ori>, + previous_phys_cache: WriteStorage<'a, PreviousPhysCache>, +} - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 - #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 - fn run( - job: &mut Job, - ( - entities, - uids, - terrain, - dt, - mut physics_metrics, - event_bus, - scales, - stickies, - masses, - colliders, - gravities, - mut physics_states, - mut positions, - mut velocities, - mut orientations, - mut previous_phys_cache, - mountings, - projectiles, - beams, - shockwaves, - char_states, - ): Self::SystemData, - ) { - let mut event_emitter = event_bus.emitter(); +#[derive(SystemData)] +pub struct PhysicsSystemData<'a> { + r: PhysicsSystemDataRead<'a>, + w: PhysicsSystemDataWrite<'a>, +} - // Add/reset physics state components +impl<'a> PhysicsSystemData<'a> { + /// Add/reset physics state components + fn reset(&mut self) { span!(guard, "Add/reset physics state components"); for (entity, _, _, _, _) in ( - &entities, - &colliders, - &positions, - &velocities, - &orientations, + &self.r.entities, + &self.r.colliders, + &self.w.positions, + &self.w.velocities, + &self.w.orientations, ) .join() { - let _ = physics_states + let _ = self.w.physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); } drop(guard); + } - // Apply pushback - // - // Note: We now do this first because we project velocity ahead. This is slighty - // imperfect and implies that we might get edge-cases where entities - // standing right next to the edge of a wall may get hit by projectiles - // fired into the wall very close to them. However, this sort of thing is - // already possible with poorly-defined hitboxes anyway so it's not too - // much of a concern. - // - // If this situation becomes a problem, this code should be integrated with the - // terrain collision code below, although that's not trivial to do since - // it means the step needs to take into account the speeds of both - // entities. + fn maintain_pushback_cache(&mut self) { span!(guard, "Maintain pushback cache"); //Add PreviousPhysCache for all relevant entities for entity in ( - &entities, - &velocities, - &positions, - !&previous_phys_cache, - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + !&self.w.previous_phys_cache, + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() .map(|(e, _, _, _, _, _, _)| e) .collect::>() { - let _ = previous_phys_cache.insert(entity, PreviousPhysCache { + let _ = self.w.previous_phys_cache.insert(entity, PreviousPhysCache { velocity_dt: Vec3::zero(), center: Vec3::zero(), collision_boundary: 0.0, @@ -178,16 +148,16 @@ impl<'a> System<'a> for Sys { //Update PreviousPhysCache for (_, vel, position, mut phys_cache, collider, scale, cs, _, _, _) in ( - &entities, - &velocities, - &positions, - &mut previous_phys_cache, - colliders.maybe(), - scales.maybe(), - char_states.maybe(), - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + &mut self.w.previous_phys_cache, + self.r.colliders.maybe(), + self.r.scales.maybe(), + self.r.char_states.maybe(), + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() { @@ -196,7 +166,7 @@ impl<'a> System<'a> for Sys { let z_limits = (z_limits.0 * scale, z_limits.1 * scale); let half_height = (z_limits.1 - z_limits.0) / 2.0; - phys_cache.velocity_dt = vel.0 * dt.0; + phys_cache.velocity_dt = vel.0 * self.r.dt.0; let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -209,23 +179,26 @@ impl<'a> System<'a> for Sys { phys_cache.scaled_radius = flat_radius; } drop(guard); - + } + fn apply_pushback(&mut self, job: &mut Job) { span!(guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); let metrics = ( - &entities, - &positions, - &mut velocities, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&mountings, - stickies.maybe(), - &mut physics_states, + &psdr.entities, + positions, + &mut psdw.velocities, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.mountings, + psdr.stickies.maybe(), + &mut psdw.physics_states, // TODO: if we need to avoid collisions for other things consider moving whether it // should interact into the collider component or into a separate component - projectiles.maybe(), - char_states.maybe(), + psdr.projectiles.maybe(), + psdr.char_states.maybe(), ) .par_join() .filter(|(_, _, _, _, _, _, _, sticky, physics, _, _)| { @@ -275,17 +248,17 @@ impl<'a> System<'a> for Sys { _, char_state_other_maybe, ) in ( - &entities, - &uids, - &positions, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&projectiles, - !&mountings, - !&beams, - !&shockwaves, - char_states.maybe(), + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), ) .join() { @@ -358,7 +331,7 @@ impl<'a> System<'a> for Sys { } // Change velocity - vel.0 += vel_delta * dt.0; + vel.0 += vel_delta * psdr.dt.0; // Metrics PhysicsMetrics { @@ -373,435 +346,622 @@ impl<'a> System<'a> for Sys { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; - physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; + psdw.physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; + psdw.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; drop(guard); + } + fn handle_movement_and_terrain(&mut self, job: &mut Job) { + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let land_on_grounds = ( - &entities, - scales.maybe(), - stickies.maybe(), - &colliders, - &mut positions, - &mut velocities, - &mut orientations, - &mut physics_states, - !&mountings, - ) - .par_join() - .map_init( - || { - prof_span!(guard, "physics e<>t rayon job"); - guard - }, - |_guard, - (entity, _scale, sticky, collider, mut pos, mut vel, _ori, mut physics_state, _), - | { - let mut landed_on_ground = None; - - if sticky.is_some() && physics_state.on_surface().is_some() { - vel.0 = Vec3::zero(); - return landed_on_ground; - } - - // TODO: Use this - //let scale = scale.map(|s| s.0).unwrap_or(1.0); - - let old_vel = *vel; - // Integrate forces - // Friction is assumed to be a constant dependent on location - let friction = FRIC_AIR - .max(if physics_state.on_ground { - FRIC_GROUND - } else { - 0.0 - }) - .max(if physics_state.in_liquid.is_some() { - FRIC_FLUID - } else { - 0.0 - }); - let in_loaded_chunk = terrain - .get_key(terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); - let downward_force = if !in_loaded_chunk { - 0.0 // No gravity in unloaded chunks - } else if physics_state - .in_liquid - .map(|depth| depth > 0.75) - .unwrap_or(false) - { - (1.0 - BOUYANCY) * GRAVITY - } else { - GRAVITY - } * gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(dt.0, vel.0, downward_force, friction); - - // Don't move if we're not in a loaded chunk - let mut pos_delta = if in_loaded_chunk { - // this is an approximation that allows most framerates to - // behave in a similar manner. - let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * dt.0 - } else { - Vec3::zero() - }; - - match *collider { - Collider::Box { - radius, - z_min, - z_max, - } => { - // Scale collider - // TODO: Use scale & actual proportions when pathfinding is good enough to manage irregular entity - // sizes - let radius = radius.min(0.45); // * scale; - let z_min = z_min; // * scale; - let z_max = z_max.clamped(1.2, 1.95); // * scale; - - // Probe distances - let hdist = radius.ceil() as i32; - // Neighbouring blocks iterator - let near_iter = (-hdist..hdist + 1) - .map(move |i| { - (-hdist..hdist + 1).map(move |j| { - (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 - ..z_max.ceil() as i32 + 1) - .map(move |k| (i, j, k)) - }) - }) - .flatten() - .flatten(); - - // Function for iterating over the blocks the player at a specific position - // collides with - fn collision_iter<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: &'a impl Fn(&Block) -> bool, - height: &'a impl Fn(&Block) -> f32, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> impl Iterator> + 'a { - near_iter.filter_map(move |(i, j, k)| { - let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); - - if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { - let player_aabb = Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start), - max: pos + Vec3::new(radius, radius, z_range.end), - }; - let block_aabb = Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) - + Vec3::new(1.0, 1.0, height(&block)), - }; - - if player_aabb.collides_with_aabb(block_aabb) { - return Some(block_aabb); - } - } - - None - }) - } - - let z_range = z_min..z_max; - // Function for determining whether the player at a specific position collides - // with blocks with the given criteria - fn collision_with<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: impl Fn(&Block) -> bool, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> bool { - collision_iter(pos, terrain, &|block| block.is_solid() && hit(block), &Block::solid_height, near_iter, radius, z_range).count() - > 0 - } - - let was_on_ground = physics_state.on_ground; - physics_state.on_ground = false; - - let mut on_ground = false; - let mut on_ceiling = false; - let mut attempts = 0; // Don't loop infinitely here - - // Don't jump too far at once - let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) - .ceil() - .max(1.0); - let old_pos = pos.0; - fn block_true(_: &Block) -> bool { true } - for _ in 0..increments as usize { - pos.0 += pos_delta / increments; - - const MAX_ATTEMPTS: usize = 16; - - // While the player is colliding with the terrain... - while collision_with(pos.0, &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - && attempts < MAX_ATTEMPTS - { - // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; - - // Determine the block that we are colliding with most (based on minimum - // collision axis) - let (_block_pos, block_aabb, block_height) = near_iter - .clone() - // Calculate the block's position in world space - .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - // Make sure the block is actually solid - .filter_map(|block_pos| { - if let Some(block) = terrain - .get(block_pos) - .ok() - .filter(|block| block.is_solid()) - { - // Calculate block AABB - Some(( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), - }, - block.solid_height(), - )) - } else { - None - } - }) - // Determine whether the block's AABB collides with the player's AABB - .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) - // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) - .min_by_key(|(_, block_aabb, _)| { - ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) - .map(|e| e.abs()) - .sum() - * 1_000_000.0) as i32 - }) - .expect("Collision detected, but no colliding blocks found!"); - - // Find the intrusion vector of the collision - let dir = player_aabb.collision_vector_with_aabb(block_aabb); - - // Determine an appropriate resolution vector (i.e: the minimum distance - // needed to push out of the block) - let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); - let resolve_dir = -dir.map(|e| { - if e.abs().to_bits() == max_axis.to_bits() { - e - } else { - 0.0 - } - }); - - // When the resolution direction is pointing upwards, we must be on the - // ground - if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { - on_ground = true; - - if !was_on_ground { - landed_on_ground = Some((entity, *vel)); - } - } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { - on_ceiling = true; - } - - // When the resolution direction is non-vertical, we must be colliding - // with a wall If the space above is free... - if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - // ...and we're being pushed out horizontally... - && resolve_dir.z == 0.0 - // ...and the vertical resolution direction is sufficiently great... - && -dir.z > 0.1 - // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... - && (vel.0.z <= 0.0 || terrain - .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - .map(|block| block.is_solid()) - .unwrap_or(false)) - // ...and there is a collision with a block beneath our current hitbox... - && collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) - { - // ...block-hop! - pos.0.z = (pos.0.z + 0.1).floor() + block_height; - vel.0.z = 0.0; - on_ground = true; - break; - } else { - // Correct the velocity - vel.0 = vel.0.map2(resolve_dir, |e, d| { - if d * e.signum() < 0.0 { 0.0 } else { e } - }); - pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); - } - - // Resolve the collision normally - pos.0 += resolve_dir; - - attempts += 1; - } - - if attempts == MAX_ATTEMPTS { + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); + let (pos_writes, land_on_grounds) = + ( + &psdr.entities, + psdr.scales.maybe(), + psdr.stickies.maybe(), + &psdr.colliders, + positions, + &mut psdw.velocities, + &psdw.orientations, + &mut psdw.physics_states, + previous_phys_cache, + !&psdr.mountings, + ) + .par_join() + .fold( + || (Vec::new(), Vec::new()), + |(mut pos_writes, mut land_on_grounds), + ( + entity, + scale, + sticky, + collider, + pos, + mut vel, + _ori, + mut physics_state, + previous_cache, + _, + )| { + // defer the writes of positions to allow an inner loop over terrain-like + // entities + let old_pos = *pos; + let mut pos = *pos; + if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = Vec3::zero(); - pos.0 = old_pos; - break; + return (pos_writes, land_on_grounds); } - } - if on_ceiling { - physics_state.on_ceiling = true; - } - - if on_ground { - physics_state.on_ground = true; - // If the space below us is free, then "snap" to the ground - } else if collision_with( - pos.0 - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) && vel.0.z < 0.0 - && vel.0.z > -1.5 - && was_on_ground - && !collision_with( - pos.0 - Vec3::unit_z() * 0.05, - &terrain, - |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), - near_iter.clone(), - radius, - z_range.clone(), - ) - { - let snap_height = terrain - .get( - Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05) - .map(|e| e.floor() as i32), - ) - .ok() - .filter(|block| block.is_solid()) - .map(|block| block.solid_height()) - .unwrap_or(0.0); - pos.0.z = (pos.0.z - 0.05).floor() + snap_height; - physics_state.on_ground = true; - } - - let dirs = [ - Vec3::unit_x(), - Vec3::unit_y(), - -Vec3::unit_x(), - -Vec3::unit_y(), - ]; - - if let (wall_dir, true) = - dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { - if collision_with( - pos.0 + *dir * 0.01, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) { - (a + dir, true) - } else { - (a, hit) - } - }) - { - physics_state.on_wall = Some(wall_dir); - } else { - physics_state.on_wall = None; - } - - // Figure out if we're in water - physics_state.in_liquid = collision_iter( - pos.0, - &terrain, - &|block| block.is_liquid(), - // The liquid part of a liquid block always extends 1 block high. - &|_block| 1.0, - near_iter.clone(), - radius, - z_min..z_max, - ) - .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) - .map(|block_aabb| block_aabb.max.z - pos.0.z); - }, - Collider::Point => { - let (dist, block) = terrain.ray(pos.0, pos.0 + pos_delta) - .until(|block: &Block| block.is_filled()) - .ignore_error().cast(); - - pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; - - // Can't fail since we do ignore_error above - if block.unwrap().is_some() { - let block_center = pos.0.map(|e| e.floor()) + 0.5; - let block_rpos = (pos.0 - block_center) - .try_normalized() - .unwrap_or(Vec3::zero()); - - // See whether we're on the top/bottom of a block, or the side - if block_rpos.z.abs() - > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() - { - if block_rpos.z > 0.0 { - physics_state.on_ground = true; - } else { - physics_state.on_ceiling = true; - } - vel.0.z = 0.0; + let scale = if let Collider::Voxel { .. } = collider { + scale.map(|s| s.0).unwrap_or(1.0) } else { - physics_state.on_wall = - Some(if block_rpos.x.abs() > block_rpos.y.abs() { - vel.0.x = 0.0; - Vec3::unit_x() * -block_rpos.x.signum() - } else { - vel.0.y = 0.0; - Vec3::unit_y() * -block_rpos.y.signum() - }); + // TODO: Use scale & actual proportions when pathfinding is good + // enough to manage irregular entity sizes + 1.0 + }; + + let old_vel = *vel; + // Integrate forces + // Friction is assumed to be a constant dependent on location + let friction = FRIC_AIR + .max(if physics_state.on_ground { + FRIC_GROUND + } else { + 0.0 + }) + .max(if physics_state.in_liquid.is_some() { + FRIC_FLUID + } else { + 0.0 + }); + let in_loaded_chunk = psdr.terrain + .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .is_some(); + let downward_force = + if !in_loaded_chunk { + 0.0 // No gravity in unloaded chunks + } else if physics_state + .in_liquid + .map(|depth| depth > 0.75) + .unwrap_or(false) + { + (1.0 - BOUYANCY) * GRAVITY + } else { + GRAVITY + } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); + + // Don't move if we're not in a loaded chunk + let pos_delta = if in_loaded_chunk { + // this is an approximation that allows most framerates to + // behave in a similar manner. + let dt_lerp = 0.2; + (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + } else { + Vec3::zero() + }; + + match &*collider { + Collider::Voxel { .. } => { + // for now, treat entities with voxel colliders as their bounding + // cylinders for the purposes of colliding them with terrain + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Box { + radius, + z_min, + z_max, + } => { + // Scale collider + let radius = radius.min(0.45) * scale; + let z_min = *z_min * scale; + let z_max = z_max.clamped(1.2, 1.95) * scale; + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Point => { + let (dist, block) = psdr.terrain + .ray(pos.0, pos.0 + pos_delta) + .until(|block: &Block| block.is_filled()) + .ignore_error() + .cast(); + + pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; + + // Can't fail since we do ignore_error above + if block.unwrap().is_some() { + let block_center = pos.0.map(|e| e.floor()) + 0.5; + let block_rpos = (pos.0 - block_center) + .try_normalized() + .unwrap_or(Vec3::zero()); + + // See whether we're on the top/bottom of a block, or the side + if block_rpos.z.abs() + > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() + { + if block_rpos.z > 0.0 { + physics_state.on_ground = true; + } else { + physics_state.on_ceiling = true; + } + vel.0.z = 0.0; + } else { + physics_state.on_wall = + Some(if block_rpos.x.abs() > block_rpos.y.abs() { + vel.0.x = 0.0; + Vec3::unit_x() * -block_rpos.x.signum() + } else { + vel.0.y = 0.0; + Vec3::unit_y() * -block_rpos.y.signum() + }); + } + } + + physics_state.in_liquid = psdr.terrain + .get(pos.0.map(|e| e.floor() as i32)) + .ok() + .and_then(|vox| vox.is_liquid().then_some(1.0)); + }, } - } - physics_state.in_liquid = terrain.get(pos.0.map(|e| e.floor() as i32)) - .ok() - .and_then(|vox| vox.is_liquid().then_some(1.0)); - }, - } + // Collide with terrain-like entities + for ( + entity_other, + other, + pos_other, + previous_cache_other, + mass_other, + collider_other, + _, + _, + _, + _, + char_state_other_maybe, + ) in ( + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + &psdr.colliders, + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), + ) + .join() + { + let collision_boundary = previous_cache.collision_boundary + + previous_cache_other.collision_boundary; + if previous_cache + .center + .distance_squared(previous_cache_other.center) + > collision_boundary.powi(2) + || entity == entity_other + { + continue; + } - landed_on_ground - }).fold(Vec::new, |mut lands_on_grounds, landed_on_ground| { - if let Some(land_on_ground) = landed_on_ground { - lands_on_grounds.push(land_on_ground); - } - lands_on_grounds - }).reduce(Vec::new, |mut land_on_grounds_a, mut land_on_grounds_b| { - land_on_grounds_a.append(&mut land_on_grounds_b); - land_on_grounds_a - }); + if let Collider::Voxel { id } = collider_other { + // use bounding cylinder regardless of our collider + // TODO: extract point-terrain collision above to its own function + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + // TODO: load .vox into a Dyna, and use it (appropriately rotated) + // as the terrain + /*cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + );*/ + } + } + if pos != old_pos { + pos_writes.push((entity, pos)); + } + + (pos_writes, land_on_grounds) + }, + ) + .reduce( + || (Vec::new(), Vec::new()), + |(mut pos_writes_a, mut land_on_grounds_a), + (mut pos_writes_b, mut land_on_grounds_b)| { + pos_writes_a.append(&mut pos_writes_b); + land_on_grounds_a.append(&mut land_on_grounds_b); + (pos_writes_a, land_on_grounds_a) + }, + ); drop(guard); job.cpu_stats.measure(ParMode::Single); + let pos_writes: HashMap = pos_writes.into_iter().collect(); + for (entity, pos) in (&psdr.entities, &mut psdw.positions).join() { + if let Some(new_pos) = pos_writes.get(&entity) { + *pos = *new_pos; + } + } + + let mut event_emitter = psdr.event_bus.emitter(); land_on_grounds.into_iter().for_each(|(entity, vel)| { event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 }); }); } } + +impl<'a> System<'a> for Sys { + type SystemData = PhysicsSystemData<'a>; + + const NAME: &'static str = "phys"; + const ORIGIN: Origin = Origin::Common; + const PHASE: Phase = Phase::Create; + + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 + fn run( + job: &mut Job, + mut psd: Self::SystemData, + ) { + psd.reset(); + + // Apply pushback + // + // Note: We now do this first because we project velocity ahead. This is slighty + // imperfect and implies that we might get edge-cases where entities + // standing right next to the edge of a wall may get hit by projectiles + // fired into the wall very close to them. However, this sort of thing is + // already possible with poorly-defined hitboxes anyway so it's not too + // much of a concern. + // + // If this situation becomes a problem, this code should be integrated with the + // terrain collision code below, although that's not trivial to do since + // it means the step needs to take into account the speeds of both + // entities. + psd.maintain_pushback_cache(); + psd.apply_pushback(job); + + + psd.handle_movement_and_terrain(job); + } +} + +fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( + cylinder: (f32, f32, f32), + terrain: &'a T, + entity: Entity, + pos: &mut Pos, + mut pos_delta: Vec3, + vel: &mut Vel, + physics_state: &mut PhysicsState, + land_on_grounds: &mut Vec<(Entity, Vel)>, +) { + let (radius, z_min, z_max) = cylinder; + + // Probe distances + let hdist = radius.ceil() as i32; + // Neighbouring blocks iterator + let near_iter = (-hdist..hdist + 1) + .map(move |i| { + (-hdist..hdist + 1).map(move |j| { + (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 + ..z_max.ceil() as i32 + 1) + .map(move |k| (i, j, k)) + }) + }) + .flatten() + .flatten(); + + // Function for iterating over the blocks the player at a specific position + // collides with + fn collision_iter<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: &'a impl Fn(&Block) -> bool, + height: &'a impl Fn(&Block) -> f32, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> impl Iterator> + 'a { + near_iter.filter_map(move |(i, j, k)| { + let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); + + if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { + let player_aabb = Aabb { + min: pos + Vec3::new(-radius, -radius, z_range.start), + max: pos + Vec3::new(radius, radius, z_range.end), + }; + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, height(&block)), + }; + + if player_aabb.collides_with_aabb(block_aabb) { + return Some(block_aabb); + } + } + + None + }) + } + + let z_range = z_min..z_max; + // Function for determining whether the player at a specific position collides + // with blocks with the given criteria + fn collision_with<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: impl Fn(&Block) -> bool, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> bool { + collision_iter( + pos, + terrain, + &|block| block.is_solid() && hit(block), + &Block::solid_height, + near_iter, + radius, + z_range, + ) + .count() + > 0 + } + + let was_on_ground = physics_state.on_ground; + physics_state.on_ground = false; + + let mut on_ground = false; + let mut on_ceiling = false; + let mut attempts = 0; // Don't loop infinitely here + + // Don't jump too far at once + let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) + .ceil() + .max(1.0); + let old_pos = pos.0; + fn block_true(_: &Block) -> bool { true } + for _ in 0..increments as usize { + pos.0 += pos_delta / increments; + + const MAX_ATTEMPTS: usize = 16; + + // While the player is colliding with the terrain... + while collision_with( + pos.0, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && attempts < MAX_ATTEMPTS + { + // Calculate the player's AABB + let player_aabb = Aabb { + min: pos.0 + Vec3::new(-radius, -radius, z_min), + max: pos.0 + Vec3::new(radius, radius, z_max), + }; + + // Determine the block that we are colliding with most (based on minimum + // collision axis) + let (_block_pos, block_aabb, block_height) = near_iter + .clone() + // Calculate the block's position in world space + .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) + // Make sure the block is actually solid + .filter_map(|block_pos| { + if let Some(block) = terrain + .get(block_pos) + .ok() + .filter(|block| block.is_solid()) + { + // Calculate block AABB + Some(( + block_pos, + Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), + }, + block.solid_height(), + )) + } else { + None + } + }) + // Determine whether the block's AABB collides with the player's AABB + .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) + // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) + .min_by_key(|(_, block_aabb, _)| { + ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) + .map(|e| e.abs()) + .sum() + * 1_000_000.0) as i32 + }) + .expect("Collision detected, but no colliding blocks found!"); + + // Find the intrusion vector of the collision + let dir = player_aabb.collision_vector_with_aabb(block_aabb); + + // Determine an appropriate resolution vector (i.e: the minimum distance + // needed to push out of the block) + let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); + let resolve_dir = -dir.map(|e| { + if e.abs().to_bits() == max_axis.to_bits() { + e + } else { + 0.0 + } + }); + + // When the resolution direction is pointing upwards, we must be on the + // ground + if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { + on_ground = true; + + if !was_on_ground { + land_on_grounds.push((entity, *vel)); + } + } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { + on_ceiling = true; + } + + // When the resolution direction is non-vertical, we must be colliding + // with a wall If the space above is free... + if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) + // ...and we're being pushed out horizontally... + && resolve_dir.z == 0.0 + // ...and the vertical resolution direction is sufficiently great... + && -dir.z > 0.1 + // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... + && (vel.0.z <= 0.0 || terrain + .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) + .map(|block| block.is_solid()) + .unwrap_or(false)) + // ...and there is a collision with a block beneath our current hitbox... + && collision_with( + pos.0 + resolve_dir - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) + { + // ...block-hop! + pos.0.z = (pos.0.z + 0.1).floor() + block_height; + vel.0.z = 0.0; + on_ground = true; + break; + } else { + // Correct the velocity + vel.0 = vel.0.map2( + resolve_dir, + |e, d| { + if d * e.signum() < 0.0 { 0.0 } else { e } + }, + ); + pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); + } + + // Resolve the collision normally + pos.0 += resolve_dir; + + attempts += 1; + } + + if attempts == MAX_ATTEMPTS { + vel.0 = Vec3::zero(); + pos.0 = old_pos; + break; + } + } + + if on_ceiling { + physics_state.on_ceiling = true; + } + + if on_ground { + physics_state.on_ground = true; + // If the space below us is free, then "snap" to the ground + } else if collision_with( + pos.0 - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && vel.0.z < 0.0 + && vel.0.z > -1.5 + && was_on_ground + && !collision_with( + pos.0 - Vec3::unit_z() * 0.05, + &terrain, + |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), + near_iter.clone(), + radius, + z_range.clone(), + ) + { + let snap_height = terrain + .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05).map(|e| e.floor() as i32)) + .ok() + .filter(|block| block.is_solid()) + .map(|block| block.solid_height()) + .unwrap_or(0.0); + pos.0.z = (pos.0.z - 0.05).floor() + snap_height; + physics_state.on_ground = true; + } + + let dirs = [ + Vec3::unit_x(), + Vec3::unit_y(), + -Vec3::unit_x(), + -Vec3::unit_y(), + ]; + + if let (wall_dir, true) = dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { + if collision_with( + pos.0 + *dir * 0.01, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) { + (a + dir, true) + } else { + (a, hit) + } + }) { + physics_state.on_wall = Some(wall_dir); + } else { + physics_state.on_wall = None; + } + + // Figure out if we're in water + physics_state.in_liquid = collision_iter( + pos.0, + &*terrain, + &|block| block.is_liquid(), + // The liquid part of a liquid block always extends 1 block high. + &|_block| 1.0, + near_iter.clone(), + radius, + z_min..z_max, + ) + .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) + .map(|block_aabb| block_aabb.max.z - pos.0.z); +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9043ef5fe5..7e6dc4c454 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -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,39 @@ fn handle_spawn_training_dummy( } } +fn handle_spawn_airship( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + match server.state.read_component_copied::(target) { + Some(pos) => { + server + .state + .create_ship(pos, comp::ship::Body::DefaultAirship) + .with(comp::Scale(50.0)) + .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, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index c1c9f8f450..4005c3131b 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -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).cloned(), char_states.get(entity), ) }) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a596b31cbb..770b6d28b0 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,6 +41,7 @@ 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, object: comp::ship::Body) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -215,6 +216,18 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder { + 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: object.manifest_id().to_string() }) + .with(comp::Body::Ship(object)) + .with(comp::Gravity(1.0)) + } + fn create_projectile( &mut self, pos: comp::Pos, diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 105b417a6c..778892724c 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -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) diff --git a/voxygen/anim/src/lib.rs b/voxygen/anim/src/lib.rs index 4988954a6f..d903d6572f 100644 --- a/voxygen/anim/src/lib.rs +++ b/voxygen/anim/src/lib.rs @@ -51,6 +51,7 @@ pub mod fish_small; pub mod fixture; pub mod golem; pub mod object; +pub mod ship; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; diff --git a/voxygen/anim/src/ship/idle.rs b/voxygen/anim/src/ship/idle.rs new file mode 100644 index 0000000000..b96c9fd64e --- /dev/null +++ b/voxygen/anim/src/ship/idle.rs @@ -0,0 +1,34 @@ +use super::{ + super::{vek::*, Animation}, + ShipSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; + +pub struct IdleAnimation; + +impl Animation for IdleAnimation { + type Dependency = (Option, Option, 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 + } +} + diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs new file mode 100644 index 0000000000..2e2783add3 --- /dev/null +++ b/voxygen/anim/src/ship/mod.rs @@ -0,0 +1,71 @@ +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, + buf: &mut [FigureBoneData; super::MAX_BONE_COUNT], + ) -> Vec3 { + let bone0_mat = base_mat * Mat4::::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::::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 { + 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() + } +} + diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 6856dbbb4b..eec891e144 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -1972,7 +1972,7 @@ fn create_pipelines( &shaders.figure_vert.read().0, &shaders.figure_frag.read().0, &include_ctx, - gfx::state::CullFace::Back, + gfx::state::CullFace::Nothing, )?; // Construct a pipeline for rendering terrain diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 6aca80bb7f..a584812462 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -13,6 +13,7 @@ use common::{ humanoid::{self, Body, BodyType, EyeColor, Skin, Species}, item::{ItemDef, ModularComponentKind}, object, + ship::{self, figuredata::{ShipSpec, ShipCentralSubSpec}}, 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}, @@ -22,7 +23,7 @@ use common::{ }; use hashbrown::HashMap; use serde::Deserialize; -use std::sync::Arc; +use std::{fmt, hash::Hash, sync::Arc}; use tracing::{error, warn}; use vek::*; @@ -4102,6 +4103,17 @@ impl QuadrupedLowLateralSpec { #[derive(Deserialize)] struct ObjectCentralSpec(HashMap); +/* +#[derive(Deserialize)] +struct ShipCentralSpec(HashMap); + +#[derive(Deserialize)] +struct SidedShipCentralVoxSpec { + bone0: ObjectCentralSubSpec, + bone1: ObjectCentralSubSpec, + bone2: ObjectCentralSubSpec, +}*/ + #[derive(Deserialize)] struct SidedObjectCentralVoxSpec { bone0: ObjectCentralSubSpec, @@ -4171,3 +4183,99 @@ impl ObjectCentralSpec { (central, Vec3::from(spec.bone1.offset)) } } + +/*make_vox_spec!( + ship::Body, + struct ShipSpec { + central: ShipCentralSpec = "server.manifests.ship_manifest", + }, + |FigureKey { body, .. }, spec| { + [ + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone0, + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone1 + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone2 + )), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + }, +); + +impl ShipCentralSpec { + fn mesh_bone &ObjectCentralSubSpec>(&self, obj: &ship::Body, f: F) -> BoneMeshes { + let spec = match self.0.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(&bone.central.0); + + (central, Vec3::from(bone.offset)) + } +}*/ +fn mesh_ship_bone &ShipCentralSubSpec>(map: &HashMap, 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(&bone.central.0); + + (central, Vec3::from(bone.offset)) +} + +impl BodySpec for ship::Body { + type Spec = ShipSpec; + + #[allow(unused_variables)] + fn load_spec() -> Result, assets::Error> { + Self::Spec::load("") + } + + fn bone_meshes( + FigureKey { body, .. }: &FigureKey, + spec: &Self::Spec, + ) -> [Option; 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, + ] + } +} diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index db5146c923..12281f4edf 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -21,7 +21,7 @@ use anim::{ biped_large::BipedLargeSkeleton, biped_small::BipedSmallSkeleton, bird_medium::BirdMediumSkeleton, bird_small::BirdSmallSkeleton, character::CharacterSkeleton, dragon::DragonSkeleton, fish_medium::FishMediumSkeleton, fish_small::FishSmallSkeleton, - golem::GolemSkeleton, object::ObjectSkeleton, quadruped_low::QuadrupedLowSkeleton, + golem::GolemSkeleton, object::ObjectSkeleton, ship::ShipSkeleton, quadruped_low::QuadrupedLowSkeleton, quadruped_medium::QuadrupedMediumSkeleton, quadruped_small::QuadrupedSmallSkeleton, theropod::TheropodSkeleton, Animation, Skeleton, }; @@ -101,6 +101,7 @@ struct FigureMgrStates { biped_small_states: HashMap>, golem_states: HashMap>, object_states: HashMap>, + ship_states: HashMap>, } impl FigureMgrStates { @@ -120,6 +121,7 @@ impl FigureMgrStates { biped_small_states: HashMap::new(), golem_states: HashMap::new(), object_states: HashMap::new(), + ship_states: HashMap::new(), } } @@ -180,6 +182,7 @@ impl FigureMgrStates { .map(DerefMut::deref_mut), Body::Golem(_) => self.golem_states.get_mut(&entity).map(DerefMut::deref_mut), Body::Object(_) => self.object_states.get_mut(&entity).map(DerefMut::deref_mut), + Body::Ship(_) => self.ship_states.get_mut(&entity).map(DerefMut::deref_mut), } } @@ -205,6 +208,7 @@ impl FigureMgrStates { Body::BipedSmall(_) => self.biped_small_states.remove(&entity).map(|e| e.meta), Body::Golem(_) => self.golem_states.remove(&entity).map(|e| e.meta), Body::Object(_) => self.object_states.remove(&entity).map(|e| e.meta), + Body::Ship(_) => self.ship_states.remove(&entity).map(|e| e.meta), } } @@ -224,6 +228,7 @@ impl FigureMgrStates { self.biped_small_states.retain(|k, v| f(k, &mut *v)); self.golem_states.retain(|k, v| f(k, &mut *v)); self.object_states.retain(|k, v| f(k, &mut *v)); + self.ship_states.retain(|k, v| f(k, &mut *v)); } fn count(&self) -> usize { @@ -242,6 +247,7 @@ impl FigureMgrStates { + self.biped_small_states.len() + self.golem_states.len() + self.object_states.len() + + self.ship_states.len() } fn count_visible(&self) -> usize { @@ -314,6 +320,11 @@ impl FigureMgrStates { .iter() .filter(|(_, c)| c.visible()) .count() + + self + .ship_states + .iter() + .filter(|(_, c)| c.visible()) + .count() } } @@ -332,6 +343,7 @@ pub struct FigureMgr { biped_large_model_cache: FigureModelCache, biped_small_model_cache: FigureModelCache, object_model_cache: FigureModelCache, + ship_model_cache: FigureModelCache, golem_model_cache: FigureModelCache, states: FigureMgrStates, } @@ -353,6 +365,7 @@ impl FigureMgr { biped_large_model_cache: FigureModelCache::new(), biped_small_model_cache: FigureModelCache::new(), object_model_cache: FigureModelCache::new(), + ship_model_cache: FigureModelCache::new(), golem_model_cache: FigureModelCache::new(), states: FigureMgrStates::default(), } @@ -384,6 +397,7 @@ impl FigureMgr { self.biped_small_model_cache .clean(&mut self.col_lights, tick); self.object_model_cache.clean(&mut self.col_lights, tick); + self.ship_model_cache.clean(&mut self.col_lights, tick); self.golem_model_cache.clean(&mut self.col_lights, tick); } @@ -4088,6 +4102,79 @@ impl FigureMgr { _ => target_base, }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); + state.update( + renderer, + pos.0, + ori, + scale, + col, + dt, + state_animation_rate, + model, + lpindex, + true, + is_player, + camera, + &mut update_buf, + terrain, + ); + }, + Body::Ship(body) => { + let (model, skeleton_attr) = self.ship_model_cache.get_or_create_model( + renderer, + &mut self.col_lights, + *body, + inventory, + tick, + player_camera_mode, + player_character_state, + scene_data.runtime, + ); + + let state = + self.states.ship_states.entry(entity).or_insert_with(|| { + FigureState::new(renderer, ShipSkeleton::default()) + }); + + let (character, last_character) = match (character, last_character) { + (Some(c), Some(l)) => (c, l), + _ => (&CharacterState::Idle, &Last { + 0: CharacterState::Idle, + }), + }; + + if !character.same_variant(&last_character.0) { + state.state_time = 0.0; + } + + let target_base = match ( + physics.on_ground, + vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + physics.in_liquid.is_some(), // In water + ) { + // Standing + (true, false, false) => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + _ => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + }; + + let target_bones = match &character { + // TODO! + _ => target_base, + }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); state.update( renderer, @@ -4313,6 +4400,7 @@ impl FigureMgr { biped_large_model_cache, biped_small_model_cache, object_model_cache, + ship_model_cache, golem_model_cache, states: FigureMgrStates { @@ -4330,6 +4418,7 @@ impl FigureMgr { biped_small_states, golem_states, object_states, + ship_states, }, } = self; let col_lights = &*col_lights_; @@ -4572,6 +4661,23 @@ impl FigureMgr { ), ) }), + Body::Ship(body) => ship_states + .get(&entity) + .filter(|state| filter_state(&*state)) + .map(move |state| { + ( + state.locals(), + state.bone_consts(), + ship_model_cache.get_model( + col_lights, + *body, + inventory, + tick, + player_camera_mode, + character_state, + ), + ) + }), } { let model_entry = model_entry?; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 822b3d6156..8bcbffd4bf 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -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).cloned(), 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).cloned(), 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).cloned(), 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.cloned(), cs); (e, cylinder) }) // Roughly filter out entities farther than interaction distance