//! 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> { 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 { 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, ) -> Option { 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 ); } }