diff --git a/CHANGELOG.md b/CHANGELOG.md index 0632109896..c13fb25a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New large birds npcs - Day period dependant wildlife spawns - You can now block and parry with melee weapons +- Lift is now calculated for gliders based on dimensions (currently same for all) ### Changed @@ -81,12 +82,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Projectiles now generally have a different arc because they no longer have their own gravity modifier - Increased agent system target search efficiency speeding up the server - Added more parallelization to terrain serialization and removed extra cloning speeding up the server +- Energy now recharges while gliding ### Removed - Removed command: "debug", use "/kit debug" instead - Gravity component has been removed - In-air movement has been removed +- Energy cost of deploying the glider has been removed ### Fixed diff --git a/assets/voxygen/voxel/humanoid_glider_manifest.ron b/assets/voxygen/voxel/humanoid_glider_manifest.ron index 4c1672e0f2..67275f3191 100644 --- a/assets/voxygen/voxel/humanoid_glider_manifest.ron +++ b/assets/voxygen/voxel/humanoid_glider_manifest.ron @@ -5,51 +5,51 @@ ), map: { "Starter": ( - vox_spec: ("glider.glider_starter", (-15.0, -5.0, -5.0)), + vox_spec: ("glider.glider_starter", (-15.0, -5.0, 0.0)), color: None ), "PlainCloth": ( - vox_spec: ("glider.glider_basic_white", (-25.0, -20.0, -5.0)), + vox_spec: ("glider.glider_basic_white", (-25.0, -20.0, 0.0)), color: None ), "RedCloth": ( - vox_spec: ("glider.glider_basic_red", (-25.0, -20.0, -5.0)), + vox_spec: ("glider.glider_basic_red", (-25.0, -20.0, 0.0)), color: None ), "Blue0": ( - vox_spec: ("glider.glider_blue", (-26.0, -26.0, -5.0)), + vox_spec: ("glider.glider_blue", (-26.0, -26.0, 0.0)), color: None ), "ButterflyMorpho": ( - vox_spec: ("glider.glider_butterfly1", (-26.0, -13.0, -5.0)), + vox_spec: ("glider.glider_butterfly1", (-26.0, -13.0, 0.0)), color: None ), "ButterflyMonarch": ( - vox_spec: ("glider.glider_butterfly2", (-26.0, -13.0, -5.0)), + vox_spec: ("glider.glider_butterfly2", (-26.0, -13.0, 0.0)), color: None ), "MothLuna": ( - vox_spec: ("glider.glider_moth", (-26.0, -22.0, -5.0)), + vox_spec: ("glider.glider_moth", (-26.0, -22.0, 0.0)), color: None ), "SandRaptor": ( - vox_spec: ("glider.glider_sandraptor", (-26.0, -25.0, -5.0)), + vox_spec: ("glider.glider_sandraptor", (-26.0, -25.0, 0.0)), color: None ), "SnowRaptor": ( - vox_spec: ("glider.glider_snowraptor", (-26.0, -25.0, -5.0)), + vox_spec: ("glider.glider_snowraptor", (-26.0, -25.0, 0.0)), color: None ), "WoodRaptor": ( - vox_spec: ("glider.glider_woodraptor", (-26.0, -25.0, -5.0)), + vox_spec: ("glider.glider_woodraptor", (-26.0, -25.0, 0.0)), color: None ), "Purple0": ( - vox_spec: ("glider.glider_cultists", (-26.0, -16.0, -5.0)), + vox_spec: ("glider.glider_cultists", (-26.0, -26.0, 0.0)), color: None ), "Leaves": ( - vox_spec: ("glider.glider_leaves", (-26.0, -26.0, -5.0)), + vox_spec: ("glider.glider_leaves", (-26.0, -26.0, 0.0)), color: None ), }, diff --git a/client/src/lib.rs b/client/src/lib.rs index a9e1fc582a..4c77cb928f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1224,7 +1224,7 @@ impl Client { .map(|cs| { matches!( cs, - comp::CharacterState::GlideWield | comp::CharacterState::Glide + comp::CharacterState::GlideWield | comp::CharacterState::Glide(_) ) }); diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 6f7a375e65..4b9abe1cb2 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -50,7 +50,7 @@ pub enum CharacterState { Dance, Talk, Sneak, - Glide, + Glide(glide::Data), GlideWield, /// A stunned state Stunned(stunned::Data), @@ -173,7 +173,7 @@ impl CharacterState { CharacterState::Climb(_) | CharacterState::Equipping(_) | CharacterState::Dance - | CharacterState::Glide + | CharacterState::Glide(_) | CharacterState::GlideWield | CharacterState::Talk | CharacterState::Roll(_), diff --git a/common/src/comp/energy.rs b/common/src/comp/energy.rs index f1545675e1..e540cb6ad5 100644 --- a/common/src/comp/energy.rs +++ b/common/src/comp/energy.rs @@ -17,7 +17,6 @@ pub struct Energy { pub enum EnergySource { Ability, Climb, - Glide, LevelUp, HitEnemy, Regen, diff --git a/common/src/comp/fluid_dynamics.rs b/common/src/comp/fluid_dynamics.rs index 6508160168..98c8aaf56b 100644 --- a/common/src/comp/fluid_dynamics.rs +++ b/common/src/comp/fluid_dynamics.rs @@ -1,10 +1,10 @@ use super::{ body::{object, Body}, - Density, Vel, + CharacterState, Density, Ori, Vel, }; use crate::{ consts::{AIR_DENSITY, WATER_DENSITY}, - util::Dir, + util::{Dir, Plane, Projection}, }; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; @@ -88,7 +88,12 @@ impl Default for Fluid { } impl Body { - pub fn aerodynamic_forces(&self, rel_flow: &Vel, fluid_density: f32) -> Vec3 { + pub fn aerodynamic_forces( + &self, + rel_flow: &Vel, + fluid_density: f32, + character_state: Option<&CharacterState>, + ) -> Vec3 { let v_sq = rel_flow.0.magnitude_squared(); if v_sq < 0.25 { // don't bother with miniscule forces @@ -96,7 +101,72 @@ impl Body { } else { let rel_flow_dir = Dir::new(rel_flow.0 / v_sq.sqrt()); // All the coefficients come pre-multiplied by their reference area - 0.5 * fluid_density * v_sq * self.parasite_drag_coefficient() * *rel_flow_dir + 0.5 * fluid_density + * v_sq + * character_state + .and_then(|cs| match cs { + CharacterState::Glide(data) => { + Some((data.aspect_ratio, data.planform_area, data.ori)) + }, + _ => None, + }) + .map(|(ar, area, ori)| { + if ar > 25.0 { + tracing::warn!( + "Calculating lift for wings with an aspect ratio of {}. The \ + formulas are only valid for aspect ratios below 25.", + ar + ) + }; + (ar.min(24.0), area, ori) + }) + .map(|(ar, area, ori)| { + // We have an elliptical wing; proceed to calculate its lift and drag + + // aoa will be positive when we're pitched up and negative otherwise + let aoa = angle_of_attack(&ori, &rel_flow_dir); + // c_l will be positive when aoa is positive (we have positive lift, + // producing an upward force) and negative otherwise + let c_l = lift_coefficient(ar, area, aoa); + + // lift dir will be orthogonal to the local relative flow vector. + // Local relative flow is the resulting vector of (relative) freestream + // flow + downwash (created by the vortices + // of the wing tips) + let lift_dir: Dir = { + // induced angle of attack + let aoa_i = c_l / (PI * ar); + // effective angle of attack; the aoa as seen by aerofoil after + // downwash + let aoa_eff = aoa - aoa_i; + // Angle between chord line and local relative wind is aoa_eff + // radians. Direction of lift is + // perpendicular to local relative wind. + // At positive lift, local relative wind will be below our cord line + // at an angle of aoa_eff. Thus if + // we pitch down by aoa_eff radians then + // our chord line will be colinear with local relative wind vector + // and our up will be the direction + // of lift. + ori.pitched_down(aoa_eff).up() + }; + + // drag coefficient due to lift + let c_d = { + // Oswald's efficiency factor (empirically derived--very magical) + // (this definition should not be used for aspect ratios > 25) + let e = 1.78 * (1.0 - 0.045 * ar.powf(0.68)) - 0.64; + + zero_lift_drag_coefficient(area) + + self.parasite_drag_coefficient() + + c_l.powi(2) / (PI * e * ar) + }; + debug_assert!(c_d.is_sign_positive()); + debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative()); + + c_l * *lift_dir + c_d * *rel_flow_dir + }) + .unwrap_or_else(|| self.parasite_drag_coefficient() * *rel_flow_dir) } } @@ -125,24 +195,28 @@ impl Body { } else { 1.0 }; - cd * std::f32::consts::PI * dim.x * dim.z + cd * PI * dim.x * dim.z }, // Cross-section, zero-lift angle; exclude the wings (width * 0.2) Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => { let dim = self.dimensions().map(|a| a * 0.5); + // "Field Estimates of Body Drag Coefficient on the Basis of Dives in Passerine + // Birds", Anders Hedenström and Felix Liechti, 2001 let cd = match self { - Body::BirdMedium(_) => 0.2, - Body::BirdLarge(_) => 0.4, + Body::BirdLarge(_) | Body::BirdMedium(_) => 0.2, + // arbitrary _ => 0.7, }; - cd * std::f32::consts::PI * dim.x * 0.2 * dim.z + cd * PI * dim.x * 0.2 * dim.z }, // Cross-section, zero-lift angle; exclude the fins (width * 0.2) Body::FishMedium(_) | Body::FishSmall(_) => { let dim = self.dimensions().map(|a| a * 0.5); - 0.031 * std::f32::consts::PI * dim.x * 0.2 * dim.z + // "A Simple Method to Determine Drag Coefficients in Aquatic Animals", + // D. Bilo and W. Nachtigall, 1980 + 0.031 * PI * dim.x * 0.2 * dim.z }, Body::Object(object) => match object { @@ -158,7 +232,7 @@ impl Body { | object::Body::FireworkYellow | object::Body::MultiArrow => { let dim = self.dimensions().map(|a| a * 0.5); - 0.02 * std::f32::consts::PI * dim.x * dim.z + 0.02 * PI * dim.x * dim.z }, // spherical-ish objects @@ -176,12 +250,12 @@ impl Body { | object::Body::Pumpkin4 | object::Body::Pumpkin5 => { let dim = self.dimensions().map(|a| a * 0.5); - 0.5 * std::f32::consts::PI * dim.x * dim.z + 0.5 * PI * dim.x * dim.z }, _ => { let dim = self.dimensions(); - 2.0 * (std::f32::consts::PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) + 2.0 * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) }, }, @@ -189,17 +263,91 @@ impl Body { // Airships tend to use the square of the cube root of its volume for // reference area let dim = self.dimensions(); - (std::f32::consts::PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) + (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) }, } } } -/* -## References: +/// Geometric angle of attack +/// +/// # Note +/// This ignores spanwise flow (i.e. we remove the spanwise flow component). +/// With greater yaw comes greater loss of accuracy as more flow goes +/// unaccounted for. +pub fn angle_of_attack(ori: &Ori, rel_flow_dir: &Dir) -> f32 { + rel_flow_dir + .projected(&Plane::from(ori.right())) + .map(|flow_dir| PI / 2.0 - ori.up().angle_between(flow_dir.to_vec())) + .unwrap_or(0.0) +} -1. "Field Estimates of Body Drag Coefficient on the Basis of Dives in Passerine Birds", - Anders Hedenström and Felix Liechti, 2001 -2. "A Simple Method to Determine Drag Coefficients in Aquatic Animals", - D. Bilo and W. Nachtigall, 1980 -*/ +/// Total lift coefficient for a finite wing of symmetric aerofoil shape and +/// elliptical pressure distribution. +pub fn lift_coefficient(aspect_ratio: f32, planform_area: f32, aoa: f32) -> f32 { + let aoa_abs = aoa.abs(); + let stall_angle = PI * 0.1; + planform_area + * if aoa_abs < stall_angle { + lift_slope(aspect_ratio, None) * aoa + } else { + // This is when flow separation and turbulence starts to kick in. + // Going to just make something up (based on some data), as the alternative is + // to just throw your hands up and return 0 + let aoa_s = aoa.signum(); + let c_l_max = lift_slope(aspect_ratio, None) * stall_angle; + let deg_45 = PI / 4.0; + if aoa_abs < deg_45 { + // drop directly to 0.6 * max lift at stall angle + // then climb back to max at 45° + Lerp::lerp(0.6 * c_l_max, c_l_max, aoa_abs / deg_45) * aoa_s + } else { + // let's just say lift goes down linearly again until we're at 90° + Lerp::lerp(c_l_max, 0.0, (aoa_abs - deg_45) / deg_45) * aoa_s + } + } +} + +/// The zero-lift profile drag coefficient is the parasite drag on the wings +/// at the angle of attack which generates no lift +pub fn zero_lift_drag_coefficient(planform_area: f32) -> f32 { + // TODO: verify that it's correct to multiply by planform + // avg value for Harris' hawk (Parabuteo unicinctus) [1] + planform_area * 0.02 +} + +/// The change in lift over change in angle of attack¹. Multiplying by angle +/// of attack gives the lift coefficient (for a finite wing, not aerofoil). +/// Aspect ratio is the ratio of total wing span squared over planform area. +/// +/// # Notes +/// Only valid for symmetric, elliptical wings at small² angles of attack³. +/// Does not apply to twisted, cambered or delta wings. (It still gives a +/// reasonably accurate approximation if the wing shape is not truly +/// elliptical.) +/// 1. geometric angle of attack, i.e. the pitch angle relative to +/// freestream flow +/// 2. up to around ~18°, at which point maximum lift has been achieved and +/// thereafter falls precipitously, causing a stall (this is the stall +/// angle) 3. effective aoa, i.e. geometric aoa - induced aoa; assumes +/// no sideslip +// TODO: Look into handling tapered wings +fn lift_slope(aspect_ratio: f32, sweep_angle: Option) -> f32 { + // lift slope for a thin aerofoil, given by Thin Aerofoil Theory + let a0 = 2.0 * PI; + if let Some(sweep) = sweep_angle { + // for swept wings we use Kuchemann's modification to Helmbold's + // equation + let a0_cos_sweep = a0 * sweep.cos(); + let x = a0_cos_sweep / (PI * aspect_ratio); + a0_cos_sweep / ((1.0 + x.powi(2)).sqrt() + x) + } else if aspect_ratio < 4.0 { + // for low aspect ratio wings (AR < 4) we use Helmbold's equation + let x = a0 / (PI * aspect_ratio); + a0 / ((1.0 + x.powi(2)).sqrt() + x) + } else { + // for high aspect ratio wings (AR > 4) we use the equation given by + // Prandtl's lifting-line theory + a0 / (1.0 + (a0 / (PI * aspect_ratio))) + } +} diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs index 6acd70b359..3dea6afb8c 100644 --- a/common/src/comp/ori.rs +++ b/common/src/comp/ori.rs @@ -148,8 +148,26 @@ impl Ori { self.to_quat() * local } - pub fn to_horizontal(self) -> Option { - Dir::from_unnormalized(self.look_dir().xy().into()).map(|ori| ori.into()) + pub fn to_horizontal(self) -> Self { + let fw = self.look_dir(); + Dir::from_unnormalized(fw.xy().into()) + .or_else(|| { + // if look_dir is straight down, pitch up, or if straight up, pitch down + Dir::from_unnormalized( + if fw.dot(Vec3::unit_z()) < 0.0 { + self.up() + } else { + self.down() + } + .xy() + .into(), + ) + }) + .map(|dir| dir.into()) + .expect( + "If the horizontal component of a Dir can not be normalized, the horizontal \ + component of a Dir perpendicular to it must be", + ) } pub fn pitched_up(self, angle_radians: f32) -> Self { diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index 7835c33463..93be892c14 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -1,64 +1,178 @@ use super::utils::handle_climb; use crate::{ - comp::{inventory::slot::EquipSlot, CharacterState, Ori, StateUpdate}, + comp::{ + fluid_dynamics::angle_of_attack, inventory::slot::EquipSlot, CharacterState, Ori, + StateUpdate, Vel, + }, states::behavior::{CharacterBehavior, JoinData}, - util::Dir, + util::{Dir, Plane, Projection}, }; use serde::{Deserialize, Serialize}; -use vek::Vec2; - -const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; -const GLIDE_ACCEL: f32 = 5.0; -const GLIDE_MAX_SPEED: f32 = 30.0; +use std::f32::consts::PI; +use vek::*; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Data; +pub struct Data { + /// The aspect ratio is the ratio of the span squared to actual planform + /// area + pub aspect_ratio: f32, + pub planform_area: f32, + pub ori: Ori, + last_vel: Vel, +} + +impl Data { + /// A glider is modelled as an elliptical wing and has a span length + /// (distance from wing tip to wing tip) and a chord length (distance from + /// leading edge to trailing edge through its centre) measured in block + /// units. + /// + /// https://en.wikipedia.org/wiki/Elliptical_wing + pub fn new(span_length: f32, chord_length: f32, ori: Ori) -> Self { + let planform_area = PI * chord_length * span_length * 0.25; + let aspect_ratio = span_length.powi(2) / planform_area; + Self { + aspect_ratio, + planform_area, + ori, + last_vel: Vel::zero(), + } + } +} + +fn tgt_dir(data: &JoinData) -> Dir { + let look_ori = Ori::from(data.inputs.look_dir); + look_ori + .yawed_right(PI / 3.0 * look_ori.right().xy().dot(data.inputs.move_dir)) + .pitched_up(PI * 0.05) + .pitched_down( + data.inputs + .look_dir + .xy() + .try_normalized() + .map_or(0.0, |ld| PI * 0.1 * ld.dot(data.inputs.move_dir)), + ) + .look_dir() +} impl CharacterBehavior for Data { fn behavior(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); // If player is on ground, end glide - if data.physics.on_ground { + if data.physics.on_ground + && (data.vel.0 - data.physics.ground_vel).magnitude_squared() < 2_f32.powi(2) + { update.character = CharacterState::GlideWield; - return update; - } - if data - .physics - .in_liquid() - .map(|depth| depth > 0.5) - .unwrap_or(false) + update.ori = update.ori.to_horizontal(); + } else if data.physics.in_liquid().is_some() + || data.inventory.equipped(EquipSlot::Glider).is_none() { update.character = CharacterState::Idle; + update.ori = update.ori.to_horizontal(); + } else if !handle_climb(&data, &mut update) { + let air_flow = data + .physics + .in_fluid + .map(|fluid| fluid.relative_flow(data.vel)) + .unwrap_or_default(); + + let ori = { + let slerp_s = { + let angle = self.ori.look_dir().angle_between(*data.inputs.look_dir); + let rate = 0.4 * PI / angle; + (data.dt.0 * rate).min(0.1) + }; + + Dir::from_unnormalized(air_flow.0) + .map(|flow_dir| { + let tgt_dir = tgt_dir(data); + let tgt_dir_ori = Ori::from(tgt_dir); + let tgt_dir_up = tgt_dir_ori.up(); + // The desired up vector of our glider. + // We begin by projecting the flow dir on the plane with the normal of + // our tgt_dir to get an idea of how it will hit the glider + let tgt_up = flow_dir + .projected(&Plane::from(tgt_dir)) + .map(|d| { + let d = if d.dot(*tgt_dir_up).is_sign_negative() { + // when the final direction of flow is downward we don't roll + // upside down but instead mirror the target up vector + Quaternion::rotation_3d(PI, *tgt_dir_ori.right()) * d + } else { + d + }; + // slerp from untilted up towards the direction by a factor of + // lateral wind to prevent overly reactive adjustments + let lateral_wind_speed = + air_flow.0.projected(&self.ori.right()).magnitude(); + tgt_dir_up.slerped_to(d, lateral_wind_speed / 15.0) + }) + .unwrap_or_else(Dir::up); + let global_roll = tgt_dir_up.rotation_between(tgt_up); + let global_pitch = angle_of_attack(&tgt_dir_ori, &flow_dir); + + self.ori.slerped_towards( + tgt_dir_ori.prerotated(global_roll).pitched_up(global_pitch), + slerp_s, + ) + }) + .unwrap_or_else(|| self.ori.slerped_towards(self.ori.uprighted(), slerp_s)) + }; + + update.ori = { + let slerp_s = { + let angle = data.ori.look_dir().angle_between(*data.inputs.look_dir); + let rate = data.body.base_ori_rate() * PI / angle; + (data.dt.0 * rate).min(0.1) + }; + + let rot_from_drag = { + let speed_factor = + air_flow.0.magnitude_squared().min(40_f32.powi(2)) / 40_f32.powi(2); + + Quaternion::rotation_3d( + -PI / 2.0 * speed_factor, + ori.up() + .cross(air_flow.0) + .try_normalized() + .unwrap_or_else(|| *data.ori.right()), + ) + }; + + let rot_from_accel = { + let accel = data.vel.0 - self.last_vel.0; + let accel_factor = accel.magnitude_squared().min(1.0) / 1.0; + + Quaternion::rotation_3d( + PI / 2.0 * accel_factor, + ori.up() + .cross(accel) + .try_normalized() + .unwrap_or_else(|| *data.ori.right()), + ) + }; + + update.ori.slerped_towards( + ori.to_horizontal() + .prerotated(rot_from_drag * rot_from_accel), + slerp_s, + ) + }; + update.pos.0 = { + // offset character pos such that it's the center of rotation is not around the + // character + let center_off = data.body.height() * 0.7; + update.pos.0 + *data.ori.up() * center_off - *update.ori.up() * center_off + }; + update.character = CharacterState::Glide(Self { + ori, + last_vel: *data.vel, + ..*self + }); + } else { + update.ori = update.ori.to_horizontal(); } - if data.inventory.equipped(EquipSlot::Glider).is_none() { - update.character = CharacterState::Idle - }; - - let horiz_vel = Vec2::::from(update.vel.0); - let horiz_speed_sq = horiz_vel.magnitude_squared(); - - // Move player according to movement direction vector - if horiz_speed_sq < GLIDE_MAX_SPEED.powi(2) { - update.vel.0 += Vec2::broadcast(data.dt.0) * data.inputs.move_dir * GLIDE_ACCEL; - } - - // Determine orientation vector from movement direction vector - if let Some(dir) = Dir::from_unnormalized(update.vel.0) { - update.ori = update.ori.slerped_towards(Ori::from(dir), 2.0 * data.dt.0); - }; - - // Apply Glide antigrav lift - 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); - - update.vel.0.z += lift * data.dt.0; - } - - // If there is a wall in front of character and they are trying to climb go to - // climb - handle_climb(&data, &mut update); update } @@ -66,6 +180,7 @@ impl CharacterBehavior for Data { fn unwield(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle; + update.ori = update.ori.to_horizontal(); update } } diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs index 080ef017d3..23c0b9b807 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -1,7 +1,10 @@ use super::utils::*; use crate::{ - comp::{slot::EquipSlot, CharacterState, EnergySource, InventoryAction, StateUpdate}, - states::behavior::{CharacterBehavior, JoinData}, + comp::{slot::EquipSlot, CharacterState, InventoryAction, StateUpdate}, + states::{ + behavior::{CharacterBehavior, JoinData}, + glide, + }, }; pub struct Data; @@ -18,18 +21,7 @@ impl CharacterBehavior for Data { // If not on the ground while wielding glider enter gliding state if !data.physics.on_ground { - // Expend energy to slow a fall - let energy_cost = (0.5 * (data.vel.0.z + 15.0).min(0.0).powi(2)) as i32; - if update - .energy - .try_change_by(-energy_cost, EnergySource::Glide) - .is_ok() - { - update.character = CharacterState::Glide; - } else { - update.energy.set_to(0, EnergySource::Glide); - update.character = CharacterState::Idle; - } + update.character = CharacterState::Glide(glide::Data::new(10.0, 0.6, *data.ori)); } if data .physics diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ec7de76a56..72e497601b 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -363,15 +363,24 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, submers /// Updates components to move entity as if it's flying pub fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) -> bool { - if let Some(force) = data.body.fly_thrust() { + let glider = match data.character { + CharacterState::Glide(data) => Some(data), + _ => None, + }; + if let Some(force) = data + .body + .fly_thrust() + .or_else(|| glider.is_some().then_some(0.0)) + { let thrust = efficiency * force; - let accel = thrust / data.mass.0; + handle_orientation(data, update, efficiency); + // Elevation control match data.body { // flappy flappy - Body::Dragon(_) | Body::BirdMedium(_) | Body::BirdLarge(_) => { + Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => { let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0)); update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0)); }, @@ -468,7 +477,7 @@ pub fn attempt_sneak(data: &JoinData, update: &mut StateUpdate) { } /// Checks that player can `Climb` and updates `CharacterState` if so -pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) { +pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) -> bool { if data.inputs.climb.is_some() && data.physics.on_wall.is_some() && !data.physics.on_ground @@ -482,6 +491,9 @@ pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) { && update.energy.current() > 100 { update.character = CharacterState::Climb(climb::Data::create_adjusted_by_skills(data)); + true + } else { + false } } diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index b2336bffdb..ef9679fab1 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -287,7 +287,7 @@ impl<'a> System<'a> for Sys { CharacterState::Idle => states::idle::Data.handle_event(&j, action), CharacterState::Talk => states::talk::Data.handle_event(&j, action), CharacterState::Climb(data) => data.handle_event(&j, action), - CharacterState::Glide => states::glide::Data.handle_event(&j, action), + CharacterState::Glide(data) => data.handle_event(&j, action), CharacterState::GlideWield => { states::glide_wield::Data.handle_event(&j, action) }, @@ -349,7 +349,7 @@ impl<'a> System<'a> for Sys { CharacterState::Idle => states::idle::Data.behavior(&j), CharacterState::Talk => states::talk::Data.behavior(&j), CharacterState::Climb(data) => data.behavior(&j), - CharacterState::Glide => states::glide::Data.behavior(&j), + CharacterState::Glide(data) => data.behavior(&j), CharacterState::GlideWield => states::glide_wield::Data.behavior(&j), CharacterState::Stunned(data) => data.behavior(&j), CharacterState::Sit => states::sit::Data::behavior(&states::sit::Data, &j), diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 80d881fdab..c83d6861e9 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -45,6 +45,7 @@ fn integrate_forces( body: &Body, density: &Density, mass: &Mass, + character_state: Option<&CharacterState>, fluid: &Fluid, gravity: f32, ) -> Vel { @@ -58,7 +59,7 @@ fn integrate_forces( // Aerodynamic/hydrodynamic forces if !rel_flow.0.is_approx_zero() { debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or()); - let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0); + let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, character_state); debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or()); if !impulse.is_approx_zero() { let new_v = vel.0 + impulse / mass.0; @@ -564,6 +565,7 @@ impl<'a> PhysicsData<'a> { velocities, read.stickies.maybe(), &read.bodies, + read.character_states.maybe(), &write.physics_states, &read.masses, &read.densities, @@ -575,7 +577,18 @@ impl<'a> PhysicsData<'a> { prof_span!(guard, "velocity update rayon job"); guard }, - |_guard, (pos, vel, sticky, body, physics_state, mass, density, _)| { + |_guard, + ( + pos, + vel, + sticky, + body, + character_state, + physics_state, + mass, + density, + _, + )| { let in_loaded_chunk = read .terrain .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) @@ -597,7 +610,14 @@ impl<'a> PhysicsData<'a> { }, Some(fluid) => { vel.0 = integrate_forces( - &dt, *vel, body, density, mass, &fluid, GRAVITY, + &dt, + *vel, + body, + density, + mass, + character_state, + &fluid, + GRAVITY, ) .0 }, @@ -688,7 +708,8 @@ impl<'a> PhysicsData<'a> { let mut tgt_pos = pos.0 + pos_delta; let was_on_ground = physics_state.on_ground; - let block_snap = body.map_or(false, |b| !matches!(b, Body::Ship(_))); + let block_snap = + body.map_or(false, |b| !matches!(b, Body::Object(_) | Body::Ship(_))); let climbing = character_state.map_or(false, |cs| matches!(cs, CharacterState::Climb(_))); @@ -841,11 +862,12 @@ impl<'a> PhysicsData<'a> { depth, vel: Vel::zero(), }) - .or_else(|| { - Some(Fluid::Air { + .or_else(|| match physics_state.in_fluid { + Some(Fluid::Water { .. }) | None => Some(Fluid::Air { elevation: pos.0.z, - vel: Vel::zero(), - }) + vel: Vel::default(), + }), + fluid => fluid, }); tgt_pos = pos.0; @@ -1517,11 +1539,12 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( depth, vel: Vel::zero(), }) - .or_else(|| { - Some(Fluid::Air { + .or_else(|| match physics_state.in_fluid { + Some(Fluid::Water { .. }) | None => Some(Fluid::Air { elevation: pos.0.z, - vel: Vel::zero(), - }) + vel: Vel::default(), + }), + fluid => fluid, }); } diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index d56129c3f7..4d999387db 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -190,6 +190,7 @@ impl<'a> System<'a> for Sys { | CharacterState::Sit { .. } | CharacterState::Dance { .. } | CharacterState::Sneak { .. } + | CharacterState::Glide { .. } | CharacterState::GlideWield { .. } | CharacterState::Wielding { .. } | CharacterState::Equipping { .. } @@ -232,9 +233,8 @@ impl<'a> System<'a> for Sys { poise.regen_rate = (poise.regen_rate + POISE_REGEN_ACCEL * dt).min(10.0); } }, - // Ability and glider use does not regen and sets the rate back to zero. - CharacterState::Glide { .. } - | CharacterState::BasicMelee { .. } + // Ability use does not regen and sets the rate back to zero. + CharacterState::BasicMelee { .. } | CharacterState::DashMelee { .. } | CharacterState::LeapMelee { .. } | CharacterState::SpinMelee { .. } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3406783fd4..8c86b041df 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -223,7 +223,7 @@ impl<'a> System<'a> for Sys { }); let is_gliding = matches!( read_data.char_states.get(entity), - Some(CharacterState::GlideWield) | Some(CharacterState::Glide) + Some(CharacterState::GlideWield) | Some(CharacterState::Glide(_)) ) && !physics_state.on_ground; // This controls how picky NPCs are about their pathfinding. Giants are larger diff --git a/voxygen/anim/src/character/glidewield.rs b/voxygen/anim/src/character/glidewield.rs index cc8dd616db..ac7cb6f85f 100644 --- a/voxygen/anim/src/character/glidewield.rs +++ b/voxygen/anim/src/character/glidewield.rs @@ -2,21 +2,11 @@ use super::{ super::{vek::*, Animation}, CharacterSkeleton, SkeletonAttr, }; -use common::comp::item::ToolKind; pub struct GlideWieldAnimation; -type GlideWieldAnimationDependency = ( - Option, - Option, - Vec3, - Vec3, - Vec3, - f32, -); - impl Animation for GlideWieldAnimation { - type Dependency = GlideWieldAnimationDependency; + type Dependency = (); type Skeleton = CharacterSkeleton; #[cfg(feature = "use-dyn-lib")] @@ -26,13 +16,12 @@ impl Animation for GlideWieldAnimation { fn update_skeleton_inner( skeleton: &Self::Skeleton, - (_active_tool_kind, _second_tool_kind, velocity, _orientation, _last_ori, _global_time): Self::Dependency, + _: Self::Dependency, _anim_time: f32, rate: &mut f32, s_a: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let speed = Vec2::::from(velocity).magnitude(); *rate = 1.0; next.hand_l.position = Vec3::new(-2.0 - s_a.hand.0, s_a.hand.1, s_a.hand.2 + 15.0); @@ -41,14 +30,9 @@ impl Animation for GlideWieldAnimation { next.hand_r.position = Vec3::new(2.0 + s_a.hand.0, s_a.hand.1, s_a.hand.2 + 15.0); next.hand_r.orientation = Quaternion::rotation_x(3.35) * Quaternion::rotation_y(-0.2); next.glider.scale = Vec3::one() * 1.0; + next.glider.orientation = Quaternion::rotation_x(0.35); - if speed > 0.5 { - next.glider.orientation = Quaternion::rotation_x(0.8); - next.glider.position = Vec3::new(0.0, -10.0, 15.0); - } else { - next.glider.orientation = Quaternion::rotation_x(0.35); - next.glider.position = Vec3::new(0.0, -9.0, 17.0); - } + next.glider.position = Vec3::new(0.0, -5.0, 13.0); next } diff --git a/voxygen/anim/src/character/gliding.rs b/voxygen/anim/src/character/gliding.rs index 68276ad03a..996d745140 100644 --- a/voxygen/anim/src/character/gliding.rs +++ b/voxygen/anim/src/character/gliding.rs @@ -2,19 +2,11 @@ use super::{ super::{vek::*, Animation}, CharacterSkeleton, SkeletonAttr, }; -use common::comp::item::ToolKind; -use std::{f32::consts::PI, ops::Mul}; +use std::ops::Mul; pub struct GlidingAnimation; -type GlidingAnimationDependency = ( - Option, - Option, - Vec3, - Vec3, - Vec3, - f32, -); +type GlidingAnimationDependency = (Vec3, Quaternion, Quaternion, f32, f32); impl Animation for GlidingAnimation { type Dependency = GlidingAnimationDependency; @@ -27,89 +19,63 @@ impl Animation for GlidingAnimation { fn update_skeleton_inner( skeleton: &Self::Skeleton, - (_active_tool_kind, _second_tool_kind, velocity, orientation, last_ori, global_time): Self::Dependency, + (velocity, orientation, glider_orientation, global_time, acc_vel): Self::Dependency, anim_time: f32, _rate: &mut f32, s_a: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let speed = Vec2::::from(velocity).magnitude(); - - let quick = (anim_time * 7.0).sin(); - let quicka = (anim_time * 7.0 + PI / 2.0).sin(); - let wave_stop = (anim_time * 1.5).min(PI / 2.0).sin(); - let slow = (anim_time * 3.0).sin(); - let slowb = (anim_time * 3.0 + PI).sin(); - let slowa = (anim_time * 3.0 + PI / 2.0).sin(); + let speednorm = velocity.magnitude().min(50.0) / 50.0; + let slow = (acc_vel * 0.5).sin(); let head_look = Vec2::new( - ((global_time + anim_time) / 5.0).floor().mul(7331.0).sin() * 0.5, - ((global_time + anim_time) / 5.0).floor().mul(1337.0).sin() * 0.25, + ((global_time + anim_time) as f32 / 4.0) + .floor() + .mul(7331.0) + .sin() + * 0.5, + ((global_time + anim_time) as f32 / 4.0) + .floor() + .mul(1337.0) + .sin() + * 0.25, ); - let ori: Vec2 = Vec2::from(orientation); - let last_ori = Vec2::from(last_ori); - let tilt = if ::vek::Vec2::new(ori, last_ori) - .map(|o| o.magnitude_squared()) - .map(|m| m > 0.0001 && m.is_finite()) - .reduce_and() - && ori.angle_between(last_ori).is_finite() - { - ori.angle_between(last_ori).min(0.05) - * last_ori.determine_side(Vec2::zero(), ori).signum() - } else { - 0.0 - }; + let speedlog = speednorm.powi(2); + let chest_ori = Quaternion::rotation_z(slow * 0.01); + let chest_global_inv = (orientation * chest_ori).inverse(); + let glider_ori = chest_global_inv * glider_orientation; + let glider_pos = Vec3::new(0.0, -5.0 + speedlog * 2.0, 13.0); - let tiltcancel = if anim_time > 1.0 { 1.0 } else { anim_time }; + next.head.orientation = Quaternion::rotation_x(0.5 + head_look.y * speednorm) + * Quaternion::rotation_z(head_look.x); - next.head.position = Vec3::new(0.0, s_a.head.0 + 1.0, s_a.head.1); - next.head.orientation = Quaternion::rotation_x(0.35 - slow * 0.10 + head_look.y) - * Quaternion::rotation_z(head_look.x + slowa * 0.15); - - next.chest.orientation = Quaternion::rotation_z(slowa * 0.02); - - next.belt.orientation = Quaternion::rotation_z(slowa * 0.1 + tilt * tiltcancel * 12.0); - next.belt.position = Vec3::new(0.0, s_a.belt.0, s_a.belt.1); - - next.shorts.orientation = Quaternion::rotation_z(slowa * 0.12 + tilt * tiltcancel * 16.0); - next.shorts.position = Vec3::new(0.0, s_a.shorts.0, s_a.shorts.1); - - next.hand_l.position = Vec3::new(-9.5, -3.0, 10.0); - next.hand_l.orientation = - Quaternion::rotation_x(-2.7 + slowa * -0.1) * Quaternion::rotation_y(0.2); - - next.hand_r.position = Vec3::new(9.5, -3.0, 10.0); - next.hand_r.orientation = - Quaternion::rotation_x(-2.7 + slowa * -0.10) * Quaternion::rotation_y(-0.2); - - next.foot_l.position = Vec3::new( - -s_a.foot.0, - s_a.foot.1 + slowa * -1.0 + tilt * tiltcancel * -35.0, - -1.0 + s_a.foot.2, - ); - next.foot_l.orientation = Quaternion::rotation_x( - (wave_stop * -0.7 - quicka * -0.21 + slow * 0.19) * speed * 0.04, - ) * Quaternion::rotation_z(tilt * tiltcancel * 20.0); - - next.foot_r.position = Vec3::new( - s_a.foot.0, - s_a.foot.1 + slowa * 1.0 + tilt * tiltcancel * 35.0, - -1.0 + s_a.foot.2, - ); - next.foot_r.orientation = Quaternion::rotation_x( - (wave_stop * -0.8 + quick * -0.25 + slowb * 0.13) * speed * 0.04, - ) * Quaternion::rotation_z(tilt * tiltcancel * 20.0); - - next.glider.position = Vec3::new(0.0, -13.0 + slow * 0.10, 8.0); - next.glider.orientation = - Quaternion::rotation_x(0.8) * Quaternion::rotation_y(slowa * 0.04); + next.glider.position = glider_pos; + next.glider.orientation = glider_ori; next.glider.scale = Vec3::one(); - next.torso.position = Vec3::new(0.0, -1.0, 0.0) / 11.0 * s_a.scaler; - next.torso.orientation = Quaternion::rotation_x(-0.03 * speed.max(12.0) + slow * 0.04) - * Quaternion::rotation_y(tilt * tiltcancel * 32.0); + next.chest.orientation = chest_ori; + + //necessary for overwriting jump anim + next.belt.orientation = Quaternion::rotation_z(0.0); + next.shorts.orientation = Quaternion::rotation_z(0.0); + next.belt.position = Vec3::new(0.0, s_a.belt.0, s_a.belt.1); + next.shorts.position = Vec3::new(0.0, s_a.shorts.0, s_a.shorts.1); + + next.hand_l.position = + glider_pos + glider_ori * Vec3::new(-s_a.hand.0 + -2.0, s_a.hand.1 + 8.0, s_a.hand.2); + next.hand_l.orientation = Quaternion::rotation_x(3.35) * Quaternion::rotation_y(0.2); + + next.hand_r.position = + glider_pos + glider_ori * Vec3::new(s_a.hand.0 + 2.0, s_a.hand.1 + 8.0, s_a.hand.2); + next.hand_r.orientation = Quaternion::rotation_x(3.35) * Quaternion::rotation_y(-0.2); + + next.foot_l.position = Vec3::new(-s_a.foot.0, s_a.foot.1 + speedlog * -1.0, s_a.foot.2); + next.foot_l.orientation = Quaternion::rotation_x(-speedlog + slow * -0.3 * speedlog); + + next.foot_r.position = Vec3::new(s_a.foot.0, s_a.foot.1 + speedlog * -1.0, s_a.foot.2); + next.foot_r.orientation = Quaternion::rotation_x(-speedlog + slow * 0.3 * speedlog); next } diff --git a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs index 0c89e29f2e..8e9523edfa 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs @@ -3,7 +3,7 @@ use crate::audio::sfx::SfxEvent; use common::{ comp::{ bird_large, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, InputKind, - PhysicsState, + Ori, PhysicsState, }, states, terrain::BlockKind, @@ -236,7 +236,7 @@ fn maps_land_on_ground_to_run() { #[test] fn maps_glider_open() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Glide {}, + &CharacterState::Glide(states::glide::Data::new(10.0, 1.0, Ori::default())), &Default::default(), &PreviousEntityState { event: SfxEvent::Jump, @@ -255,7 +255,7 @@ fn maps_glider_open() { #[test] fn maps_glide() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Glide {}, + &CharacterState::Glide(states::glide::Data::new(10.0, 1.0, Ori::default())), &Default::default(), &PreviousEntityState { event: SfxEvent::Glide, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 07dae79cbe..32c91928df 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -1474,18 +1474,10 @@ impl FigureMgr { ) } }, - CharacterState::Glide { .. } => { + CharacterState::Glide(data) => { anim::character::GlidingAnimation::update_skeleton( &target_base, - ( - active_tool_kind, - second_tool_kind, - rel_vel, - // TODO: Update to use the quaternion. - ori * anim::vek::Vec3::::unit_y(), - state.last_ori * anim::vek::Vec3::::unit_y(), - time, - ), + (rel_vel, ori, data.ori.into(), time, state.acc_vel), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1519,15 +1511,7 @@ impl FigureMgr { CharacterState::GlideWield { .. } => { anim::character::GlideWieldAnimation::update_skeleton( &target_base, - ( - active_tool_kind, - second_tool_kind, - rel_vel, - // TODO: Update to use the quaternion. - ori * anim::vek::Vec3::::unit_y(), - state.last_ori * anim::vek::Vec3::::unit_y(), - time, - ), + (), state.state_time, &mut state_animation_rate, skeleton_attr,