summaryrefslogtreecommitdiff
path: root/src/items.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/items.rs')
-rw-r--r--src/items.rs728
1 files changed, 728 insertions, 0 deletions
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::<u32>()
+}
+
+/// 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: 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<Stack>, 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<Slot, Armor>,
+ #[serde(default)]
+ pub pack: Vec<Stack>,
+ /// 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<Stack> {
+ 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<Item = &Weapon> {
+ 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<Item = &mut Armor> {
+ 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<String, String> {
+ 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<String, String> {
+ 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<String, String> {
+ 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<String, String> {
+ 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<usize> {
+ 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<Stack, String> {
+ 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<Weapon>) -> Gear {
+ let packed = self.pack.iter().filter_map(|s| match &s.item {
+ Item::Weapon(w) => Some(w.clone()),
+ _ => None,
+ });
+ let mut own: Vec<Weapon> = [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");
+ }
+ }
+}