use crate::game_params::types::BigWorldDistance;
use crate::game_params::types::Km;
pub const KM_TO_M: f32 = 1000.0;
pub const KNOTS_TO_MPS: f32 = 1.0 / 3.0;
pub const MPS_TO_KNOTS: f32 = 1.0 / KNOTS_TO_MPS;
pub const SHIP_TIME_SCALE: f32 = 2.0;
pub const SHIP_TIME_SCALE_INV: f32 = 1.0 / SHIP_TIME_SCALE;
pub const TORPEDO_DAMAGE_CONSTANT: f32 = 3.0;
pub const HULL_HEALTH_ROUND: f32 = 50.0;
pub const DEFAULT_UW_DAMAGE_COEFF: f32 = 0.333;
pub const BW_TO_BALLISTIC: f32 = 30.0;
pub const BALLISTIC_TO_BW: f32 = 1.0 / BW_TO_BALLISTIC;
pub const BW_TO_SHIP: f32 = 15.0;
pub const SHIP_TO_BW: f32 = 1.0 / BW_TO_SHIP;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DispersionEllipse {
pub horizontal: BigWorldDistance,
pub vertical: BigWorldDistance,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DispersionCurve {
pub min_radius: f32,
pub ideal_radius: f32,
pub ideal_distance: f32,
pub radius_on_zero: f32,
pub radius_on_delim: f32,
pub radius_on_max: f32,
pub delim: f32,
}
pub fn dispersion_horizontal(
min_radius: f32,
ideal_radius: f32,
ideal_distance: f32,
dist: Km,
ideal_radius_coef: f32,
) -> BigWorldDistance {
let min_r = min_radius * ideal_radius_coef;
let ideal_r = ideal_radius * ideal_radius_coef;
BigWorldDistance::from(min_r + dist.value() * BALLISTIC_TO_BW * KM_TO_M * (ideal_r - min_r) / ideal_distance)
}
pub fn clamped_dispersion_coeff(
radius_on_zero: f32,
radius_on_delim: f32,
radius_on_max: f32,
delim: f32,
dist: Km,
max_dist: Km,
) -> f32 {
let delim_dist = max_dist.value() * delim;
let f = dist.value() / delim_dist;
if f < 1.0 {
lerp(radius_on_zero, radius_on_delim, f)
} else {
lerp(radius_on_delim, radius_on_max, (dist.value() - delim_dist) / (max_dist.value() - delim_dist))
}
}
fn lerp(a: f32, b: f32, factor: f32) -> f32 {
a + (b - a) * factor.min(1.0)
}
pub fn dispersion_ellipse(
curve: &DispersionCurve,
dist: Km,
max_dist: Km,
ideal_radius_coef: f32,
) -> DispersionEllipse {
let clamped = Km::from(dist.value().min(max_dist.value()));
let horizontal =
dispersion_horizontal(curve.min_radius, curve.ideal_radius, curve.ideal_distance, clamped, ideal_radius_coef);
let coeff = clamped_dispersion_coeff(
curve.radius_on_zero,
curve.radius_on_delim,
curve.radius_on_max,
curve.delim,
clamped,
max_dist,
);
DispersionEllipse { horizontal, vertical: horizontal * coeff }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal_constants() {
assert_eq!(KM_TO_M, 1000.0);
assert_eq!(KNOTS_TO_MPS, 1.0 / 3.0);
assert_eq!(MPS_TO_KNOTS, 3.0);
assert_eq!(SHIP_TIME_SCALE, 2.0);
assert_eq!(SHIP_TIME_SCALE_INV, 0.5);
assert_eq!(TORPEDO_DAMAGE_CONSTANT, 3.0);
assert_eq!(HULL_HEALTH_ROUND, 50.0);
assert_eq!(DEFAULT_UW_DAMAGE_COEFF, 0.333);
}
#[test]
fn bw_to_ballistic_inverse() {
assert!((BALLISTIC_TO_BW - 1.0 / 30.0).abs() < 1e-9);
}
#[test]
fn gearing_torpedo_range() {
let range_km = 350.0 * BW_TO_BALLISTIC / KM_TO_M;
assert!((range_km - 10.5).abs() < 0.1, "got {range_km}");
}
#[test]
fn shimakaze_torpedo_range() {
let range_km = 667.0 * BW_TO_BALLISTIC / KM_TO_M;
assert!((range_km - 20.0).abs() < 0.1, "got {range_km}");
}
#[test]
fn bw_to_ship_inverse() {
assert!((SHIP_TO_BW - 1.0 / 15.0).abs() < 1e-9);
}
#[test]
fn north_carolina_dispersion() {
let d = dispersion_horizontal(2.0, 12.0, 1000.0, Km::from(21143.0 / 1000.0), 1.0).to_meters().value();
assert!((d - 271.0).abs() < 1.0, "got {d}");
}
#[test]
fn yamato_dispersion() {
let d = dispersion_horizontal(2.8, 10.0, 1000.0, Km::from(26630.0 / 1000.0), 1.0).to_meters().value();
assert!((d - 273.0).abs() < 3.0, "got {d}");
}
#[test]
fn lerp_clamps_factor_above_one() {
assert_eq!(lerp(2.0, 4.0, 1.5), 4.0);
assert!((lerp(2.0, 4.0, 0.5) - 3.0).abs() < 1e-6);
}
#[test]
fn clamped_coeff_segments_and_boundary() {
assert!((clamped_dispersion_coeff(1.0, 1.5, 2.0, 0.5, Km::from(5.0), Km::from(20.0)) - 1.25).abs() < 1e-6);
assert!((clamped_dispersion_coeff(1.0, 1.5, 2.0, 0.5, Km::from(15.0), Km::from(20.0)) - 1.75).abs() < 1e-6);
assert!((clamped_dispersion_coeff(1.0, 1.5, 2.0, 0.5, Km::from(20.0), Km::from(20.0)) - 2.0).abs() < 1e-6);
}
#[test]
fn horizontal_matches_legacy_north_carolina() {
let h = dispersion_horizontal(2.0, 12.0, 1000.0, Km::from(21143.0 / 1000.0), 1.0);
assert!((h.to_meters().value() - 271.0).abs() < 1.0, "got {}", h.to_meters().value());
}
#[test]
fn ellipse_vertical_is_horizontal_times_radius_on_max_at_max_range() {
let curve = DispersionCurve {
min_radius: 2.0,
ideal_radius: 12.0,
ideal_distance: 1000.0,
radius_on_zero: 1.0,
radius_on_delim: 1.4,
radius_on_max: 1.8,
delim: 0.5,
};
let e = dispersion_ellipse(&curve, Km::from(21.143), Km::from(21.143), 1.0);
assert!((e.vertical.value() - e.horizontal.value() * 1.8).abs() < 1e-3);
}
#[test]
fn ellipse_vertical_first_segment_uses_interpolated_coeff() {
let curve = DispersionCurve {
min_radius: 2.0,
ideal_radius: 12.0,
ideal_distance: 1000.0,
radius_on_zero: 1.0,
radius_on_delim: 2.0,
radius_on_max: 3.0,
delim: 0.5,
};
let e = dispersion_ellipse(&curve, Km::from(5.0), Km::from(20.0), 1.0);
assert!((e.vertical.value() - e.horizontal.value() * 1.5).abs() < 1e-3);
}
}