From 2d92d2f18cc510ed3637640c33f013cc142d9fba Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 23:36:37 -0400 Subject: [PATCH] fix(unit-rules): read what is left, not what it was built with Three of the same mistake. A torso-mounted cockpit counts its centre torso's plate a second time and has to count the plate still there - a stripped Enforcer III was paid twice for armour it had lost. An armoured system slot stays worth something when the slot is shot out, and modular armour the same: MegaMek pays for the plating whether or not what it protected still works. And a Banshee writes `Hip (ARMORED)`, which the leg-damage rule missed for want of stripping the marker - it walked as though its hip were whole. --- TODO.md | 19 ++++++++++ crates/helm-bv/src/loadout.rs | 70 ++++++++++++++++++++++++++++------- crates/helm-bv/src/mek.rs | 38 +++++++++++++------ 3 files changed, 102 insertions(+), 25 deletions(-) diff --git a/TODO.md b/TODO.md index f3b30bb..5e3f42c 100644 --- a/TODO.md +++ b/TODO.md @@ -350,6 +350,25 @@ The long pole, and the reason `helm-core` does no I/O. Tracked as them and probably an explosive penalty on a magazine one of us empties and the other does not. + **Three more, all the same mistake in different clothes: reading what a + design was built with where MegaMek reads what is left, or the reverse.** + + - A **torso-mounted cockpit** counts its centre torso's plate a second + time, for keeping the pilot alive rather than the Mek - and it has to + be the plate that is still there. A stripped Enforcer III ENF-7D was + being paid twice for armour it no longer had, 97.5 points of it. + - An **armoured system slot** is worth something, and stays worth it when + the slot is shot out: the plating is on the machine whether or not what + it was protecting still works. helm stopped counting, so a Banshee + BNC-11X with its arm blown off came out 36 light. + - **Modular armour** goes the same way, which is the opposite of what it + looks like: MegaMek pays for the pack whether or not the location + holding it is still attached. + + And one that was not a rule at all: a Banshee writes `Hip (ARMORED)`, and + the leg-damage rule matched the bare name. The design walked as though + its hip were whole and came out three hundred points heavy. + - [x] **A catalogue small enough to ship.** `helm catalogue` writes the fields the rules actually read and drops the rest: the bridge dumps a `namesVector`, a `techAdvancement` and several static tables on every one diff --git a/crates/helm-bv/src/loadout.rs b/crates/helm-bv/src/loadout.rs index f39d4e7..78c7632 100644 --- a/crates/helm-bv/src/loadout.rs +++ b/crates/helm-bv/src/loadout.rs @@ -122,6 +122,9 @@ pub struct Loadout<'a> { present: BTreeSet<&'a str>, /// Cooling shot off the design, which the `heat sinks:` line still counts. lost_cooling: i64, + /// Modular armour points, from the design rather than from what is left of + /// it - see the pass that counts them. + modular_armor: i64, /// Whether the design was built with jump jets, whether or not any are /// left. A design that lost all of them is not one that never had any. had_jump_jets: bool, @@ -175,6 +178,11 @@ impl<'a> Loadout<'a> { let mut per_location: BTreeMap = BTreeMap::new(); let mut armored_gyro_slots = 0; let mut armored_system_slots = 0; + // Modular armour, in slots, counted off the design rather than off + // what survived. MegaMek pays for the pack whether or not the location + // holding it is still on the machine, the same way it pays for + // armoured plating. + let mut modular_slots: BTreeMap = BTreeMap::new(); // A mechanical jump booster carries its own size, written into the // slot as `MechanicalJumpBooster:SIZE:4.0` - so how far it throws the // Mek is in the design rather than in the catalogue, and the line @@ -198,6 +206,40 @@ impl<'a> Loadout<'a> { let none = BTreeSet::new(); let empty = condition.empty_ammo.get(location).unwrap_or(&none); let gone = shot_away.get(location).unwrap_or(&none); + // Armoured system slots, counted whether or not a hit took them + // out: MegaMek keeps paying for the plating, and a Banshee + // BNC-11X with its arm blown off was coming out 36 points light + // because helm stopped. + for line in slots { + for slot in line.split('|') { + let marked = clean(slot); + if !marked.armored + || marked.name.is_empty() + || marked.name == "-Empty-" + || catalogue.resolve(&marked.name).is_some() + { + continue; + } + if marked.name.eq_ignore_ascii_case("Gyro") { + armored_gyro_slots += 1; + } else { + armored_system_slots += 1; + } + } + } + // Modular armour the same way: MegaMek pays for the pack whether + // or not the location holding it is still attached. + for line in slots { + for slot in line.split('|') { + let name = clean(slot).name; + if catalogue + .resolve(&name) + .is_some_and(|e| e.has_flag("F_MODULAR_ARMOR")) + { + *modular_slots.entry(name).or_default() += 1; + } + } + } for (index, line) in slots.iter().enumerate() { if gone.contains(&index) || empty.contains(&index) { // A heat sink shot out of an arm is cooling the design no @@ -239,15 +281,11 @@ impl<'a> Loadout<'a> { continue; } let Some(entry) = catalogue.resolve(&name) else { - // Not equipment: a system slot, which is only - // interesting here if somebody armoured it. - if armored { - if name.eq_ignore_ascii_case("Gyro") { - armored_gyro_slots += 1; - } else { - armored_system_slots += 1; - } - } + // Not equipment: a system slot. Armouring one is worth + // something and is counted in the pass above, so that + // a slot which has been shot out still counts - the + // plating is on the machine whether or not what it was + // protecting still works. continue; }; let counts = per_location @@ -318,6 +356,14 @@ impl<'a> Loadout<'a> { mountings: Vec::new(), present: BTreeSet::new(), lost_cooling: lost_cooling.round() as i64, + modular_armor: modular_slots + .iter() + .filter_map(|(name, slots)| { + let entry = catalogue.resolve(name)?; + let per = entry.criticals.filter(|c| *c > 0).unwrap_or(1).max(1) as usize; + Some(entry.damage_capacity? * (*slots / per).max(1) as i64) + }) + .sum(), had_jump_jets, armored_gyro_slots, armored_system_slots, @@ -574,11 +620,7 @@ impl<'a> Loadout<'a> { /// Total armour points carried as modular armour, which is armour bolted /// on rather than built in and is declared as equipment. pub fn modular_armor(&self) -> i64 { - self.items - .iter() - .filter(|i| i.entry.has_flag("F_MODULAR_ARMOR")) - .filter_map(|i| i.entry.damage_capacity) - .sum() + self.modular_armor } /// Which limbs carry an actuator enhancement system, as a weight diff --git a/crates/helm-bv/src/mek.rs b/crates/helm-bv/src/mek.rs index 674090a..1a14a25 100644 --- a/crates/helm-bv/src/mek.rs +++ b/crates/helm-bv/src/mek.rs @@ -108,18 +108,10 @@ impl<'a> Mek<'a> { ))?; let armor_kind = unit.armor_kind(); - // A torso-mounted cockpit puts the pilot in the centre torso, and that - // location's armour is counted a second time for protecting them. - let torso_cockpit_armor = if unit.cockpit_kind() == CockpitKind::TorsoMounted { - unit.armor_locations.get("CT").copied().unwrap_or(0) - + unit.armor_locations.get("RTC").copied().unwrap_or(0) - } else { - 0 - }; // What it still has rather than what it was built with. The plate is // the only term damage changes: structure, gyro and the rest are worth // the same until the location itself is gone. - let armor = unit + let armor: Vec<(String, i64, ArmorKind)> = unit .armor_locations .iter() .map(|(loc, points)| { @@ -128,6 +120,21 @@ impl<'a> Mek<'a> { }) .collect(); + // A torso-mounted cockpit puts the pilot in the centre torso, and that + // location's armour is counted a second time for protecting them - + // the plate that is still there, not the plate it was built with. An + // Enforcer III stripped to nothing was being paid twice for armour it + // no longer had. + let torso_cockpit_armor = if unit.cockpit_kind() == CockpitKind::TorsoMounted { + armor + .iter() + .filter(|(location, _, _)| location == "CT" || location == "RTC") + .map(|(_, points, _)| *points) + .sum() + } else { + 0 + }; + let armor_bonus = harjel_by_armor_location(&loadout); let engine = unit.engine_spec().ok_or(Unsupported::Missing("engine"))?; @@ -332,6 +339,15 @@ fn system_hits(unit: &Unit, condition: &crate::Condition, system: &str) -> i64 { .sum() } +/// A critical slot's system name, without the markers a `.mtf` hangs off it. +/// +/// A Banshee BNC-11X writes `Hip (ARMORED)`, and matching the bare name misses +/// it - which left the design walking as though its hip were whole and worth +/// three hundred points more than MegaMek says. +fn system_name(slot: &str) -> &str { + slot.split(" (").next().unwrap_or(slot).trim() +} + /// What a Mek can still walk with its legs damaged. /// /// The largest thing damage does to a battle value after the plate itself, and @@ -361,13 +377,13 @@ fn walk_after_damage(declared: i64, unit: &Unit, condition: &crate::Condition) - }; // A leg is a location with a hip in it, which is true of a quad's four // and a tripod's three without either being named here. - if !criticals.iter().any(|slot| slot.trim() == "Hip") { + if !criticals.iter().any(|slot| system_name(slot) == "Hip") { continue; } let names: Vec<&str> = slots .iter() .filter_map(|at| criticals.get(*at)) - .map(|s| s.trim()) + .map(|s| system_name(s)) .collect(); if names.contains(&"Hip") { hips += 1; -- 2.51.2