//! Space: star systems with proper orbits, the places in them you can fly //! around, and the rules of flight. //! //! There are two scales. A system is measured in AU and days: planets follow //! Kepler orbits around the star, moons around their planets, and the //! calendar moves them on. You never fly at that scale. Instead you travel //! between places (a planet's orbit, a moon, a belt, a station), and each //! place is a small local map you fly around in, measured in local units: //! one unit is one terminal cell across and half a cell down (cells are about //! twice as tall as they are wide), so circles come out round on screen. //! Local maps use screen-style coordinates, with y pointing down. //! //! Above both is the galaxy: star systems in light-years, joined by lanes //! you jump along (see `galaxy`). pub mod flight; pub mod galaxy; pub mod locale; pub mod orbit; pub mod survey; pub mod system; use std::f64::consts::{PI, TAU}; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; use crate::seed; /// The star system a universe first started everyone in. It's one of the /// four peoples' seats in its galaxy (see `galaxy`). pub fn home_system(universe: u64) -> system::System { system::generate(seed::derive(universe, "system", 0)) } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Vec2 { pub x: f64, pub y: f64, } impl Vec2 { pub const ZERO: Vec2 = Vec2::new(0.0, 0.0); pub const fn new(x: f64, y: f64) -> Vec2 { Vec2 { x, y } } /// On a local map, the unit vector pointing along `heading`: 0 is up the /// screen, and headings increase clockwise. pub fn from_heading(heading: f64) -> Vec2 { Vec2::new(heading.sin(), -heading.cos()) } /// The heading this vector points along, the inverse of `from_heading`. pub fn heading(self) -> f64 { self.x.atan2(-self.y).rem_euclid(TAU) } pub fn rotated(self, angle: f64) -> Vec2 { let (s, c) = angle.sin_cos(); Vec2::new(self.x * c - self.y * s, self.x * s + self.y * c) } pub fn length(self) -> f64 { self.x.hypot(self.y) } pub fn distance(self, other: Vec2) -> f64 { (self - other).length() } pub fn dot(self, other: Vec2) -> f64 { self.x * other.x + self.y * other.y } /// This vector at length 1, or zero if it has no length. pub fn normalised(self) -> Vec2 { let len = self.length(); if len > 0.0 { self * (1.0 / len) } else { Vec2::ZERO } } } impl Add for Vec2 { type Output = Vec2; fn add(self, o: Vec2) -> Vec2 { Vec2::new(self.x + o.x, self.y + o.y) } } impl Sub for Vec2 { type Output = Vec2; fn sub(self, o: Vec2) -> Vec2 { Vec2::new(self.x - o.x, self.y - o.y) } } impl Mul for Vec2 { type Output = Vec2; fn mul(self, k: f64) -> Vec2 { Vec2::new(self.x * k, self.y * k) } } impl Neg for Vec2 { type Output = Vec2; fn neg(self) -> Vec2 { Vec2::new(-self.x, -self.y) } } impl AddAssign for Vec2 { fn add_assign(&mut self, o: Vec2) { *self = *self + o; } } impl SubAssign for Vec2 { fn sub_assign(&mut self, o: Vec2) { *self = *self - o; } } /// The signed angle to turn through to get from `from` to `to`, the short /// way round: between -π and π. pub fn turn_between(from: f64, to: f64) -> f64 { (to - from + PI).rem_euclid(TAU) - PI } #[cfg(test)] mod tests { use super::*; #[test] fn headings_round_trip() { for i in 0..16 { let h = TAU * f64::from(i) / 16.0; assert!((Vec2::from_heading(h).heading() - h).abs() < 1e-9); } let up = Vec2::from_heading(0.0); assert!(up.x.abs() < 1e-9 && (up.y + 1.0).abs() < 1e-9, "heading 0 points up the screen"); assert!((Vec2::from_heading(TAU / 4.0).x - 1.0).abs() < 1e-9, "a quarter turn is right"); } #[test] fn turning_takes_the_short_way() { assert!((turn_between(0.1, TAU - 0.1) + 0.2).abs() < 1e-9); assert!((turn_between(TAU - 0.1, 0.1) - 0.2).abs() < 1e-9); } #[test] fn rotation_is_anticlockwise() { let v = Vec2::new(1.0, 0.0).rotated(PI / 2.0); assert!(v.x.abs() < 1e-9 && (v.y - 1.0).abs() < 1e-9); } }