summaryrefslogtreecommitdiff
path: root/src/space/mod.rs
blob: 74b092e94898a2eeaf00cbe39408963e4deafe48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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);
    }
}