From 6716139e7f93fe7450de919ea74c5258b5f8944a Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 18:47:47 -0400 Subject: [PATCH] feat(unit-rules): helm repairs Prints the list, with --why adding what the damage cost in battle value. --- README.md | 5 ++ crates/helm-cli/src/main.rs | 107 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/README.md b/README.md index cd01092..9b14152 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,11 @@ helm cannot score yet, failing only on the first: helm check --megamek --bridge-dir +And what it would take to put right - points of plate, points of frame, the +equipment shot out and the magazines to fill, location by location: + + helm repairs --megamek --bridge-dir --why + `--clusters` is the one that matters. A ranked list of the worst disagreements shows whichever design is strangest; grouping them by what they have in common shows which *rule* is missing, and its second section ranks the failures by how diff --git a/crates/helm-cli/src/main.rs b/crates/helm-cli/src/main.rs index 69e1c25..ae63a2f 100644 --- a/crates/helm-cli/src/main.rs +++ b/crates/helm-cli/src/main.rs @@ -24,6 +24,7 @@ fn main() -> ExitCode { Some("catalogue") => catalogue(&args[1..]), Some("force") => force(&args[1..]), Some("check") => check(&args[1..]), + Some("repairs") => repairs(&args[1..]), Some("-h") | Some("--help") | Some("help") | None => { usage(); return ExitCode::SUCCESS; @@ -50,6 +51,7 @@ USAGE helm bv-report --megamek --bridge-dir helm force --megamek --bridge-dir helm check --megamek --bridge-dir + helm repairs --megamek --bridge-dir BUILD --megamek A MegaMek install. Reads data/mekfiles/unit_files.zip, @@ -86,6 +88,11 @@ CHECK more plate or frame than a design was built with. Exits non-zero when a file has something wrong with it. +REPAIRS + What a force needs putting right, location by location: points of plate, + points of frame, the equipment shot out and the magazines to fill. + --why Add what each machine's damage cost it in battle value. + FORCE What each machine in a .mul is worth in the state the file leaves it in, as flown by the crew the file seats. @@ -590,6 +597,106 @@ fn check(args: &[String]) -> Result<(), String> { )) } +/// What a post-match `.mul` would take to put right. +/// +/// The other half of reading a file that came back from a battle: `--why` says +/// what the damage cost, and this says what it would take to undo. +fn repairs(args: &[String]) -> Result<(), String> { + let path = args.first().filter(|a| !a.starts_with("--")).ok_or( + "give a .mul to read: helm repairs --megamek --bridge-dir ", + )?; + let opts = parse_opts(&args[1..])?; + let megamek = opts.megamek.ok_or("--megamek is required")?; + let bridge = opts + .bridge_dir + .ok_or("--bridge-dir is required: the catalogue lives there")?; + + let library = read_library(&megamek)?; + let catalogue = + helm_bridge::read_catalogue(&bridge.join("equipment.jsonl")).map_err(|e| e.to_string())?; + let text = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?; + let mul = helm_unitfile::parse_mul(&text).map_err(|e| format!("{path}: {e}"))?; + + let mut force = helm_bv::Total::default(); + for entity in mul.machines() { + let Some(unit) = library + .units + .iter() + .find(|u| u.answers_to(&entity.chassis, &entity.model)) + else { + println!("{:<34} not in the library", entity.display_name()); + continue; + }; + let condition = helm_bv::Condition::from(entity); + let repairs = helm_bv::repairs(unit, &condition); + let total = helm_bv::Total::of(&repairs); + force.armor += total.armor; + force.structure += total.structure; + force.items += total.items; + force.reloads += total.reloads; + force.gone += total.gone; + + if total.is_nothing() { + println!("{:<34} came back whole", entity.display_name()); + continue; + } + println!( + "{:<34} {} armour, {} structure, {} items, {} magazines", + entity.display_name(), + total.armor, + total.structure, + total.items, + total.reloads + ); + for repair in &repairs { + let mut parts = Vec::new(); + if repair.armor > 0 { + parts.push(format!("{} armour", repair.armor)); + } + if repair.rear_armor > 0 { + parts.push(format!("{} rear armour", repair.rear_armor)); + } + if repair.structure > 0 { + parts.push(format!("{} structure", repair.structure)); + } + println!( + " {:<16} {}{}", + repair.location, + parts.join(", "), + if repair.gone { " (location gone)" } else { "" } + ); + for slot in &repair.destroyed { + println!(" {:<16} slot {}: {}", "", slot.at + 1, slot.what); + } + for slot in &repair.reload { + println!( + " {:<16} slot {}: {} is empty", + "", + slot.at + 1, + slot.what + ); + } + } + if opts.why { + print_attribution(unit, &catalogue, &condition)?; + } + } + + println!( + "\n{} armour, {} structure, {} items, {} magazines{}", + force.armor, + force.structure, + force.items, + force.reloads, + if force.gone > 0 { + format!(", {} locations gone", force.gone) + } else { + String::new() + } + ); + Ok(()) +} + /// Say where a damaged machine's battle value went, term by term. /// /// Against the design as it left the factory, because that is the figure a -- 2.51.2