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");