use super::{force::weighed, Force, Subject};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Atmosphere {
density: f32,
}
impl Atmosphere {
pub const DEFAULT_DENSITY: f32 = 0.0625;
pub const fn new() -> Self {
Self {
density: Self::DEFAULT_DENSITY,
}
}
pub const fn vacuum() -> Self {
Self { density: 0.0 }
}
pub const fn with_density(mut self, density: f32) -> Self {
self.density = clamped(density);
self
}
pub fn set_density(&mut self, density: f32) {
self.density = clamped(density);
}
pub fn density(&self) -> f32 {
self.density
}
}
impl Force for Atmosphere {
fn apply(&self, subject: &mut Subject) {
let take = (self.density / weighed(subject.mass())).clamp(0.0, 1.0);
let velocity = subject.velocity_mut();
velocity.dx -= velocity.dx * take;
velocity.dy -= velocity.dy * take;
}
}
impl Default for Atmosphere {
fn default() -> Self {
Self::new()
}
}
const fn clamped(density: f32) -> f32 {
if density > 1.0 {
1.0
} else if density > 0.0 {
density
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::{
super::{force::Mob, Gravity, Velocity},
*,
};
#[test]
fn the_default_density_settles_a_fall_where_the_cheap_cap_would_have() {
assert_eq!(
Gravity::DEFAULT_STRENGTH / Atmosphere::DEFAULT_DENSITY,
Gravity::DEFAULT_TERMINAL_VELOCITY
);
const AIR: Atmosphere = Atmosphere::new();
const PULL: Gravity = Gravity::new().with_terminal_velocity(f32::MAX);
let mut mob = Mob::new();
for _ in 0..1_000 {
mob.shove(&AIR);
mob.shove(&PULL);
}
assert!(
(mob.velocity.dy - Gravity::DEFAULT_TERMINAL_VELOCITY).abs() < 1e-3,
"settled at {} rather than the terminal velocity",
mob.velocity.dy
);
let mut mob = Mob::new();
for _ in 0..1_000 {
mob.shove(&PULL);
mob.shove(&AIR);
}
let settled = Gravity::DEFAULT_TERMINAL_VELOCITY - Gravity::DEFAULT_STRENGTH;
assert!(
(mob.velocity.dy - settled).abs() < 1e-3,
"settled at {} rather than {settled}",
mob.velocity.dy
);
}
#[test]
fn a_vacuum_does_nothing_at_all() {
const SPACE: Atmosphere = Atmosphere::vacuum();
assert_eq!(SPACE.density(), 0.0);
assert_eq!(
Mob::moving(3.0, -7.5).under(&SPACE, 100),
Velocity::new(3.0, -7.5)
);
let mut air = Atmosphere::new();
air.set_density(0.0);
assert_eq!(air, SPACE);
}
#[test]
fn the_air_drags_both_axes_towards_a_standstill() {
let air = Atmosphere::new();
let mut mob = Mob::moving(2.0, -2.0);
let (mut previous_x, mut previous_y) = (mob.velocity.dx, mob.velocity.dy);
for _ in 0..600 {
mob.shove(&air);
assert!(mob.velocity.dx >= 0.0 && mob.velocity.dx <= previous_x);
assert!(mob.velocity.dy <= 0.0 && mob.velocity.dy >= previous_y);
(previous_x, previous_y) = (mob.velocity.dx, mob.velocity.dy);
}
assert!(mob.velocity.dx < 0.001, "still moving: {}", mob.velocity.dx);
}
#[test]
fn mass_divides_the_drag() {
let air = Atmosphere::new();
let drifting = |mass| {
let mut mob = Mob::with_mass(mass);
mob.velocity = Velocity::new(4.0, 0.0);
mob.under(&air, 30)
};
let (feather, ordinary, anvil) = (drifting(0.25), drifting(1.0), drifting(4.0));
assert!(
feather.dx < ordinary.dx && ordinary.dx < anvil.dx,
"the air did not tell them apart: {} {} {}",
feather.dx,
ordinary.dx,
anvil.dx
);
for mass in [0.0, -3.0, f32::NAN] {
assert_eq!(
drifting(mass),
ordinary,
"a mass of {mass} was not read as 1.0"
);
}
}
#[test]
fn something_light_enough_is_stopped_rather_than_blown_backwards() {
let mut mote = Mob::with_mass(0.001);
mote.velocity = Velocity::new(4.0, 4.0);
assert_eq!(mote.under(&Atmosphere::new(), 1), Velocity::default());
}
#[test]
fn a_density_that_means_nothing_is_taken_quietly() {
assert_eq!(Atmosphere::new().with_density(9.0).density(), 1.0);
assert_eq!(Atmosphere::new().with_density(-9.0).density(), 0.0);
assert_eq!(Atmosphere::new().with_density(f32::NAN).density(), 0.0);
let mut air = Atmosphere::default();
assert_eq!(air.density(), Atmosphere::DEFAULT_DENSITY);
air.set_density(9.0);
assert_eq!(air.density(), 1.0);
air.set_density(f32::NAN);
assert_eq!(air.density(), 0.0);
}
}