diff options
| author | skylar <me@casper> | 2026-11-11 15:24:37 -0800 |
|---|---|---|
| committer | skylar <me@casper> | 2026-11-11 15:24:37 -0800 |
| commit | 9ccf951473e21a8b90c25161156fa09a9b227a7b (patch) | |
| tree | e7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/space/mod.rs | |
| parent | d36c54d996f3c14ae811312dbbe621e12a39edde (diff) | |
Orrery 0.1.0: the galaxy, flight, cities, inventory and saves
Grows the derelict crawler into the whole game, and gets it ready to
publish.
- A galaxy of star systems joined by lanes, split between four peoples'
territories, with a star map and a system map. Systems are modelled
with Kepler orbits, moons inside their Hill spheres, belts past the
frost line and stations at Lagrange points.
- Flight around one place at a time: arcade flight with a throttle
fallback where held keys can't be detected, mining rocks for ore,
landing, docking at stations, and boarding wrecks that come and go by
the calendar.
- Your ship to walk about, and capital cities to land at: generated
plans with walls, quarters and streets, building interiors, shops,
taverns, scholars and armourers.
- Fighting with a weapon and a gun ready, six damage types and how each
monster takes them, and armour that guards, wears and can be mended.
- Everything you own has a weight: a vim-keyed inventory, a pack and the
ship's lockers, and trade in ore, salvage, arms and fuel.
- Characters with a people and a ship of their own, saved as versioned
JSON on every scene change and on quit, with a character sheet, a
saves list and a pause menu wherever you are.
- `orrery weapons`, `armour`, `galaxy`, `system` and `town` print the
generators' output without the TUI.
- Release: README install steps for macOS and Linux, a Makefile whose
install also works with macOS's install, an MIT licence, and Cargo
metadata with rust-version 1.88.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/space/mod.rs')
| -rw-r--r-- | src/space/mod.rs | 154 |
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); + } +} |