Basic fluid dynamics and physical properties for entities

This commit is contained in:
Ludvig Böklin 2021-03-23 10:51:53 +01:00
parent bcf9a8089f
commit 762c68cfbb
51 changed files with 1121 additions and 574 deletions

View File

@ -38,7 +38,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sort inventory button - Sort inventory button
- Option to change the master volume when window is unfocused - Option to change the master volume when window is unfocused
- Crafting stations in towns - Crafting stations in towns
- Option to change the master volume when window is unfocused
- Entities now have mass
- Entities now have density
- Buoyancy is calculated from the difference in density between an entity and surrounding fluid
- Drag is now calculated based on physical properties
### Changed ### Changed
@ -64,10 +68,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Certain uses of client-authoritative physics now subject the player to server-authoritative physics. - Certain uses of client-authoritative physics now subject the player to server-authoritative physics.
- Dodge roll iframes and staff explosion are now unlocked by default, with points refunded for existing characters. - Dodge roll iframes and staff explosion are now unlocked by default, with points refunded for existing characters.
- Dash melee now stops after hitting something. Infinite dash also now replaced with dash through. - Dash melee now stops after hitting something. Infinite dash also now replaced with dash through.
- Collisions, knockbacks, jumping and drag are now physical forces applied to the entity's body mass
- Turning rate has been made more consistent across angles
- Gravity has been lowered so that physics can work more reasonably
- Jump has been decreased in height but extended in length as a result of the new gravity
- Fall damage has been adjusted with the new gravity in mind
- Projectiles now generally have a different arc because they no longer have their own gravity modifier
### Removed ### Removed
- Removed command: "debug", use "/kit debug" instead - Removed command: "debug", use "/kit debug" instead
- Gravity component has been removed
- In-air movement has been removed
### Fixed ### Fixed

View File

@ -9,6 +9,6 @@ LeapMelee(
knockback: 12.0, knockback: 12.0,
range: 4.5, range: 4.5,
max_angle: 30.0, max_angle: 30.0,
forward_leap_strength: 28.0, forward_leap_strength: 20.0,
vertical_leap_strength: 8.0, vertical_leap_strength: 8.0,
) )

View File

@ -9,6 +9,5 @@ BasicRanged(
), ),
projectile_body: Object(Arrow), projectile_body: Object(Arrow),
projectile_light: None, projectile_light: None,
projectile_gravity: Some(Gravity(0.2)), projectile_speed: 60.0,
projectile_speed: 100.0,
) )

View File

@ -11,8 +11,7 @@ ChargedRanged(
recover_duration: 0.5, recover_duration: 0.5,
projectile_body: Object(MultiArrow), projectile_body: Object(MultiArrow),
projectile_light: None, projectile_light: None,
projectile_gravity: Some(Gravity(0.2)), initial_projectile_speed: 60.0,
initial_projectile_speed: 100.0, scaled_projectile_speed: 90.0,
scaled_projectile_speed: 400.0,
move_speed: 0.3, move_speed: 0.3,
) )

View File

@ -12,7 +12,6 @@ RepeaterRanged(
), ),
projectile_body: Object(Arrow), projectile_body: Object(Arrow),
projectile_light: None, projectile_light: None,
projectile_gravity: Some(Gravity(0.2)), projectile_speed: 60.0,
projectile_speed: 100.0,
reps_remaining: 3, reps_remaining: 3,
) )

View File

@ -9,6 +9,5 @@ BasicRanged(
), ),
projectile_body: Object(Arrow), projectile_body: Object(Arrow),
projectile_light: None, projectile_light: None,
projectile_gravity: Some(Gravity(0.2)),
projectile_speed: 100.0, projectile_speed: 100.0,
) )

View File

@ -8,6 +8,5 @@ BasicRanged(
col: (0.0, 1.0, 0.33).into(), col: (0.0, 1.0, 0.33).into(),
..Default::default() ..Default::default()
}),*/ }),*/
projectile_gravity: None,
projectile_speed: 100.0, projectile_speed: 100.0,
) )

View File

@ -9,6 +9,6 @@ LeapMelee(
knockback: 25.0, knockback: 25.0,
range: 4.5, range: 4.5,
max_angle: 360.0, max_angle: 360.0,
forward_leap_strength: 24.0, forward_leap_strength: 20.0,
vertical_leap_strength: 8.0, vertical_leap_strength: 8.0,
) )

View File

@ -5,7 +5,7 @@ ComboMelee(
damage_increase: 10, damage_increase: 10,
base_poise_damage: 25, base_poise_damage: 25,
poise_damage_increase: 0, poise_damage_increase: 0,
knockback: 10.0, knockback: 5.0,
range: 4.5, range: 4.5,
angle: 50.0, angle: 50.0,
base_buildup_duration: 0.2, base_buildup_duration: 0.2,

View File

@ -12,6 +12,5 @@ BasicRanged(
col: (1.0, 0.75, 0.11).into(), col: (1.0, 0.75, 0.11).into(),
..Default::default() ..Default::default()
}),*/ }),*/
projectile_gravity: Some(Gravity(0.3)),
projectile_speed: 60.0, projectile_speed: 60.0,
) )

View File

@ -12,6 +12,5 @@ BasicRanged(
col: (1.0, 0.75, 0.11).into(), col: (1.0, 0.75, 0.11).into(),
..Default::default() ..Default::default()
}),*/ }),*/
projectile_gravity: Some(Gravity(0.3)),
projectile_speed: 60.0, projectile_speed: 60.0,
) )

View File

@ -6,7 +6,7 @@ ComboMelee(
damage_increase: 10, damage_increase: 10,
base_poise_damage: 10, base_poise_damage: 10,
poise_damage_increase: 0, poise_damage_increase: 0,
knockback: 10.0, knockback: 1.0,
range: 4.0, range: 4.0,
angle: 30.0, angle: 30.0,
base_buildup_duration: 0.15, base_buildup_duration: 0.15,
@ -20,7 +20,7 @@ ComboMelee(
damage_increase: 15, damage_increase: 15,
base_poise_damage: 13, base_poise_damage: 13,
poise_damage_increase: 0, poise_damage_increase: 0,
knockback: 12.0, knockback: 2.0,
range: 3.5, range: 3.5,
angle: 40.0, angle: 40.0,
base_buildup_duration: 0.1, base_buildup_duration: 0.1,
@ -34,7 +34,7 @@ ComboMelee(
damage_increase: 20, damage_increase: 20,
base_poise_damage: 15, base_poise_damage: 15,
poise_damage_increase: 0, poise_damage_increase: 0,
knockback: 14.0, knockback: 4.0,
range: 6.0, range: 6.0,
angle: 10.0, angle: 10.0,
base_buildup_duration: 0.15, base_buildup_duration: 0.15,

View File

@ -12,6 +12,5 @@ BasicRanged(
col: (1.0, 0.75, 0.11).into(), col: (1.0, 0.75, 0.11).into(),
..Default::default() ..Default::default()
}),*/ }),*/
projectile_gravity: Some(Gravity(5.0)),
projectile_speed: 70.0, projectile_speed: 70.0,
) )

View File

@ -9,6 +9,5 @@ BasicRanged(
), ),
projectile_body: Object(ArrowTurret), projectile_body: Object(ArrowTurret),
projectile_light: None, projectile_light: None,
projectile_gravity: Some(Gravity(0.1)), projectile_speed: 90.0,
projectile_speed: 100.0,
) )

View File

@ -11,6 +11,5 @@ BasicRanged(
col: (1.0, 0.75, 0.11).into(), col: (1.0, 0.75, 0.11).into(),
..Default::default() ..Default::default()
}),*/ }),*/
projectile_gravity: Some(Gravity(0.3)),
projectile_speed: 60.0, projectile_speed: 60.0,
) )

View File

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

View File

