Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265//! How much internal structure a Mek has, and how much plate it can carry.//!//! Battle value needs the total, and no unit file declares it: internal//! structure follows from tonnage and from how many limbs the design has, by//! the table in TechManual. Endo Steel and its relatives change what the//! structure *weighs*, never how many points it is.//!//! It lives here rather than beside the battle value rules because it is a//! fact about a design rather than a rule about scoring one: armour as a//! fraction of the maximum needs the same table, and so does anything else//! that asks what a design could have been built with.//!//! The table below was written from the published one and then checked against//! MegaMek for all 4,294 Meks in 0.51.0. That found two things worth writing//! down: a QuadVee's structure is a quad's, not a biped's, and a LAM's is a//! biped's. Both are the sort of thing that is obvious once seen and silently//! wrong until then.
/// How many of each limb a configuration has, which is the only thing that/// varies once tonnage picks a row.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Shape { Biped, Quad, Tripod,}
impl Shape { /// What MegaMek calls this shape in a `.mul`'s `type` attribute, which is /// what decides how its location indices are read back. pub fn megamek_name(&self) -> &'static str { match self { Shape::Biped => "Biped", Shape::Quad => "Quad", Shape::Tripod => "Tripod", } }
/// Read the `Config:` line. The `OmniMek` and `FrankenMek` suffixes say /// how a design was built rather than what shape it is. pub fn from_config(config: Option<&str>) -> Shape { let c = config.unwrap_or("").to_ascii_lowercase(); // QuadVee before Quad would be the same answer; Tripod has to come // first only because nothing else contains it. if c.contains("tripod") { Shape::Tripod } else if c.contains("quad") { Shape::Quad } else { Shape::Biped } }}
/// One row of the table: tonnage, then head, centre torso, side torso, arm/// and leg.////// The head is 3 up to a hundred tons and 4 above it, which is the only reason/// it is a column rather than a constant - and the thing that makes a/// superheavy's structure impossible to work out by extending the pattern.type Row = (i64, i64, i64, i64, i64, i64);
const TABLE: &[Row] = &[ (10, 3, 4, 3, 1, 2), (15, 3, 5, 4, 2, 3), (20, 3, 6, 5, 3, 4), (25, 3, 8, 6, 4, 6), (30, 3, 10, 7, 5, 7), (35, 3, 11, 8, 6, 8), (40, 3, 12, 10, 6, 10), (45, 3, 14, 11, 7, 11), (50, 3, 16, 12, 8, 12), (55, 3, 18, 13, 9, 13), (60, 3, 20, 14, 10, 14), (65, 3, 21, 15, 10, 15), (70, 3, 22, 15, 11, 15), (75, 3, 23, 16, 12, 16), (80, 3, 25, 17, 13, 17), (85, 3, 27, 18, 14, 18), (90, 3, 29, 19, 15, 19), (95, 3, 30, 20, 16, 20), (100, 3, 31, 21, 17, 21), // Superheavy from here, and the head gains a point. (105, 4, 32, 22, 17, 22), (110, 4, 33, 23, 18, 23), (115, 4, 35, 24, 19, 24), (120, 4, 36, 25, 20, 25), (125, 4, 38, 26, 21, 26), (130, 4, 39, 27, 21, 27), (135, 4, 41, 28, 22, 28), (140, 4, 42, 29, 23, 29), (145, 4, 44, 31, 24, 31), (150, 4, 45, 32, 25, 32), (155, 4, 47, 33, 26, 33), (160, 4, 48, 34, 26, 34), (165, 4, 50, 35, 27, 35), (170, 4, 51, 36, 28, 36), (175, 4, 53, 37, 29, 37), (180, 4, 54, 38, 30, 38), (185, 4, 56, 39, 31, 39), (190, 4, 57, 40, 31, 40), (195, 4, 59, 41, 32, 41), (200, 4, 60, 42, 33, 42),];
/// Internal structure by location, keyed the way a `.mtf` keys its armour.////// `None` for a tonnage the table does not have a row for, which is anything/// that is not a multiple of five between 10 and 200 tons.pub fn structure_per_location(tons: f64, shape: Shape) -> Option<Vec<(&'static str, i64)>> { let tons = tons.round() as i64; let (_, head, ct, st, arm, leg) = TABLE.iter().find(|row| row.0 == tons)?; let mut out = vec![("HD", *head), ("CT", *ct), ("LT", *st), ("RT", *st)]; match shape { Shape::Biped => out.extend([("LA", *arm), ("RA", *arm), ("LL", *leg), ("RL", *leg)]), // A quad's four legs sit where a biped's arms and legs do. Shape::Quad => out.extend([("FLL", *leg), ("FRL", *leg), ("RLL", *leg), ("RRL", *leg)]), Shape::Tripod => out.extend([ ("LA", *arm), ("RA", *arm), ("LL", *leg), ("RL", *leg), ("CL", *leg), ]), } Some(out)}
/// The most armour a design of this tonnage and shape could carry.////// Two points a location for every point of internal structure, which is the/// rule for every location but one: a head takes nine whatever it is bolted/// to, and twelve on a superheavy. The head is why this cannot be written as/// twice the structure total.////// Front and rear plate share a torso's allowance rather than each getting/// one, so a design that armours its back is not carrying more than this - it/// has moved some of the same points around.////// `None` for a tonnage the table has no row for.pub fn max_armor(tons: f64, shape: Shape) -> Option<i64> { let per = structure_per_location(tons, shape)?; let head = if tons > 100.0 { 12 } else { 9 }; Some( per.iter() .map(|(location, points)| if *location == "HD" { head } else { points * 2 }) .sum(), )}
/// Total internal structure points, counting the frame the design still has.////// MegaMek reads `getTotalInternal()` rather than the figure on the sheet, so/// a cracked torso is worth less. A location the condition does not name is/// whole, and none can be worth more than it was built with. An undamaged/// condition is the design's own total.////// `None` for a tonnage the table does not have a row for, which is anything/// that is not a multiple of five between 10 and 200 tons.////// Battle value counts the structure a Mek still has - MegaMek reads/// `getTotalInternal()`, not the figure the design was built with - so a/// cracked torso is worth less. A location the condition does not name is/// whole, and none can be worth more than it was built with.pub fn structure_total_in( tons: f64, shape: Shape, left: &std::collections::BTreeMap<String, i64>,) -> Option<i64> { Some( structure_per_location(tons, shape)? .iter() .map(|(loc, built)| left.get(*loc).copied().unwrap_or(*built).clamp(0, *built)) .sum(), )}
#[cfg(test)]mod tests { use super::*;
// Checked against MegaMek across the library; these are the rows the rest // hang off. #[test] fn totals_match_the_published_table() { assert_eq!( structure_total_in(100.0, Shape::Biped, &Default::default()), Some(152) ); assert_eq!( structure_total_in(20.0, Shape::Biped, &Default::default()), Some(33) ); assert_eq!( structure_total_in(70.0, Shape::Biped, &Default::default()), Some(107) ); assert_eq!( structure_total_in(70.0, Shape::Quad, &Default::default()), Some(115) ); assert_eq!( structure_total_in(60.0, Shape::Tripod, &Default::default()), Some(113) ); }
// A QuadVee walks on four legs and is structured like one. Reading its // config as a biped costs 8 points on a 60-tonner, which is a battle value // that is wrong by a little on thirteen designs - the hardest kind to spot. #[test] fn a_quadvee_is_structured_as_a_quad() { assert_eq!(Shape::from_config(Some("QuadVee OmniMek")), Shape::Quad); assert_eq!( structure_total_in(60.0, Shape::Quad, &Default::default()), Some(107) ); }
// A LAM is a biped that also flies, and an OmniMek is a biped that swaps // pods. Neither changes the frame. #[test] fn lam_and_omni_are_the_shape_they_started_as() { assert_eq!(Shape::from_config(Some("LAM")), Shape::Biped); assert_eq!(Shape::from_config(Some("Biped OmniMek")), Shape::Biped); assert_eq!(Shape::from_config(Some("Quad OmniMek")), Shape::Quad); assert_eq!(Shape::from_config(Some("Tripod OmniMek")), Shape::Tripod); }
// The superheavy rows, and the reason they cannot be extrapolated: the // head is worth four above a hundred tons and three below it. Checked // against MegaMek, which reports exactly these totals. #[test] fn a_superheavy_carries_a_heavier_head() { assert_eq!( structure_total_in(105.0, Shape::Biped, &Default::default()), Some(158) ); assert_eq!( structure_total_in(125.0, Shape::Tripod, &Default::default()), Some(214) ); assert_eq!( structure_total_in(135.0, Shape::Tripod, &Default::default()), Some(229) ); assert_eq!( structure_total_in(200.0, Shape::Biped, &Default::default()), Some(298) ); }
#[test] fn a_tonnage_with_no_row_has_no_answer_rather_than_a_guessed_one() { assert_eq!( structure_total_in(37.0, Shape::Biped, &Default::default()), None ); assert_eq!( structure_total_in(205.0, Shape::Biped, &Default::default()), None ); }}