summaryrefslogtreecommitdiff
path: root/src/dungeon/ship.rs
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-11 15:24:37 -0800
committerskylar <me@casper>2026-11-11 15:24:37 -0800
commit9ccf951473e21a8b90c25161156fa09a9b227a7b (patch)
treee7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/dungeon/ship.rs
parentd36c54d996f3c14ae811312dbbe621e12a39edde (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/dungeon/ship.rs')
-rw-r--r--src/dungeon/ship.rs161
1 files changed, 123 insertions, 38 deletions
diff --git a/src/dungeon/ship.rs b/src/dungeon/ship.rs
index dc9b0b8..d845376 100644
--- a/src/dungeon/ship.rs
+++ b/src/dungeon/ship.rs
@@ -25,8 +25,11 @@
use rand::RngExt;
use rand::seq::IndexedRandom;
-use super::crawl::{Item, Loot, MONSTERS, Monster};
+use super::armour;
+use super::arms::{self, Source};
+use super::crawl::{Loot, MONSTERS, Monster};
use super::{Map, Pos, Tile};
+use crate::items::{Item, Salvage};
use crate::names::{self, Culture};
use crate::seed::{self, Rng};
@@ -52,42 +55,65 @@ struct RoomKind {
salvage: (u32, u32),
/// Chance of a medpatch.
medpatch: f64,
+ /// Chance of a weapon; at 1, there are one or two, and in better shape.
+ arms: f64,
+ /// What sort of weapon it'd be, or armour.
+ source: Source,
+ /// Chance of a piece of armour; at 1, one or two, in better shape.
+ armour: f64,
}
-const ENGINE_ROOM: RoomKind =
- RoomKind { name: "the engine room", weight: 0, artifact: 0.05, salvage: (2, 3), medpatch: 0.1 };
-const BRIDGE: RoomKind =
- RoomKind { name: "the bridge", weight: 0, artifact: 0.6, salvage: (0, 2), medpatch: 0.1 };
+const ENGINE_ROOM: RoomKind = RoomKind {
+ name: "the engine room",
+ weight: 0,
+ artifact: 0.05,
+ salvage: (2, 3),
+ medpatch: 0.1,
+ arms: 0.3,
+ source: Source::Tools,
+ armour: 0.15,
+};
+const BRIDGE: RoomKind = RoomKind {
+ name: "the bridge",
+ weight: 0,
+ artifact: 0.6,
+ salvage: (0, 2),
+ medpatch: 0.1,
+ arms: 0.3,
+ source: Source::Armoury,
+ armour: 0.2,
+};
#[rustfmt::skip]
const ROOMS: &[RoomKind] = &[
- RoomKind { name: "a cargo hold", weight: 4, artifact: 0.05, salvage: (2, 4), medpatch: 0.05 },
- RoomKind { name: "the crew berths", weight: 3, artifact: 0.1, salvage: (0, 1), medpatch: 0.25 },
- RoomKind { name: "the galley", weight: 2, artifact: 0.02, salvage: (0, 2), medpatch: 0.1 },
- RoomKind { name: "the med-bay", weight: 1, artifact: 0.05, salvage: (0, 1), medpatch: 1.0 },
- RoomKind { name: "a machine shop", weight: 2, artifact: 0.05, salvage: (1, 3), medpatch: 0.0 },
- RoomKind { name: "the armoury", weight: 1, artifact: 0.15, salvage: (1, 2), medpatch: 0.3 },
- RoomKind { name: "a cryo-vault", weight: 1, artifact: 0.3, salvage: (0, 1), medpatch: 0.2 },
- RoomKind { name: "the chapel", weight: 1, artifact: 0.5, salvage: (0, 1), medpatch: 0.0 },
- RoomKind { name: "the reliquary", weight: 1, artifact: 0.9, salvage: (0, 0), medpatch: 0.0 },
- RoomKind { name: "the archive", weight: 1, artifact: 0.4, salvage: (0, 1), medpatch: 0.0 },
- RoomKind { name: "the navigator's cell", weight: 1, artifact: 0.5, salvage: (0, 1), medpatch: 0.0 },
- RoomKind { name: "an observation blister", weight: 1, artifact: 0.1, salvage: (0, 1), medpatch: 0.0 },
+ RoomKind { name: "a cargo hold", weight: 4, artifact: 0.05, salvage: (2, 4), medpatch: 0.05, arms: 0.2, source: Source::Tools, armour: 0.1 },
+ RoomKind { name: "the crew berths", weight: 3, artifact: 0.1, salvage: (0, 1), medpatch: 0.25, arms: 0.3, source: Source::Crew, armour: 0.4 },
+ RoomKind { name: "the galley", weight: 2, artifact: 0.02, salvage: (0, 2), medpatch: 0.1, arms: 0.2, source: Source::Crew, armour: 0.05 },
+ RoomKind { name: "the med-bay", weight: 1, artifact: 0.05, salvage: (0, 1), medpatch: 1.0, arms: 0.1, source: Source::Tools, armour: 0.05 },
+ RoomKind { name: "a machine shop", weight: 2, artifact: 0.05, salvage: (1, 3), medpatch: 0.0, arms: 0.5, source: Source::Tools, armour: 0.2 },
+ RoomKind { name: "the armoury", weight: 1, artifact: 0.15, salvage: (1, 2), medpatch: 0.3, arms: 1.0, source: Source::Armoury, armour: 1.0 },
+ RoomKind { name: "a cryo-vault", weight: 1, artifact: 0.3, salvage: (0, 1), medpatch: 0.2, arms: 0.15, source: Source::Crew, armour: 0.15 },
+ RoomKind { name: "the chapel", weight: 1, artifact: 0.5, salvage: (0, 1), medpatch: 0.0, arms: 0.4, source: Source::Rites, armour: 0.35 },
+ RoomKind { name: "the reliquary", weight: 1, artifact: 0.9, salvage: (0, 0), medpatch: 0.0, arms: 0.5, source: Source::Rites, armour: 0.3 },
+ RoomKind { name: "the archive", weight: 1, artifact: 0.4, salvage: (0, 1), medpatch: 0.0, arms: 0.1, source: Source::Rites, armour: 0.05 },
+ RoomKind { name: "the navigator's cell", weight: 1, artifact: 0.5, salvage: (0, 1), medpatch: 0.0, arms: 0.2, source: Source::Crew, armour: 0.15 },
+ RoomKind { name: "an observation blister", weight: 1, artifact: 0.1, salvage: (0, 1), medpatch: 0.0, arms: 0.15, source: Source::Armoury, armour: 0.1 },
];
-/// Salvage and its base value; each find varies by ±30%.
-const SALVAGE: &[(&str, u32)] = &[
- ("a coil of superconductor", 30),
- ("a cracked reactor baffle", 45),
- ("a crate of ration bricks", 10),
- ("an intact navigation core", 90),
- ("a spool of wiring", 15),
- ("a stack of hull plates", 25),
- ("a sealed canister of coolant", 20),
- ("a working servo", 35),
- ("a box of votive candles", 8),
- ("a cantor's data-slate", 40),
- ("a half-charged fuel cell", 50),
+/// Salvage: what it's called, its base value, and what it weighs in grams.
+/// Each find's value varies by ±30%.
+const SALVAGE: &[(&str, u32, u32)] = &[
+ ("coil of superconductor", 30, 6000),
+ ("cracked reactor baffle", 45, 9000),
+ ("crate of ration bricks", 10, 8000),
+ ("intact navigation core", 90, 5000),
+ ("spool of wiring", 15, 3000),
+ ("stack of hull plates", 25, 12000),
+ ("sealed canister of coolant", 20, 7000),
+ ("working servo", 35, 4000),
+ ("box of votive candles", 8, 2000),
+ ("cantor's data-slate", 40, 500),
+ ("half-charged fuel cell", 50, 6000),
];
/// A room as generated, before its contents are placed.
@@ -146,13 +172,14 @@ pub fn generate(seed: u64) -> Derelict {
let kind = plan.kind;
if rng.random_bool(kind.artifact) {
let pos = *cells.choose(&mut rng).unwrap();
- loot.push(Loot { pos, item: Item::Artifact(names::artifact(&mut rng)) });
+ loot.push(Loot::new(pos, Item::Artifact(names::artifact(&mut rng))));
}
for _ in 0..rng.random_range(kind.salvage.0..=kind.salvage.1) {
let pos = *cells.choose(&mut rng).unwrap();
- let &(name, base) = SALVAGE.choose(&mut rng).unwrap();
+ let &(title, base, mass) = SALVAGE.choose(&mut rng).unwrap();
let value = (f64::from(base) * rng.random_range(0.7..1.3)).round() as u32;
- loot.push(Loot { pos, item: Item::Salvage { name, value } });
+ let salvage = Salvage { title: title.into(), value, mass };
+ loot.push(Loot::new(pos, Item::Salvage(salvage)));
}
let medpatches = if kind.medpatch >= 1.0 {
rng.random_range(1..=2)
@@ -160,18 +187,18 @@ pub fn generate(seed: u64) -> Derelict {
u32::from(rng.random_bool(kind.medpatch))
};
for _ in 0..medpatches {
- loot.push(Loot { pos: *cells.choose(&mut rng).unwrap(), item: Item::Medpatch });
+ loot.push(Loot::new(*cells.choose(&mut rng).unwrap(), Item::Medpatch));
}
}
// Every derelict is worth boarding: if no room rolled an artifact, put
// one as far from the airlock as the ship goes.
- if !loot.iter().any(|l| matches!(l.item, Item::Artifact(_))) {
+ if !loot.iter().any(|l| matches!(l.stack.item, Item::Artifact(_))) {
let farthest = map
.positions()
.filter(|&p| free(&map, p))
.max_by_key(|&p| (dist[map.index(p)], p.x, p.y))
.expect("a derelict always has floor");
- loot.push(Loot { pos: farthest, item: Item::Artifact(names::artifact(&mut rng)) });
+ loot.push(Loot::new(farthest, Item::Artifact(names::artifact(&mut rng))));
}
let lairs: Vec<Pos> = map
@@ -188,7 +215,51 @@ pub fn generate(seed: u64) -> Derelict {
if kind.unique && monsters.iter().any(|m| m.kind.name == kind.name) {
continue;
}
- monsters.push(Monster { kind, pos, hp: kind.hp, awake: false });
+ monsters.push(Monster::new(kind, pos));
+ }
+
+ // Weapons roll dice of their own, so arming the derelicts left every
+ // ship's rooms, loot and lairs where they were.
+ let mut rng = seed::rng(seed::derive(seed, "arms", 0));
+ let mut armed = false;
+ for plan in &plans {
+ let cells: Vec<Pos> = plan.cells.iter().copied().filter(|&p| free(&map, p)).collect();
+ if cells.is_empty() {
+ continue;
+ }
+ let kind = plan.kind;
+ let stocked = kind.arms >= 1.0;
+ let n =
+ if stocked { rng.random_range(1..=2) } else { u32::from(rng.random_bool(kind.arms)) };
+ for _ in 0..n {
+ let pos = *cells.choose(&mut rng).unwrap();
+ let weapon = arms::find(&mut rng, kind.source, stocked);
+ loot.push(Loot::new(pos, Item::Weapon(weapon)));
+ armed = true;
+ }
+ }
+ // And there's always something to fight with aboard.
+ if !armed && let Some(plan) = plans.iter().find(|p| p.cells.iter().any(|&c| free(&map, c))) {
+ let cells: Vec<Pos> = plan.cells.iter().copied().filter(|&p| free(&map, p)).collect();
+ let pos = *cells.choose(&mut rng).unwrap();
+ loot.push(Loot::new(pos, Item::Weapon(arms::find(&mut rng, plan.kind.source, false))));
+ }
+
+ // Armour too: armouries and berths mostly, warded pieces in chapels.
+ let mut rng = seed::rng(seed::derive(seed, "armour", 0));
+ for plan in &plans {
+ let cells: Vec<Pos> = plan.cells.iter().copied().filter(|&p| free(&map, p)).collect();
+ if cells.is_empty() {
+ continue;
+ }
+ let kind = plan.kind;
+ let stocked = kind.armour >= 1.0;
+ let n =
+ if stocked { rng.random_range(1..=2) } else { u32::from(rng.random_bool(kind.armour)) };
+ for _ in 0..n {
+ let pos = *cells.choose(&mut rng).unwrap();
+ loot.push(Loot::new(pos, Item::Armor(armour::find(&mut rng, kind.source, stocked))));
+ }
}
Derelict { name, description, map, start, monsters, loot }
@@ -436,13 +507,27 @@ mod tests {
assert!(map.tile(l.pos).walkable(), "seed {seed}: loot in a wall");
assert_ne!(l.pos, d.start);
}
- assert!(d.loot.iter().any(|l| matches!(l.item, Item::Artifact(_))));
+ assert!(d.loot.iter().any(|l| matches!(l.stack.item, Item::Artifact(_))));
+ assert!(
+ d.loot.iter().any(|l| matches!(l.stack.item, Item::Weapon(_))),
+ "seed {seed}: unarmed"
+ );
assert!(map.rooms.len() >= 4, "seed {seed}: only {} rooms", map.rooms.len());
assert!(d.monsters.iter().filter(|m| m.kind.unique).count() <= 1);
}
}
#[test]
+ fn armour_turns_up_in_most_wrecks() {
+ let armoured = (0..200)
+ .filter(|&seed| {
+ generate(seed).loot.iter().any(|l| matches!(l.stack.item, Item::Armor(_)))
+ })
+ .count();
+ assert!((80..195).contains(&armoured), "armour aboard {armoured} of 200 wrecks");
+ }
+
+ #[test]
fn same_seed_same_ship() {
let (a, b) = (generate(99), generate(99));
assert_eq!(a.name, b.name);