From 136a60badf62ffa8160b02fcea5519bd09f070c1 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 21 Aug 2026 17:21:59 -0400 Subject: [PATCH] feat(unit-rules): a transport bay costs money, and its doors cost more A `.blk`'s transporters block was read by nothing. An ordinary cargo bay is free and pays a thousand a door; a berth is priced per person at the tonnage one of them takes up, and a bay that holds a vehicle or a platoon by what it holds. Troop space and battle armour handles are not bays and are not charged for at all. 5,351 of 5,430 Meks and combat vehicles now price what MegaMek says. Change-Id: I2d8ec347c434bab3c648a3ab273d6b5898c4bce6 --- TODO.md | 70 +++++++++++++++++---------- crates/helm-cost/src/lib.rs | 63 ++++++++++++++++++++++++ crates/helm-cost/src/mek.rs | 10 ++++ crates/helm-cost/src/vehicle.rs | 5 +- crates/helm-cost/tests/conformance.rs | 14 ++++-- 5 files changed, 131 insertions(+), 31 deletions(-) diff --git a/TODO.md b/TODO.md index 43a702f..418b360 100644 --- a/TODO.md +++ b/TODO.md @@ -52,10 +52,9 @@ with MegaMek except where a deviation is registered. `CombatVehicleBVCalculator` scores upstream is scored here. 7. **Battle armour and infantry.** Done. The same argument with different calculators, and the reason `LegAttack` and `SwarmMek` are synthesised. -8. **C-bill cost.** 4,146 of 4,249 Meks and 895 of 1,181 combat vehicles - price correctly. What is left is a Mek tail of a hundred, a vehicle tail - of three hundred, and then the chassis nobody has started. See the entry - below. +8. **C-bill cost.** 4,189 of 4,249 Meks and 1,162 of 1,181 combat vehicles + price correctly. What is left is seventy-nine one-off designs and then the + chassis nobody has started. See the entry below. 9. **The Alpha Strike conversion.** The third. It already has a registered deviation, so the shape of the work is known. 10. **Retire `bridge/` for battle value.** Once 8 and 9 exist the JDK container @@ -452,7 +451,7 @@ The long pole, and the reason `helm-core` does no I/O. Tracked as attributes a disagreement three ways - which *term* of the sum differs, which *item* the design carries, and whether it is the multiplier. - **4,146 of 4,249 Meks price correctly.** 36 more are declined rather + **4,189 of 4,249 Meks price correctly.** 36 more are declined rather than guessed at: a LAM or a QuadVee carries conversion gear priced by what it turns into. @@ -478,10 +477,16 @@ The long pole, and the reason `helm-core` does no I/O. Tracked as - A superheavy's frame is its own set of rates rather than a multiple of the ordinary ones - standard and endo steel ten times, endo-composite twice, composite the same - and its actuators and gyro are doubled. - - A PPC capacitor is explosive only once it is charged, and a design on - the shelf never is. MegaMek asks the *mounting* and helm was asking - the equipment type, which bought a Masakari (Warhawk) L a second - location's worth of Clan CASE. + - An **autocannon is explosive only once it has been fired** and a + rotary only once it has jammed. MegaMek asks the *mounting* and helm + was asking the equipment type, which bought a Dark Crow 3 two arms' + worth of Clan CASE it does not carry. + - **Jets are priced off the jump the design has left**, not off the + count of jets: modular armour costs a point of it, and a mechanical + jump booster takes the whole jump over at its own rate. + - MegaMek **rounds a weight to the millionth** before welding it to the + next half ton, and without that seven percent of fifty tons is four + tons rather than three and a half. Two more worth knowing: the Clan CASE count for pricing is not the one battle value uses (battle value skips a location already protected by @@ -489,15 +494,23 @@ The long pole, and the reason `helm-core` does no I/O. Tracked as every *slot* of an armoured component, so the marks have to be divided by what the item takes up. + **A transport bay costs money and its doors cost more**, on either + chassis: an ordinary cargo bay is free and pays a thousand a door, a + berth is priced per person at the tonnage one of them takes up, and a + bay that holds a vehicle or a platoon by what it holds. Troop space and + battle armour handles are not bays and are not charged for at all. That + one rule was 54 designs. + **Combat vehicles** are a second calculation in the same crate, and a shorter one: no cockpit, no gyro, no actuators, and a turret ring - instead. 895 of 1,181 price correctly. Five of its rules are worth + instead. 1,162 of 1,181 price correctly. Seven of its rules are worth knowing: - **The tonnage multiplier is per motion type**, not the Mek's flat hundredth: a hundredth on tracks, a two-hundredth on wheels or on the water, a fiftieth on a skirt of air or under water, a seventy-fifth on - a hydrofoil and a thirtieth under a rotor. + a hydrofoil, a thirtieth under a rotor, a twenty-fifth on a wing that + only works in ground effect and a two-hundred-and-fiftieth on rails. - **The engine is rated for what the suspension does not carry.** A tracked hull carries nothing and a hovercraft's skirt a great deal, on a published table of bands that the cost report implies exactly. @@ -509,26 +522,31 @@ The long pole, and the reason `helm-core` does no I/O. Tracked as and is sold a 50, which is 350 C-bills. - An **omni hull** is a quarter dearer, and a `.blk` says so by writing the `` tag at all rather than by what it writes in it. + - **Each way of moving has its own superheavy ceiling** - fifty tons on + a skirt of air, eighty on wheels or a ground-effect wing, a hundred on + tracks, three hundred on the water, thirty under a rotor - and a hull + past its own is framed twice as heavily. + - **A power amplifier is paid for lasers, PPCs, plasma and flamers** + rather than for every weapon with no ammunition, and the engine's free + heat sinks are five in a fission plant and one in a fuel cell, which + is not the same set as the engines that can fire an energy weapon. Support vehicles are declined: `SupportVehicleCostCalculator` upstream is a different calculation rather than a variation, with its frame priced from a construction table and its own chassis-modification list. - What is left, in the order it is worth doing: - - - **Jump jets on twelve designs**, mostly ones whose jets are not what - `jump mp:` says - a partial wing, a mechanical booster, or the Thunder - Fox TFT-L8's own typo, which `crates/helm-bv/UPSTREAM.md` already - records. - - **A Clan design's CASE count** (23), which is still one out either way - on a handful of designs. - - **The long tail**: a targeting computer (7), compact heat sinks (4), a - head turret (3), a partial wing (3), tracks (3). - - **The vehicle tail** (286): a targeting computer on 38, a sponson - turret on 24, an armoured motive system on 18, a VTOL jet booster on - 11, and smaller counts on the engine, the heat sinks, the frame and - the power amplifiers. The harness attributes each to a term or an - item. + What is left is seventy-nine designs, and it is a flat tail rather than + a rule: no single cause accounts for more than ten of them. In the order + it is worth doing: + + - **A targeting computer on ten designs**, each one step of the ceiling + out, so something is counted as aimed that is not. + - **Patchwork armour** (3): a design with a different plate per location + is priced per location, and helm prices the whole hull at one rate. + - **The rest**: a Clan CASE count still one out either way (5), a + smart robotic control system (4), a Mek's own head turret (3), MASC + (2), and eight designs with one cause each. `ALL=1` on the harness + prints every one of them. - **The other chassis**, each its own `getCost` upstream: battle armour, infantry, ProtoMeks, aerospace, and the support vehicles above. Meks and combat vehicles are 5,430 of the library's 10,988. diff --git a/crates/helm-cost/src/lib.rs b/crates/helm-cost/src/lib.rs index 59e6577..b2b79de 100644 --- a/crates/helm-cost/src/lib.rs +++ b/crates/helm-cost/src/lib.rs @@ -112,3 +112,66 @@ pub fn breakdown(unit: &Unit, catalogue: &Catalogue) -> Result f64 { ((tons * 1e6).round() / 1e6 * 2.0).ceil() / 2.0 } + +/// What a design's transport bays cost. +/// +/// A `.blk`'s `` block lists one bay per line as +/// `type:size:doors:...`. Most of them are free and pay only for their doors, +/// at a thousand each; the ones that hold people or vehicles are charged by +/// what they hold. Seats and quarters have no doors to pay for. +/// +/// Troop space and battle armour handles are not bays and are not charged +/// for at all - MegaMek only walks the bays. +pub(crate) fn bays(unit: &helm_core::Unit) -> f64 { + let Some((_, block)) = unit + .fields + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case("transporters")) + else { + return 0.0; + }; + let mut total = 0.0; + for line in block.lines() { + let mut parts = line.trim().split(':'); + let Some(kind) = parts.next().map(|k| k.trim().to_ascii_lowercase()) else { + continue; + }; + let size = parts + .next() + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0.0); + let doors = parts + .next() + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0.0); + // A seat or a berth is priced per person and has no doors. The number + // of people is the tonnage over what one of them takes up. + let per_person = |tons: f64| (size / tons).floor(); + let (cost, has_doors) = match kind.as_str() { + "standardseats" => (100.0 * size, false), + "pillionseats" => (10.0 * size, false), + "ejectionseats" => (25_000.0 * size, false), + "1stclassquarters" => (30_000.0 * per_person(10.0), false), + "2ndclassquarters" | "crewquarters" => (15_000.0 * per_person(7.0), false), + "steeragequarters" => (5_000.0 * per_person(5.0), false), + "liquidcargobay" => (100.0 * size.ceil(), true), + "refrigeratedcargobay" => (200.0 * size.ceil(), true), + "insulatedcargobay" => (250.0 * size.ceil(), true), + "livestockcargobay" => (2_500.0 * size.ceil(), true), + "infantrybay" => (15_000.0 * size.ceil(), true), + "battlearmorbay" => (15_000.0 * (size * 2.0 * 4.0).ceil(), true), + "mekbay" => (20_000.0 * size, true), + "lightvehiclebay" | "heavyvehiclebay" => (10_000.0 * size, true), + "protomekbay" => (10_000.0 * size.ceil(), true), + // An ordinary cargo bay is free and pays for its doors. Troop + // space and battle armour handles are not bays at all. + "cargobay" => (0.0, true), + _ => continue, + }; + total += cost; + if has_doors { + total += 1_000.0 * doors; + } + } + total +} diff --git a/crates/helm-cost/src/mek.rs b/crates/helm-cost/src/mek.rs index e6efa92..acdd139 100644 --- a/crates/helm-cost/src/mek.rs +++ b/crates/helm-cost/src/mek.rs @@ -523,6 +523,7 @@ fn equipment(unit: &Unit, catalogue: &Catalogue, tons: f64, rating: i64) -> f64 + clan_case(unit, catalogue) + targeting_computer(&items, unit) + integral_compact_sinks(unit, &items) + + crate::bays(unit) } /// What the compact heat sinks inside the engine cost. @@ -732,6 +733,15 @@ pub(crate) fn variable( if entry.has_flag("S_SUPERCHARGER") { return rating * 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 are ten thousand a ton, which is the commonest price there is. + if entry.has_flag("F_MASH") { + return (2.5 + declared_size(unit, entry)) * 10_000.0; + } + if entry.has_flag("F_DRONE_CARRIER_CONTROL") { + return (2.0 + declared_size(unit, entry) * 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. diff --git a/crates/helm-cost/src/vehicle.rs b/crates/helm-cost/src/vehicle.rs index be42773..5faaf4b 100644 --- a/crates/helm-cost/src/vehicle.rs +++ b/crates/helm-cost/src/vehicle.rs @@ -449,7 +449,10 @@ fn equipment( }; total + each * *count as f64 }); - mounted + crate::mek::targeting_computer(loadout, unit) + crate::mek::clan_case(unit, catalogue) + mounted + + crate::mek::targeting_computer(loadout, unit) + + crate::mek::clan_case(unit, catalogue) + + crate::bays(unit) } /// Everything that multiplies the whole price rather than adding to it. diff --git a/crates/helm-cost/tests/conformance.rs b/crates/helm-cost/tests/conformance.rs index 0081499..a419997 100644 --- a/crates/helm-cost/tests/conformance.rs +++ b/crates/helm-cost/tests/conformance.rs @@ -11,10 +11,10 @@ use std::path::PathBuf; /// Designs where our figure is not MegaMek's, at the last measurement. /// /// A ratchet rather than a target: it may fall and may not rise without -/// somebody saying why. It counts two populations at once - 103 Meks of 4,249 -/// and 286 combat vehicles of 1,181 - because they are one figure with two +/// somebody saying why. It counts two populations at once - 60 Meks of 4,249 +/// and 19 combat vehicles of 1,181 - because they are one figure with two /// calculations behind it. -const DIFFERING: usize = 389; +const DIFFERING: usize = 79; #[test] #[ignore = "needs a MegaMek install and a bridge dump; set HELM_MEGAMEK and HELM_BRIDGE"] @@ -308,7 +308,13 @@ fn cost_agrees_with_megamek() { println!(" {n:>6} {name}"); } worst.sort_by(|a, b| b.0.total_cmp(&a.0)); - for (_, line) in worst.iter().take(20) { + // The twenty furthest out, or every one of them with `ALL=1`. + let show = if std::env::var("ALL").is_ok() { + worst.len() + } else { + 20 + }; + for (_, line) in worst.iter().take(show) { println!(" {line}"); } assert!(total > 4_000, "only {total} designs were checked"); -- 2.51.2