use super::{Force, Subject};
use crate::Direction;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Gravity {
strength: f32,
terminal: f32,
direction: Direction,
}
impl Gravity {
pub const DEFAULT_STRENGTH: f32 = 0.25;
pub const DEFAULT_TERMINAL_VELOCITY: f32 = 4.0;
pub const DEFAULT_DIRECTION: Direction = Direction::Down;
pub const fn new() -> Self {
Self {
strength: Self::DEFAULT_STRENGTH,
terminal: Self::DEFAULT_TERMINAL_VELOCITY,
direction: Self::DEFAULT_DIRECTION,
}
}
pub const fn with_strength(mut self, strength: f32) -> Self {
self.strength = strength;
self
}
pub const fn with_terminal_velocity(mut self, terminal: f32) -> Self {
self.terminal = terminal;
self
}
pub const fn with_direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn set_strength(&mut self, strength: f32) {
self.strength = strength;
}
pub fn set_terminal_velocity(&mut self, terminal: f32) {
self.terminal = terminal;
}
pub fn set_direction(&mut self, direction: Direction) {
self.direction = direction;
}
pub fn strength(&self) -> f32 {
self.strength
}
pub fn terminal_velocity(&self) -> f32 {
self.terminal
}
pub fn direction(&self) -> Direction {
self.direction
}
}
impl Force for Gravity {
fn apply(&self, subject: &mut Subject) {
let velocity = subject.velocity_mut();
match self.direction {
Direction::Down => velocity.dy = (velocity.dy + self.strength).min(self.terminal),
Direction::Up => velocity.dy = (velocity.dy - self.strength).max(-self.terminal),
Direction::Right => velocity.dx = (velocity.dx + self.strength).min(self.terminal),
Direction::Left => velocity.dx = (velocity.dx - self.strength).max(-self.terminal),
diagonal => {
let (ux, uy) = diagonal.unit();
velocity.dx += self.strength * ux;
velocity.dy += self.strength * uy;
let along = velocity.dx * ux + velocity.dy * uy;
if along > self.terminal {
let excess = along - self.terminal;
velocity.dx -= excess * ux;
velocity.dy -= excess * uy;
}
}
}
}
}
impl Default for Gravity {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{
super::{
force::{Mob, DIRECTIONS},
Velocity,
},
*,
};
#[test]
fn gravity_accumulates_then_holds_at_terminal_velocity() {
const GRAVITY: Gravity = Gravity::new();
assert_eq!(GRAVITY.strength(), Gravity::DEFAULT_STRENGTH);
assert_eq!(
GRAVITY.terminal_velocity(),
Gravity::DEFAULT_TERMINAL_VELOCITY
);
assert_eq!(GRAVITY.direction(), Direction::Down);
let mut mob = Mob::new();
mob.shove(&GRAVITY);
assert_eq!(mob.velocity, Velocity::new(0.0, Gravity::DEFAULT_STRENGTH));
mob.shove(&GRAVITY);
assert_eq!(mob.velocity.dy, 2.0 * Gravity::DEFAULT_STRENGTH);
assert_eq!(mob.velocity.dx, 0.0);
let settled = Mob::new().under(&GRAVITY, 1_000);
assert_eq!(settled.dy, Gravity::DEFAULT_TERMINAL_VELOCITY);
}
#[test]
fn buoyancy_rises_past_the_terminal_velocity() {
let buoyancy = Gravity::new()
.with_strength(-0.5)
.with_terminal_velocity(1.0);
assert_eq!(Mob::new().under(&buoyancy, 100).dy, -50.0);
}
#[test]
fn retuned_gravity_uses_the_new_constants() {
let mut gravity = Gravity::default();
gravity.set_strength(0.1);
gravity.set_terminal_velocity(0.35);
assert_eq!(Mob::new().under(&gravity, 10).dy, 0.35);
}
#[test]
fn gravity_falls_the_same_whatever_the_mass() {
let gravity = Gravity::new();
let (mut feather, mut anvil) = (Mob::with_mass(0.01), Mob::with_mass(100.0));
for _ in 0..100 {
feather.shove(&gravity);
anvil.shove(&gravity);
assert_eq!(feather.velocity, anvil.velocity);
}
assert_eq!(feather.velocity.dy, Gravity::DEFAULT_TERMINAL_VELOCITY);
}
#[test]
fn each_straight_direction_pulls_and_caps_along_its_own_axis() {
for (direction, settled) in [
(Direction::Down, (0.0, 4.0)),
(Direction::Up, (0.0, -4.0)),
(Direction::Right, (4.0, 0.0)),
(Direction::Left, (-4.0, 0.0)),
] {
let gravity = Gravity::new().with_direction(direction);
let (ux, uy) = direction.unit();
let first = Mob::new().under(&gravity, 1);
assert_eq!(
(first.dx, first.dy),
(
Gravity::DEFAULT_STRENGTH * ux,
Gravity::DEFAULT_STRENGTH * uy
),
"{direction:?} pulled somewhere else"
);
let velocity = Mob::new().under(&gravity, 1_000);
assert_eq!(
(velocity.dx, velocity.dy),
settled,
"{direction:?} did not settle at its terminal velocity"
);
}
}
#[test]
fn a_sideways_pull_leaves_the_other_axis_alone() {
let gravity = Gravity::new().with_direction(Direction::Right);
let velocity = Mob::moving(0.0, -9.0).under(&gravity, 1_000);
assert_eq!(velocity.dy, -9.0);
assert_eq!(velocity.dx, Gravity::DEFAULT_TERMINAL_VELOCITY);
}
#[test]
fn a_diagonal_pull_caps_along_itself_and_not_across() {
let direction = Direction::DownRight;
let (ux, uy) = direction.unit();
let gravity = Gravity::new().with_direction(direction);
let (acrossx, acrossy) = (uy, -ux);
let velocity = Mob::moving(3.0 * acrossx, 3.0 * acrossy).under(&gravity, 1_000);
let along = velocity.dx * ux + velocity.dy * uy;
let across = velocity.dx * acrossx + velocity.dy * acrossy;
assert!(
(along - Gravity::DEFAULT_TERMINAL_VELOCITY).abs() < 1e-4,
"the pull did not settle at its terminal velocity: {along}"
);
assert!(
(across - 3.0).abs() < 1e-4,
"the sideways movement was capped too: {across}"
);
}
#[test]
fn every_direction_accelerates_by_the_whole_strength() {
for direction in DIRECTIONS {
let (ux, uy) = direction.unit();
let velocity = Mob::new().under(&Gravity::new().with_direction(direction), 1);
let along = velocity.dx * ux + velocity.dy * uy;
assert!(
(along - Gravity::DEFAULT_STRENGTH).abs() < 1e-6,
"{direction:?} pulled by {along}"
);
}
}
}