Glider physics

This commit is contained in:
Ludvig Böklin 2021-04-27 14:41:48 +00:00 committed by Joshua Barretto
parent de25358ab7
commit 34660462e9
18 changed files with 483 additions and 239 deletions

View File

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

View File

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

View File

@ -1224,7 +1224,7 @@ impl Client {
.map(|cs| {
matches!(
cs,
comp::CharacterState::GlideWield | comp::CharacterState::Glide
comp::CharacterState::GlideWield | comp::CharacterState::Glide(_)
)
});

View File

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

View File

@ -17,7 +17,6 @@ pub struct Energy {
pub enum EnergySource {
Ability,
Climb,
Glide,
LevelUp,
HitEnemy,
Regen,

View File

@ -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<f32> {
pub fn aerodynamic_forces(
&self,
rel_flow: &Vel,
fluid_density: f32,
character_state: Option<&CharacterState>,
) -> Vec3<f32> {
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>) -> 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)))
}
}

View File

@ -148,8 +148,26 @@ impl Ori {
self.to_quat() * local
}
pub fn to_horizontal(self) -> Option<Self> {
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 {

View File

@ -1,71 +1,186 @@
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;
}
if 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)
};
let horiz_vel = Vec2::<f32>::from(update.vel.0);
let horiz_speed_sq = horiz_vel.magnitude_squared();
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);
// 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);
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))
};
// 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.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)
};
update.vel.0.z += lift * data.dt.0;
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 there is a wall in front of character and they are trying to climb go to
// climb
handle_climb(&data, &mut update);
update
}
fn unwield(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data);
update.character = CharacterState::Idle;
update.ori = update.ori.to_horizontal();
update
}
}

View File

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

View File

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

View File

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

View File

@ -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<Vox = Block> + 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,
});
}

View File

@ -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 { .. }

View File

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

View File

@ -2,21 +2,11 @@ use super::{
super::{vek::*, Animation},
CharacterSkeleton, SkeletonAttr,
};
use common::comp::item::ToolKind;
pub struct GlideWieldAnimation;
type GlideWieldAnimationDependency = (
Option<ToolKind>,
Option<ToolKind>,
Vec3<f32>,
Vec3<f32>,
Vec3<f32>,
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::<f32>::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;
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
}

View File

@ -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<ToolKind>,
Option<ToolKind>,
Vec3<f32>,
Vec3<f32>,
Vec3<f32>,
f32,
);
type GlidingAnimationDependency = (Vec3<f32>, Quaternion<f32>, Quaternion<f32>, 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::<f32>::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<f32> = 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
}

View File

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

View File

@ -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::<f32>::unit_y(),
state.last_ori * anim::vek::Vec3::<f32>::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::<f32>::unit_y(),
state.last_ori * anim::vek::Vec3::<f32>::unit_y(),
time,
),
(),
state.state_time,
&mut state_animation_rate,
skeleton_attr,