Skip to main content

wows_core/
units.rs

1//! Distance, angle, mass and time newtypes and their unit conversions.
2//!
3//! The game mixes several distance units: real meters, GameParams BigWorld units
4//! (1 unit = 30 m), replay world units (1 unit = 15 m, what packet positions and
5//! radii are in), ship-model units (1 unit = 15 m, used by hull geometry), plus
6//! kilometers and millimeters. These newtypes keep units honest at the type
7//! level; cross-unit arithmetic and comparison convert to a common unit.
8//!
9//! [`BigWorldDistance`] and [`WorldDistance`] are two different spaces that a
10//! name alone will not keep apart, and they differ by exactly 2. Read each
11//! type's doc before reaching for either.
12//!
13//! [`Degrees`] and [`Radians`] serve the same purpose for angles: GameParams
14//! states ricochet, normalization and impact angles in degrees, while every
15//! trigonometric step wants radians.
16
17use std::fmt;
18use std::ops::Add;
19use std::ops::Mul;
20use std::ops::Sub;
21
22/// Conversion factor: 1 GameParams BigWorld unit = 30 meters.
23const BW_TO_METERS: f32 = 30.0;
24
25/// Conversion factor: 1 ship-model unit = 15 meters. The engine's own name for
26/// this number is `BW_TO_SHIP`, which reads as a BigWorld-to-ship ratio and is
27/// not what it does; the client multiplies by it to leave ship space for meters.
28const SHIP_TO_METERS: f32 = 15.0;
29
30/// Conversion factor: 1 replay world unit = 15 meters.
31///
32/// This is the factor that places a shell impact against a victim's hull, and
33/// it is anchored on the hull geometry itself: at 15 the body-frame impacts sit
34/// within a real ship's beam and freeboard, and predicted fire sections agree
35/// with the server 0.711 of the time against 0.401 for the best position-blind
36/// guess. At 30 the same impacts land a p99 of 46 m off the centreline of a
37/// hull whose widest half-beam is 21 m, and agreement collapses to 0.469. A
38/// sweep is single-peaked at 16-18.
39///
40/// Two other measurements want 30 for the same coordinates and are NOT
41/// reconciled with this: `Vec3::distance_xz` scales by 30 to produce the firing
42/// range the ballistics solver runs on, and the longest observed salvo measured
43/// against each ship's artillery `maxDist` over 416 shooters lands on a median
44/// of 31.09. Those read a separation between two distant points; this reads an
45/// offset between an impact and the ship it struck. Both cannot be right about
46/// one space, so one of the two position sources is not in the units the other
47/// assumes. Until that is settled, do not change this constant to satisfy the
48/// range evidence: the hull geometry is what this factor exists to serve, and
49/// 30 puts impacts inside no ship that exists.
50const WORLD_TO_METERS: f32 = SHIP_TO_METERS;
51
52/// Distance in meters.
53#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
56pub struct Meters(f32);
57
58/// Distance in the coordinate units **GameParams** uses (1 unit = 30 meters):
59/// consumable `distShip`/`distTorpedo`, projectile `maxDist`, and the other
60/// range fields read off the ship dict.
61///
62/// The 30 is exact against the port. Hydroacoustic Search's `C_4_7` variant
63/// carries `distShip = 133.3333`, and its in-game ship-detection range is
64/// 4.0 km; Black's radar carries `distShip = 250.0` against a published 7.5 km.
65///
66/// **Replay packet positions are not in this space.** Entity positions and hit
67/// points off the wire measure 15 m per unit ([`WorldDistance`]). The two are
68/// separated by measurement, not by naming.
69///
70/// **Unreconciled:** four live call sites type packet-borne *radii* as this, or
71/// convert them at 30, and nothing here establishes which is right. Either they
72/// are 2x out, which would mean the minimap has been drawing meters-derived
73/// rings at half size, or `WorldDistance`'s 15 is. The hull half-beam
74/// measurement behind `WorldDistance` is the tighter of the two and needs no
75/// GameParams value, so the call sites are the likelier suspects, but nobody
76/// has checked whether a zone radius travels in the same units as a position:
77///
78/// - `wows-battle-world/src/ingest/entities.rs` (smoke radius from `EntityCreate`)
79/// - `wows-replays/src/analyzer/decoder/decode.rs` (`WardAdded` radius)
80/// - `wows-toolkit/src/replay/minimap_view/tactics.rs` (`space_size * 30.0`)
81/// - `minimap-renderer/src/renderer.rs` (`m / 30.0 / space_size`)
82///
83/// Until someone measures a drawn radius against a known in-game one, treat a
84/// packet radius as unresolved rather than assuming either constant.
85#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
88pub struct BigWorldDistance(f32);
89
90/// Distance in the coordinate units a **replay packet stream** is in (1 unit =
91/// 15 meters): entity positions, shell impact points, and any offset between
92/// two of them.
93///
94/// The 15 is measured, three ways, on the 15.1.0 corpus:
95///
96/// - **Hull length.** Shell impacts on a ship, rotated into its body frame,
97///   reach 8 to 10 units from the hull origin on ships whose bow fire node sits
98///   104 to 128 m out. At 15 m/unit that is 120 to 152 m, just past the bow
99///   node and inside a ~135 m half-length. At 30 it would be 240 to 300 m, i.e.
100///   half-kilometre ships.
101/// - **Hull beam.** The same impacts reach 1.1 to 1.6 units abeam, which at
102///   15 m/unit is a 34 to 49 m beam for ships whose real beams are 26 to 38 m.
103///   Generous by the few meters an impact point sits proud of the plate;
104///   at 30 it would put every beam past 68 m.
105/// - **Gun range.** The longest shot in a replay, as `|target - origin|` from
106///   the salvo packet, runs 247 to 808 units. At 15 m/unit those are 3.7 to
107///   12.1 km, every one inside the firing ship's GameParams `maxDist`. At 30,
108///   ten of the twenty-six replays measured put shots beyond their own ship's
109///   maximum range.
110///
111/// A fourth, functional measurement agrees: sweeping the scale used to place
112/// shell impacts into hull fire sections, and scoring each against the section
113/// the server actually lit, gives a single-peaked curve topping out at 15 to 16.
114///
115/// So this space and [`ShipModelDistance`] measure the same, which is why
116/// [`WORLD_TO_METERS`] is defined as [`SHIP_TO_METERS`]. They are still separate
117/// types because they are separate spaces: a world position is not a
118/// ship-model coordinate, and only one of the two is anchored by the waterline
119/// and dispersion evidence on `ShipModelDistance`.
120#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
123pub struct WorldDistance(f32);
124
125/// Distance in ship-model coordinate units (1 unit = 15 meters). Hull geometry
126/// uses this space: `.visual` node matrices, the `.skel_ext` nodes hung off them,
127/// and the meshes they position.
128///
129/// The 15 is measured, and good to roughly +/-3%. That is the precision budget
130/// of anything derived from it: a position 100 m from the hull origin carries
131/// about 3 m of scale uncertainty.
132///
133/// The tightest evidence does not come from geometry at all. Solving the
134/// main-battery dispersion formula against published port dispersion gives
135/// 14.976 for North Carolina and 14.851 for Yamato
136/// (`wowsunpack::game_params::ttx::constants::BW_TO_SHIP`). That route is only
137/// independent of the geometry if the dispersion formula's ship space and the
138/// `.visual`/`.skel_ext` space are the same space, which is inferred from the
139/// shared engine import name and not otherwise established.
140///
141/// Geometric measurements agree but individually are looser: Iowa's waterline
142/// beam puts the scale at 15.06 m/unit, while the model bounding box against
143/// `A_Hull.size[0]` implies about 14.8 across the roster. The exported hierarchy
144/// carries no scale of its own; `Scene Root`, `export` and the hull nodes are
145/// identity. See `docs/FIRE_CHANCE.md` section 5.1.
146#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
149pub struct ShipModelDistance(f32);
150
151/// Distance in kilometers.
152#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
155pub struct Km(f32);
156
157/// Distance in millimeters.
158#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
161pub struct Millimeters(f32);
162
163/// Speed in meters per second (shell muzzle velocity).
164#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
167pub struct MetersPerSecond(f32);
168
169/// An angle in degrees. GameParams states every angle in degrees.
170#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
173pub struct Degrees(f32);
174
175/// An angle in radians. Trigonometry is only defined on this type, so a value
176/// in degrees cannot reach `cos`/`sin` without an explicit conversion.
177#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
180pub struct Radians(f32);
181
182/// Mass in kilograms.
183#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
186pub struct Kilograms(f32);
187
188/// A duration in seconds.
189#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
192pub struct Seconds(f32);
193
194impl From<f32> for Meters {
195    fn from(v: f32) -> Self {
196        Self(v)
197    }
198}
199impl From<i32> for Meters {
200    fn from(v: i32) -> Self {
201        Self(v as f32)
202    }
203}
204
205impl From<f32> for BigWorldDistance {
206    fn from(v: f32) -> Self {
207        Self(v)
208    }
209}
210impl From<i32> for BigWorldDistance {
211    fn from(v: i32) -> Self {
212        Self(v as f32)
213    }
214}
215
216impl From<f32> for ShipModelDistance {
217    fn from(v: f32) -> Self {
218        Self(v)
219    }
220}
221
222impl From<f32> for WorldDistance {
223    fn from(v: f32) -> Self {
224        Self(v)
225    }
226}
227
228impl From<f32> for Km {
229    fn from(v: f32) -> Self {
230        Self(v)
231    }
232}
233impl From<i32> for Km {
234    fn from(v: i32) -> Self {
235        Self(v as f32)
236    }
237}
238
239impl From<f32> for Millimeters {
240    fn from(v: f32) -> Self {
241        Self(v)
242    }
243}
244impl From<i32> for Millimeters {
245    fn from(v: i32) -> Self {
246        Self(v as f32)
247    }
248}
249
250impl From<f32> for MetersPerSecond {
251    fn from(v: f32) -> Self {
252        Self(v)
253    }
254}
255impl From<i32> for MetersPerSecond {
256    fn from(v: i32) -> Self {
257        Self(v as f32)
258    }
259}
260
261impl From<f32> for Degrees {
262    fn from(v: f32) -> Self {
263        Self(v)
264    }
265}
266
267impl From<f32> for Radians {
268    fn from(v: f32) -> Self {
269        Self(v)
270    }
271}
272
273impl From<f32> for Kilograms {
274    fn from(v: f32) -> Self {
275        Self(v)
276    }
277}
278
279impl From<f32> for Seconds {
280    fn from(v: f32) -> Self {
281        Self(v)
282    }
283}
284
285impl Meters {
286    /// Const constructor for use in static/const contexts.
287    pub const fn new(v: f32) -> Self {
288        Self(v)
289    }
290
291    pub fn value(self) -> f32 {
292        self.0
293    }
294    pub fn to_bigworld(self) -> BigWorldDistance {
295        BigWorldDistance(self.0 / BW_TO_METERS)
296    }
297    /// Convert to ship-model units (1 unit = 15 meters).
298    /// Use this for distances that will be compared against ship geometry
299    /// (hull models, armor meshes), which are in ship-model coordinates.
300    pub fn to_ship_model(self) -> ShipModelDistance {
301        ShipModelDistance(self.0 / SHIP_TO_METERS)
302    }
303    pub fn to_km(self) -> Km {
304        Km(self.0 / 1000.0)
305    }
306    pub fn to_mm(self) -> Millimeters {
307        Millimeters(self.0 * 1000.0)
308    }
309    /// Convert to replay world units (1 unit = 15 meters). Use this for
310    /// distances that will be compared against packet positions.
311    pub fn to_world(self) -> WorldDistance {
312        WorldDistance(self.0 / WORLD_TO_METERS)
313    }
314}
315
316impl BigWorldDistance {
317    pub fn value(self) -> f32 {
318        self.0
319    }
320    pub fn to_meters(self) -> Meters {
321        Meters(self.0 * BW_TO_METERS)
322    }
323    pub fn to_km(self) -> Km {
324        self.to_meters().to_km()
325    }
326}
327
328impl ShipModelDistance {
329    pub const ZERO: ShipModelDistance = ShipModelDistance(0.0);
330
331    /// Const constructor for use in static/const contexts.
332    pub const fn new(v: f32) -> Self {
333        Self(v)
334    }
335
336    pub fn value(self) -> f32 {
337        self.0
338    }
339    /// Clamp to a non-negative distance.
340    pub fn max_zero(self) -> Self {
341        Self(self.0.max(0.0))
342    }
343    pub fn to_meters(self) -> Meters {
344        Meters(self.0 * SHIP_TO_METERS)
345    }
346    pub fn to_bigworld(self) -> BigWorldDistance {
347        self.to_meters().to_bigworld()
348    }
349}
350
351impl WorldDistance {
352    /// Const constructor for use in static/const contexts.
353    pub const fn new(v: f32) -> Self {
354        Self(v)
355    }
356
357    pub fn value(self) -> f32 {
358        self.0
359    }
360    pub fn to_meters(self) -> Meters {
361        Meters(self.0 * WORLD_TO_METERS)
362    }
363    pub fn to_km(self) -> Km {
364        self.to_meters().to_km()
365    }
366}
367
368impl Km {
369    /// Const constructor for use in static/const contexts.
370    pub const fn new(v: f32) -> Self {
371        Self(v)
372    }
373
374    pub fn value(self) -> f32 {
375        self.0
376    }
377    pub fn to_meters(self) -> Meters {
378        Meters(self.0 * 1000.0)
379    }
380    pub fn to_bigworld(self) -> BigWorldDistance {
381        self.to_meters().to_bigworld()
382    }
383}
384
385impl Millimeters {
386    /// Const constructor for use in static/const contexts.
387    pub const fn new(v: f32) -> Self {
388        Self(v)
389    }
390
391    pub fn value(self) -> f32 {
392        self.0
393    }
394    pub fn to_meters(self) -> Meters {
395        Meters(self.0 / 1000.0)
396    }
397    pub fn to_bigworld(self) -> BigWorldDistance {
398        self.to_meters().to_bigworld()
399    }
400}
401
402impl MetersPerSecond {
403    /// Const constructor for use in static/const contexts.
404    pub const fn new(v: f32) -> Self {
405        Self(v)
406    }
407
408    pub fn value(self) -> f32 {
409        self.0
410    }
411
412    /// Distance covered at this speed over `duration`.
413    pub fn travel_over(self, duration: Seconds) -> Meters {
414        Meters(self.0 * duration.0)
415    }
416}
417
418impl Degrees {
419    /// Const constructor for use in static/const contexts.
420    pub const fn new(v: f32) -> Self {
421        Self(v)
422    }
423
424    pub fn value(self) -> f32 {
425        self.0
426    }
427    pub fn to_radians(self) -> Radians {
428        Radians(self.0.to_radians())
429    }
430    pub fn abs(self) -> Self {
431        Self(self.0.abs())
432    }
433}
434
435impl Radians {
436    /// Const constructor for use in static/const contexts.
437    pub const fn new(v: f32) -> Self {
438        Self(v)
439    }
440
441    pub fn value(self) -> f32 {
442        self.0
443    }
444    pub fn to_degrees(self) -> Degrees {
445        Degrees(self.0.to_degrees())
446    }
447    pub fn abs(self) -> Self {
448        Self(self.0.abs())
449    }
450    pub fn cos(self) -> f32 {
451        self.0.cos()
452    }
453    pub fn sin(self) -> f32 {
454        self.0.sin()
455    }
456    /// Clamp to a non-negative angle. Angles reduced by a correction (armor
457    /// normalization, for one) must not pass through zero into a negative
458    /// angle whose cosine would climb again.
459    pub fn max_zero(self) -> Self {
460        Self(self.0.max(0.0))
461    }
462}
463
464impl Kilograms {
465    /// Const constructor for use in static/const contexts.
466    pub const fn new(v: f32) -> Self {
467        Self(v)
468    }
469
470    pub fn value(self) -> f32 {
471        self.0
472    }
473}
474
475impl Seconds {
476    /// Const constructor for use in static/const contexts.
477    pub const fn new(v: f32) -> Self {
478        Self(v)
479    }
480
481    pub fn value(self) -> f32 {
482        self.0
483    }
484}
485
486/// Conventional English-port rounding: whole meters.
487impl fmt::Display for Meters {
488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489        write!(f, "{:.0} m", self.0)
490    }
491}
492
493/// Conventional English-port rounding: one decimal place.
494impl fmt::Display for Km {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        write!(f, "{:.1} km", self.0)
497    }
498}
499
500/// Conventional English-port rounding: whole millimeters.
501impl fmt::Display for Millimeters {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        write!(f, "{:.0} mm", self.0)
504    }
505}
506
507/// Conventional English-port rounding: whole meters per second.
508impl fmt::Display for MetersPerSecond {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        write!(f, "{:.0} m/s", self.0)
511    }
512}
513
514impl fmt::Display for Degrees {
515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516        write!(f, "{:.1} deg", self.0)
517    }
518}
519
520impl fmt::Display for Kilograms {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        write!(f, "{:.0} kg", self.0)
523    }
524}
525
526impl fmt::Display for Seconds {
527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528        write!(f, "{:.1} s", self.0)
529    }
530}
531
532impl Add for Degrees {
533    type Output = Degrees;
534    fn add(self, rhs: Degrees) -> Degrees {
535        Degrees(self.0 + rhs.0)
536    }
537}
538impl Sub for Degrees {
539    type Output = Degrees;
540    fn sub(self, rhs: Degrees) -> Degrees {
541        Degrees(self.0 - rhs.0)
542    }
543}
544
545impl Add for Radians {
546    type Output = Radians;
547    fn add(self, rhs: Radians) -> Radians {
548        Radians(self.0 + rhs.0)
549    }
550}
551impl Sub for Radians {
552    type Output = Radians;
553    fn sub(self, rhs: Radians) -> Radians {
554        Radians(self.0 - rhs.0)
555    }
556}
557
558impl Add for Seconds {
559    type Output = Seconds;
560    fn add(self, rhs: Seconds) -> Seconds {
561        Seconds(self.0 + rhs.0)
562    }
563}
564impl Sub for Seconds {
565    type Output = Seconds;
566    fn sub(self, rhs: Seconds) -> Seconds {
567        Seconds(self.0 - rhs.0)
568    }
569}
570
571impl Mul<f32> for Seconds {
572    type Output = Seconds;
573    fn mul(self, rhs: f32) -> Seconds {
574        Seconds(self.0 * rhs)
575    }
576}
577
578impl Mul<f32> for Meters {
579    type Output = Meters;
580    fn mul(self, rhs: f32) -> Meters {
581        Meters(self.0 * rhs)
582    }
583}
584
585impl Mul<f32> for BigWorldDistance {
586    type Output = BigWorldDistance;
587    fn mul(self, rhs: f32) -> BigWorldDistance {
588        BigWorldDistance(self.0 * rhs)
589    }
590}
591
592impl Mul<f32> for Km {
593    type Output = Km;
594    fn mul(self, rhs: f32) -> Km {
595        Km(self.0 * rhs)
596    }
597}
598
599impl Mul<f32> for Millimeters {
600    type Output = Millimeters;
601    fn mul(self, rhs: f32) -> Millimeters {
602        Millimeters(self.0 * rhs)
603    }
604}
605
606impl Add for Meters {
607    type Output = Meters;
608    fn add(self, rhs: Meters) -> Meters {
609        Meters(self.0 + rhs.0)
610    }
611}
612impl Sub for Meters {
613    type Output = Meters;
614    fn sub(self, rhs: Meters) -> Meters {
615        Meters(self.0 - rhs.0)
616    }
617}
618
619impl Add for BigWorldDistance {
620    type Output = BigWorldDistance;
621    fn add(self, rhs: BigWorldDistance) -> BigWorldDistance {
622        BigWorldDistance(self.0 + rhs.0)
623    }
624}
625impl Sub for BigWorldDistance {
626    type Output = BigWorldDistance;
627    fn sub(self, rhs: BigWorldDistance) -> BigWorldDistance {
628        BigWorldDistance(self.0 - rhs.0)
629    }
630}
631
632impl Add for ShipModelDistance {
633    type Output = ShipModelDistance;
634    fn add(self, rhs: ShipModelDistance) -> ShipModelDistance {
635        ShipModelDistance(self.0 + rhs.0)
636    }
637}
638impl Sub for ShipModelDistance {
639    type Output = ShipModelDistance;
640    fn sub(self, rhs: ShipModelDistance) -> ShipModelDistance {
641        ShipModelDistance(self.0 - rhs.0)
642    }
643}
644
645impl Add for Km {
646    type Output = Km;
647    fn add(self, rhs: Km) -> Km {
648        Km(self.0 + rhs.0)
649    }
650}
651impl Sub for Km {
652    type Output = Km;
653    fn sub(self, rhs: Km) -> Km {
654        Km(self.0 - rhs.0)
655    }
656}
657
658impl Add for Millimeters {
659    type Output = Millimeters;
660    fn add(self, rhs: Millimeters) -> Millimeters {
661        Millimeters(self.0 + rhs.0)
662    }
663}
664impl Sub for Millimeters {
665    type Output = Millimeters;
666    fn sub(self, rhs: Millimeters) -> Millimeters {
667        Millimeters(self.0 - rhs.0)
668    }
669}
670
671impl Add<BigWorldDistance> for Meters {
672    type Output = Meters;
673    fn add(self, rhs: BigWorldDistance) -> Meters {
674        Meters(self.0 + rhs.to_meters().0)
675    }
676}
677impl Sub<BigWorldDistance> for Meters {
678    type Output = Meters;
679    fn sub(self, rhs: BigWorldDistance) -> Meters {
680        Meters(self.0 - rhs.to_meters().0)
681    }
682}
683
684impl Add<Meters> for BigWorldDistance {
685    type Output = BigWorldDistance;
686    fn add(self, rhs: Meters) -> BigWorldDistance {
687        BigWorldDistance(self.0 + rhs.to_bigworld().0)
688    }
689}
690impl Sub<Meters> for BigWorldDistance {
691    type Output = BigWorldDistance;
692    fn sub(self, rhs: Meters) -> BigWorldDistance {
693        BigWorldDistance(self.0 - rhs.to_bigworld().0)
694    }
695}
696
697impl Add<Km> for Meters {
698    type Output = Meters;
699    fn add(self, rhs: Km) -> Meters {
700        Meters(self.0 + rhs.to_meters().0)
701    }
702}
703impl Sub<Km> for Meters {
704    type Output = Meters;
705    fn sub(self, rhs: Km) -> Meters {
706        Meters(self.0 - rhs.to_meters().0)
707    }
708}
709
710impl Add<Meters> for Km {
711    type Output = Km;
712    fn add(self, rhs: Meters) -> Km {
713        Km(self.0 + rhs.to_km().0)
714    }
715}
716impl Sub<Meters> for Km {
717    type Output = Km;
718    fn sub(self, rhs: Meters) -> Km {
719        Km(self.0 - rhs.to_km().0)
720    }
721}
722
723impl std::ops::Div<f32> for Meters {
724    type Output = Meters;
725    fn div(self, rhs: f32) -> Meters {
726        Meters(self.0 / rhs)
727    }
728}
729
730impl std::ops::Div<f32> for BigWorldDistance {
731    type Output = BigWorldDistance;
732    fn div(self, rhs: f32) -> BigWorldDistance {
733        BigWorldDistance(self.0 / rhs)
734    }
735}
736
737impl std::ops::Div<f32> for Km {
738    type Output = Km;
739    fn div(self, rhs: f32) -> Km {
740        Km(self.0 / rhs)
741    }
742}
743
744impl std::ops::Div<f32> for Millimeters {
745    type Output = Millimeters;
746    fn div(self, rhs: f32) -> Millimeters {
747        Millimeters(self.0 / rhs)
748    }
749}
750
751impl std::iter::Sum for Meters {
752    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
753        Meters(iter.map(|m| m.0).sum())
754    }
755}
756
757impl std::iter::Sum for BigWorldDistance {
758    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
759        BigWorldDistance(iter.map(|d| d.0).sum())
760    }
761}
762
763impl std::iter::Sum for Km {
764    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
765        Km(iter.map(|k| k.0).sum())
766    }
767}
768
769impl std::iter::Sum for Millimeters {
770    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
771        Millimeters(iter.map(|m| m.0).sum())
772    }
773}
774
775impl PartialEq<BigWorldDistance> for Meters {
776    fn eq(&self, other: &BigWorldDistance) -> bool {
777        self.0 == other.to_meters().0
778    }
779}
780impl PartialOrd<BigWorldDistance> for Meters {
781    fn partial_cmp(&self, other: &BigWorldDistance) -> Option<std::cmp::Ordering> {
782        self.0.partial_cmp(&other.to_meters().0)
783    }
784}
785
786impl PartialEq<Meters> for BigWorldDistance {
787    fn eq(&self, other: &Meters) -> bool {
788        self.0 == other.to_bigworld().0
789    }
790}
791impl PartialOrd<Meters> for BigWorldDistance {
792    fn partial_cmp(&self, other: &Meters) -> Option<std::cmp::Ordering> {
793        self.0.partial_cmp(&other.to_bigworld().0)
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    #[test]
802    fn meters_display_rounds_to_whole() {
803        assert_eq!(Meters::from(740.6).to_string(), "741 m");
804    }
805
806    #[test]
807    fn km_display_one_decimal() {
808        assert_eq!(Km::from(10.5).to_string(), "10.5 km");
809    }
810
811    #[test]
812    fn millimeters_display_rounds_to_whole() {
813        assert_eq!(Millimeters::from(406.0).to_string(), "406 mm");
814    }
815
816    #[test]
817    fn meters_per_second_display_rounds_to_whole() {
818        assert_eq!(MetersPerSecond::from(820.0).to_string(), "820 m/s");
819    }
820
821    /// Iowa's bow fire node sits at model z 6.489, which is 97.3 m forward of the
822    /// hull origin on a 262 m ship.
823    #[test]
824    fn ship_model_units_are_fifteen_meters() {
825        assert_eq!(ShipModelDistance::from(6.489).to_meters().value(), 97.335);
826        assert_eq!(Meters::from(97.335).to_ship_model().value(), 6.489);
827    }
828
829    /// A shell impact 8 world units from a battleship's hull origin is 120 m
830    /// out, which is a bow hit. At 30 it would be 240 m out, off the end of any
831    /// ship that exists.
832    #[test]
833    fn replay_world_units_are_fifteen_meters() {
834        assert_eq!(WorldDistance::from(8.0).to_meters().value(), 120.0);
835        assert_eq!(Meters::from(120.0).to_world().value(), 8.0);
836    }
837
838    /// The two spaces differ by exactly 2, which is small enough that a value in
839    /// the wrong one still looks plausible. Pinned so neither can drift into the
840    /// other unnoticed.
841    #[test]
842    fn game_params_and_replay_spaces_are_not_the_same() {
843        let one = 1.0f32;
844        assert_eq!(BigWorldDistance::from(one).to_meters().value(), 30.0);
845        assert_eq!(WorldDistance::from(one).to_meters().value(), 15.0);
846    }
847
848    /// The two angle units differ by a factor no name will catch, and the
849    /// penetration chain reads normalization in one and plate angles in the other.
850    #[test]
851    fn angles_round_trip_between_units() {
852        assert!((Degrees::from(60.0).to_radians().value() - std::f32::consts::FRAC_PI_3).abs() < 1e-6);
853        assert!((Radians::from(std::f32::consts::FRAC_PI_3).to_degrees().value() - 60.0).abs() < 1e-4);
854    }
855
856    /// A fuse burns for a time; what the armor chain needs is the distance the
857    /// shell covers in it.
858    #[test]
859    fn travel_over_multiplies_speed_by_time() {
860        assert!((MetersPerSecond::from(224.0).travel_over(Seconds::from(0.033)).value() - 7.392).abs() < 1e-3);
861    }
862
863    /// Hydroacoustic Search `C_4_7` carries `distShip = 133.3333` and detects
864    /// ships at 4.0 km in game; Black's radar carries 250.0 against 7.5 km.
865    /// These are what anchor the GameParams scale.
866    #[test]
867    fn game_params_distances_match_the_published_consumable_ranges() {
868        assert!((BigWorldDistance::from(133.3333).to_km().value() - 4.0).abs() < 0.001);
869        assert_eq!(BigWorldDistance::from(250.0).to_km().value(), 7.5);
870    }
871}