From f3a27c4f48d56c8bcef0f0dccd36ec9ceb24939c Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Sat, 22 Aug 2026 08:37:09 -0400 Subject: [PATCH] fix(unit-library): three doc comments state the rule wrongly A small cockpit costs a point of piloting, not gunnery, and a compact heat sink sheds one. The CLI's argument parsing gets tests, --help is checked against the dispatch, and comments that explained a figure with an invented reason give the figure. Change-Id: I0ad475381c7ea77e8eef04a7840fc275781a01d4 --- crates/helm-bv/src/battlearmor.rs | 11 ++- crates/helm-bv/src/defensive.rs | 9 +- crates/helm-bv/src/equipment.rs | 6 +- crates/helm-bv/src/infantry.rs | 3 +- crates/helm-bv/src/machine.rs | 13 ++- crates/helm-bv/src/rules.rs | 2 - crates/helm-bv/src/wear.rs | 15 ++-- crates/helm-cli/src/main.rs | 133 +++++++++++++++++++++++++++++- crates/helm-cli/tests/index.rs | 19 ++++- crates/helm-core/src/design.rs | 21 ++--- crates/helm-cost/src/items.rs | 57 ++++++------- crates/helm-cost/src/lib.rs | 9 +- 12 files changed, 213 insertions(+), 85 deletions(-) diff --git a/crates/helm-bv/src/battlearmor.rs b/crates/helm-bv/src/battlearmor.rs index 2317b8b..ee87591 100644 --- a/crates/helm-bv/src/battlearmor.rs +++ b/crates/helm-bv/src/battlearmor.rs @@ -109,7 +109,7 @@ pub fn read_in<'a>( suit_armor: Some(suit), shape: helm_core::Shape::Biped, is_clan: unit.is_clan(), - // No frame: a suit is armour over a person. + // A suit has no internal structure. structure_points: 0, structure_kind: StructureKind::Standard, armor: vec![("Trooper".to_string(), points, ArmorKind::Standard)], @@ -369,11 +369,10 @@ pub fn systems(loadout: &Loadout<'_>) -> f64 { /// The plate a suit wears, from the numeric code a `.blk` declares. /// -/// Three families. Most plate is worth two and a half a point like anything -/// else. Fire resistant, laser reflective and reactive plate is worth three -/// and a half, because it is proof against a whole kind of weapon. And stealth -/// plate of every sort is worth the ordinary rate and makes the suit harder to -/// see, which is worth more than the plate is. +/// Three families. Most plate is worth two and a half a point. Fire +/// resistant, laser reflective and reactive plate is worth three and a half. +/// Stealth plate of every sort is worth the ordinary rate and adds to the +/// target movement modifier instead. fn suit_armor(unit: &Unit) -> Result { let code: i64 = unit .armor diff --git a/crates/helm-bv/src/defensive.rs b/crates/helm-bv/src/defensive.rs index 5f1329c..e7ef86e 100644 --- a/crates/helm-bv/src/defensive.rs +++ b/crates/helm-bv/src/defensive.rs @@ -75,9 +75,8 @@ pub fn defensive(machine: &Machine<'_>) -> Result /// Armour, per location so that patchwork plate is scored as what it is. fn armor(machine: &Machine<'_>) -> Result { - // A Blue Shield Particle Field Damper is not a shield - it is a field that - // blunts energy fire, and it makes both the plate and the frame worth a - // fifth more. Named like something a Mek holds, and four designs carry one. + // A Blue Shield Particle Field Damper adds a fifth to the plate's + // multiplier and to the frame's. Four designs carry one. let blue_shield = if machine.loadout.has_flag("F_BLUE_SHIELD") { 0.2 } else { @@ -219,8 +218,8 @@ fn explosive(machine: &Machine<'_>) -> f64 { let mut penalty = 0.0; - // A Blue Shield costs a point for every location it cannot protect - it is - // itself the thing that goes off. + // A Blue Shield is itself explosive: a point for every location it does + // not protect. if machine.loadout.has_flag("F_BLUE_SHIELD") { penalty -= BLUE_SHIELD_LOCATIONS .iter() diff --git a/crates/helm-bv/src/equipment.rs b/crates/helm-bv/src/equipment.rs index 8087592..e1f237f 100644 --- a/crates/helm-bv/src/equipment.rs +++ b/crates/helm-bv/src/equipment.rs @@ -161,9 +161,9 @@ pub fn is_shield(entry: &EquipmentEntry) -> bool { /// What a piece of equipment is worth to *this* design. /// /// Most equipment is worth what the catalogue says. A melee weapon is not: a -/// hatchet is worth a fifth of the Mek swinging it, so MegaMek stores a +/// hatchet is worth a fifth of the design's weight, so MegaMek stores a /// sentinel instead of a number and works the value out per unit. Triple -/// strength myomer doubles all of them, which is most of what it is for. +/// strength myomer doubles all of them. pub fn battle_value(entry: &EquipmentEntry, tons: f64, tsm: bool) -> f64 { if !entry.variable_battle_value { return entry.battle_value.unwrap_or(0.0); @@ -645,7 +645,7 @@ mod tests { assert!((ram_plate_battle_value(55.0, 7, 0) - 20.9).abs() < 1e-9); } - // A hatchet is worth a fifth of the Mek swinging it, and twice that with + // A hatchet is worth a fifth of the design's weight, and twice that with // triple-strength myomer. The catalogue cannot hold either number. #[test] fn a_melee_weapon_is_worth_a_share_of_the_mek_carrying_it() { diff --git a/crates/helm-bv/src/infantry.rs b/crates/helm-bv/src/infantry.rs index 02a46c6..d4ce231 100644 --- a/crates/helm-bv/src/infantry.rs +++ b/crates/helm-bv/src/infantry.rs @@ -242,8 +242,7 @@ impl<'a> Platoon<'a> { /// hard they are to hit. fn defensive(&self) -> Defensive { let mut troopers = self.troopers as f64 * 1.5 * self.damage_divisor; - // A trooper who smells like somebody else's platoon is harder to - // shoot at deliberately. + // A pheromone effuser adds a twentieth of the platoon. if self.has("gas_effuser_pheromone") { troopers += self.troopers as f64 * 0.05; } diff --git a/crates/helm-bv/src/machine.rs b/crates/helm-bv/src/machine.rs index 07545e9..aa6b57a 100644 --- a/crates/helm-bv/src/machine.rs +++ b/crates/helm-bv/src/machine.rs @@ -316,15 +316,14 @@ impl<'a> Machine<'a> { /// can have both its ratings exactly right and its battle value wrong by five /// percent. Two hundred and six designs were failing on precisely that. /// -/// A cramped cockpit is worth less because the pilot is fighting it; an -/// interface or a virtual-reality pod is worth much more because the pilot is -/// wired into the machine. +/// A small or torso-mounted cockpit is worth 0.95, an interface cockpit 1.3, +/// and everything else 1.0. fn cockpit_modifier(cockpit: &CockpitKind, is_drone: bool) -> f64 { match cockpit { CockpitKind::Small | CockpitKind::TorsoMounted => 0.95, // Only where the cockpit itself is ordinary. MegaMek tests the drone - // system after the cramped cockpits and before the wired-in ones, so a - // small cockpit that also has one is charged once, not twice. + // system after 0.95 and before 1.3, so a small cockpit that also has + // one is charged once, not twice. _ if is_drone => 0.95, CockpitKind::Interface => 1.3, _ => 1.0, @@ -756,8 +755,8 @@ mod tests { assert!(armor_codes("Nowhere").is_empty()); } - // A drone is worth the same five percent less as a cramped cockpit, and - // MegaMek charges whichever it finds first rather than both. + // A drone and a small cockpit are both 0.95, and MegaMek charges + // whichever it finds first rather than both. #[test] fn a_drone_is_charged_once_even_in_a_small_cockpit() { assert_eq!(cockpit_modifier(&CockpitKind::Standard, true), 0.95); diff --git a/crates/helm-bv/src/rules.rs b/crates/helm-bv/src/rules.rs index 5e8c263..72d52e5 100644 --- a/crates/helm-bv/src/rules.rs +++ b/crates/helm-bv/src/rules.rs @@ -70,7 +70,6 @@ const NONE: Adjustments = Adjustments { pub const RULES: &[Rule] = &[ Rule { name: "battle armour", - // Small, and awkward to hit even standing still. apply: |machine| { if machine.kind == crate::machine::Chassis::BattleArmor { Adjustments { @@ -114,7 +113,6 @@ pub const RULES: &[Rule] = &[ }, Rule { name: "chameleon shield", - // Harder to see, and expensive to run. apply: |machine| { if machine.loadout.has_flag("F_CHAMELEON_SHIELD") { Adjustments { diff --git a/crates/helm-bv/src/wear.rs b/crates/helm-bv/src/wear.rs index 8c1e392..ea3fdb6 100644 --- a/crates/helm-bv/src/wear.rs +++ b/crates/helm-bv/src/wear.rs @@ -256,10 +256,9 @@ fn location_of(key: &str) -> &str { /// Put armour back a point at a time, until the figure comes up to the target. /// -/// Onto the least damaged location first, which buffs out the scratches and -/// leaves the holes: a machine that took a leg full of autocannon should still -/// look like it afterwards. Never onto a location that was shot through - see -/// [`breached`]. +/// Onto the least damaged location first, so the plate that was lost stays +/// lost where most of it was. Never onto a location that was shot through - +/// see [`breached`]. fn patch( unit: &Unit, from: Condition, @@ -303,9 +302,8 @@ fn patch( /// Take armour off a point at a time, from wherever there is most of it left. /// -/// Off the thickest location rather than a fixed one, so the plate that is -/// left stays even - a machine worn to a figure should look like one that took -/// fire, not one with a bald leg. +/// Off the thickest location rather than a fixed one, so what is left stays +/// even across the design. fn refine( from: Condition, target: i64, @@ -412,8 +410,7 @@ mod tests { assert_eq!(condition.armor.get("HD"), Some(&9)); } - /// Evenly, not off one location: a machine worn to a figure should look - /// like one that took fire across the front, not one with a bald leg. + /// Evenly, rather than off one location until it is bare. #[test] fn wear_is_spread_across_the_locations() { let condition = worn_by(&plate(), 500, &[], 0); diff --git a/crates/helm-cli/src/main.rs b/crates/helm-cli/src/main.rs index b132c85..d798573 100644 --- a/crates/helm-cli/src/main.rs +++ b/crates/helm-cli/src/main.rs @@ -7,6 +7,10 @@ //! helm index --megamek --bridge-dir --out units.json //! helm catalogue --bridge-dir --out //! helm force --megamek --bridge-dir +//! helm check --megamek --bridge-dir +//! helm repairs --megamek --bridge-dir +//! helm wear --to --megamek --bridge-dir +//! helm wire-check --megamek --bridge-dir --out //! //! Argument parsing is by hand. A derive-macro dependency would cost more to //! compile than it saves to read at this size. @@ -63,6 +67,8 @@ USAGE helm check --megamek --bridge-dir helm repairs --megamek --bridge-dir helm wear --to --megamek --bridge-dir + helm catalogue --bridge-dir --out + helm wire-check --megamek --bridge-dir --out BUILD --megamek A MegaMek install. Reads data/mekfiles/unit_files.zip, @@ -131,7 +137,7 @@ FORCE ); } -#[derive(Default)] +#[derive(Debug, Default)] struct Opts { megamek: Option, out: Option, @@ -2003,3 +2009,128 @@ fn force(args: &[String]) -> Result<(), String> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every flag consumes itself and its value and nothing else. + /// + /// The parser walks the arguments with an index it steps itself, so a + /// flag that takes a value and steps one lands on its own value and reads + /// it as a flag, and one that takes none and steps two eats whatever + /// follows. Both are silent, and the second is how an argument goes + /// missing between the command line and the run. + /// + /// The test is that **exactly one** of `--flag` and `--flag 1` parses. A + /// flag that wants a value refuses the first and takes the second; one + /// that wants none takes the first and chokes on `1`. A flag that steps + /// wrong accepts both, or neither. + #[test] + fn every_flag_consumes_exactly_what_it_takes() { + for flag in FLAGS { + // `1` because two of them want a number and the rest do not care. + let alone = parse_opts(&[(*flag).to_string()]).is_ok(); + let valued = parse_opts(&[(*flag).to_string(), "1".to_string()]).is_ok(); + assert!( + alone != valued, + "{flag}: parses alone = {alone}, parses with a value = {valued}; \ + one of those has to be false or it is stepping the cursor wrong" + ); + } + } + + /// A flag whose value is missing is an error rather than a default. + #[test] + fn a_flag_with_no_value_says_so() { + for flag in FLAGS { + if parse_opts(&[(*flag).to_string()]).is_ok() { + continue; + } + let e = parse_opts(&[(*flag).to_string()]).unwrap_err(); + assert!(e.contains(flag), "{flag}'s complaint does not name it: {e}"); + } + } + + #[test] + fn an_unknown_flag_is_refused_rather_than_ignored() { + let e = parse_opts(&["--wintermute".to_string()]).unwrap_err(); + assert!(e.contains("--wintermute"), "{e}"); + } + + /// Two flags turn another on, and a caller that gave only the one still + /// gets the behaviour it asked for. + #[test] + fn a_flag_that_implies_another_sets_it() { + assert!( + parse_opts(&["--label".into(), "*".into()]) + .unwrap() + .clusters + ); + assert!(parse_opts(&["--seed".into(), "7".into()]).unwrap().battle); + } + + #[test] + fn a_number_that_is_not_one_is_refused() { + assert!(parse_opts(&["--to".into(), "lots".into()]).is_err()); + assert!(parse_opts(&["--seed".into(), "lots".into()]).is_err()); + } + + /// Every command `main` dispatches is one `--help` mentions. + /// + /// A command nobody can find is a command nobody uses, and the two lists + /// drifted apart twice before this test existed. + #[test] + fn the_usage_text_names_every_command() { + let source = include_str!("main.rs"); + let usage = source + .split_once("USAGE") + .expect("a usage block") + .1 + .split_once("\n\n") + .expect("a usage block that ends") + .0; + for command in COMMANDS { + assert!( + usage.contains(&format!("helm {command}")), + "`helm {command}` is dispatched and not in the usage text" + ); + } + } + + /// The flags `parse_opts` knows, for the tests that ask about all of them. + const FLAGS: &[&str] = &[ + "--megamek", + "--out", + "--bridge-dir", + "--megamek-version", + "--helm-version", + "--unit", + "--clusters", + "--all-types", + "--label", + "--bench", + "--write", + "--to", + "--battle", + "--seed", + "--damaged", + "--why", + "--rules", + ]; + + /// The commands `main` dispatches. + const COMMANDS: &[&str] = &[ + "build", + "stats", + "bv-report", + "art", + "index", + "catalogue", + "wire-check", + "force", + "check", + "repairs", + "wear", + ]; +} diff --git a/crates/helm-cli/tests/index.rs b/crates/helm-cli/tests/index.rs index c25ab46..c5ae337 100644 --- a/crates/helm-cli/tests/index.rs +++ b/crates/helm-cli/tests/index.rs @@ -111,15 +111,28 @@ fn the_index_carries_everything_a_force_fields() { // And nothing else: a gun emplacement, a building and a handheld weapon // are all in the library and none of them is a design a force offers. + // + // The three support types joined the list when helm-bv learned to score + // them; anything else appearing here means the index has widened past + // what can be priced, and a browser would be handed a null. let kinds: std::collections::BTreeSet<&str> = units .iter() .filter_map(|u| u["unit_type"].as_str()) .collect(); assert_eq!( kinds, - ["BattleArmor", "Infantry", "Mek", "Tank", "VTOL"] - .into_iter() - .collect(), + [ + "BattleArmor", + "Infantry", + "LargeSupportTank", + "Mek", + "SupportTank", + "SupportVTOL", + "Tank", + "VTOL", + ] + .into_iter() + .collect(), "the index widened past what helm-bv can price" ); } diff --git a/crates/helm-core/src/design.rs b/crates/helm-core/src/design.rs index 77fabf7..b8db0d2 100644 --- a/crates/helm-core/src/design.rs +++ b/crates/helm-core/src/design.rs @@ -61,9 +61,9 @@ pub struct HeatSinks { /// What one heat sink sheds, by the family it belongs to. /// -/// A double sheds two and a single one, and a compact sheds like a double -/// while filling less of the design - so the kind is not a label, it is the -/// dissipation. +/// A double sheds two and a single one. So does a compact: what it buys is +/// space rather than heat - half a critical slot each, for half a ton more - +/// which is why the kind is read for its dissipation and not for its name. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HeatSinkKind { Single, @@ -150,10 +150,11 @@ pub enum StructureKind { /// The gyro, which decides how many slots it fills and what it is worth. /// -/// A compact fills two of the centre torso's slots and an extra-light six, -/// and the defensive rating pays for the gyro by the ton of design - so a -/// design that armours its gyro is paying for a different share of it -/// depending on which one it has. +/// Standard and heavy-duty fill four of the centre torso's slots, a compact +/// two, and an XL six - counted off the designs in the library, which is +/// where `gyro:XL Gyro` is spelled. The defensive rating pays for the gyro by +/// the ton of design, so a design that armours its gyro is paying for a +/// different share of it depending on which one it has. #[derive(Debug, Clone, PartialEq, Eq)] pub enum GyroKind { Standard, @@ -169,9 +170,9 @@ pub enum GyroKind { /// The cockpit, which decides what the pilot can do and what it costs. /// -/// A small cockpit saves weight and costs the pilot a point of gunnery; an -/// interface cockpit stands in for the gyro, which is why a Ryoken III -/// (Skinwalker) declares none. +/// A small cockpit saves a ton and the head slot life support would fill, and +/// costs the pilot a point of piloting; an interface cockpit stands in for +/// the gyro, which is why a Ryoken III (Skinwalker) declares none. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CockpitKind { Standard, diff --git a/crates/helm-cost/src/items.rs b/crates/helm-cost/src/items.rs index dcda514..7f8794e 100644 --- a/crates/helm-cost/src/items.rs +++ b/crates/helm-cost/src/items.rs @@ -173,16 +173,14 @@ struct Priced { /// the item. Anything not listed is nought, which is a known shortfall rather /// than a claim - see TODO.md. const PRICED_BY_THE_DESIGN: &[Priced] = &[ - // Sized by the design's builder rather than by the design, and the slot - // line says how big: `Communications Equipment:SIZE:2.0` is two tons of - // it at ten thousand a ton. + // Sized by the slot line: `Communications Equipment:SIZE:2.0` is two tons + // at ten thousand a ton. Priced { flag: "F_COMMUNICATIONS", price: |f| f.size() * 10_000.0, }, - // A field hospital is two and a half tons of theatre plus one per bed, - // and a drone carrier's control system two tons plus a half per drone. - // Both at ten thousand a ton, which is the commonest price there is. + // Two and a half tons plus one per bed, and two tons plus a half per + // drone, both at ten thousand a ton. Priced { flag: "F_MASH", price: |f| (2.5 + f.size()) * 10_000.0, @@ -191,9 +189,9 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_DRONE_CARRIER_CONTROL", price: |f| (2.0 + f.size() * 0.5) * 10_000.0, }, - // A small turret is a tenth of what it swings, shared between however - // many of them the design mounts: four light machine guns on one sponson - // is half a ton of ring, which is two thousand C-bills. + // A tenth of the weight it carries, split between the design's rings: + // four light machine guns on one sponson is half a ton, two thousand + // C-bills. Priced { flag: "F_SPONSON_TURRET", price: |f| small_turret(f.unit, f.catalogue, "(ST)", "F_SPONSON_TURRET") * 4_000.0, @@ -202,8 +200,7 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_PINTLE_TURRET", price: |f| small_turret(f.unit, f.catalogue, "(PT)", "F_PINTLE_TURRET") * 1_000.0, }, - // Before `F_MASC`, both of them: a supercharger is strapped to the engine - // and a jet booster is bolted to it, and both carry the myomer flag. + // Both before `F_MASC`: both carry that flag as well. Priced { flag: "S_SUPERCHARGER", price: |f| f.rating * 10_000.0, @@ -212,9 +209,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_JET_BOOSTER", price: |f| f.rating * 10_000.0, }, - // MASC proper is wound through the frame, so it is priced by the engine - // it drives *and* by how much myomer there is: a ton per twenty of the - // design, or per twenty-five where the myomer is Clan. + // Priced by the engine rating and by the design's weight: a ton per + // twenty, or per twenty-five where the design is Clan. Priced { flag: "F_MASC", price: |f| { @@ -222,7 +218,7 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ f.rating * 1_000.0 * (f.tons / per).round().max(1.0) }, }, - // A club is sized by the Mek swinging it. + // Sized by the design's weight, in fifteen-ton steps. Priced { flag: "S_HATCHET", price: |f| (f.tons / 15.0).ceil() * 5_000.0, @@ -231,8 +227,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "S_CLAW", price: |f| f.tons * 200.0, }, - // A sword is forged to the Mek's weight and sold by the ton, rounded up - // to the half ton it is actually made in. + // A twentieth of the design's weight, rounded up to the half ton, at ten + // thousand a ton. Priced { flag: "S_SWORD", price: |f| crate::next_half_ton(f.tons / 20.0) * 10_000.0, @@ -241,14 +237,13 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "S_MACE", price: |_| 130_000.0, }, - // A blade folds into the arm, and the housing is a ton of machinery - // before any of the blade. + // A twentieth of the design's weight plus a ton of housing. Priced { flag: "S_RETRACTABLE_BLADE", price: |f| ((f.tons / 20.0).ceil() + 1.0) * 10_000.0, }, - // Tracks are laid over the whole machine and driven by the whole engine: - // five hundred a point of engine, by the ton, and sold whole. + // Five hundred a point of engine rating per ton of design, and seven + // fifty where the item is a QuadVee's wheels. Priced { flag: "F_TRACKS", price: |f| { @@ -260,8 +255,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ (per * f.rating * f.tons / 75.0).ceil() }, }, - // A wing is built to a fraction of the design's weight and sold by the - // ton. Clan construction gets away with less of it. + // A twentieth of the design's weight where the wing is Clan and seven + // hundredths where it is not, in half tons at fifty thousand each. Priced { flag: "F_PARTIAL_WING", price: |f| { @@ -273,8 +268,7 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_TALON", price: |f| (f.tons / 15.0).ceil() * 300.0, }, - // A remote operating system is a flat fee and a rate: somebody has to fly - // the thing from somewhere. + // Ten thousand flat and a thousand a ton. Priced { flag: "F_DRONE_OPERATING_SYSTEM", price: |f| 10_000.0 + f.tons * 1_000.0, @@ -287,8 +281,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_SPIKES", price: |f| f.tons * 50.0, }, - // An enhanced actuator in a leg is dearer than one in an arm, and the - // slot line is what says which limb it is in. + // Seven hundred a ton in a leg and five hundred in an arm; the slot line + // says which limb it is in. Priced { flag: "F_ACTUATOR_ENHANCEMENT_SYSTEM", price: |f| { @@ -308,8 +302,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ if f.unit.is_mek() { f.tons * 225.0 } else { 0.0 } }, }, - // A hull that swims costs a thousand a ton; one that only wades costs a - // twelfth of that in five-thousand-C-bill steps. + // A thousand a ton fully amphibious. Limited is eight hundredths of the + // tonnage, rounded up, at five thousand each. Priced { flag: "F_FULLY_AMPHIBIOUS", price: |f| f.tons * 1_000.0, @@ -318,9 +312,8 @@ const PRICED_BY_THE_DESIGN: &[Priced] = &[ flag: "F_LIMITED_AMPHIBIOUS", price: |f| (f.tons * 0.08).ceil() * 5_000.0, }, - // Armouring what makes the vehicle move is a tenth of the vehicle where - // the plate is Clan and a seventh where it is not, welded in half tons - // like everything else. + // A tenth of the design's weight where the item is Clan and fifteen + // hundredths where it is not, in half tons at a hundred thousand each. Priced { flag: "F_ARMORED_MOTIVE_SYSTEM", price: |f| { diff --git a/crates/helm-cost/src/lib.rs b/crates/helm-cost/src/lib.rs index 6c15bcb..9a67328 100644 --- a/crates/helm-cost/src/lib.rs +++ b/crates/helm-cost/src/lib.rs @@ -87,12 +87,12 @@ pub fn breakdown(unit: &Unit, catalogue: &Catalogue) -> Result f64 { ((tons * 1e6).round() / 1e6 * 2.0).ceil() / 2.0 } @@ -102,8 +102,7 @@ pub(crate) fn next_half_ton(tons: f64) -> f64 { /// The tonnage is in no file - a `.mtf` writes points and a `.blk` writes /// points - so it is worked back out of them. A ton of standard plate buys /// sixteen points and every other type is a multiple of that, which the -/// catalogue carries, and the result is welded in half tons like everything -/// else. +/// catalogue carries, and the result is rounded to the half ton. pub(crate) fn plate_cost(points: i64, entry: &helm_core::EquipmentEntry) -> f64 { if points <= 0 { return 0.0; -- 2.51.2