summaryrefslogtreecommitdiff
path: root/src/space/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/space/mod.rs')
-rw-r--r--src/space/mod.rs154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/space/mod.rs b/src/space/mod.rs
new file mode 100644
index 0000000..74b092e
--- /dev/null
+++ b/src/space/mod.rs
@@ -0,0 +1,154 @@
+//! 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<f64> 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);
+ }
+}