//! 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"); } } }