@ -29,8 +29,8 @@ sum_type! {
MountState(comp::MountState), MountState(comp::MountState),
Mounting(comp::Mounting), Mounting(comp::Mounting),
Mass(comp::Mass), Mass(comp::Mass),
Density(comp::Density),
Collider(comp::Collider), Collider(comp::Collider),
Gravity(comp::Gravity),
Sticky(comp::Sticky), Sticky(comp::Sticky),
CharacterState(comp::CharacterState), CharacterState(comp::CharacterState),
Pos(comp::Pos), Pos(comp::Pos),
@ -64,8 +64,8 @@ sum_type! {
MountState(PhantomData<comp::MountState>), MountState(PhantomData<comp::MountState>),
Mounting(PhantomData<comp::Mounting>), Mounting(PhantomData<comp::Mounting>),
Mass(PhantomData<comp::Mass>), Mass(PhantomData<comp::Mass>),
Density(PhantomData<comp::Density>),
Collider(PhantomData<comp::Collider>), Collider(PhantomData<comp::Collider>),
Gravity(PhantomData<comp::Gravity>),
Sticky(PhantomData<comp::Sticky>), Sticky(PhantomData<comp::Sticky>),
CharacterState(PhantomData<comp::CharacterState>), CharacterState(PhantomData<comp::CharacterState>),
Pos(PhantomData<comp::Pos>), Pos(PhantomData<comp::Pos>),
@ -99,8 +99,8 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Density(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => { EcsCompPacket::Pos(comp) => {
@ -138,8 +138,8 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Density(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => { EcsCompPacket::Pos(comp) => {
@ -179,8 +179,8 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world), EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world), EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world), EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
EcsCompPhantom::Density(_) => sync::handle_remove::<comp::Density>(entity, world),
EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world), EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world),
EcsCompPhantom::Gravity(_) => sync::handle_remove::<comp::Gravity>(entity, world),
EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world), EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world),
EcsCompPhantom::CharacterState(_) => { EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world) sync::handle_remove::<comp::CharacterState>(entity, world)

View File

@ -631,7 +631,8 @@ pub enum KnockbackDir {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
impl Knockback { impl Knockback {
pub fn calculate_impulse(self, dir: Dir) -> Vec3<f32> { pub fn calculate_impulse(self, dir: Dir) -> Vec3<f32> {
match self.direction { // TEMP until source knockback values have been updated
50.0 * match self.direction {
KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5), KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Towards => { KnockbackDir::Towards => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5) self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5)

View File

@ -3,7 +3,7 @@ use crate::{
combat::{self, CombatEffect, Knockback}, combat::{self, CombatEffect, Knockback},
comp::{ comp::{
aura, beam, inventory::item::tool::ToolKind, projectile::ProjectileConstructor, skills, aura, beam, inventory::item::tool::ToolKind, projectile::ProjectileConstructor, skills,
Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, Body, CharacterState, EnergySource, LightEmitter, StateUpdate,
}, },
states::{ states::{
behavior::JoinData, behavior::JoinData,
@ -74,7 +74,6 @@ pub enum CharacterAbility {
projectile: ProjectileConstructor, projectile: ProjectileConstructor,
projectile_body: Body, projectile_body: Body,
projectile_light: Option<LightEmitter>, projectile_light: Option<LightEmitter>,
projectile_gravity: Option<Gravity>,
projectile_speed: f32, projectile_speed: f32,
}, },
RepeaterRanged { RepeaterRanged {
@ -87,7 +86,6 @@ pub enum CharacterAbility {
projectile: ProjectileConstructor, projectile: ProjectileConstructor,
projectile_body: Body, projectile_body: Body,
projectile_light: Option<LightEmitter>, projectile_light: Option<LightEmitter>,
projectile_gravity: Option<Gravity>,
projectile_speed: f32, projectile_speed: f32,
reps_remaining: u32, reps_remaining: u32,
}, },
@ -197,7 +195,6 @@ pub enum CharacterAbility {
recover_duration: f32, recover_duration: f32,
projectile_body: Body, projectile_body: Body,
projectile_light: Option<LightEmitter>, projectile_light: Option<LightEmitter>,
projectile_gravity: Option<Gravity>,
initial_projectile_speed: f32, initial_projectile_speed: f32,
scaled_projectile_speed: f32, scaled_projectile_speed: f32,
move_speed: f32, move_speed: f32,
@ -1163,7 +1160,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
projectile, projectile,
projectile_body, projectile_body,
projectile_light, projectile_light,
projectile_gravity,
projectile_speed, projectile_speed,
energy_cost: _, energy_cost: _,
} => CharacterState::BasicRanged(basic_ranged::Data { } => CharacterState::BasicRanged(basic_ranged::Data {
@ -1173,7 +1169,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
projectile: *projectile, projectile: *projectile,
projectile_body: *projectile_body, projectile_body: *projectile_body,
projectile_light: *projectile_light, projectile_light: *projectile_light,
projectile_gravity: *projectile_gravity,
projectile_speed: *projectile_speed, projectile_speed: *projectile_speed,
ability_info, ability_info,
}, },
@ -1416,7 +1411,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
recover_duration, recover_duration,
projectile_body, projectile_body,
projectile_light, projectile_light,
projectile_gravity,
initial_projectile_speed, initial_projectile_speed,
scaled_projectile_speed, scaled_projectile_speed,
move_speed, move_speed,
@ -1433,7 +1427,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
scaled_knockback: *scaled_knockback, scaled_knockback: *scaled_knockback,
projectile_body: *projectile_body, projectile_body: *projectile_body,
projectile_light: *projectile_light, projectile_light: *projectile_light,
projectile_gravity: *projectile_gravity,
initial_projectile_speed: *initial_projectile_speed, initial_projectile_speed: *initial_projectile_speed,
scaled_projectile_speed: *scaled_projectile_speed, scaled_projectile_speed: *scaled_projectile_speed,
move_speed: *move_speed, move_speed: *move_speed,
@ -1453,7 +1446,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
projectile, projectile,
projectile_body, projectile_body,
projectile_light, projectile_light,
projectile_gravity,
projectile_speed, projectile_speed,
reps_remaining, reps_remaining,
} => CharacterState::RepeaterRanged(repeater_ranged::Data { } => CharacterState::RepeaterRanged(repeater_ranged::Data {
@ -1466,7 +1458,6 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
projectile: *projectile, projectile: *projectile,
projectile_body: *projectile_body, projectile_body: *projectile_body,
projectile_light: *projectile_light, projectile_light: *projectile_light,
projectile_gravity: *projectile_gravity,
projectile_speed: *projectile_speed, projectile_speed: *projectile_speed,
ability_info, ability_info,
}, },

View File

@ -16,6 +16,7 @@ pub mod theropod;
use crate::{ use crate::{
assets::{self, Asset}, assets::{self, Asset},
consts::{HUMAN_DENSITY, WATER_DENSITY},
make_case_elim, make_case_elim,
npc::NpcKind, npc::NpcKind,
}; };
@ -24,7 +25,7 @@ use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
use vek::*; use vek::*;
use super::BuffKind; use super::{BuffKind, Density, Mass};
make_case_elim!( make_case_elim!(
body, body,
@ -145,90 +146,152 @@ impl<
impl Body { impl Body {
pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) } pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) }
// Note: this might need to be refined to something more complex for realistic /// Average density of the body
// behavior with less cylindrical bodies (e.g. wolfs) // Units are based on kg/m³
#[allow(unreachable_patterns)] pub fn density(&self) -> Density {
pub fn radius(&self) -> f32 { let d = match self {
// TODO: Improve these values (some might be reliant on more info in inner type) // based on a house sparrow (Passer domesticus)
match self { Body::BirdMedium(_) => 700.0,
Body::Humanoid(humanoid) => match (humanoid.species, humanoid.body_type) { Body::BirdSmall(_) => 700.0,
(humanoid::Species::Orc, humanoid::BodyType::Male) => 0.75,
(humanoid::Species::Orc, humanoid::BodyType::Female) => 0.75, // based on its mass divided by the volume of a bird scaled up to the size of the dragon
(humanoid::Species::Human, humanoid::BodyType::Male) => 0.75, Body::Dragon(_) => 3_700.0,
(humanoid::Species::Human, humanoid::BodyType::Female) => 0.75,
(humanoid::Species::Elf, humanoid::BodyType::Male) => 0.75, Body::Golem(_) => WATER_DENSITY * 2.5,
(humanoid::Species::Elf, humanoid::BodyType::Female) => 0.75, Body::Humanoid(_) => HUMAN_DENSITY,
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 0.75, Body::Ship(ship) => ship.density().0,
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 0.75, Body::Object(object) => object.density().0,
(humanoid::Species::Undead, humanoid::BodyType::Male) => 0.75, _ => HUMAN_DENSITY,
(humanoid::Species::Undead, humanoid::BodyType::Female) => 0.75, };
(humanoid::Species::Danari, humanoid::BodyType::Male) => 0.75, Density(d)
(humanoid::Species::Danari, humanoid::BodyType::Female) => 0.75, }
_ => 0.75,
// Values marked with ~✅ are checked based on their RL equivalent.
// Discrepancy in size compared to their RL equivalents has not necessarily been
// taken into account.
pub fn mass(&self) -> Mass {
let m = match self {
Body::BipedLarge(body) => match body.species {
biped_large::Species::Slysaurok => 400.0,
biped_large::Species::Occultsaurok => 400.0,
biped_large::Species::Mightysaurok => 400.0,
biped_large::Species::Mindflayer => 420.0,
biped_large::Species::Minotaur => 500.0,
_ => 400.0,
}, },
Body::QuadrupedSmall(_) => 0.6, Body::BipedSmall(_) => 50.0,
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Grolgar => 2.0, // ravens are 0.69-2 kg, crows are 0.51 kg on average
quadruped_medium::Species::Tarasque => 2.0, Body::BirdMedium(_) => 1.0,
quadruped_medium::Species::Lion => 2.0, // australian magpies are around 0.22-0.35 kg
quadruped_medium::Species::Saber => 2.0, Body::BirdSmall(_) => 0.3,
quadruped_medium::Species::Catoblepas => 2.0,
quadruped_medium::Species::Horse => 1.5, Body::Dragon(_) => 20_000.0,
quadruped_medium::Species::Deer => 1.5, Body::FishMedium(_) => 2.5,
quadruped_medium::Species::Donkey => 1.5, Body::FishSmall(_) => 1.0,
quadruped_medium::Species::Kelpie => 1.5, Body::Golem(_) => 10_000.0,
quadruped_medium::Species::Barghest => 1.8, Body::Humanoid(humanoid) => {
quadruped_medium::Species::Cattle => 1.8, // humanoids are quite a bit larger than in real life, so we multiply their mass
quadruped_medium::Species::Highland => 1.8, // to scale it up proportionally (remember cube law)
quadruped_medium::Species::Yak => 1.8, 1.0 * match (humanoid.species, humanoid.body_type) {
quadruped_medium::Species::Panda => 1.8, (humanoid::Species::Orc, humanoid::BodyType::Male) => 120.0,
quadruped_medium::Species::Bear => 1.8, (humanoid::Species::Orc, humanoid::BodyType::Female) => 120.0,
_ => 1.5, (humanoid::Species::Human, humanoid::BodyType::Male) => 77.0, // ~✅
(humanoid::Species::Human, humanoid::BodyType::Female) => 59.0, // ~✅
(humanoid::Species::Elf, humanoid::BodyType::Male) => 77.0,
(humanoid::Species::Elf, humanoid::BodyType::Female) => 59.0,
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 70.0,
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 70.0,
(humanoid::Species::Undead, humanoid::BodyType::Male) => 70.0,
(humanoid::Species::Undead, humanoid::BodyType::Female) => 50.0,
(humanoid::Species::Danari, humanoid::BodyType::Male) => 80.0,
(humanoid::Species::Danari, humanoid::BodyType::Female) => 60.0,
}
}, },
Body::Object(obj) => obj.mass().0,
Body::QuadrupedLow(body) => match body.species { Body::QuadrupedLow(body) => match body.species {
quadruped_low::Species::Asp => 2.5, quadruped_low::Species::Alligator => 360.0, // ~✅
quadruped_low::Species::Monitor => 2.3, quadruped_low::Species::Asp => 300.0,
quadruped_low::Species::Crocodile => 2.4, // saltwater crocodiles can weigh around 1 ton, but our version is the size of an
quadruped_low::Species::Salamander => 2.4, // alligator or smaller, so whatever
quadruped_low::Species::Pangolin => 2.0, quadruped_low::Species::Crocodile => 360.0,
quadruped_low::Species::Lavadrake => 2.5, quadruped_low::Species::Deadwood => 400.0,
quadruped_low::Species::Deadwood => 0.5, quadruped_low::Species::Lavadrake => 500.0,
_ => 1.6, quadruped_low::Species::Monitor => 100.0,
quadruped_low::Species::Pangolin => 100.0,
quadruped_low::Species::Salamander => 65.0,
quadruped_low::Species::Tortoise => 200.0,
_ => 200.0,
},
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Bear => 500.0, // ~✅ (350-700 kg)
quadruped_medium::Species::Cattle => 575.0, // ~✅ (500-650 kg)
quadruped_medium::Species::Deer => 80.0,
quadruped_medium::Species::Donkey => 200.0,
quadruped_medium::Species::Highland => 200.0,
quadruped_medium::Species::Horse => 500.0, // ~✅
quadruped_medium::Species::Kelpie => 200.0,
quadruped_medium::Species::Lion => 170.0, // ~✅ (110-225 kg)
quadruped_medium::Species::Panda => 200.0,
quadruped_medium::Species::Saber => 130.0,
quadruped_medium::Species::Yak => 200.0,
_ => 200.0,
},
Body::QuadrupedSmall(body) => match body.species {
quadruped_small::Species::Batfox => 50.0,
quadruped_small::Species::Boar => 80.0, // ~✅ (60-100 kg)
quadruped_small::Species::Dodarock => 150.0,
quadruped_small::Species::Holladon => 150.0,
quadruped_small::Species::Hyena => 70.0, // ~✅ (vaguely)
quadruped_small::Species::Truffler => 150.0,
_ => 80.0,
}, },
Body::Theropod(body) => match body.species { Body::Theropod(body) => match body.species {
theropod::Species::Snowraptor => 1.5, // for reference, elephants are in the range of 2.6-6.9 tons
theropod::Species::Sandraptor => 1.5, // and Tyrannosaurus rex were ~8.4-14 tons
theropod::Species::Woodraptor => 1.5, theropod::Species::Archaeos => 13_000.0,
theropod::Species::Archaeos => 3.5, theropod::Species::Ntouka => 13_000.0,
theropod::Species::Odonto => 3.5, theropod::Species::Odonto => 13_000.0,
theropod::Species::Yale => 0.8,
theropod::Species::Ntouka => 3.0,
_ => 1.8,
},
Body::BirdMedium(_) => 1.0,
Body::FishMedium(_) => 1.0,
Body::Dragon(_) => 8.0,
Body::BirdSmall(_) => 0.6,
Body::FishSmall(_) => 0.6,
Body::BipedLarge(body) => match body.species {
biped_large::Species::Slysaurok => 2.0,
biped_large::Species::Occultsaurok => 2.0,
biped_large::Species::Mightysaurok => 2.0,
biped_large::Species::Mindflayer => 2.2,
biped_large::Species::Minotaur => 3.0,
_ => 2.3, theropod::Species::Sandraptor => 500.0,
theropod::Species::Snowraptor => 500.0,
theropod::Species::Sunlizard => 500.0,
theropod::Species::Woodraptor => 500.0,
theropod::Species::Yale => 1_000.0,
}, },
Body::Golem(_) => 2.5, Body::Ship(ship) => ship.mass().0,
Body::BipedSmall(_) => 0.75, };
Body::Object(_) => 0.4, Mass(m)
Body::Ship(_) => 1.0,
}
} }
pub fn height(&self) -> f32 { /// The width (shoulder to shoulder), length (nose to tail) and height
/// respectively
pub fn dimensions(&self) -> Vec3<f32> {
match self { match self {
Body::Humanoid(humanoid) => match (humanoid.species, humanoid.body_type) { Body::BipedLarge(body) => match body.species {
biped_large::Species::Cyclops => Vec3::new(4.6, 3.0, 6.5),
biped_large::Species::Dullahan => Vec3::new(4.6, 3.0, 5.5),
biped_large::Species::Mightysaurok => Vec3::new(4.0, 3.0, 3.4),
biped_large::Species::Mindflayer => Vec3::new(4.4, 3.0, 8.0),
biped_large::Species::Minotaur => Vec3::new(6.0, 3.0, 8.0),
biped_large::Species::Occultsaurok => Vec3::new(4.0, 3.0, 3.4),
biped_large::Species::Slysaurok => Vec3::new(4.0, 3.0, 3.4),
biped_large::Species::Werewolf => Vec3::new(4.0, 3.0, 3.5),
_ => Vec3::new(4.6, 3.0, 6.0),
},
Body::BipedSmall(_) => Vec3::new(1.0, 0.75, 1.4),
Body::BirdMedium(body) => match body.species {
bird_medium::Species::Cockatrice => Vec3::new(2.0, 1.0, 1.8),
_ => Vec3::new(2.0, 1.0, 1.1),
},
Body::BirdSmall(_) => Vec3::new(1.2, 0.6, 1.1),
Body::Dragon(_) => Vec3::new(16.0, 10.0, 16.0),
Body::FishMedium(_) => Vec3::new(0.5, 2.0, 0.8),
Body::FishSmall(_) => Vec3::new(0.3, 1.2, 0.6),
Body::Golem(_) => Vec3::new(5.0, 5.0, 5.0),
Body::Humanoid(humanoid) => {
let height = match (humanoid.species, humanoid.body_type) {
(humanoid::Species::Orc, humanoid::BodyType::Male) => 2.3, (humanoid::Species::Orc, humanoid::BodyType::Male) => 2.3,
(humanoid::Species::Orc, humanoid::BodyType::Female) => 2.2, (humanoid::Species::Orc, humanoid::BodyType::Female) => 2.2,
(humanoid::Species::Human, humanoid::BodyType::Male) => 2.3, (humanoid::Species::Human, humanoid::BodyType::Male) => 2.3,
@ -241,69 +304,70 @@ impl Body {
(humanoid::Species::Undead, humanoid::BodyType::Female) => 2.1, (humanoid::Species::Undead, humanoid::BodyType::Female) => 2.1,
(humanoid::Species::Danari, humanoid::BodyType::Male) => 1.5, (humanoid::Species::Danari, humanoid::BodyType::Male) => 1.5,
(humanoid::Species::Danari, humanoid::BodyType::Female) => 1.4, (humanoid::Species::Danari, humanoid::BodyType::Female) => 1.4,
};
Vec3::new(1.5, 0.5, height)
},
Body::Object(object) => object.dimensions(),
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Barghest => Vec3::new(2.0, 3.6, 2.5),
quadruped_medium::Species::Bear => Vec3::new(2.0, 3.6, 2.0),
quadruped_medium::Species::Catoblepas => Vec3::new(2.0, 4.0, 2.9),
quadruped_medium::Species::Cattle => Vec3::new(2.0, 3.6, 2.0),
quadruped_medium::Species::Deer => Vec3::new(2.0, 3.0, 2.0),
quadruped_medium::Species::Dreadhorn => Vec3::new(2.0, 3.0, 2.5),
quadruped_medium::Species::Grolgar => Vec3::new(2.0, 4.0, 2.0),
quadruped_medium::Species::Highland => Vec3::new(2.0, 3.6, 2.0),
quadruped_medium::Species::Horse => Vec3::new(2.0, 3.0, 2.0),
quadruped_medium::Species::Lion => Vec3::new(2.0, 3.0, 2.0),
quadruped_medium::Species::Moose => Vec3::new(2.0, 4.0, 2.5),
quadruped_medium::Species::Saber => Vec3::new(2.0, 4.0, 2.0),
quadruped_medium::Species::Tarasque => Vec3::new(2.0, 4.0, 2.6),
quadruped_medium::Species::Yak => Vec3::new(2.0, 3.6, 2.0),
_ => Vec3::new(2.0, 3.0, 2.0),
}, },
Body::QuadrupedSmall(body) => match body.species { Body::QuadrupedSmall(body) => match body.species {
quadruped_small::Species::Dodarock => 1.5, quadruped_small::Species::Dodarock => Vec3::new(1.2, 1.2, 1.5),
quadruped_small::Species::Holladon => 1.5, quadruped_small::Species::Holladon => Vec3::new(1.2, 1.2, 1.5),
quadruped_small::Species::Truffler => 2.0, quadruped_small::Species::Truffler => Vec3::new(1.2, 1.2, 2.0),
_ => 1.0, _ => Vec3::new(1.2, 1.2, 1.0),
},
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Tarasque => 2.6,
quadruped_medium::Species::Lion => 2.0,
quadruped_medium::Species::Saber => 2.0,
quadruped_medium::Species::Catoblepas => 2.9,
quadruped_medium::Species::Barghest => 2.5,
quadruped_medium::Species::Dreadhorn => 2.5,
quadruped_medium::Species::Moose => 2.5,
_ => 1.6,
}, },
Body::QuadrupedLow(body) => match body.species { Body::QuadrupedLow(body) => match body.species {
quadruped_low::Species::Monitor => 1.5, quadruped_low::Species::Asp => Vec3::new(1.0, 2.5, 1.3),
quadruped_low::Species::Tortoise => 2.0, quadruped_low::Species::Crocodile => Vec3::new(1.0, 2.4, 1.3),
quadruped_low::Species::Rocksnapper => 2.9, quadruped_low::Species::Deadwood => Vec3::new(1.0, 0.5, 1.3),
quadruped_low::Species::Maneater => 4.0, quadruped_low::Species::Lavadrake => Vec3::new(1.0, 2.5, 1.3),
_ => 1.3, quadruped_low::Species::Maneater => Vec3::new(1.0, 1.6, 4.0),
quadruped_low::Species::Monitor => Vec3::new(1.0, 2.3, 1.5),
quadruped_low::Species::Pangolin => Vec3::new(1.0, 2.0, 1.3),
quadruped_low::Species::Rocksnapper => Vec3::new(1.0, 1.6, 2.9),
quadruped_low::Species::Salamander => Vec3::new(1.0, 2.4, 1.3),
quadruped_low::Species::Tortoise => Vec3::new(1.0, 1.6, 2.0),
_ => Vec3::new(1.0, 1.6, 1.3),
}, },
Body::Ship(ship) => ship.dimensions(),
Body::Theropod(body) => match body.species { Body::Theropod(body) => match body.species {
theropod::Species::Snowraptor => 2.6, theropod::Species::Archaeos => Vec3::new(4.0, 7.0, 8.0),
theropod::Species::Sandraptor => 2.6, theropod::Species::Ntouka => Vec3::new(4.0, 6.0, 8.0),
theropod::Species::Woodraptor => 2.6, theropod::Species::Odonto => Vec3::new(4.0, 6.5, 8.0),
theropod::Species::Sunlizard => 2.5, theropod::Species::Sandraptor => Vec3::new(2.0, 3.0, 2.6),
theropod::Species::Yale => 3.0, theropod::Species::Snowraptor => Vec3::new(2.0, 3.0, 2.6),
_ => 8.0, theropod::Species::Sunlizard => Vec3::new(2.0, 3.6, 2.5),
theropod::Species::Woodraptor => Vec3::new(2.0, 3.0, 2.6),
theropod::Species::Yale => Vec3::new(1.5, 3.2, 6.0),
}, },
Body::BirdMedium(body) => match body.species { }
bird_medium::Species::Cockatrice => 1.8, }
_ => 1.1,
},
Body::FishMedium(_) => 0.8,
Body::Dragon(_) => 16.0,
Body::BirdSmall(_) => 1.1,
Body::FishSmall(_) => 0.6,
Body::BipedLarge(body) => match body.species {
biped_large::Species::Slysaurok => 3.4,
biped_large::Species::Occultsaurok => 3.4,
biped_large::Species::Mightysaurok => 3.4,
biped_large::Species::Mindflayer => 8.0,
biped_large::Species::Minotaur => 8.0,
biped_large::Species::Dullahan => 5.5,
biped_large::Species::Cyclops => 6.5,
biped_large::Species::Werewolf => 3.5,
_ => 6.0, // Note: This is used for collisions, but it's not very accurate for shapes that
}, // are very much not cylindrical. Eventually this ought to be replaced by more
Body::Golem(_) => 5.0, // accurate collision shapes.
Body::BipedSmall(_) => 1.4, pub fn radius(&self) -> f32 {
Body::Object(object) => match object { let dim = self.dimensions();
object::Body::Crossbow => 1.7, dim.x.max(dim.y) / 2.0
object::Body::TrainingDummy => 2.2,
_ => 1.0,
},
Body::Ship(_) => 1.0,
}
} }
pub fn height(&self) -> f32 { self.dimensions().z }
pub fn base_energy(&self) -> u32 { pub fn base_energy(&self) -> u32 {
match self { match self {
Body::BipedLarge(biped_large) => match biped_large.species { Body::BipedLarge(biped_large) => match biped_large.species {

View File

@ -1,6 +1,11 @@
use crate::{comp::item::Reagent, make_case_elim}; use crate::{
comp::{item::Reagent, Density, Mass},
consts::{IRON_DENSITY, WATER_DENSITY},
make_case_elim,
};
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vek::Vec3;
make_case_elim!( make_case_elim!(
body, body,
@ -241,4 +246,43 @@ impl Body {
Reagent::Yellow => Body::FireworkYellow, Reagent::Yellow => Body::FireworkYellow,
} }
} }
pub fn density(&self) -> Density {
let density = match self {
Body::Anvil | Body::Cauldron => IRON_DENSITY,
Body::Arrow | Body::MultiArrow => 500.0,
Body::Bomb => 2000.0, // I have no idea what it's supposed to be
Body::Crate => 300.0, // let's say it's a lot of wood and maybe some contents
Body::Scarecrow => 900.0,
Body::TrainingDummy => 2000.0,
// let them sink
_ => 1.1 * WATER_DENSITY,
};
Density(density)
}
pub fn mass(&self) -> Mass {
let m = match self {
// I think MultiArrow is one of several arrows, not several arrows combined?
Body::Arrow | Body::MultiArrow => 0.003,
Body::Bomb => {
0.5 * IRON_DENSITY * std::f32::consts::PI / 6.0 * self.dimensions().x.powi(3)
},
Body::Scarecrow => 50.0,
Body::Cauldron => 5.0,
Body::TrainingDummy => 60.0,
_ => 1.0,
};
Mass(m)
}
pub fn dimensions(&self) -> Vec3<f32> {
match self {
Body::Arrow | Body::ArrowSnake | Body::MultiArrow => Vec3::new(0.01, 0.8, 0.01),
Body::BoltFire => Vec3::new(0.1, 0.1, 0.1),
_ => Vec3::broadcast(0.2),
}
}
} }

View File

@ -1,5 +1,10 @@
use crate::make_case_elim; use crate::{
comp::{Density, Mass},
consts::AIR_DENSITY,
make_case_elim,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vek::Vec3;
make_case_elim!( make_case_elim!(
body, body,
@ -20,6 +25,33 @@ impl Body {
Body::DefaultAirship => "Human_Airship", Body::DefaultAirship => "Human_Airship",
} }
} }
pub fn dimensions(&self) -> Vec3<f32> { Vec3::new(25.0, 50.0, 40.0) }
fn balloon_vol(&self) -> f32 {
let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 {
(std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d
};
let dim = self.dimensions();
spheroid_vol(dim.z, dim.y)
}
fn hull_vol(&self) -> f32 {
// height from bottom of keel to deck
let deck_height = 10_f32;
let dim = self.dimensions();
(std::f32::consts::PI / 6.0) * (deck_height * 1.5).powi(2) * dim.y
}
pub fn hull_density(&self) -> Density {
let oak_density = 600_f32;
let ratio = 0.1;
Density(ratio * oak_density + (1.0 - ratio) * AIR_DENSITY)
}
pub fn density(&self) -> Density { Density(AIR_DENSITY) }
pub fn mass(&self) -> Mass { Mass((self.hull_vol() + self.balloon_vol()) * self.density().0) }
} }
/// Terrain is 11.0 scale relative to small-scale voxels, and all figures get /// Terrain is 11.0 scale relative to small-scale voxels, and all figures get

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
combat::Attack, combat::Attack,
comp::{tool::ToolKind, Energy, InputAttr, InputKind, Ori, Pos, Vel}, comp::{tool::ToolKind, Density, Energy, InputAttr, InputKind, Ori, Pos, Vel},
event::{LocalEvent, ServerEvent}, event::{LocalEvent, ServerEvent},
states::{behavior::JoinData, *}, states::{behavior::JoinData, *},
}; };
@ -16,6 +16,7 @@ pub struct StateUpdate {
pub pos: Pos, pub pos: Pos,
pub vel: Vel, pub vel: Vel,
pub ori: Ori, pub ori: Ori,
pub density: Density,
pub energy: Energy, pub energy: Energy,
pub swap_equipped_weapons: bool, pub swap_equipped_weapons: bool,
pub queued_inputs: BTreeMap<InputKind, InputAttr>, pub queued_inputs: BTreeMap<InputKind, InputAttr>,
@ -30,6 +31,7 @@ impl From<&JoinData<'_>> for StateUpdate {
pos: *data.pos, pos: *data.pos,
vel: *data.vel, vel: *data.vel,
ori: *data.ori, ori: *data.ori,
density: *data.density,
energy: *data.energy, energy: *data.energy,
swap_equipped_weapons: false, swap_equipped_weapons: false,
character: data.character.clone(), character: data.character.clone(),

View File

@ -0,0 +1,204 @@
use super::{
body::{object, Body},
Density, Vel,
};
use crate::{
consts::{AIR_DENSITY, WATER_DENSITY},
util::Dir,
};
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
use vek::*;
/// Fluid medium in which the entity exists
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Fluid {
Air { vel: Vel, elevation: f32 },
Water { vel: Vel, depth: f32 },
}
impl Fluid {
/// Specific mass
pub fn density(&self) -> Density {
match self {
Self::Air { .. } => Density(AIR_DENSITY),
Self::Water { .. } => Density(WATER_DENSITY),
}
}
/// Pressure from entity velocity
pub fn dynamic_pressure(&self, vel: &Vel) -> f32 {
0.5 * self.density().0 * self.relative_flow(vel).0.magnitude_squared()
}
/*
pub fn static_pressure(&self) -> f32 {
match self {
Self::Air { elevation, .. } => Self::air_pressure(*elevation),
Self::Water { depth, .. } => Self::water_pressure(*depth),
}
}
/// Absolute static pressure of air at elevation
pub fn air_pressure(elevation: f32) -> f32 {
// At low altitudes above sea level, the pressure decreases by about 1.2 kPa for
// every 100 metres.
// https://en.wikipedia.org/wiki/Atmospheric_pressure#Altitude_variation
ATMOSPHERE - elevation / 12.0
}
/// Absolute static pressure of water at depth
pub fn water_pressure(depth: f32) -> f32 { WATER_DENSITY * GRAVITY * depth + ATMOSPHERE }
*/
/// Velocity of fluid, if applicable
pub fn flow_vel(&self) -> Vel {
match self {
Self::Air { vel, .. } => *vel,
Self::Water { vel, .. } => *vel,
}
}
// Very simple but useful in reducing mental overhead
pub fn relative_flow(&self, vel: &Vel) -> Vel { Vel(self.flow_vel().0 - vel.0) }
pub fn is_liquid(&self) -> bool { matches!(self, Fluid::Water { .. }) }
pub fn elevation(&self) -> Option<f32> {
match self {
Fluid::Air { elevation, .. } => Some(*elevation),
_ => None,
}
}
pub fn depth(&self) -> Option<f32> {
match self {
Fluid::Water { depth, .. } => Some(*depth),
_ => None,
}
}
}
impl Default for Fluid {
fn default() -> Self {
Self::Air {
elevation: 0.0,
vel: Vel::zero(),
}
}
}
impl Body {
pub fn aerodynamic_forces(&self, rel_flow: &Vel, fluid_density: f32) -> Vec3<f32> {
let v_sq = rel_flow.0.magnitude_squared();
if v_sq < 0.25 {
// don't bother with miniscule forces
Vec3::zero()
} 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
}
}
/// Parasite drag is the sum of pressure drag and skin friction.
/// Skin friction is the drag arising from the shear forces between a fluid
/// and a surface, while pressure drag is due to flow separation. Both are
/// viscous effects.
fn parasite_drag_coefficient(&self) -> f32 {
// Reference area and drag coefficient assumes best-case scenario of the
// orientation producing least amount of drag
match self {
// Cross-section, head/feet first
Body::BipedLarge(_) | Body::BipedSmall(_) | Body::Golem(_) | Body::Humanoid(_) => {
let dim = self.dimensions().xy().map(|a| a * 0.5);
0.7 * PI * dim.x * dim.y
},
// Cross-section, nose/tail first
Body::Theropod(_)
| Body::QuadrupedMedium(_)
| Body::QuadrupedSmall(_)
| Body::QuadrupedLow(_) => {
let dim = self.dimensions().map(|a| a * 0.5);
let cd = if matches!(self, Body::QuadrupedLow(_)) {
0.7
} else {
1.0
};
cd * std::f32::consts::PI * dim.x * dim.z
},
// Cross-section, zero-lift angle; exclude the wings (width * 0.2)
Body::BirdMedium(_) | Body::BirdSmall(_) | Body::Dragon(_) => {
let dim = self.dimensions().map(|a| a * 0.5);
let cd = match self {
Body::BirdMedium(_) => 0.2,
Body::BirdSmall(_) => 0.4,
_ => 0.7,
};
cd * std::f32::consts::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
},
Body::Object(object) => match object {
// very streamlined objects
object::Body::Arrow
| object::Body::ArrowSnake
| object::Body::FireworkBlue
| object::Body::FireworkGreen
| object::Body::FireworkPurple
| object::Body::FireworkRed
| object::Body::FireworkWhite
| 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
},
// spherical-ish objects
object::Body::BoltFire
| object::Body::BoltFireBig
| object::Body::BoltNature
| object::Body::Bomb
| object::Body::PotionBlue
| object::Body::PotionGreen
| object::Body::PotionRed
| object::Body::Pouch
| object::Body::Pumpkin
| object::Body::Pumpkin2
| object::Body::Pumpkin3
| 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
},
_ => {
let dim = self.dimensions();
2.0 * (std::f32::consts::PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
},
},
Body::Ship(_) => {
// 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)
},
}
}
}
/*
## References:
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
*/

View File

@ -14,6 +14,7 @@ pub mod compass;
mod controller; mod controller;
pub mod dialogue; pub mod dialogue;
#[cfg(not(target_arch = "wasm32"))] mod energy; #[cfg(not(target_arch = "wasm32"))] mod energy;
pub mod fluid_dynamics;
#[cfg(not(target_arch = "wasm32"))] pub mod group; #[cfg(not(target_arch = "wasm32"))] pub mod group;
mod health; mod health;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -67,6 +68,7 @@ pub use self::{
InputKind, InventoryAction, InventoryEvent, InventoryManip, MountState, Mounting, InputKind, InventoryAction, InventoryEvent, InventoryManip, MountState, Mounting,
}, },
energy::{Energy, EnergyChange, EnergySource}, energy::{Energy, EnergyChange, EnergySource},
fluid_dynamics::Fluid,
group::Group, group::Group,
home_chunk::HomeChunk, home_chunk::HomeChunk,
inputs::CanBuild, inputs::CanBuild,
@ -79,7 +81,7 @@ pub use self::{
misc::Object, misc::Object,
ori::Ori, ori::Ori,
phys::{ phys::{
Collider, ForceUpdate, Gravity, Mass, PhysicsState, Pos, PosVelDefer, PreviousPhysCache, Collider, Density, ForceUpdate, Mass, PhysicsState, Pos, PosVelDefer, PreviousPhysCache,
Scale, Sticky, Vel, Scale, Sticky, Vel,
}, },
player::Player, player::Player,

View File

@ -148,6 +148,10 @@ impl Ori {
self.to_quat() * local 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 pitched_up(self, angle_radians: f32) -> Self { pub fn pitched_up(self, angle_radians: f32) -> Self {
self.rotated(Quaternion::rotation_x(angle_radians)) self.rotated(Quaternion::rotation_x(angle_radians))
} }
@ -252,7 +256,7 @@ impl From<Ori> for vek::vec::repr_simd::Vec3<f32> {
} }
impl From<Ori> for Vec2<f32> { impl From<Ori> for Vec2<f32> {
fn from(ori: Ori) -> Self { ori.look_vec().xy() } fn from(ori: Ori) -> Self { ori.look_dir().to_horizontal().unwrap_or_default().xy() }
} }
impl From<Ori> for vek::vec::repr_simd::Vec2<f32> { impl From<Ori> for vek::vec::repr_simd::Vec2<f32> {

View File

@ -1,4 +1,5 @@
use crate::uid::Uid; use super::Fluid;
use crate::{consts::WATER_DENSITY, uid::Uid};
use hashbrown::HashSet; use hashbrown::HashSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage, NullStorage}; use specs::{Component, DerefFlaggedStorage, NullStorage};
@ -19,6 +20,10 @@ impl Component for Pos {
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Vel(pub Vec3<f32>); pub struct Vel(pub Vec3<f32>);
impl Vel {
pub fn zero() -> Self { Vel(Vec3::zero()) }
}
impl Component for Vel { impl Component for Vel {
// TODO: why not regular vec storage???? // TODO: why not regular vec storage????
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
@ -66,13 +71,30 @@ impl Component for Scale {
} }
// Mass // Mass
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Mass(pub f32); pub struct Mass(pub f32);
impl Default for Mass {
fn default() -> Mass { Mass(1.0) }
}
impl Component for Mass { impl Component for Mass {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
/// The average density (specific mass) of an entity.
/// Units used for reference is kg/m³
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Density(pub f32);
impl Default for Density {
fn default() -> Density { Density(WATER_DENSITY) }
}
impl Component for Density {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
// Collider // Collider
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Collider { pub enum Collider {
@ -92,6 +114,11 @@ impl Collider {
} }
} }
pub fn get_height(&self) -> f32 {
let (z_min, z_max) = self.get_z_limits(1.0);
z_max - z_min
}
pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) {
match self { match self {
Collider::Voxel { .. } => (0.0, 1.0), Collider::Voxel { .. } => (0.0, 1.0),
@ -105,13 +132,6 @@ impl Component for Collider {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Gravity(pub f32);
impl Component for Gravity {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Sticky; pub struct Sticky;
@ -126,7 +146,7 @@ pub struct PhysicsState {
pub on_ceiling: bool, pub on_ceiling: bool,
pub on_wall: Option<Vec3<f32>>, pub on_wall: Option<Vec3<f32>>,
pub touch_entities: HashSet<Uid>, pub touch_entities: HashSet<Uid>,
pub in_liquid: Option<f32>, // Depth pub in_fluid: Option<Fluid>,
pub ground_vel: Vec3<f32>, pub ground_vel: Vec3<f32>,
} }
@ -149,6 +169,8 @@ impl PhysicsState {
.or_else(|| self.on_ceiling.then_some(Vec3::unit_z())) .or_else(|| self.on_ceiling.then_some(Vec3::unit_z()))
.or(self.on_wall) .or(self.on_wall)
} }
pub fn in_liquid(&self) -> Option<f32> { self.in_fluid.and_then(|fluid| fluid.depth()) }
} }
impl Component for PhysicsState { impl Component for PhysicsState {

View File

@ -2,5 +2,17 @@
pub const MAX_PICKUP_RANGE: f32 = 8.0; pub const MAX_PICKUP_RANGE: f32 = 8.0;
pub const MAX_MOUNT_RANGE: f32 = 14.0; pub const MAX_MOUNT_RANGE: f32 = 14.0;
pub const GRAVITY: f32 = 9.81 * 5.0; pub const GRAVITY: f32 = 25.0;
pub const FRIC_GROUND: f32 = 0.15; pub const FRIC_GROUND: f32 = 0.15;
// Values for air taken from http://www-mdp.eng.cam.ac.uk/web/library/enginfo/aerothermal_dvd_only/aero/atmos/atmos.html
// Values below are for dry air at 15°C, sea level, 1 standard atmosphere
// pub const ATMOSPHERE: f32 = 101325.0; // Pa
// kg/m³
pub const AIR_DENSITY: f32 = 1.225;
pub const WATER_DENSITY: f32 = 999.1026;
pub const IRON_DENSITY: f32 = 7870.0;
// pub const HUMAN_DENSITY: f32 = 1010.0; // real value
pub const HUMAN_DENSITY: f32 = 990.0; // value we use to make humanoids gently float

View File

@ -63,7 +63,6 @@ pub enum ServerEvent {
body: comp::Body, body: comp::Body,
light: Option<comp::LightEmitter>, light: Option<comp::LightEmitter>,
projectile: comp::Projectile, projectile: comp::Projectile,
gravity: Option<comp::Gravity>,
speed: f32, speed: f32,
object: Option<comp::Object>, object: Option<comp::Object>,
}, },

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
comp::{Body, CharacterState, Gravity, LightEmitter, ProjectileConstructor, StateUpdate}, comp::{Body, CharacterState, LightEmitter, ProjectileConstructor, StateUpdate},
event::ServerEvent, event::ServerEvent,
states::{ states::{
behavior::{CharacterBehavior, JoinData}, behavior::{CharacterBehavior, JoinData},
@ -20,7 +20,6 @@ pub struct StaticData {
pub projectile: ProjectileConstructor, pub projectile: ProjectileConstructor,
pub projectile_body: Body, pub projectile_body: Body,
pub projectile_light: Option<LightEmitter>, pub projectile_light: Option<LightEmitter>,
pub projectile_gravity: Option<Gravity>,
pub projectile_speed: f32, pub projectile_speed: f32,
/// What key is used to press ability /// What key is used to press ability
pub ability_info: AbilityInfo, pub ability_info: AbilityInfo,
@ -82,7 +81,6 @@ impl CharacterBehavior for Data {
body: self.static_data.projectile_body, body: self.static_data.projectile_body,
projectile, projectile,
light: self.static_data.projectile_light, light: self.static_data.projectile_light,
gravity: self.static_data.projectile_gravity,
speed: self.static_data.projectile_speed, speed: self.static_data.projectile_speed,
object: None, object: None,
}); });

View File

@ -1,8 +1,8 @@
use crate::{ use crate::{
comp::{ comp::{
self, item::MaterialStatManifest, Beam, Body, CharacterState, Combo, ControlAction, self, item::MaterialStatManifest, Beam, Body, CharacterState, Combo, ControlAction,
Controller, ControllerInputs, Energy, Health, InputAttr, InputKind, Inventory, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
InventoryAction, Melee, Ori, PhysicsState, Pos, SkillSet, StateUpdate, Stats, Vel, InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, StateUpdate, Stats, Vel,
}, },
resources::DeltaTime, resources::DeltaTime,
uid::Uid, uid::Uid,
@ -80,6 +80,8 @@ pub struct JoinData<'a> {
pub pos: &'a Pos, pub pos: &'a Pos,
pub vel: &'a Vel, pub vel: &'a Vel,
pub ori: &'a Ori, pub ori: &'a Ori,
pub mass: &'a Mass,
pub density: &'a Density,
pub dt: &'a DeltaTime, pub dt: &'a DeltaTime,
pub controller: &'a Controller, pub controller: &'a Controller,
pub inputs: &'a ControllerInputs, pub inputs: &'a ControllerInputs,
@ -113,6 +115,8 @@ pub struct JoinStruct<'a> {
pub pos: &'a mut Pos, pub pos: &'a mut Pos,
pub vel: &'a mut Vel, pub vel: &'a mut Vel,
pub ori: &'a mut Ori, pub ori: &'a mut Ori,
pub mass: &'a Mass,
pub density: &'a mut Density,
pub energy: RestrictedMut<'a, Energy>, pub energy: RestrictedMut<'a, Energy>,
pub inventory: RestrictedMut<'a, Inventory>, pub inventory: RestrictedMut<'a, Inventory>,
pub controller: &'a mut Controller, pub controller: &'a mut Controller,
@ -141,6 +145,8 @@ impl<'a> JoinData<'a> {
pos: j.pos, pos: j.pos,
vel: j.vel, vel: j.vel,
ori: j.ori, ori: j.ori,
mass: j.mass,
density: j.density,
energy: j.energy.get_unchecked(), energy: j.energy.get_unchecked(),
inventory: j.inventory.get_unchecked(), inventory: j.inventory.get_unchecked(),
controller: j.controller, controller: j.controller,

View File

@ -4,8 +4,8 @@ use crate::{
DamageSource, GroupTarget, Knockback, KnockbackDir, DamageSource, GroupTarget, Knockback, KnockbackDir,
}, },
comp::{ comp::{
projectile, Body, CharacterState, EnergyChange, EnergySource, Gravity, LightEmitter, projectile, Body, CharacterState, EnergyChange, EnergySource, LightEmitter, Projectile,
Projectile, StateUpdate, StateUpdate,
}, },
event::ServerEvent, event::ServerEvent,
states::{ states::{
@ -40,7 +40,6 @@ pub struct StaticData {
/// Projectile information /// Projectile information
pub projectile_body: Body, pub projectile_body: Body,
pub projectile_light: Option<LightEmitter>, pub projectile_light: Option<LightEmitter>,
pub projectile_gravity: Option<Gravity>,
pub initial_projectile_speed: f32, pub initial_projectile_speed: f32,
pub scaled_projectile_speed: f32, pub scaled_projectile_speed: f32,
/// Move speed efficiency /// Move speed efficiency
@ -138,7 +137,6 @@ impl CharacterBehavior for Data {
body: self.static_data.projectile_body, body: self.static_data.projectile_body,
projectile, projectile,
light: self.static_data.projectile_light, light: self.static_data.projectile_light,
gravity: self.static_data.projectile_gravity,
speed: self.static_data.initial_projectile_speed speed: self.static_data.initial_projectile_speed
+ charge_frac * self.static_data.scaled_projectile_speed, + charge_frac * self.static_data.scaled_projectile_speed,
object: None, object: None,

View File

@ -64,12 +64,15 @@ impl CharacterBehavior for Data {
) { ) {
(wall_dir, climb) (wall_dir, climb)
} else { } else {
if input_is_pressed(data, InputKind::Jump) { if let Some(impulse) = input_is_pressed(data, InputKind::Jump)
.then(|| data.body.jump_impulse())
.flatten()
{
// They've climbed atop something, give them a boost // They've climbed atop something, give them a boost
update update
.local_events .local_events
.push_front(LocalEvent::Jump(data.entity, BASE_JUMP_IMPULSE * 0.5)); .push_front(LocalEvent::Jump(data.entity, 0.5 * impulse / data.mass.0));
} };
update.character = CharacterState::Idle {}; update.character = CharacterState::Idle {};
return update; return update;
}; };

View File

@ -7,11 +7,11 @@ use crate::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vek::Vec2; use vek::Vec2;
// Gravity is 9.81 * 4, so this makes gravity equal to .15
const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90;
const GLIDE_ACCEL: f32 = 5.0; const GLIDE_ACCEL: f32 = 5.0;
const GLIDE_MAX_SPEED: f32 = 30.0;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data; pub struct Data;
impl CharacterBehavior for Data { impl CharacterBehavior for Data {
@ -25,7 +25,7 @@ impl CharacterBehavior for Data {
} }
if data if data
.physics .physics
.in_liquid .in_liquid()
.map(|depth| depth > 0.5) .map(|depth| depth > 0.5)
.unwrap_or(false) .unwrap_or(false)
{ {
@ -34,21 +34,21 @@ impl CharacterBehavior for Data {
if data.inventory.equipped(EquipSlot::Glider).is_none() { if data.inventory.equipped(EquipSlot::Glider).is_none() {
update.character = CharacterState::Idle update.character = CharacterState::Idle
}; };
// If there is a wall in front of character and they are trying to climb go to
// climb let horiz_vel = Vec2::<f32>::from(update.vel.0);
handle_climb(&data, &mut update); let horiz_speed_sq = horiz_vel.magnitude_squared();
// Move player according to movement direction vector // 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; update.vel.0 += Vec2::broadcast(data.dt.0) * data.inputs.move_dir * GLIDE_ACCEL;
}
// Determine orientation vector from movement direction vector // Determine orientation vector from movement direction vector
let horiz_vel = Vec2::<f32>::from(update.vel.0);
if let Some(dir) = Dir::from_unnormalized(update.vel.0) { if let Some(dir) = Dir::from_unnormalized(update.vel.0) {
update.ori = update.ori.slerped_towards(Ori::from(dir), 2.0 * data.dt.0); update.ori = update.ori.slerped_towards(Ori::from(dir), 2.0 * data.dt.0);
}; };
// Apply Glide antigrav lift // Apply Glide antigrav lift
let horiz_speed_sq = horiz_vel.magnitude_squared();
if update.vel.0.z < 0.0 { if update.vel.0.z < 0.0 {
let lift = (GLIDE_ANTIGRAV + update.vel.0.z.powi(2) * 0.15) 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); * (horiz_speed_sq * f32::powf(0.075, 2.0)).clamp(0.2, 1.0);
@ -56,6 +56,10 @@ impl CharacterBehavior for Data {
update.vel.0.z += lift * data.dt.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 update
} }

View File

@ -32,7 +32,7 @@ impl CharacterBehavior for Data {
} }
if data if data
.physics .physics
.in_liquid .in_liquid()
.map(|depth| depth > 0.5) .map(|depth| depth > 0.5)
.unwrap_or(false) .unwrap_or(false)
{ {

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
comp::{Body, CharacterState, Gravity, LightEmitter, ProjectileConstructor, StateUpdate}, comp::{Body, CharacterState, LightEmitter, ProjectileConstructor, StateUpdate},
event::ServerEvent, event::ServerEvent,
states::{ states::{
behavior::{CharacterBehavior, JoinData}, behavior::{CharacterBehavior, JoinData},
@ -28,7 +28,6 @@ pub struct StaticData {
pub projectile: ProjectileConstructor, pub projectile: ProjectileConstructor,
pub projectile_body: Body, pub projectile_body: Body,
pub projectile_light: Option<LightEmitter>, pub projectile_light: Option<LightEmitter>,
pub projectile_gravity: Option<Gravity>,
pub projectile_speed: f32, pub projectile_speed: f32,
/// What key is used to press ability /// What key is used to press ability
pub ability_info: AbilityInfo, pub ability_info: AbilityInfo,
@ -155,7 +154,6 @@ impl CharacterBehavior for Data {
body: self.static_data.projectile_body, body: self.static_data.projectile_body,
projectile, projectile,
light: self.static_data.projectile_light, light: self.static_data.projectile_light,
gravity: self.static_data.projectile_gravity,
speed: self.static_data.projectile_speed, speed: self.static_data.projectile_speed,
object: None, object: None,
}); });

View File

@ -5,8 +5,8 @@ use crate::{
item::{Hands, ItemKind, Tool, ToolKind}, item::{Hands, ItemKind, Tool, ToolKind},
quadruped_low, quadruped_medium, quadruped_small, ship, quadruped_low, quadruped_medium, quadruped_small, ship,
skills::{Skill, SwimSkill}, skills::{Skill, SwimSkill},
theropod, Body, CharacterAbility, CharacterState, InputAttr, InputKind, InventoryAction, theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind,
StateUpdate, InventoryAction, StateUpdate,
}, },
consts::{FRIC_GROUND, GRAVITY}, consts::{FRIC_GROUND, GRAVITY},
event::{LocalEvent, ServerEvent}, event::{LocalEvent, ServerEvent},
@ -18,23 +18,6 @@ use std::time::Duration;
use vek::*; use vek::*;
pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0; pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
const BASE_HUMANOID_AIR_ACCEL: f32 = 2.0;
const BASE_FLIGHT_ACCEL: f32 = 2.0;
const BASE_HUMANOID_WATER_ACCEL: f32 = 150.0;
const BASE_HUMANOID_WATER_SPEED: f32 = 180.0;
pub const BASE_JUMP_IMPULSE: f32 = 16.0;
// const BASE_HUMANOID_CLIMB_ACCEL: f32 = 10.0;
// const ROLL_SPEED: f32 = 17.0;
// const CHARGE_SPEED: f32 = 20.0;
// const GLIDE_ACCEL: f32 = 15.0;
// const GLIDE_SPEED: f32 = 45.0;
// const BLOCK_ACCEL: f32 = 30.0;
// const BLOCK_SPEED: f32 = 75.0;
// Gravity is 9.81 * 4, so this makes gravity equal to .15 //TODO: <- is wrong
//
// const GLIDE_ANTIGRAV: f32 = GRAVITY * 0.96;
// const CLIMB_SPEED: f32 = 5.0;
// const CLIMB_COST: i32 = 5;
impl Body { impl Body {
pub fn base_accel(&self) -> f32 { pub fn base_accel(&self) -> f32 {
@ -119,7 +102,7 @@ impl Body {
quadruped_low::Species::Basilisk => 120.0, quadruped_low::Species::Basilisk => 120.0,
quadruped_low::Species::Deadwood => 140.0, quadruped_low::Species::Deadwood => 140.0,
}, },
Body::Ship(_) => 30.0, Body::Ship(_) => 0.0,
} }
} }
@ -128,8 +111,7 @@ impl Body {
pub fn max_speed_approx(&self) -> f32 { pub fn max_speed_approx(&self) -> f32 {
// Inverse kinematics: at what velocity will acceleration // Inverse kinematics: at what velocity will acceleration
// be cancelled out by friction drag? // be cancelled out by friction drag?
// Note: we assume no air (this is fine, current physics // Note: we assume no air, since it's such a small factor.
// uses max(air_drag, ground_drag)).
// Derived via... // Derived via...
// v = (v + dv / 30) * (1 - drag).powi(2) (accel cancels drag) // v = (v + dv / 30) * (1 - drag).powi(2) (accel cancels drag)
// => 1 = (1 + (dv / 30) / v) * (1 - drag).powi(2) // => 1 = (1 + (dv / 30) / v) * (1 - drag).powi(2)
@ -142,53 +124,94 @@ impl Body {
v v
} }
/// The turn rate in 180°/s (or (rotations per second)/2)
pub fn base_ori_rate(&self) -> f32 { pub fn base_ori_rate(&self) -> f32 {
match self { match self {
Body::Humanoid(_) => 20.0, Body::Humanoid(_) => 4.0,
Body::QuadrupedSmall(_) => 15.0, Body::QuadrupedSmall(_) => 3.0,
Body::QuadrupedMedium(_) => 8.0, Body::QuadrupedMedium(_) => 1.6,
Body::BirdMedium(_) => 30.0, Body::BirdMedium(_) => 6.0,
Body::FishMedium(_) => 5.0, Body::FishMedium(_) => 6.0,
Body::Dragon(_) => 5.0, Body::Dragon(_) => 1.0,
Body::BirdSmall(_) => 35.0, Body::BirdSmall(_) => 7.0,
Body::FishSmall(_) => 10.0, Body::FishSmall(_) => 7.0,
Body::BipedLarge(_) => 8.0, Body::BipedLarge(_) => 1.6,
Body::BipedSmall(_) => 12.0, Body::BipedSmall(_) => 2.4,
Body::Object(_) => 10.0, Body::Object(_) => 2.0,
Body::Golem(_) => 8.0, Body::Golem(_) => 0.8,
Body::Theropod(theropod) => match theropod.species { Body::Theropod(theropod) => match theropod.species {
theropod::Species::Archaeos => 2.5, theropod::Species::Archaeos => 0.5,
theropod::Species::Odonto => 2.5, theropod::Species::Odonto => 0.5,
theropod::Species::Ntouka => 2.5, theropod::Species::Ntouka => 0.5,
_ => 7.0, _ => 1.4,
}, },
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species { Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
quadruped_low::Species::Monitor => 9.0, quadruped_low::Species::Monitor => 1.8,
quadruped_low::Species::Asp => 8.0, quadruped_low::Species::Asp => 1.6,
quadruped_low::Species::Tortoise => 3.0, quadruped_low::Species::Tortoise => 0.6,
quadruped_low::Species::Rocksnapper => 4.0, quadruped_low::Species::Rocksnapper => 0.8,
quadruped_low::Species::Maneater => 5.0, quadruped_low::Species::Maneater => 1.0,
quadruped_low::Species::Lavadrake => 4.0, quadruped_low::Species::Lavadrake => 0.8,
_ => 6.0, _ => 1.2,
}, },
Body::Ship(_) => 0.175, Body::Ship(_) => 0.035,
} }
} }
/// Returns flying speed if the body type can fly, otherwise None /// Returns thrust force if the body type can swim, otherwise None
pub fn can_fly(&self) -> Option<f32> { pub fn swim_thrust(&self) -> Option<f32> {
match self { match self {
Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) => Some(1.0), Body::Object(_) | Body::Ship(_) => None,
Body::Ship(ship::Body::DefaultAirship) => Some(1.0), Body::BipedLarge(_) | Body::Golem(_) => Some(200.0 * self.mass().0),
Body::BipedSmall(_) => Some(100.0 * self.mass().0),
Body::BirdMedium(_) => Some(50.0 * self.mass().0),
Body::BirdSmall(_) => Some(50.0 * self.mass().0),
Body::FishMedium(_) => Some(50.0 * self.mass().0),
Body::FishSmall(_) => Some(50.0 * self.mass().0),
Body::Dragon(_) => Some(200.0 * self.mass().0),
Body::Humanoid(_) => Some(200.0 * self.mass().0),
Body::Theropod(body) => match body.species {
theropod::Species::Sandraptor
| theropod::Species::Snowraptor
| theropod::Species::Sunlizard
| theropod::Species::Woodraptor
| theropod::Species::Yale => Some(200.0 * self.mass().0),
_ => Some(100.0 * self.mass().0),
},
Body::QuadrupedLow(_) => Some(300.0 * self.mass().0),
Body::QuadrupedMedium(_) => Some(300.0 * self.mass().0),
Body::QuadrupedSmall(_) => Some(300.0 * self.mass().0),
}
}
/// Returns thrust force if the body type can fly, otherwise None
pub fn fly_thrust(&self) -> Option<f32> {
match self {
Body::BirdMedium(_) => Some(GRAVITY * self.mass().0 * 2.0),
Body::BirdSmall(_) => Some(GRAVITY * self.mass().0 * 2.0),
Body::Dragon(_) => Some(200_000.0),
Body::Ship(ship::Body::DefaultAirship) => Some(300_000.0),
_ => None, _ => None,
} }
} }
/// Returns jump impulse if the body type can jump, otherwise None
pub fn jump_impulse(&self) -> Option<f32> { pub fn jump_impulse(&self) -> Option<f32> {
match self { match self {
Body::Object(_) | Body::Ship(_) => None, Body::Object(_) | Body::Ship(_) => None,
_ => Some(BASE_JUMP_IMPULSE), Body::BipedLarge(_) | Body::Dragon(_) | Body::Golem(_) | Body::QuadrupedLow(_) => {
Some(0.1 * self.mass().0)
},
Body::QuadrupedMedium(_) => Some(0.4 * self.mass().0),
Body::Theropod(body) => match body.species {
theropod::Species::Snowraptor
| theropod::Species::Sandraptor
| theropod::Species::Woodraptor => Some(0.4 * self.mass().0),
_ => None,
},
_ => Some(0.4 * self.mass().0),
} }
.map(|f| f * GRAVITY)
} }
pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) } pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
@ -196,21 +219,22 @@ impl Body {
/// Handles updating `Components` to move player based on state of `JoinData` /// Handles updating `Components` to move player based on state of `JoinData`
pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
if let Some(depth) = data.physics.in_liquid { let submersion = data
swim_move(data, update, efficiency, depth); .physics
} else if input_is_pressed(data, InputKind::Fly) .in_liquid()
.map(|depth| depth / data.body.height());
if input_is_pressed(data, InputKind::Fly)
&& submersion.map_or(true, |sub| sub < 1.0)
&& (!data.physics.on_ground || data.body.jump_impulse().is_none()) && (!data.physics.on_ground || data.body.jump_impulse().is_none())
&& data.body.can_fly().is_some() && data.body.fly_thrust().is_some()
{ {
fly_move( fly_move(data, update, efficiency);
data, } else if let Some(submersion) = (!data.physics.on_ground && data.body.swim_thrust().is_some())
update, .then_some(submersion)
efficiency .flatten()
* data {
.body swim_move(data, update, efficiency, submersion);
.can_fly()
.expect("can_fly is_some right above this"),
);
} else { } else {
basic_move(data, update, efficiency); basic_move(data, update, efficiency);
} }
@ -219,16 +243,23 @@ pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
/// Updates components to move player as if theyre on ground or in air /// Updates components to move player as if theyre on ground or in air
#[allow(clippy::assign_op_pattern)] // TODO: Pending review in #587 #[allow(clippy::assign_op_pattern)] // TODO: Pending review in #587
fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
let accel = if data.physics.on_ground { handle_orientation(data, update, efficiency);
data.body.base_accel()
if let Some(accel) = data
.physics
.on_ground
.then_some(data.body.base_accel() * efficiency)
{
// Should ability to backpedal be separate from ability to strafe?
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* if data.body.can_strafe() {
data.inputs.move_dir
} else { } else {
BASE_HUMANOID_AIR_ACCEL let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
}; };
}
update.vel.0 =
update.vel.0 + Vec2::broadcast(data.dt.0) * data.inputs.move_dir * accel * efficiency;
handle_orientation(data, update, data.body.base_ori_rate());
} }
/// Handles forced movement /// Handles forced movement
@ -238,17 +269,15 @@ pub fn handle_forced_movement(
movement: ForcedMovement, movement: ForcedMovement,
efficiency: f32, efficiency: f32,
) { ) {
handle_orientation(data, update, efficiency);
match movement { match movement {
ForcedMovement::Forward { strength } => { ForcedMovement::Forward { strength } => {
let accel = if data.physics.on_ground { if let Some(accel) = data.physics.on_ground.then_some(data.body.base_accel()) {
data.body.base_accel()
} else {
BASE_HUMANOID_AIR_ACCEL
};
update.vel.0 += Vec2::broadcast(data.dt.0) update.vel.0 += Vec2::broadcast(data.dt.0)
* accel * accel
* (data.inputs.move_dir * efficiency + Vec2::from(update.ori) * strength); * (data.inputs.move_dir * efficiency + Vec2::from(update.ori) * strength);
}
}, },
ForcedMovement::Leap { ForcedMovement::Leap {
vertical, vertical,
@ -279,75 +308,118 @@ pub fn handle_forced_movement(
+ move_input * data.inputs.move_dir.try_normalized().unwrap_or_default(); + move_input * data.inputs.move_dir.try_normalized().unwrap_or_default();
}, },
} }
handle_orientation(data, update, data.body.base_ori_rate() * efficiency);
} }
pub fn handle_orientation(data: &JoinData, update: &mut StateUpdate, rate: f32) { pub fn handle_orientation(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
// Set direction based on move direction let strafe_aim = update.character.is_aimed() && data.body.can_strafe();
let ori_dir = if (update.character.is_aimed() && data.body.can_strafe()) if let Some(dir) = (strafe_aim || update.character.is_attack())
|| update.character.is_attack() .then(|| data.inputs.look_dir.to_horizontal().unwrap_or_default())
.or_else(|| Dir::from_unnormalized(data.inputs.move_dir.into()))
{ {
data.inputs.look_dir.xy() let rate = {
} else if !data.inputs.move_dir.is_approx_zero() { let angle = update.ori.look_dir().angle_between(*dir);
data.inputs.move_dir data.body.base_ori_rate() * efficiency * std::f32::consts::PI / angle
} else { };
update.ori.into() update.ori = update
.ori
.slerped_towards(dir.into(), (data.dt.0 * rate).min(0.1));
}; };
// Smooth orientation
update.ori = Dir::slerp_to_vec3(update.ori.look_dir(), ori_dir.into(), rate * data.dt.0).into();
} }
/// Updates components to move player as if theyre swimming /// Updates components to move player as if theyre swimming
fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth: f32) { fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, submersion: f32) -> bool {
let mut water_accel = BASE_HUMANOID_WATER_ACCEL; if let Some(force) = data.body.swim_thrust() {
let mut water_speed = BASE_HUMANOID_WATER_SPEED; handle_orientation(data, update, efficiency * 0.2);
let force = efficiency * force;
let mut water_accel = force / data.mass.0;
if let Ok(Some(level)) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) { if let Ok(Some(level)) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
water_speed *= 1.4_f32.powi(level.into());
water_accel *= 1.4_f32.powi(level.into()); water_accel *= 1.4_f32.powi(level.into());
} }
// Update velocity let dir = if data.body.can_strafe() {
update.vel.0 += Vec2::broadcast(data.dt.0) data.inputs.move_dir
* data.inputs.move_dir
* if update.vel.0.magnitude_squared() < water_speed.powi(2) {
water_accel
} else { } else {
0.0 let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
};
// Autoswim to stay afloat
let move_z = if submersion < 1.0 && data.inputs.move_z.abs() < std::f32::EPSILON {
(submersion - 0.1).max(0.0)
} else {
data.inputs.move_z
};
update.vel.0 += Vec3::broadcast(data.dt.0)
* Vec3::new(dir.x, dir.y, move_z)
.try_normalized()
.unwrap_or_default()
* water_accel
* (submersion - 0.2).clamp(0.0, 1.0).powi(2);
true
} else {
false
} }
* efficiency;
handle_orientation(
data,
update,
data.body.base_ori_rate() * if data.physics.on_ground { 0.5 } else { 0.1 },
);
// Swim
update.vel.0.z = (update.vel.0.z
+ data.dt.0
* GRAVITY
* 4.0
* data
.inputs
.move_z
.clamped(-1.0, depth.clamped(0.0, 1.0).powi(3)))
.min(water_speed);
} }
/// Updates components to move entity as if it's flying /// Updates components to move entity as if it's flying
fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { pub fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) -> bool {
// Update velocity (counteract gravity with lift) if let Some(force) = data.body.fly_thrust() {
update.vel.0 += Vec3::unit_z() * data.dt.0 * GRAVITY let thrust = efficiency * force;
+ Vec3::new(
data.inputs.move_dir.x,
data.inputs.move_dir.y,
data.inputs.move_z,
) * data.dt.0
* BASE_FLIGHT_ACCEL
* efficiency;
handle_orientation(data, update, data.body.base_ori_rate()); let accel = thrust / data.mass.0;
handle_orientation(data, update, efficiency);
// Elevation control
match data.body {
// flappy flappy
Body::Dragon(_) | Body::BirdMedium(_) | Body::BirdSmall(_) => {
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));
},
// floaty floaty
Body::Ship(ship @ ship::Body::DefaultAirship) => {
let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
// Reset to default on no input
let change = if data.inputs.move_z.abs() > std::f32::EPSILON {
-data.inputs.move_z
} else {
(def - data.density.0).max(-1.0).min(1.0)
};
Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
};
let def_density = ship.density().0;
if data.physics.in_liquid().is_some() {
let hull_density = ship.hull_density().0;
update.density.0 =
regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
} else {
update.density.0 =
regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
};
},
// oopsie woopsie
// TODO: refactor to make this state impossible
_ => {},
};
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* if data.body.can_strafe() {
data.inputs.move_dir
} else {
let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
};
true
} else {
false
}
} }
/// Checks if an input related to an attack is held. If one is, moves entity /// Checks if an input related to an attack is held. If one is, moves entity
@ -408,7 +480,7 @@ pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) {
&& !data.physics.on_ground && !data.physics.on_ground
&& !data && !data
.physics .physics
.in_liquid .in_liquid()
.map(|depth| depth > 1.0) .map(|depth| depth > 1.0)
.unwrap_or(false) .unwrap_or(false)
//&& update.vel.0.z < 0.0 //&& update.vel.0.z < 0.0
@ -442,7 +514,7 @@ pub fn attempt_glide_wield(data: &JoinData, update: &mut StateUpdate) {
if data.inventory.equipped(EquipSlot::Glider).is_some() if data.inventory.equipped(EquipSlot::Glider).is_some()
&& !data && !data
.physics .physics
.in_liquid .in_liquid()
.map(|depth| depth > 1.0) .map(|depth| depth > 1.0)
.unwrap_or(false) .unwrap_or(false)
&& data.body.is_humanoid() && data.body.is_humanoid()
@ -453,23 +525,16 @@ pub fn attempt_glide_wield(data: &JoinData, update: &mut StateUpdate) {
/// Checks that player can jump and sends jump event if so /// Checks that player can jump and sends jump event if so
pub fn handle_jump(data: &JoinData, update: &mut StateUpdate, strength: f32) -> bool { pub fn handle_jump(data: &JoinData, update: &mut StateUpdate, strength: f32) -> bool {
if input_is_pressed(data, InputKind::Jump) (input_is_pressed(data, InputKind::Jump) && data.physics.on_ground)
&& data.physics.on_ground .then(|| data.body.jump_impulse())
&& !data .flatten()
.physics .map(|impulse| {
.in_liquid
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& data.body.jump_impulse().is_some()
{
update.local_events.push_front(LocalEvent::Jump( update.local_events.push_front(LocalEvent::Jump(
data.entity, data.entity,
data.body.jump_impulse().unwrap() * strength, strength * impulse / data.mass.0,
)); ));
true })
} else { .is_some()
false
}
} }
fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) { fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) {
@ -666,7 +731,12 @@ impl MovementDirection {
pub fn get_2d_dir(self, data: &JoinData) -> Vec2<f32> { pub fn get_2d_dir(self, data: &JoinData) -> Vec2<f32> {
use MovementDirection::*; use MovementDirection::*;
match self { match self {
Look => data.inputs.look_dir.xy(), Look => data
.inputs
.look_dir
.to_horizontal()
.unwrap_or_default()
.xy(),
Move => data.inputs.move_dir, Move => data.inputs.move_dir,
} }
.try_normalized() .try_normalized()

View File

@ -112,6 +112,8 @@ impl Dir {
pub fn back() -> Self { -Dir::new(Vec3::<f32>::unit_y()) } pub fn back() -> Self { -Dir::new(Vec3::<f32>::unit_y()) }
pub fn to_horizontal(self) -> Option<Self> { Self::from_unnormalized(self.xy().into()) }
pub fn vec(&self) -> &Vec3<f32> { &self.0 } pub fn vec(&self) -> &Vec3<f32> { &self.0 }
pub fn to_vec(self) -> Vec3<f32> { self.0 } pub fn to_vec(self) -> Vec3<f32> { self.0 }

View File

@ -199,9 +199,9 @@ impl State {
ecs.register::<comp::Mounting>(); ecs.register::<comp::Mounting>();
ecs.register::<comp::MountState>(); ecs.register::<comp::MountState>();
ecs.register::<comp::Mass>(); ecs.register::<comp::Mass>();
ecs.register::<comp::Density>();
ecs.register::<comp::Collider>(); ecs.register::<comp::Collider>();
ecs.register::<comp::Sticky>(); ecs.register::<comp::Sticky>();
ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>(); ecs.register::<comp::CharacterState>();
ecs.register::<comp::Object>(); ecs.register::<comp::Object>();
ecs.register::<comp::Group>(); ecs.register::<comp::Group>();

View File

@ -10,8 +10,9 @@ use common::{
item::MaterialStatManifest, item::MaterialStatManifest,
slot::{EquipSlot, Slot}, slot::{EquipSlot, Slot},
}, },
Beam, Body, CharacterState, Combo, Controller, Energy, Health, Inventory, Melee, Mounting, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, Inventory, Mass,
Ori, PhysicsState, Poise, PoiseState, Pos, SkillSet, StateUpdate, Stats, Vel, Melee, Mounting, Ori, PhysicsState, Poise, PoiseState, Pos, SkillSet, StateUpdate, Stats,
Vel,
}, },
event::{EventBus, LocalEvent, ServerEvent}, event::{EventBus, LocalEvent, ServerEvent},
resources::DeltaTime, resources::DeltaTime,
@ -32,6 +33,7 @@ fn incorporate_update(join: &mut JoinStruct, mut state_update: StateUpdate) {
*join.pos = state_update.pos; *join.pos = state_update.pos;
*join.vel = state_update.vel; *join.vel = state_update.vel;
*join.ori = state_update.ori; *join.ori = state_update.ori;
*join.density = state_update.density;
// Note: might be changed every tick by timer anyway // Note: might be changed every tick by timer anyway
if join.energy.get_unchecked() != &state_update.energy { if join.energy.get_unchecked() != &state_update.energy {
*join.energy.get_mut_unchecked() = state_update.energy *join.energy.get_mut_unchecked() = state_update.energy
@ -67,6 +69,7 @@ pub struct ReadData<'a> {
lazy_update: Read<'a, LazyUpdate>, lazy_update: Read<'a, LazyUpdate>,
healths: ReadStorage<'a, Health>, healths: ReadStorage<'a, Health>,
bodies: ReadStorage<'a, Body>, bodies: ReadStorage<'a, Body>,
masses: ReadStorage<'a, Mass>,
physics_states: ReadStorage<'a, PhysicsState>, physics_states: ReadStorage<'a, PhysicsState>,
melee_attacks: ReadStorage<'a, Melee>, melee_attacks: ReadStorage<'a, Melee>,
beams: ReadStorage<'a, Beam>, beams: ReadStorage<'a, Beam>,
@ -93,6 +96,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Pos>, WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>, WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>, WriteStorage<'a, Ori>,
WriteStorage<'a, Density>,
WriteStorage<'a, Energy>, WriteStorage<'a, Energy>,
WriteStorage<'a, Inventory>, WriteStorage<'a, Inventory>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
@ -112,6 +116,7 @@ impl<'a> System<'a> for Sys {
mut positions, mut positions,
mut velocities, mut velocities,
mut orientations, mut orientations,
mut densities,
mut energies, mut energies,
mut inventories, mut inventories,
mut controllers, mut controllers,
@ -128,14 +133,15 @@ impl<'a> System<'a> for Sys {
mut pos, mut pos,
mut vel, mut vel,
mut ori, mut ori,
mass,
mut density,
energy, energy,
inventory, inventory,
mut controller, mut controller,
health, health,
body, body,
physics, physics,
stat, (stat, skill_set),
skill_set,
combo, combo,
) in ( ) in (
&read_data.entities, &read_data.entities,
@ -144,14 +150,15 @@ impl<'a> System<'a> for Sys {
&mut positions, &mut positions,
&mut velocities, &mut velocities,
&mut orientations, &mut orientations,
&read_data.masses,
&mut densities,
&mut energies.restrict_mut(), &mut energies.restrict_mut(),
&mut inventories.restrict_mut(), &mut inventories.restrict_mut(),
&mut controllers, &mut controllers,
read_data.healths.maybe(), read_data.healths.maybe(),
&read_data.bodies, &read_data.bodies,
&read_data.physics_states, &read_data.physics_states,
&read_data.stats, (&read_data.stats, &read_data.skill_sets),
&read_data.skill_sets,
&read_data.combos, &read_data.combos,
) )
.join() .join()
@ -253,6 +260,8 @@ impl<'a> System<'a> for Sys {
pos: &mut pos, pos: &mut pos,
vel: &mut vel, vel: &mut vel,
ori: &mut ori, ori: &mut ori,
mass: &mass,
density: &mut density,
energy, energy,
inventory, inventory,
controller: &mut controller, controller: &mut controller,

View File

@ -5,15 +5,17 @@ use spatial_grid::SpatialGrid;
use common::{ use common::{
comp::{ comp::{
body::ship::figuredata::{VoxelCollider, VOXEL_COLLIDER_MANIFEST}, body::ship::figuredata::{VoxelCollider, VOXEL_COLLIDER_MANIFEST},
BeamSegment, Body, CharacterState, Collider, Gravity, Mass, Mounting, Ori, PhysicsState, BeamSegment, Body, CharacterState, Collider, Density, Fluid, Mass, Mounting, Ori,
Pos, PosVelDefer, PreviousPhysCache, Projectile, Scale, Shockwave, Sticky, Vel, PhysicsState, Pos, PosVelDefer, PreviousPhysCache, Projectile, Scale, Shockwave, Sticky,
Vel,
}, },
consts::{FRIC_GROUND, GRAVITY}, consts::{AIR_DENSITY, FRIC_GROUND, GRAVITY},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
outcome::Outcome, outcome::Outcome,
resources::DeltaTime, resources::DeltaTime,
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
uid::Uid, uid::Uid,
util::Projection,
vol::{BaseVol, ReadVol}, vol::{BaseVol, ReadVol},
}; };
use common_base::{prof_span, span}; use common_base::{prof_span, span};
@ -24,39 +26,84 @@ use specs::{
Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect, Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect,
WriteStorage, WriteStorage,
}; };
use std::ops::Range; use std::{f32::consts::PI, ops::Range};
use vek::*; use vek::*;
pub const BOUYANCY: f32 = 1.0; /// The density of the fluid as a function of submersion ratio in given fluid
// Friction values used for linear damping. They are unitless quantities. The /// where it is assumed that any unsubmersed part is is air.
// value of these quantities must be between zero and one. They represent the // This is a pretty silly way of doing it as it assumes everything is spherical
// amount an object will slow down within 1/60th of a second. Eg. if the // in shape and uniform in mass distribution, but the result feels good enough.
// friction is 0.01, and the speed is 1.0, then after 1/60th of a second the // TODO: Make the shape a capsule?
// speed will be 0.99. after 1 second the speed will be 0.54, which is 0.99 ^ fn fluid_density(height: f32, fluid: &Fluid) -> Density {
// 60. // If depth is less than our height (partial submersion), remove
pub const FRIC_AIR: f32 = 0.0025; // fluid density based on the ratio of displacement to full volume.
pub const FRIC_FLUID: f32 = 0.4; // The displacement is modelled as a gradually submersed sphere.
let immersion = fluid.depth().map_or(1.0, |depth| {
if height < depth {
1.0
} else {
let hemisphere_filled_vol = |r: f32, h: f32| -> f32 {
let r_ = (r.powi(2) - (r - h).powi(2)).sqrt();
(1.0 / 6.0) * PI * h * (3.0 * r_.powi(2) + h.powi(2))
};
let hemisphere_vol = |r: f32| -> f32 { (2.0 / 3.0) * PI * r.powi(3) };
let r = height / 2.0;
let sphere_vol = 2.0 * hemisphere_vol(r);
if depth < r {
hemisphere_filled_vol(r, depth) / sphere_vol
} else {
1.0 - hemisphere_filled_vol(r, height - depth) / sphere_vol
}
}
});
// Integrates forces, calculates the new velocity based off of the old velocity Density(fluid.density().0 * immersion + AIR_DENSITY * (1.0 - immersion))
// dt = delta time }
// lv = linear velocity
// damp = linear damping
// Friction is a type of damping.
fn integrate_forces(dt: f32, mut lv: Vec3<f32>, grav: f32, damp: f32) -> Vec3<f32> {
// Clamp dt to an effective 10 TPS, to prevent gravity from slamming the players
// into the floor when stationary if other systems cause the server to lag
// (as observed in the 0.9 release party).
let dt = dt.min(0.1);
// this is not linear damping, because it is proportional to the original #[allow(clippy::too_many_arguments)]
// velocity this "linear" damping in in fact, quite exponential. and thus fn integrate_forces(
// must be interpolated accordingly dt: &DeltaTime,
let linear_damp = (1.0 - damp.min(1.0)).powf(dt * 60.0); mut vel: Vel,
body: &Body,
density: &Density,
mass: &Mass,
fluid: &Fluid,
gravity: f32,
) -> Vel {
let dim = body.dimensions();
let height = dim.z;
let rel_flow = fluid.relative_flow(&vel);
let fluid_density = fluid_density(height, fluid);
debug_assert!(mass.0 > 0.0);
debug_assert!(density.0 > 0.0);
// TODO: investigate if we can have air friction provide the neccessary limits // Aerodynamic/hydrodynamic forces
// here if !rel_flow.0.is_approx_zero() {
lv.z = (lv.z - grav * dt).max(-80.0).min(lv.z); debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or());
lv * linear_damp let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0);
debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or());
if !impulse.is_approx_zero() {
let new_v = vel.0 + impulse / mass.0;
// If the new velocity is in the opposite direction, it's because the forces
// involved are too high for the current tick to handle. We deal with this by
// removing the component of our velocity vector along the direction of force.
// This way we can only ever lose velocity and will never experience a reverse
// in direction from events such as falling into water at high velocities.
if new_v.dot(vel.0) < 0.0 {
vel.0 -= vel.0.projected(&impulse);
} else {
vel.0 = new_v;
}
};
debug_assert!(!vel.0.map(|a| a.is_nan()).reduce_or());
};
// Hydrostatic/aerostatic forces
// modify gravity to account for the effective density as a result of buoyancy
let down_force = dt.0 * gravity * (density.0 - fluid_density.0) / density.0;
vel.0.z -= down_force;
vel
} }
fn calc_z_limit( fn calc_z_limit(
@ -88,7 +135,6 @@ pub struct PhysicsRead<'a> {
stickies: ReadStorage<'a, Sticky>, stickies: ReadStorage<'a, Sticky>,
masses: ReadStorage<'a, Mass>, masses: ReadStorage<'a, Mass>,
colliders: ReadStorage<'a, Collider>, colliders: ReadStorage<'a, Collider>,
gravities: ReadStorage<'a, Gravity>,
mountings: ReadStorage<'a, Mounting>, mountings: ReadStorage<'a, Mounting>,
projectiles: ReadStorage<'a, Projectile>, projectiles: ReadStorage<'a, Projectile>,
beams: ReadStorage<'a, BeamSegment>, beams: ReadStorage<'a, BeamSegment>,
@ -96,6 +142,7 @@ pub struct PhysicsRead<'a> {
char_states: ReadStorage<'a, CharacterState>, char_states: ReadStorage<'a, CharacterState>,
bodies: ReadStorage<'a, Body>, bodies: ReadStorage<'a, Body>,
character_states: ReadStorage<'a, CharacterState>, character_states: ReadStorage<'a, CharacterState>,
densities: ReadStorage<'a, Density>,
} }
#[derive(SystemData)] #[derive(SystemData)]
@ -255,7 +302,7 @@ impl<'a> PhysicsData<'a> {
positions, positions,
&mut write.velocities, &mut write.velocities,
previous_phys_cache, previous_phys_cache,
read.masses.maybe(), &read.masses,
read.colliders.maybe(), read.colliders.maybe(),
!&read.mountings, !&read.mountings,
read.stickies.maybe(), read.stickies.maybe(),
@ -288,7 +335,6 @@ impl<'a> PhysicsData<'a> {
char_state_maybe, char_state_maybe,
)| { )| {
let z_limits = calc_z_limit(char_state_maybe, collider); let z_limits = calc_z_limit(char_state_maybe, collider);
let mass = mass.map(|m| m.0).unwrap_or(previous_cache.scale);
// Resets touch_entities in physics // Resets touch_entities in physics
physics.touch_entities.clear(); physics.touch_entities.clear();
@ -320,13 +366,14 @@ impl<'a> PhysicsData<'a> {
.get(entity) .get(entity)
.zip(positions.get(entity)) .zip(positions.get(entity))
.zip(previous_phys_cache.get(entity)) .zip(previous_phys_cache.get(entity))
.map(|((uid, pos), previous_cache)| { .zip(read.masses.get(entity))
.map(|(((uid, pos), previous_cache), mass)| {
( (
entity, entity,
uid, uid,
pos, pos,
previous_cache, previous_cache,
read.masses.get(entity), mass,
read.colliders.get(entity), read.colliders.get(entity),
read.char_states.get(entity), read.char_states.get(entity),
) )
@ -358,16 +405,6 @@ impl<'a> PhysicsData<'a> {
let z_limits_other = let z_limits_other =
calc_z_limit(char_state_other_maybe, collider_other); calc_z_limit(char_state_other_maybe, collider_other);
let mass_other = mass_other
.map(|m| m.0)
.unwrap_or(previous_cache_other.scale);
// This check after the pos check, as we currently don't have
// that many
// massless entites [citation needed]
if mass_other == 0.0 {
return;
}
entity_entity_collision_checks += 1; entity_entity_collision_checks += 1;
const MIN_COLLISION_DIST: f32 = 0.3; const MIN_COLLISION_DIST: f32 = 0.3;
@ -418,8 +455,8 @@ impl<'a> PhysicsData<'a> {
{ {
let force = 400.0 let force = 400.0
* (collision_dist - diff.magnitude()) * (collision_dist - diff.magnitude())
* mass_other * mass_other.0
/ (mass + mass_other); / (mass.0 + mass_other.0);
vel_delta += vel_delta +=
Vec3::from(diff.normalized()) * force * step_delta; Vec3::from(diff.normalized()) * force * step_delta;
@ -542,10 +579,12 @@ impl<'a> PhysicsData<'a> {
// We do this in a first pass because it helps keep things more stable for // We do this in a first pass because it helps keep things more stable for
// entities that are anchored to other entities (such as airships). // entities that are anchored to other entities (such as airships).
( (
&read.entities,
positions, positions,
velocities, velocities,
&read.bodies,
&write.physics_states, &write.physics_states,
&read.masses,
&read.densities,
!&read.mountings, !&read.mountings,
) )
.par_join() .par_join()
@ -554,38 +593,31 @@ impl<'a> PhysicsData<'a> {
prof_span!(guard, "velocity update rayon job"); prof_span!(guard, "velocity update rayon job");
guard guard
}, },
|_guard, (entity, pos, vel, physics_state, _)| { |_guard, (pos, vel, body, physics_state, mass, density, _)| {
let in_loaded_chunk = read let in_loaded_chunk = read
.terrain .terrain
.get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)))
.is_some(); .is_some();
// Integrate forces
// Friction is assumed to be a constant dependent on location
let friction = if physics_state.on_ground { 0.0 } else { 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 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
} * read.gravities.get(entity).map(|g| g.0).unwrap_or_default();
vel.0 = integrate_forces(read.dt.0, vel.0, downward_force, friction); // Apply physics only if in a loaded chunk
if in_loaded_chunk {
// Clamp dt to an effective 10 TPS, to prevent gravity from slamming the
// players into the floor when stationary if other systems cause the server
// to lag (as observed in the 0.9 release party).
let dt = DeltaTime(read.dt.0.min(0.1));
match physics_state.in_fluid {
None => {
vel.0.z -= dt.0 * GRAVITY;
},
Some(fluid) => {
vel.0 = integrate_forces(
&dt, *vel, body, density, mass, &fluid, GRAVITY,
)
.0
},
}
}
}, },
); );
@ -671,8 +703,7 @@ impl<'a> PhysicsData<'a> {
let mut tgt_pos = pos.0 + pos_delta; let mut tgt_pos = pos.0 + pos_delta;
let was_on_ground = physics_state.on_ground; 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, |body| body.jump_impulse().is_some());
let climbing = let climbing =
character_state.map_or(false, |cs| matches!(cs, CharacterState::Climb(_))); character_state.map_or(false, |cs| matches!(cs, CharacterState::Climb(_)));
@ -816,11 +847,21 @@ impl<'a> PhysicsData<'a> {
} }
} }
physics_state.in_liquid = read physics_state.in_fluid = read
.terrain .terrain
.get(pos.0.map(|e| e.floor() as i32)) .get(pos.0.map(|e| e.floor() as i32))
.ok() .ok()
.and_then(|vox| vox.is_liquid().then_some(1.0)); .and_then(|vox| vox.is_liquid().then_some(1.0))
.map(|depth| Fluid::Water {
depth,
vel: Vel::zero(),
})
.or_else(|| {
Some(Fluid::Air {
elevation: pos.0.z,
vel: Vel::zero(),
})
});
tgt_pos = pos.0; tgt_pos = pos.0;
}, },
@ -989,12 +1030,19 @@ impl<'a> PhysicsData<'a> {
.on_wall .on_wall
.map(|dir| ori_from.mul_direction(dir)) .map(|dir| ori_from.mul_direction(dir))
}); });
physics_state.in_liquid = match ( physics_state.in_fluid = match (
physics_state.in_liquid, physics_state.in_fluid,
physics_state_delta.in_liquid, physics_state_delta.in_fluid,
) { ) {
// this match computes `x <|> y <|> liftA2 max x y` (Some(x), Some(y)) => x
(Some(x), Some(y)) => Some(x.max(y)), .depth()
.and_then(|xh| {
y.depth()
.map(|yh| xh > yh)
.unwrap_or(true)
.then_some(x)
})
.or(Some(y)),
(x @ Some(_), _) => x, (x @ Some(_), _) => x,
(_, y @ Some(_)) => y, (_, y @ Some(_)) => y,
_ => None, _ => None,
@ -1108,7 +1156,7 @@ impl<'a> System<'a> for Sys {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>( fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
cylinder: (f32, f32, f32), cylinder: (f32, f32, f32), // effective collision cylinder
terrain: &'a T, terrain: &'a T,
entity: Entity, entity: Entity,
pos: &mut Pos, pos: &mut Pos,
@ -1450,8 +1498,27 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
physics_state.ground_vel = ground_vel; physics_state.ground_vel = ground_vel;
} }
// Set in_liquid state physics_state.in_fluid = max_liquid_z
physics_state.in_liquid = max_liquid_z.map(|max_z| max_z - pos.0.z); .map(|max_z| max_z - pos.0.z) // NOTE: assumes min_z == 0.0
.map(|depth| {
physics_state
.in_liquid()
// This is suboptimal because it doesn't check for true depth,
// so it can cause problems for situations like swimming down
// a river and spawning or teleporting in(/to) water
.map(|old_depth| (old_depth + old_pos.z - pos.0.z).max(depth))
.unwrap_or(depth)
})
.map(|depth| Fluid::Water {
depth,
vel: Vel::zero(),
})
.or_else(|| {
Some(Fluid::Air {
elevation: pos.0.z,
vel: Vel::zero(),
})
});
} }
fn voxel_collider_bounding_sphere( fn voxel_collider_bounding_sphere(

View File

@ -7,7 +7,7 @@ use common::{
beam, beam,
buff::{BuffCategory, BuffData, BuffKind, BuffSource}, buff::{BuffCategory, BuffData, BuffKind, BuffSource},
inventory::loadout::Loadout, inventory::loadout::Loadout,
shockwave, Agent, Alignment, Body, Gravity, Health, HomeChunk, Inventory, Item, ItemDrop, shockwave, Agent, Alignment, Body, Health, HomeChunk, Inventory, Item, ItemDrop,
LightEmitter, Object, Ori, Poise, Pos, Projectile, Scale, SkillSet, Stats, Vel, LightEmitter, Object, Ori, Poise, Pos, Projectile, Scale, SkillSet, Stats, Vel,
WaypointArea, WaypointArea,
}, },
@ -163,7 +163,6 @@ pub fn handle_shoot(
body: Body, body: Body,
light: Option<LightEmitter>, light: Option<LightEmitter>,
projectile: Projectile, projectile: Projectile,
gravity: Option<Gravity>,
speed: f32, speed: f32,
object: Option<Object>, object: Option<Object>,
) { ) {
@ -200,9 +199,6 @@ pub fn handle_shoot(
if let Some(light) = light { if let Some(light) = light {
builder = builder.with(light) builder = builder.with(light)
} }
if let Some(gravity) = gravity {
builder = builder.with(gravity)
}
if let Some(object) = object { if let Some(object) = object {
builder = builder.with(object) builder = builder.with(object)
} }

View File

@ -478,8 +478,14 @@ pub fn handle_delete(server: &mut Server, entity: EcsEntity) {
pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) { pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) {
let state = &server.state; let state = &server.state;
if vel.z <= -30.0 { if vel.z <= -30.0 {
let falldmg = (vel.z.powi(2) / 20.0 - 40.0) * 7.5; let mass = state
.ecs()
.read_storage::<comp::Mass>()
.get(entity)
.copied()
.unwrap_or_default();
let inventories = state.ecs().read_storage::<Inventory>(); let inventories = state.ecs().read_storage::<Inventory>();
let falldmg = mass.0 * vel.z.powi(2) / 200.0;
let stats = state.ecs().read_storage::<Stats>(); let stats = state.ecs().read_storage::<Stats>();
// Handle health change // Handle health change
if let Some(mut health) = state.ecs().write_storage::<comp::Health>().get_mut(entity) { if let Some(mut health) = state.ecs().write_storage::<comp::Health>().get_mut(entity) {
@ -495,7 +501,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
// Handle poise change // Handle poise change
if let Some(mut poise) = state.ecs().write_storage::<comp::Poise>().get_mut(entity) { if let Some(mut poise) = state.ecs().write_storage::<comp::Poise>().get_mut(entity) {
let poise_damage = PoiseChange { let poise_damage = PoiseChange {
amount: -(falldmg / 2.0) as i32, amount: -(mass.0 * vel.magnitude_squared() / 1500.0) as i32,
source: PoiseSource::Falling, source: PoiseSource::Falling,
}; };
let poise_change = poise_damage.modify_poise_damage(inventories.get(entity)); let poise_change = poise_damage.modify_poise_damage(inventories.get(entity));

View File

@ -73,12 +73,9 @@ impl Server {
body, body,
light, light,
projectile, projectile,
gravity,
speed, speed,
object, object,
} => handle_shoot( } => handle_shoot(self, entity, dir, body, light, projectile, speed, object),
self, entity, dir, body, light, projectile, gravity, speed, object,
),
ServerEvent::Shockwave { ServerEvent::Shockwave {
properties, properties,
pos, pos,

View File

@ -184,6 +184,8 @@ impl StateExt for State {
)) ))
.unwrap_or_default(), .unwrap_or_default(),
) )
.with(body.mass())
.with(body.density())
.with(match body { .with(match body {
comp::Body::Ship(ship) => comp::Collider::Voxel { comp::Body::Ship(ship) => comp::Collider::Voxel {
id: ship.manifest_entry().to_string(), id: ship.manifest_entry().to_string(),
@ -208,7 +210,6 @@ impl StateExt for State {
.with(health) .with(health)
.with(poise) .with(poise)
.with(comp::Alignment::Npc) .with(comp::Alignment::Npc)
.with(comp::Gravity(1.0))
.with(comp::CharacterState::default()) .with(comp::CharacterState::default())
.with(inventory) .with(inventory)
.with(comp::Buffs::default()) .with(comp::Buffs::default())
@ -217,19 +218,20 @@ impl StateExt for State {
} }
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
let body = comp::Body::Object(object);
self.ecs_mut() self.ecs_mut()
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(comp::Vel(Vec3::zero())) .with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default()) .with(comp::Ori::default())
.with(comp::Mass(5.0)) .with(body.mass())
.with(body.density())
.with(comp::Collider::Box { .with(comp::Collider::Box {
radius: comp::Body::Object(object).radius(), radius: body.radius(),
z_min: 0.0, z_min: 0.0,
z_max: comp::Body::Object(object).height(), z_max: body.height(),
}) })
.with(comp::Body::Object(object)) .with(body)
.with(comp::Gravity(1.0))
} }
fn create_ship( fn create_ship(
@ -238,18 +240,19 @@ impl StateExt for State {
ship: comp::ship::Body, ship: comp::ship::Body,
mountable: bool, mountable: bool,
) -> EcsEntityBuilder { ) -> EcsEntityBuilder {
let body = comp::Body::Ship(ship);
let mut builder = self let mut builder = self
.ecs_mut() .ecs_mut()
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(comp::Vel(Vec3::zero())) .with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default()) .with(comp::Ori::default())
.with(comp::Mass(50.0)) .with(body.mass())
.with(body.density())
.with(comp::Collider::Voxel { .with(comp::Collider::Voxel {
id: ship.manifest_entry().to_string(), id: ship.manifest_entry().to_string(),
}) })
.with(comp::Body::Ship(ship)) .with(body)
.with(comp::Gravity(1.0))
.with(comp::Scale(comp::ship::AIRSHIP_SCALE)) .with(comp::Scale(comp::ship::AIRSHIP_SCALE))
.with(comp::Controller::default()) .with(comp::Controller::default())
.with(comp::inventory::Inventory::new_empty()) .with(comp::inventory::Inventory::new_empty())
@ -279,7 +282,8 @@ impl StateExt for State {
.with(pos) .with(pos)
.with(vel) .with(vel)
.with(comp::Ori::from_unnormalized_vec(vel.0).unwrap_or_default()) .with(comp::Ori::from_unnormalized_vec(vel.0).unwrap_or_default())
.with(comp::Mass(0.0)) .with(body.mass())
.with(body.density())
.with(comp::Collider::Point) .with(comp::Collider::Point)
.with(body) .with(body)
.with(projectile) .with(projectile)
@ -406,7 +410,6 @@ impl StateExt for State {
z_min: 0.0, z_min: 0.0,
z_max: 1.75, z_max: 1.75,
}); });
self.write_component_ignore_entity_dead(entity, comp::Gravity(1.0));
self.write_component_ignore_entity_dead(entity, comp::CharacterState::default()); self.write_component_ignore_entity_dead(entity, comp::CharacterState::default());
self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid)); self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid));
self.write_component_ignore_entity_dead(entity, comp::Buffs::default()); self.write_component_ignore_entity_dead(entity, comp::Buffs::default());
@ -450,6 +453,8 @@ impl StateExt for State {
z_max: body.height(), z_max: body.height(),
}); });
self.write_component_ignore_entity_dead(entity, body); self.write_component_ignore_entity_dead(entity, body);
self.write_component_ignore_entity_dead(entity, body.mass());
self.write_component_ignore_entity_dead(entity, body.density());
let (health_level, energy_level) = ( let (health_level, energy_level) = (
skill_set skill_set
.skill_level(Skill::General(GeneralSkill::HealthIncrease)) .skill_level(Skill::General(GeneralSkill::HealthIncrease))

View File

@ -19,6 +19,7 @@ use common::{
InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos,
Scale, SkillSet, Stats, UnresolvedChatMsg, Vel, Scale, SkillSet, Stats, UnresolvedChatMsg, Vel,
}, },
consts::GRAVITY,
effect::{BuffEffect, Effect}, effect::{BuffEffect, Effect},
event::{Emitter, EventBus, ServerEvent}, event::{Emitter, EventBus, ServerEvent},
path::TraversalConfig, path::TraversalConfig,
@ -233,10 +234,10 @@ impl<'a> System<'a> for Sys {
node_tolerance, node_tolerance,
slow_factor, slow_factor,
on_ground: physics_state.on_ground, on_ground: physics_state.on_ground,
in_liquid: physics_state.in_liquid.is_some(), in_liquid: physics_state.in_liquid().is_some(),
min_tgt_dist: 1.0, min_tgt_dist: 1.0,
can_climb: body.map(|b| b.can_climb()).unwrap_or(false), can_climb: body.map(|b| b.can_climb()).unwrap_or(false),
can_fly: body.map(|b| b.can_fly().is_some()).unwrap_or(false), can_fly: body.map(|b| b.fly_thrust().is_some()).unwrap_or(false),
}; };
if traversal_config.can_fly { if traversal_config.can_fly {
@ -743,7 +744,7 @@ impl<'a> AgentData<'a> {
* speed.min(agent.rtsim_controller.speed_factor); * speed.min(agent.rtsim_controller.speed_factor);
self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly); self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly);
controller.inputs.climb = Some(comp::Climb::Up); controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid.is_some()); //.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
controller.inputs.move_z = bearing.z controller.inputs.move_z = bearing.z
+ if self.traversal_config.can_fly { + if self.traversal_config.can_fly {
@ -1543,38 +1544,36 @@ impl<'a> AgentData<'a> {
0.0 0.0
}; };
// Hacky distance offset for ranged weapons. This is let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
// intentionally hacky for now before we make ranged
// NPCs lead targets and implement varying aiming
// skill
let distance_offset = match tactic {
Tactic::Bow => {
0.0004 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
Tactic::Staff => {
0.0015 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
Tactic::QuadLowRanged => {
0.03 /* Yay magic numbers */ * self.pos.0.distance_squared(tgt_pos.0)
},
_ => 0.0,
};
// Apply the distance and eye offsets to make the // FIXME: Retrieve actual projectile speed!
// look_dir the vector from projectile launch to // We have to assume projectiles are faster than base speed because there are
// target point // skills that increase it, and in most cases this will cause agents to
if let Some(dir) = Dir::from_unnormalized( // overshoot
Vec3::new( if let Some(dir) = match tactic {
tgt_pos.0.x, Tactic::Bow
tgt_pos.0.y, | Tactic::FixedTurret
tgt_pos.0.z + tgt_eye_offset + distance_offset, | Tactic::QuadLowRanged
) - Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset), | Tactic::QuadMedJump
) { | Tactic::RotatingTurret
| Tactic::Staff
| Tactic::Turret
if dist_sqrd > 0.0 =>
{
aim_projectile(
90.0, // + self.vel.0.magnitude(),
Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset),
)
}
_ => Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
),
} {
controller.inputs.look_dir = dir; controller.inputs.look_dir = dir;
} }
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
// Match on tactic. Each tactic has different controls // Match on tactic. Each tactic has different controls
// depending on the distance from the agent to the target // depending on the distance from the agent to the target
match tactic { match tactic {
@ -2633,3 +2632,18 @@ fn try_owner_alignment<'a>(
} }
alignment alignment
} }
/// Projectile motion: Returns the direction to aim for the projectile to reach
/// target position. Does not take any forces but gravity into account.
fn aim_projectile(speed: f32, pos: Vec3<f32>, tgt: Vec3<f32>) -> Option<Dir> {
let mut to_tgt = tgt - pos;
let dist_sqrd = to_tgt.xy().magnitude_squared();
let u_sqrd = speed.powi(2);
to_tgt.z = (u_sqrd
- (u_sqrd.powi(2) - GRAVITY * (GRAVITY * dist_sqrd + 2.0 * to_tgt.z * u_sqrd))
.sqrt()
.max(0.0))
/ GRAVITY;
Dir::from_unnormalized(to_tgt)
}

View File

@ -76,7 +76,7 @@ impl<'a> System<'a> for Sys {
const ENABLE_RECURSIVE_FIREWORKS: bool = true; const ENABLE_RECURSIVE_FIREWORKS: bool = true;
if ENABLE_RECURSIVE_FIREWORKS { if ENABLE_RECURSIVE_FIREWORKS {
use common::{ use common::{
comp::{object, Body, Gravity, LightEmitter, Projectile}, comp::{object, Body, LightEmitter, Projectile},
util::Dir, util::Dir,
}; };
use rand::Rng; use rand::Rng;
@ -132,7 +132,6 @@ impl<'a> System<'a> for Sys {
owner: *owner, owner: *owner,
ignore_group: true, ignore_group: true,
}, },
gravity: Some(Gravity(1.0)),
speed, speed,
object: Some(Object::Firework { object: Some(Object::Firework {
owner: *owner, owner: *owner,

View File

@ -1,7 +1,7 @@
use common::{ use common::{
comp::{ comp::{
item::MaterialStatManifest, Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState, item::MaterialStatManifest, Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState,
Collider, Combo, Energy, Gravity, Group, Health, Inventory, Item, LightEmitter, Mass, Collider, Combo, Density, Energy, Group, Health, Inventory, Item, LightEmitter, Mass,
MountState, Mounting, Ori, Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky, MountState, Mounting, Ori, Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky,
Vel, Vel,
}, },
@ -58,9 +58,9 @@ pub struct TrackedComps<'a> {
pub mount_state: ReadStorage<'a, MountState>, pub mount_state: ReadStorage<'a, MountState>,
pub group: ReadStorage<'a, Group>, pub group: ReadStorage<'a, Group>,
pub mass: ReadStorage<'a, Mass>, pub mass: ReadStorage<'a, Mass>,
pub density: ReadStorage<'a, Density>,
pub collider: ReadStorage<'a, Collider>, pub collider: ReadStorage<'a, Collider>,
pub sticky: ReadStorage<'a, Sticky>, pub sticky: ReadStorage<'a, Sticky>,
pub gravity: ReadStorage<'a, Gravity>,
pub inventory: ReadStorage<'a, Inventory>, pub inventory: ReadStorage<'a, Inventory>,
pub character_state: ReadStorage<'a, CharacterState>, pub character_state: ReadStorage<'a, CharacterState>,
pub shockwave: ReadStorage<'a, Shockwave>, pub shockwave: ReadStorage<'a, Shockwave>,
@ -143,6 +143,10 @@ impl<'a> TrackedComps<'a> {
.cloned() .cloned()
.map(|c| comps.push(c.into())); .map(|c| comps.push(c.into()));
self.mass.get(entity).copied().map(|c| comps.push(c.into())); self.mass.get(entity).copied().map(|c| comps.push(c.into()));
self.density
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.collider self.collider
.get(entity) .get(entity)
.cloned() .cloned()
@ -151,10 +155,6 @@ impl<'a> TrackedComps<'a> {
.get(entity) .get(entity)
.copied() .copied()
.map(|c| comps.push(c.into())); .map(|c| comps.push(c.into()));
self.gravity
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.inventory self.inventory
.get(entity) .get(entity)
.cloned() .cloned()
@ -201,9 +201,9 @@ pub struct ReadTrackers<'a> {
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>, pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
pub group: ReadExpect<'a, UpdateTracker<Group>>, pub group: ReadExpect<'a, UpdateTracker<Group>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>, pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub density: ReadExpect<'a, UpdateTracker<Density>>,
pub collider: ReadExpect<'a, UpdateTracker<Collider>>, pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>, pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>, pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub shockwave: ReadExpect<'a, UpdateTracker<Shockwave>>, pub shockwave: ReadExpect<'a, UpdateTracker<Shockwave>>,
pub beam_segment: ReadExpect<'a, UpdateTracker<BeamSegment>>, pub beam_segment: ReadExpect<'a, UpdateTracker<BeamSegment>>,
@ -241,9 +241,9 @@ impl<'a> ReadTrackers<'a> {
.with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter) .with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter)
.with_component(&comps.uid, &*self.group, &comps.group, filter) .with_component(&comps.uid, &*self.group, &comps.group, filter)
.with_component(&comps.uid, &*self.mass, &comps.mass, filter) .with_component(&comps.uid, &*self.mass, &comps.mass, filter)
.with_component(&comps.uid, &*self.density, &comps.density, filter)
.with_component(&comps.uid, &*self.collider, &comps.collider, filter) .with_component(&comps.uid, &*self.collider, &comps.collider, filter)
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter) .with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
.with_component(&comps.uid, &*self.gravity, &comps.gravity, filter)
.with_component(&comps.uid, &*self.inventory, &comps.inventory, filter) .with_component(&comps.uid, &*self.inventory, &comps.inventory, filter)
.with_component( .with_component(
&comps.uid, &comps.uid,
@ -279,9 +279,9 @@ pub struct WriteTrackers<'a> {
mount_state: WriteExpect<'a, UpdateTracker<MountState>>, mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
group: WriteExpect<'a, UpdateTracker<Group>>, group: WriteExpect<'a, UpdateTracker<Group>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>, mass: WriteExpect<'a, UpdateTracker<Mass>>,
density: WriteExpect<'a, UpdateTracker<Density>>,
collider: WriteExpect<'a, UpdateTracker<Collider>>, collider: WriteExpect<'a, UpdateTracker<Collider>>,
sticky: WriteExpect<'a, UpdateTracker<Sticky>>, sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
inventory: WriteExpect<'a, UpdateTracker<Inventory>>, inventory: WriteExpect<'a, UpdateTracker<Inventory>>,
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>, character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
shockwave: WriteExpect<'a, UpdateTracker<Shockwave>>, shockwave: WriteExpect<'a, UpdateTracker<Shockwave>>,
@ -309,9 +309,9 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers.mount_state.record_changes(&comps.mount_state); trackers.mount_state.record_changes(&comps.mount_state);
trackers.group.record_changes(&comps.group); trackers.group.record_changes(&comps.group);
trackers.mass.record_changes(&comps.mass); trackers.mass.record_changes(&comps.mass);
trackers.density.record_changes(&comps.density);
trackers.collider.record_changes(&comps.collider); trackers.collider.record_changes(&comps.collider);
trackers.sticky.record_changes(&comps.sticky); trackers.sticky.record_changes(&comps.sticky);
trackers.gravity.record_changes(&comps.gravity);
trackers.inventory.record_changes(&comps.inventory); trackers.inventory.record_changes(&comps.inventory);
trackers trackers
.character_state .character_state
@ -350,9 +350,9 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
log_counts!(mounting, "Mountings"); log_counts!(mounting, "Mountings");
log_counts!(mount_state, "Mount States"); log_counts!(mount_state, "Mount States");
log_counts!(mass, "Masses"); log_counts!(mass, "Masses");
log_counts!(mass, "Densities");
log_counts!(collider, "Colliders"); log_counts!(collider, "Colliders");
log_counts!(sticky, "Stickies"); log_counts!(sticky, "Stickies");
log_counts!(gravity, "Gravitys");
log_counts!(loadout, "Loadouts"); log_counts!(loadout, "Loadouts");
log_counts!(character_state, "Character States"); log_counts!(character_state, "Character States");
log_counts!(shockwave, "Shockwaves"); log_counts!(shockwave, "Shockwaves");
@ -380,9 +380,9 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<MountState>(); world.register_tracker::<MountState>();
world.register_tracker::<Group>(); world.register_tracker::<Group>();
world.register_tracker::<Mass>(); world.register_tracker::<Mass>();
world.register_tracker::<Density>();
world.register_tracker::<Collider>(); world.register_tracker::<Collider>();
world.register_tracker::<Sticky>(); world.register_tracker::<Sticky>();
world.register_tracker::<Gravity>();
world.register_tracker::<Inventory>(); world.register_tracker::<Inventory>();
world.register_tracker::<CharacterState>(); world.register_tracker::<CharacterState>();
world.register_tracker::<Shockwave>(); world.register_tracker::<Shockwave>();

View File

@ -122,11 +122,7 @@ impl EventMapper for MovementEventMapper {
// it was dispatched // it was dispatched
internal_state.event = mapped_event; internal_state.event = mapped_event;
internal_state.on_ground = physics.on_ground; internal_state.on_ground = physics.on_ground;
if physics.in_liquid.is_some() { internal_state.in_water = physics.in_liquid().is_some();
internal_state.in_water = true;
} else {
internal_state.in_water = false;
}
let dt = ecs.fetch::<DeltaTime>().0; let dt = ecs.fetch::<DeltaTime>().0;
internal_state.distance_travelled += vel.0.magnitude() * dt; internal_state.distance_travelled += vel.0.magnitude() * dt;
} }
@ -197,8 +193,8 @@ impl MovementEventMapper {
underfoot_block_kind: BlockKind, underfoot_block_kind: BlockKind,
) -> SfxEvent { ) -> SfxEvent {
// Match run / roll / swim state // Match run / roll / swim state
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1 if physics_state.in_liquid().is_some() && vel.magnitude() > 0.1
|| !previous_state.in_water && physics_state.in_liquid.is_some() || !previous_state.in_water && physics_state.in_liquid().is_some()
{ {
return SfxEvent::Swim; return SfxEvent::Swim;
} else if physics_state.on_ground && vel.magnitude() > 0.1 } else if physics_state.on_ground && vel.magnitude() > 0.1
@ -240,7 +236,7 @@ impl MovementEventMapper {
vel: Vec3<f32>, vel: Vec3<f32>,
underfoot_block_kind: BlockKind, underfoot_block_kind: BlockKind,
) -> SfxEvent { ) -> SfxEvent {
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1 { if physics_state.in_liquid().is_some() && vel.magnitude() > 0.1 {
SfxEvent::Swim SfxEvent::Swim
} else if physics_state.on_ground && vel.magnitude() > 0.1 { } else if physics_state.on_ground && vel.magnitude() > 0.1 {
match underfoot_block_kind { match underfoot_block_kind {
@ -261,7 +257,7 @@ impl MovementEventMapper {
vel: Vec3<f32>, vel: Vec3<f32>,
underfoot_block_kind: BlockKind, underfoot_block_kind: BlockKind,
) -> SfxEvent { ) -> SfxEvent {
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1 { if physics_state.in_liquid().is_some() && vel.magnitude() > 0.1 {
SfxEvent::Swim SfxEvent::Swim
} else if physics_state.on_ground && vel.magnitude() > 0.1 { } else if physics_state.on_ground && vel.magnitude() > 0.1 {
match underfoot_block_kind { match underfoot_block_kind {

View File

@ -783,7 +783,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::character::StandAnimation::update_skeleton( (true, false, false) => anim::character::StandAnimation::update_skeleton(
@ -1411,7 +1411,7 @@ impl FigureMgr {
skeleton_attr, skeleton_attr,
), ),
CharacterState::Wielding { .. } => { CharacterState::Wielding { .. } => {
if physics.in_liquid.is_some() { if physics.in_liquid().is_some() {
anim::character::SwimWieldAnimation::update_skeleton( anim::character::SwimWieldAnimation::update_skeleton(
&target_base, &target_base,
( (
@ -1571,7 +1571,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => { (true, false, false) => {
@ -1773,7 +1773,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > 0.25, // Moving rel_vel.magnitude_squared() > 0.25, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => { (true, false, false) => {
@ -2100,7 +2100,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => { (true, false, false) => {
@ -2459,7 +2459,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::bird_medium::IdleAnimation::update_skeleton( (true, false, false) => anim::bird_medium::IdleAnimation::update_skeleton(
@ -2569,7 +2569,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Idle // Idle
(_, false, _) => anim::fish_medium::IdleAnimation::update_skeleton( (_, false, _) => anim::fish_medium::IdleAnimation::update_skeleton(
@ -2658,7 +2658,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Idle // Idle
(true, false, false) => anim::biped_small::IdleAnimation::update_skeleton( (true, false, false) => anim::biped_small::IdleAnimation::update_skeleton(
@ -3003,7 +3003,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::dragon::IdleAnimation::update_skeleton( (true, false, false) => anim::dragon::IdleAnimation::update_skeleton(
@ -3098,7 +3098,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::theropod::IdleAnimation::update_skeleton( (true, false, false) => anim::theropod::IdleAnimation::update_skeleton(
@ -3287,7 +3287,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::bird_small::IdleAnimation::update_skeleton( (true, false, false) => anim::bird_small::IdleAnimation::update_skeleton(
@ -3378,7 +3378,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Idle // Idle
(_, false, _) => anim::fish_small::IdleAnimation::update_skeleton( (_, false, _) => anim::fish_small::IdleAnimation::update_skeleton(
@ -3467,7 +3467,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Running // Running
(true, true, false) => anim::biped_large::RunAnimation::update_skeleton( (true, true, false) => anim::biped_large::RunAnimation::update_skeleton(
@ -3988,7 +3988,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::golem::IdleAnimation::update_skeleton( (true, false, false) => anim::golem::IdleAnimation::update_skeleton(
@ -4174,7 +4174,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::object::IdleAnimation::update_skeleton( (true, false, false) => anim::object::IdleAnimation::update_skeleton(
@ -4303,7 +4303,7 @@ impl FigureMgr {
let target_base = match ( let target_base = match (
physics.on_ground, physics.on_ground,
rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_liquid.is_some(), // In water physics.in_liquid().is_some(), // In water
) { ) {
// Standing // Standing
(true, false, false) => anim::ship::IdleAnimation::update_skeleton( (true, false, false) => anim::ship::IdleAnimation::update_skeleton(