From 9ccf951473e21a8b90c25161156fa09a9b227a7b Mon Sep 17 00:00:00 2001 From: skylar Date: Wed, 11 Nov 2026 15:24:37 -0800 Subject: 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 --- src/items.rs | 728 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 728 insertions(+) create mode 100644 src/items.rs (limited to 'src/items.rs') diff --git a/src/items.rs b/src/items.rs new file mode 100644 index 0000000..6c38ecd --- /dev/null +++ b/src/items.rs @@ -0,0 +1,728 @@ +//! Things you can carry: weapons, armour, food, medpatches, artifacts and +//! salvage. What each weighs, how they stack, where they go on you, and the +//! piles they lie in: your pack, the ship's lockers, the floor. +//! +//! Weights are in grams. Everything on you counts, worn, ready and packed, +//! up to `Gear::capacity`; shipyards sell bigger packs. Your own cutting +//! torch and rivet gun are always on you: they can't be dropped, stored or +//! lost. +//! +//! Armour goes on your head, body, hands and feet, each piece guarding +//! against its own kinds of blow (see `armour`). Food is ready for its +//! first items: eating works, but nothing is hungry yet. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +pub use crate::dungeon::armour::Armor; +use crate::dungeon::arms::{self, Weapon}; +use crate::names::{self, Artifact}; +use crate::seed; + +/// What you can carry to begin with, in grams. +pub const BASE_CAPACITY: u32 = 25_000; +/// What the ship's lockers hold, in grams. +pub const LOCKER_CAPACITY: u32 = 400_000; +const MEDPATCH_MASS: u32 = 150; + +/// What a bigger pack is called, how much more it lets you carry and what a +/// shipyard asks for it, in the order you can buy them. +pub const PACKS: [(&str, u32, u32); 2] = [("frame pack", 10_000, 300), ("hauler rig", 10_000, 650)]; + +/// The next bigger pack you could buy, if there is one. +pub fn next_pack(capacity: u32) -> Option<(&'static str, u32, u32)> { + let mut have = BASE_CAPACITY; + for pack in PACKS { + if capacity < have + pack.1 { + return Some(pack); + } + have += pack.1; + } + None +} + +/// The most you can carry with every pack there is. +pub fn max_capacity() -> u32 { + BASE_CAPACITY + PACKS.iter().map(|p| p.1).sum::() +} + +/// Grams as kilograms, to a decimal place: "2.1". +pub fn kg(grams: u32) -> String { + format!("{:.1}", f64::from(grams) / 1000.0) +} + +/// What sort of thing something is, in the order the inventory lists them. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Category { + Weapon, + Armor, + Food, + Medical, + Artifact, + Salvage, +} + +impl Category { + pub const ALL: [Category; 6] = [ + Category::Weapon, + Category::Armor, + Category::Food, + Category::Medical, + Category::Artifact, + Category::Salvage, + ]; + + pub fn heading(self) -> &'static str { + match self { + Category::Weapon => "WEAPONS", + Category::Armor => "ARMOR", + Category::Food => "FOOD", + Category::Medical => "MEDICAL", + Category::Artifact => "ARTIFACTS", + Category::Salvage => "SALVAGE", + } + } +} + +/// Where on you a piece of armour goes. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Slot { + Head, + Body, + Hands, + Feet, +} + +impl Slot { + pub const ALL: [Slot; 4] = [Slot::Head, Slot::Body, Slot::Hands, Slot::Feet]; + + pub fn name(self) -> &'static str { + match self { + Slot::Head => "head", + Slot::Body => "body", + Slot::Hands => "hands", + Slot::Feet => "feet", + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Food { + /// "ration bar", without an article. + pub title: String, + pub description: String, + /// How much it fills you. Nothing gets hungry yet. + pub nourishment: u32, + pub mass: u32, +} + +/// Something worth money and nothing else: sell it to a broker or a station. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Salvage { + /// "coil of superconductor", without an article. + pub title: String, + /// What it's worth, before what a buyer makes of it. + pub value: u32, + pub mass: u32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Item { + Weapon(Weapon), + Armor(Armor), + Food(Food), + Medpatch, + Artifact(Artifact), + Salvage(Salvage), +} + +impl Item { + pub fn category(&self) -> Category { + match self { + Item::Weapon(_) => Category::Weapon, + Item::Armor(_) => Category::Armor, + Item::Food(_) => Category::Food, + Item::Medpatch => Category::Medical, + Item::Artifact(_) => Category::Artifact, + Item::Salvage(_) => Category::Salvage, + } + } + + /// What one weighs, in grams. + pub fn mass(&self) -> u32 { + match self { + Item::Weapon(w) => w.mass, + Item::Armor(a) => a.mass, + Item::Food(f) => f.mass, + Item::Medpatch => MEDPATCH_MASS, + Item::Artifact(a) => artifact_mass(a), + Item::Salvage(s) => s.mass, + } + } + + /// What it's called in a list, without an article where it has one: + /// "serrated cleaver", "humming ochre lattice", "Khaseem's Mercy". + pub fn label(&self) -> String { + match self { + Item::Weapon(w) => w.title.clone(), + Item::Armor(a) => a.title.clone(), + Item::Food(f) => f.title.clone(), + Item::Medpatch => "medpatch".into(), + Item::Artifact(a) => { + let name = a.name(); + name.strip_prefix("a ").or(name.strip_prefix("an ")).unwrap_or(name).to_string() + } + Item::Salvage(s) => s.title.clone(), + } + } + + /// One of it, in a sentence: "a serrated cleaver", "your cutting + /// torch", "the Lacquered Orrery of Teshemond". + pub fn name(&self) -> String { + match self { + Item::Weapon(w) => w.name(), + Item::Armor(a) => a.name(), + Item::Artifact(a) => a.name().to_string(), + _ => names::a(&self.label()), + } + } + + /// `n` of it, in a sentence: "a medpatch", "3 medpatches". + pub fn counted(&self, n: u32) -> String { + if n == 1 { self.name() } else { format!("{n} {}", plural(&self.label())) } + } + + /// Whether more than one of it pile up as one. + pub fn stacks(&self) -> bool { + matches!(self, Item::Food(_) | Item::Medpatch | Item::Salvage(_)) + } + + /// Your own torch or rivet gun: never dropped, stored or lost. + pub fn is_kit(&self) -> bool { + matches!(self, Item::Weapon(w) if w.kit) + } +} + +/// What an artifact weighs, fixed by its true name: from a carved token to +/// a reliquary you need both arms for. +fn artifact_mass(a: &Artifact) -> u32 { + let k = seed::derive(0, &a.true_name, 1) % 1000; + 300 + (k as u32 * k as u32) / 220 +} + +/// "medpatches", "coils of superconductor". +pub fn plural(label: &str) -> String { + if let Some((head, tail)) = label.split_once(" of ") { + return format!("{} of {tail}", plural(head)); + } + let consonant_y = label.ends_with('y') + && !label.ends_with("ay") + && !label.ends_with("ey") + && !label.ends_with("oy") + && !label.ends_with("uy"); + if consonant_y { + format!("{}ies", &label[..label.len() - 1]) + } else if ["s", "x", "z", "ch", "sh"].iter().any(|end| label.ends_with(end)) { + format!("{label}es") + } else { + format!("{label}s") + } +} + +/// Some number of one thing. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Stack { + pub item: Item, + pub count: u32, +} + +impl Stack { + pub fn one(item: Item) -> Stack { + Stack { item, count: 1 } + } + + pub fn mass(&self) -> u32 { + self.item.mass().saturating_mul(self.count) + } + + /// "3 medpatches", "a serrated cleaver". + pub fn name(&self) -> String { + self.item.counted(self.count) + } +} + +/// Adds `stack` to a pile, onto a stack of the same thing if there is one. +pub fn stow(pile: &mut Vec, stack: Stack) { + if stack.count == 0 { + return; + } + if stack.item.stacks() + && let Some(same) = pile.iter_mut().find(|s| s.item == stack.item) + { + same.count += stack.count; + return; + } + pile.push(stack); +} + +/// Takes up to `count` of the stack at `i` out of a pile. +pub fn take(pile: &mut Vec, i: usize, count: u32) -> Stack { + let count = count.min(pile[i].count); + if count == pile[i].count { + pile.remove(i) + } else { + pile[i].count -= count; + Stack { item: pile[i].item.clone(), count } + } +} + +pub fn pile_mass(pile: &[Stack]) -> u32 { + pile.iter().map(Stack::mass).sum() +} + +/// How many of `item` fit in `room` grams. +pub fn how_many_fit(item: &Item, room: u32) -> u32 { + room.checked_div(item.mass()).unwrap_or(u32::MAX) +} + +/// Where something is on you. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Spot { + Worn(Slot), + /// The weapon you hit things with. + Melee, + /// The gun you fire. + Gun, + /// A stack in your pack, by index. + Pack(usize), +} + +/// Everything you carry: what you wear, your two ready weapons, and your +/// pack. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Gear { + pub melee: Weapon, + pub gun: Weapon, + #[serde(default)] + pub worn: BTreeMap, + #[serde(default)] + pub pack: Vec, + /// How much you can carry, all told, in grams. + pub capacity: u32, +} + +impl Default for Gear { + fn default() -> Gear { + Gear { + melee: arms::cutting_torch(), + gun: arms::rivet_gun(), + worn: BTreeMap::new(), + pack: Vec::new(), + capacity: BASE_CAPACITY, + } + } +} + +impl Gear { + /// What everything on you weighs, in grams. + pub fn carried(&self) -> u32 { + let worn: u32 = self.worn.values().map(|a| a.mass).sum(); + self.melee.mass + self.gun.mass + worn + pile_mass(&self.pack) + } + + /// How many more grams you could carry. + pub fn room(&self) -> u32 { + self.capacity.saturating_sub(self.carried()) + } + + /// How many of `item` you could take on. + pub fn fits(&self, item: &Item) -> u32 { + how_many_fit(item, self.room()) + } + + /// What's at `spot`, as a stack, for showing. + pub fn get(&self, spot: Spot) -> Option { + match spot { + Spot::Worn(slot) => self.worn.get(&slot).map(|a| Stack::one(Item::Armor(a.clone()))), + Spot::Melee => Some(Stack::one(Item::Weapon(self.melee.clone()))), + Spot::Gun => Some(Stack::one(Item::Weapon(self.gun.clone()))), + Spot::Pack(i) => self.pack.get(i).cloned(), + } + } + + /// Puts `stack` in your pack, whatever it weighs. + pub fn add(&mut self, stack: Stack) { + stow(&mut self.pack, stack); + } + + /// Every weapon on you, ready or packed. + #[cfg(test)] + pub fn weapons(&self) -> impl Iterator { + let packed = self.pack.iter().filter_map(|s| match &s.item { + Item::Weapon(w) => Some(w), + _ => None, + }); + [&self.melee, &self.gun].into_iter().chain(packed) + } + + pub fn medpatches(&self) -> u32 { + self.pack.iter().filter(|s| s.item == Item::Medpatch).map(|s| s.count).sum() + } + + /// Takes a medpatch out of your pack to use, if you have one. + pub fn take_medpatch(&mut self) -> bool { + let Some(i) = self.pack.iter().position(|s| s.item == Item::Medpatch) else { + return false; + }; + take(&mut self.pack, i, 1); + true + } + + /// What everything you wear takes off a blow of `dmg`, as it is now. + #[cfg(test)] + pub fn guard(&self, dmg: arms::Dmg) -> i32 { + self.worn.values().map(|a| a.guard(dmg)).sum() + } + + /// Every piece of armour on you, worn or packed. + pub fn armour_mut(&mut self) -> impl Iterator { + let packed = self.pack.iter_mut().filter_map(|s| match &mut s.item { + Item::Armor(a) => Some(a), + _ => None, + }); + self.worn.values_mut().chain(packed) + } + + /// Readies the weapon at `i` in your pack: a gun to fire, anything + /// else to hit things with. What was ready goes in its place. + pub fn ready(&mut self, i: usize) -> Result { + let Some(Stack { item: Item::Weapon(w), .. }) = self.pack.get(i) else { + return Err("That's not something to fight with.".into()); + }; + let gun = w.is_gun(); + let Item::Weapon(w) = take(&mut self.pack, i, 1).item else { unreachable!() }; + let yours = w.yours(); + let hand = if gun { &mut self.gun } else { &mut self.melee }; + let old = std::mem::replace(hand, w); + self.add(Stack::one(Item::Weapon(old))); + Ok(format!("You ready {yours}.")) + } + + /// Puts on the armour at `i` in your pack, taking off whatever was there. + pub fn wear(&mut self, i: usize) -> Result { + let Some(Stack { item: Item::Armor(_), .. }) = self.pack.get(i) else { + return Err("You can't wear that.".into()); + }; + let Item::Armor(a) = take(&mut self.pack, i, 1).item else { unreachable!() }; + let what = format!("You put on {}.", a.name()); + if let Some(old) = self.worn.insert(a.slot, a) { + self.add(Stack::one(Item::Armor(old))); + } + Ok(what) + } + + /// Takes off what you wear on `slot`, into your pack. + pub fn take_off(&mut self, slot: Slot) -> Result { + let Some(a) = self.worn.remove(&slot) else { + return Err(format!("You've nothing on your {}.", slot.name())); + }; + let what = format!("You take off {}.", a.yours()); + self.add(Stack::one(Item::Armor(a))); + Ok(what) + } + + /// Eats one of the food at `i` in your pack. + pub fn eat(&mut self, i: usize) -> Result { + let Some(Stack { item: Item::Food(f), .. }) = self.pack.get(i) else { + return Err("You can't eat that.".into()); + }; + let what = format!("You eat {}.", names::a(&f.title)); + take(&mut self.pack, i, 1); + Ok(what) + } + + /// Where in your pack the weapon is that would take the place of the + /// gun (or the one to hit with) if you put that down: your own, if it's + /// there, or else the first. + pub fn spare(&self, gun: bool) -> Option { + let fits = |s: &Stack, own: bool| matches!(&s.item, Item::Weapon(w) if w.is_gun() == gun && (w.kit || !own)); + let own = self.pack.iter().position(|s| fits(s, true)); + own.or_else(|| self.pack.iter().position(|s| fits(s, false))) + } + + /// Takes up to `count` of what's at `spot` off you, to drop or store. + /// Put down a weapon you had ready and another from your pack comes out + /// in its place; with nothing else to fight with, you keep it. + pub fn remove(&mut self, spot: Spot, count: u32) -> Result { + match spot { + Spot::Worn(slot) => match self.worn.remove(&slot) { + Some(a) => Ok(Stack::one(Item::Armor(a))), + None => Err(format!("You've nothing on your {}.", slot.name())), + }, + Spot::Melee | Spot::Gun => { + let gun = spot == Spot::Gun; + let Some(i) = self.spare(gun) else { + let what = if gun { "shoot with" } else { "hit things with" }; + return Err(format!( + "You'd have nothing to {what}. Ready something else first." + )); + }; + let Item::Weapon(spare) = take(&mut self.pack, i, 1).item else { unreachable!() }; + let hand = if gun { &mut self.gun } else { &mut self.melee }; + Ok(Stack::one(Item::Weapon(std::mem::replace(hand, spare)))) + } + Spot::Pack(i) => match self.pack.get(i) { + None => Err("There's nothing there.".into()), + Some(_) => Ok(take(&mut self.pack, i, count.max(1))), + }, + } + } + + pub fn reload_all(&mut self) { + self.melee.reload(); + self.gun.reload(); + for s in &mut self.pack { + if let Item::Weapon(w) = &mut s.item { + w.reload(); + } + } + } + + /// What's left when everything you carried stays aboard a wreck: your + /// own torch and rivet gun, if you had them on you. A hand left with + /// neither gets yours from `elsewhere` (the lockers), or failing that a + /// spare from the ship's stores: you never go about with nothing to + /// fight with. + pub fn stripped(&self, mut elsewhere: impl FnMut(bool) -> Option) -> Gear { + let packed = self.pack.iter().filter_map(|s| match &s.item { + Item::Weapon(w) => Some(w.clone()), + _ => None, + }); + let mut own: Vec = [self.melee.clone(), self.gun.clone()] + .into_iter() + .chain(packed) + .filter(|w| w.kit) + .collect(); + let mut hand = |gun: bool| match own.iter().position(|w| w.is_gun() == gun) { + Some(i) => own.remove(i), + None => elsewhere(gun) + .unwrap_or_else(|| if gun { arms::rivet_gun() } else { arms::cutting_torch() }), + }; + let (melee, gun) = (hand(false), hand(true)); + let mut gear = Gear { melee, gun, capacity: self.capacity, ..Gear::default() }; + for w in own { + gear.add(Stack::one(Item::Weapon(w))); + } + gear + } + + /// Puts right whatever a save file got wrong: the right sort of weapon + /// ready in each hand, sensible numbers, nothing empty, no more packs + /// than there are. + pub fn mend(&mut self) { + self.capacity = self.capacity.clamp(BASE_CAPACITY, max_capacity()); + self.pack.retain(|s| s.count > 0); + if self.melee.is_gun() && !self.gun.is_gun() { + std::mem::swap(&mut self.melee, &mut self.gun); + } + for gun in [false, true] { + let hand = if gun { &self.gun } else { &self.melee }; + if hand.is_gun() == gun { + continue; + } + let right = match self.spare(gun) { + Some(i) => match take(&mut self.pack, i, 1).item { + Item::Weapon(w) => w, + _ => unreachable!(), + }, + None if gun => arms::rivet_gun(), + None => arms::cutting_torch(), + }; + let hand = if gun { &mut self.gun } else { &mut self.melee }; + let wrong = std::mem::replace(hand, right); + self.add(Stack::one(Item::Weapon(wrong))); + } + for s in &mut self.pack { + if !s.item.stacks() { + s.count = 1; + } + if let Item::Weapon(w) = &mut s.item { + w.mend(); + } + } + self.melee.mend(); + self.gun.mend(); + for (slot, a) in &mut self.worn { + a.slot = *slot; + } + for a in self.armour_mut() { + a.mend(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dungeon::arms::{Dmg, Source}; + + fn found(gun: bool) -> Weapon { + let mut rng = seed::rng(3); + (0..) + .map(|_| arms::find(&mut rng, Source::Armoury, false)) + .find(|w| w.is_gun() == gun) + .unwrap() + } + + fn helmet() -> Armor { + Armor { + title: "patched vac-helmet".into(), + description: "a patched vac-helmet".into(), + slot: Slot::Head, + guards: [(Dmg::Pierce, 1), (Dmg::Blunt, 1)].into_iter().collect(), + stanches: false, + durability: 20, + max_durability: 20, + condition: arms::Condition::Worn, + named: false, + pair: false, + mass: 1800, + } + } + + #[test] + fn like_things_stack_and_unlike_ones_dont() { + let mut pile = Vec::new(); + stow(&mut pile, Stack::one(Item::Medpatch)); + stow(&mut pile, Stack { item: Item::Medpatch, count: 2 }); + stow(&mut pile, Stack::one(Item::Weapon(found(false)))); + stow(&mut pile, Stack::one(Item::Weapon(found(false)))); + assert_eq!(pile.len(), 3); + assert_eq!(pile[0].count, 3); + assert_eq!(take(&mut pile, 0, 2).count, 2); + assert_eq!(pile[0].count, 1); + assert_eq!(take(&mut pile, 0, 5).count, 1, "no more than there is"); + assert_eq!(pile.len(), 2); + } + + #[test] + fn names_count_and_pluralise() { + assert_eq!(Item::Medpatch.counted(1), "a medpatch"); + assert_eq!(Item::Medpatch.counted(3), "3 medpatches"); + assert_eq!(plural("coil of superconductor"), "coils of superconductor"); + assert_eq!(plural("box of votive candles"), "boxes of votive candles"); + assert_eq!(plural("battery"), "batteries"); + } + + #[test] + fn readying_swaps_with_what_was_ready() { + let mut gear = Gear::default(); + let blade = found(false); + let carried = gear.carried(); + gear.add(Stack::one(Item::Weapon(blade.clone()))); + assert_eq!(gear.carried(), carried + blade.mass); + gear.ready(0).unwrap(); + assert_eq!(gear.melee, blade); + assert!(matches!(&gear.pack[0].item, Item::Weapon(w) if w.kit && !w.is_gun())); + // Your own torch can go too, once it's not in your hand. + let torch = gear.remove(Spot::Pack(0), 1).unwrap(); + assert!(gear.remove(Spot::Melee, 1).is_err(), "not with nothing else to hit with"); + assert!(gear.remove(Spot::Gun, 1).is_err(), "nor to shoot with"); + // Put the blade down and what's in your pack comes out in its place. + gear.add(torch); + let dropped = gear.remove(Spot::Melee, 1).unwrap(); + assert_eq!(dropped.item, Item::Weapon(blade)); + assert!(gear.melee.kit && gear.pack.is_empty()); + } + + #[test] + fn stripped_you_keep_your_own_and_never_go_unarmed() { + let mut gear = Gear::default(); + gear.add(Stack::one(Item::Weapon(found(false)))); + gear.add(Stack { item: Item::Medpatch, count: 3 }); + let kept = gear.stripped(|_| None); + assert_eq!(kept, Gear::default(), "your own, and nothing else"); + // With your torch left in the lockers, it comes out of them. + gear.ready(0).unwrap(); + let torch = gear.remove(Spot::Pack(gear.spare(false).unwrap()), 1).unwrap(); + let mut lockers = vec![torch]; + let kept = gear.stripped(|gun| match (gun, lockers.pop()) { + (false, Some(Stack { item: Item::Weapon(w), .. })) => Some(w), + _ => None, + }); + assert!(kept.melee.kit && kept.gun.kit && kept.pack.is_empty()); + assert!(lockers.is_empty()); + } + + #[test] + fn armour_goes_on_and_comes_off() { + let mut gear = Gear::default(); + gear.add(Stack::one(Item::Armor(helmet()))); + assert_eq!(gear.wear(0).unwrap(), "You put on a patched vac-helmet."); + assert_eq!((gear.guard(Dmg::Pierce), gear.guard(Dmg::Cut)), (1, 0)); + assert!(gear.pack.is_empty()); + let mut better = helmet(); + better.guards.insert(Dmg::Pierce, 2); + gear.add(Stack::one(Item::Armor(better))); + gear.wear(0).unwrap(); + assert_eq!(gear.guard(Dmg::Pierce), 2, "the better helmet went on"); + assert_eq!(gear.pack.len(), 1, "and the old one came off"); + assert_eq!(gear.take_off(Slot::Head).unwrap(), "You take off your patched vac-helmet."); + assert_eq!((gear.guard(Dmg::Pierce), gear.pack.len()), (0, 2)); + assert!(gear.take_off(Slot::Feet).is_err()); + } + + #[test] + fn food_is_eaten_one_at_a_time() { + let mut gear = Gear::default(); + let bar = Food { + title: "ration bar".into(), + description: "dense and faintly sweet".into(), + nourishment: 1, + mass: 200, + }; + gear.add(Stack { item: Item::Food(bar), count: 2 }); + assert_eq!(gear.eat(0).unwrap(), "You eat a ration bar."); + assert_eq!(gear.pack[0].count, 1); + gear.eat(0).unwrap(); + assert!(gear.pack.is_empty()); + assert!(gear.eat(0).is_err()); + } + + #[test] + fn you_carry_what_you_have_room_for() { + let gear = Gear::default(); + assert!(gear.carried() < gear.capacity); + assert_eq!(gear.fits(&Item::Medpatch), gear.room() / MEDPATCH_MASS); + assert_eq!(next_pack(BASE_CAPACITY).unwrap().0, "frame pack"); + assert_eq!(next_pack(BASE_CAPACITY + PACKS[0].1).unwrap().0, "hauler rig"); + assert_eq!(next_pack(max_capacity()), None); + } + + #[test] + fn mending_puts_your_own_kit_back() { + let mut gear = Gear::default(); + let (blade, gun) = (found(false), found(true)); + gear.melee = gun.clone(); + gear.gun = blade.clone(); + gear.pack.clear(); + gear.capacity = 0; + gear.mend(); + assert!(!gear.melee.is_gun() && gear.gun.is_gun()); + assert_eq!(gear.weapons().count(), 2, "swapped, with nothing made up"); + assert!(gear.weapons().any(|w| *w == blade) && gear.weapons().any(|w| *w == gun)); + assert_eq!(gear.capacity, BASE_CAPACITY); + } + + #[test] + fn artifacts_weigh_something_sensible() { + let mut rng = seed::rng(5); + for _ in 0..200 { + let mass = Item::Artifact(names::artifact(&mut rng)).mass(); + assert!((300..=5000).contains(&mass), "{mass} g"); + } + } +} -- cgit v1.2.3