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/dungeon/ship.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/dungeon/ship.rs')
| -rw-r--r-- | src/dungeon/ship.rs | 161 |
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); |