From f034b675f0453d268ab88c85f14dfa021a09a6a0 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 16:16:05 -0400 Subject: [PATCH] refactor(unit-rules): give the movement rules a name of their own Mek::read_in was 238 lines and the hundred in the middle were the most tangled reading in the crate - a partial wing adding jump and heat and costing neither, a shield taking both away, myomer adding walking that a supercharger then multiplies. Reading it beside the armour and the cockpit made all three harder to follow than any of them is. Now 112 lines, and every design scores the same. Change-Id: Ic6575df02d5384b8bde9e6c21d204e6b8003a1f1 --- crates/helm-bv/src/mek.rs | 350 ++++++++++++++++++++++---------------- 1 file changed, 208 insertions(+), 142 deletions(-) diff --git a/crates/helm-bv/src/mek.rs b/crates/helm-bv/src/mek.rs index 2f6b43a..45b1e88 100644 --- a/crates/helm-bv/src/mek.rs +++ b/crates/helm-bv/src/mek.rs @@ -123,141 +123,18 @@ impl<'a> Mek<'a> { defensive::engine_side_torso_slots(&engine.kind, engine.clan, tons > 100.0) .ok_or(Unsupported::NotImplemented("this engine type"))?; - // Sprint gear is part of what a Mek can do when it is trying, and - // battle value is worked out on that: a Celerity runs 24 without MASC - // and 32 with, which is two steps of movement modifier. - // - // A supercharger carries the MASC flag as well, so "has F_MASC" is - // true for a design with only a supercharger. One or the other doubles - // the walk; both together is two and a half times, and reading the - // flag alone gives every supercharged Mek the pair's figure. - // A supercharger is itself MASC-flagged, so "does the design have - // MASC" answers yes to one carrying only a supercharger. Asked per - // item instead: a design with both gets the pair's multiplier, which a - // design-level test cannot express - a Rime Otter C carries both and - // runs 13, not 10. - let supercharger = loadout.has_flag("S_SUPERCHARGER"); - let masc = loadout.has_flag_without("F_MASC", "S_SUPERCHARGER"); - let sprint = match (masc, supercharger) { - (true, true) => 2.5, - (true, false) | (false, true) => 2.0, - (false, false) => 1.5, - }; - // Triple-strength myomer is worth a point of walking. Battle value is - // worked out with it running rather than only when the Mek is hot - // enough to trigger it, and it compounds: one more point of walk is - // one and a half more of running, which on a Banshee BNC-8S is the - // difference between a movement factor of 1.2 and 1.3. - let has_tsm = loadout.has_flag("F_TSM") || myomer == Myomer::TripleStrength; - let has_industrial_tsm = - loadout.has_flag("F_INDUSTRIAL_TSM") || myomer == Myomer::IndustrialTripleStrength; - let walk = walk + i64::from(has_tsm && !has_industrial_tsm); - // A shield is heavy and in the way: it costs walking as well as - // jumping. - let walk = (walk - - loadout.count_of("S_SHIELD_MEDIUM") as i64 - - loadout.count_of("S_SHIELD_LARGE") as i64) - .max(0); - - // Hardened plate is heavy enough to cost a point of movement. - let hardened = if armor_kind == ArmorKind::Hardened { - 1 - } else { - 0 - }; - let run_mp = ((walk as f64 * sprint).ceil() as i64 - hardened).max(0); - - // A partial wing does three things and they pull in different - // directions, so all three are worked out here rather than wherever - // each is used. It adds jump distance, it adds heat capacity, and the - // distance it adds is free of heat - the jets are only charged for - // what they lifted on their own. - // - // The file's `jump mp:` is the jets alone; MegaMek adds the wing's - // bonus on top, which is how a Flamberge 2 declares 6 and is scored at - // 7. Light and medium designs get two, heavier ones one. - let wing_jump_bonus = if loadout.has_flag("F_PARTIAL_WING") { - partial_wing_jump_bonus(tons) - } else { - 0 - }; - let wing_heat_bonus = if loadout.has_flag("F_PARTIAL_WING") { - 3 - } else { - 0 - }; - - // Jump, in MegaMek's own order, because the order changes answers. - // - // A partial wing amplifies lift rather than providing it, so it does - // nothing for a design with no jump jets - a Jade Phoenix D has none - // and was being given a point of jump it does not have. The wing is - // added before a medium shield and modular armour take their points - // off, and a large shield stops the design jumping at all. - // Counted from the jets themselves when a wing is fitted, because the - // `jump mp:` line is not consistent about whether it already includes - // the wing's contribution: a Jade Hawk JHK-03 has three jets and - // declares four, where a Flamberge 2 has six and declares six. The - // jets are the thing that does not lie. - // Underwater movement units are fitted where jump jets would go, and - // a `.mtf` declares their movement on the same `jump mp:` line - so a - // design with UMUs jumps nowhere at all. - let umu_mp = loadout.count_of("F_UMU") as i64; - - // Counted from the jets, because that is what MegaMek counts. - // `getJumpMP` calls `getJumpJetMovementPoints`, and the `jump mp:` - // line is parsed into the design and then never read for movement at - // all - so whatever it says, the hardware is the answer. - // - // Which is just as well, because it says different things. A Flamberge - // 2 has six jets and declares six; a Jade Hawk JHK-03 has three and - // declares four, its partial wing's point folded in; a Thunder Fox - // TFT-L8 has five and declares four, which looks like a plain mistake - // in the file - its own text says the jets were increased to match its - // cruise of five. MegaMek is immune to all three and so is this. - // - // The declared figure is the fallback only for a design carrying no - // jets this crate could resolve, where believing zero would be worse - // than believing the line. - let jets = if loadout.has_flag("S_SHIELD_LARGE") || umu_mp > 0 { - 0 - } else { - match loadout.count_of("F_JUMP_JET") as i64 { - 0 if !loadout.has_flag("F_JUMP_JET") => jump_mp, - counted => counted, - } - }; - let lifted = if jets > 0 { jets + wing_jump_bonus } else { 0 }; - // Modular armour is deliberately *not* charged here, and this is a - // place where battle value departs from the game rather than from a - // rule we have missed. The rules are plain that it throws the unit off - // balance: a point off walking, a point off jumping, no stacking for - // extra packs, and the penalties lift only when every point of the - // armour is gone. Battle value ignores all of it - the setting the - // calculation runs under says `noModularArmor` outright, beside - // `noHeat` and `noGravity`. - // - // Followed because MegaMek is the answer we are held to. A Koshi (Mist - // Lynx) Z carries a ton of it over its CEWS and is scored jumping six, - // not the five it would manage on a table. - let shields = loadout.count_of("S_SHIELD_MEDIUM") as i64; - let jet_jump = (lifted - shields).max(0); - - // A mechanical jump booster throws the Mek instead of jets doing it, - // and battle value takes whichever gets it further. It is hampered by - // the same things and helped by the same wing. - let booster = loadout.mechanical_jump_booster_mp(); - let booster_jump = if booster > 0 && !loadout.has_flag("S_SHIELD_LARGE") { - (booster + wing_jump_bonus - shields).max(0) - } else { - 0 - }; - let jump_mp = jet_jump.max(booster_jump); - - // Heat is charged on what the *jets* lifted on their own: the distance - // the wing adds is free, which is most of the point of fitting one, - // and a booster is not jets at all so it costs nothing either. - let jets_mp = (jet_jump - wing_jump_bonus).max(0); + // Everything about how far it moves, which is the most tangled + // reading in this crate and now has a name of its own. + let moves = movement( + Declared { + walk, + jump: jump_mp, + }, + tons, + &loadout, + &armor_kind, + myomer, + ); // A prototype double is counted apart from the declared sinks: it // shows up as one of them in the `heat sinks:` line and sheds two @@ -272,7 +149,7 @@ impl<'a> Mek<'a> { .map(|hs| hs.count.max(0) * hs.kind.dissipation()) .unwrap_or(0) + prototype_doubles * (2 - per_declared) - + wing_heat_bonus + + moves.wing_heat_bonus - loadout.lost_cooling(); Ok(Mek { @@ -286,16 +163,16 @@ impl<'a> Mek<'a> { armor_kind, engine_side_slots, gyro: unit.gyro_kind(), - run_mp, - jump_mp, - umu_mp, + run_mp: moves.run_mp, + jump_mp: moves.jump_mp, + umu_mp: moves.umu_mp, heat_capacity, heat_sink_count: unit.heat_sink_spec().map(|hs| hs.count.max(0)).unwrap_or(0), // Charged on the jets alone: the distance the wing adds costs // nothing, which is most of the point of fitting one. - move_heat: move_heat(unit, &engine.kind, jets_mp, &loadout), - has_tsm, - has_industrial_tsm, + move_heat: move_heat(unit, &engine.kind, moves.jets_mp, &loadout), + has_tsm: moves.has_tsm, + has_industrial_tsm: moves.has_industrial_tsm, torso_cockpit_armor, cockpit_modifier: cockpit_modifier( &unit.cockpit_kind(), @@ -399,6 +276,195 @@ fn armor_codes(location: &str) -> &'static [&'static str] { } } +/// What a design declares about its own movement, before anything it +/// carries has a say. +#[derive(Debug, Clone, Copy)] +struct Declared { + walk: i64, + jump: i64, +} + +/// How far a design moves, as battle value counts it. +/// +/// Every field here is read by one term of the calculation and by no other, +/// which is why they are worked out together: they share almost all of their +/// inputs and disagree about them in ways that only make sense side by side. +/// A partial wing adds jump and heat and costs neither; a shield takes both +/// away; triple-strength myomer adds walking that a supercharger then +/// multiplies. +struct Movement { + run_mp: i64, + jump_mp: i64, + umu_mp: i64, + /// What the jets lifted on their own, which is what heat is charged on. + jets_mp: i64, + /// What a partial wing adds to heat capacity. + wing_heat_bonus: i64, + has_tsm: bool, + has_industrial_tsm: bool, +} + +/// Work out how far a design moves. +/// +/// Lifted out of `Mek::read_in` whole: it is a hundred lines of rules that +/// interact, and reading it beside the armour and the cockpit made all three +/// harder to follow than any of them is. +fn movement( + declared: Declared, + tons: f64, + loadout: &Loadout<'_>, + armor_kind: &ArmorKind, + myomer: Myomer, +) -> Movement { + let walk = declared.walk; + let jump_mp = declared.jump; + // Sprint gear is part of what a Mek can do when it is trying, and + // battle value is worked out on that: a Celerity runs 24 without MASC + // and 32 with, which is two steps of movement modifier. + // + // A supercharger carries the MASC flag as well, so "has F_MASC" is + // true for a design with only a supercharger. One or the other doubles + // the walk; both together is two and a half times, and reading the + // flag alone gives every supercharged Mek the pair's figure. + // A supercharger is itself MASC-flagged, so "does the design have + // MASC" answers yes to one carrying only a supercharger. Asked per + // item instead: a design with both gets the pair's multiplier, which a + // design-level test cannot express - a Rime Otter C carries both and + // runs 13, not 10. + let supercharger = loadout.has_flag("S_SUPERCHARGER"); + let masc = loadout.has_flag_without("F_MASC", "S_SUPERCHARGER"); + let sprint = match (masc, supercharger) { + (true, true) => 2.5, + (true, false) | (false, true) => 2.0, + (false, false) => 1.5, + }; + // Triple-strength myomer is worth a point of walking. Battle value is + // worked out with it running rather than only when the Mek is hot + // enough to trigger it, and it compounds: one more point of walk is + // one and a half more of running, which on a Banshee BNC-8S is the + // difference between a movement factor of 1.2 and 1.3. + let has_tsm = loadout.has_flag("F_TSM") || myomer == Myomer::TripleStrength; + let has_industrial_tsm = + loadout.has_flag("F_INDUSTRIAL_TSM") || myomer == Myomer::IndustrialTripleStrength; + let walk = walk + i64::from(has_tsm && !has_industrial_tsm); + // A shield is heavy and in the way: it costs walking as well as + // jumping. + let walk = (walk + - loadout.count_of("S_SHIELD_MEDIUM") as i64 + - loadout.count_of("S_SHIELD_LARGE") as i64) + .max(0); + + // Hardened plate is heavy enough to cost a point of movement. + let hardened = if *armor_kind == ArmorKind::Hardened { + 1 + } else { + 0 + }; + let run_mp = ((walk as f64 * sprint).ceil() as i64 - hardened).max(0); + + // A partial wing does three things and they pull in different + // directions, so all three are worked out here rather than wherever + // each is used. It adds jump distance, it adds heat capacity, and the + // distance it adds is free of heat - the jets are only charged for + // what they lifted on their own. + // + // The file's `jump mp:` is the jets alone; MegaMek adds the wing's + // bonus on top, which is how a Flamberge 2 declares 6 and is scored at + // 7. Light and medium designs get two, heavier ones one. + let wing_jump_bonus = if loadout.has_flag("F_PARTIAL_WING") { + partial_wing_jump_bonus(tons) + } else { + 0 + }; + let wing_heat_bonus = if loadout.has_flag("F_PARTIAL_WING") { + 3 + } else { + 0 + }; + + // Jump, in MegaMek's own order, because the order changes answers. + // + // A partial wing amplifies lift rather than providing it, so it does + // nothing for a design with no jump jets - a Jade Phoenix D has none + // and was being given a point of jump it does not have. The wing is + // added before a medium shield and modular armour take their points + // off, and a large shield stops the design jumping at all. + // Counted from the jets themselves when a wing is fitted, because the + // `jump mp:` line is not consistent about whether it already includes + // the wing's contribution: a Jade Hawk JHK-03 has three jets and + // declares four, where a Flamberge 2 has six and declares six. The + // jets are the thing that does not lie. + // Underwater movement units are fitted where jump jets would go, and + // a `.mtf` declares their movement on the same `jump mp:` line - so a + // design with UMUs jumps nowhere at all. + let umu_mp = loadout.count_of("F_UMU") as i64; + + // Counted from the jets, because that is what MegaMek counts. + // `getJumpMP` calls `getJumpJetMovementPoints`, and the `jump mp:` + // line is parsed into the design and then never read for movement at + // all - so whatever it says, the hardware is the answer. + // + // Which is just as well, because it says different things. A Flamberge + // 2 has six jets and declares six; a Jade Hawk JHK-03 has three and + // declares four, its partial wing's point folded in; a Thunder Fox + // TFT-L8 has five and declares four, which looks like a plain mistake + // in the file - its own text says the jets were increased to match its + // cruise of five. MegaMek is immune to all three and so is this. + // + // The declared figure is the fallback only for a design carrying no + // jets this crate could resolve, where believing zero would be worse + // than believing the line. + let jets = if loadout.has_flag("S_SHIELD_LARGE") || umu_mp > 0 { + 0 + } else { + match loadout.count_of("F_JUMP_JET") as i64 { + 0 if !loadout.has_flag("F_JUMP_JET") => jump_mp, + counted => counted, + } + }; + let lifted = if jets > 0 { jets + wing_jump_bonus } else { 0 }; + // Modular armour is deliberately *not* charged here, and this is a + // place where battle value departs from the game rather than from a + // rule we have missed. The rules are plain that it throws the unit off + // balance: a point off walking, a point off jumping, no stacking for + // extra packs, and the penalties lift only when every point of the + // armour is gone. Battle value ignores all of it - the setting the + // calculation runs under says `noModularArmor` outright, beside + // `noHeat` and `noGravity`. + // + // Followed because MegaMek is the answer we are held to. A Koshi (Mist + // Lynx) Z carries a ton of it over its CEWS and is scored jumping six, + // not the five it would manage on a table. + let shields = loadout.count_of("S_SHIELD_MEDIUM") as i64; + let jet_jump = (lifted - shields).max(0); + + // A mechanical jump booster throws the Mek instead of jets doing it, + // and battle value takes whichever gets it further. It is hampered by + // the same things and helped by the same wing. + let booster = loadout.mechanical_jump_booster_mp(); + let booster_jump = if booster > 0 && !loadout.has_flag("S_SHIELD_LARGE") { + (booster + wing_jump_bonus - shields).max(0) + } else { + 0 + }; + let jump_mp = jet_jump.max(booster_jump); + + // Heat is charged on what the *jets* lifted on their own: the distance + // the wing adds is free, which is most of the point of fitting one, + // and a booster is not jets at all so it costs nothing either. + let jets_mp = (jet_jump - wing_jump_bonus).max(0); + + Movement { + run_mp, + jump_mp, + umu_mp, + jets_mp, + wing_heat_bonus, + has_tsm, + has_industrial_tsm, + } +} + /// Heat spent moving, which comes out of the same budget as shooting. /// /// An IndustrialMek is charged nothing - it has no combat heat scale to speak -- 2.51.2