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