use crate::game_params::ttx::modifiers::ModifierBundle;
use crate::game_params::ttx::provenance::Op;
pub struct AppliedModifier {
pub name: &'static str,
pub op: Op,
}
pub const MAX_SMALL_CATEGORY_LEVEL: u32 = 7;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StatWeaponType {
MainAp,
MainHe,
MainCs,
AtbaAp,
AtbaHe,
AtbaCs,
Other,
}
impl StatWeaponType {
pub fn is_main(self) -> bool {
matches!(self, StatWeaponType::MainAp | StatWeaponType::MainHe | StatWeaponType::MainCs)
}
pub fn is_atba(self) -> bool {
matches!(self, StatWeaponType::AtbaAp | StatWeaponType::AtbaHe | StatWeaponType::AtbaCs)
}
pub fn is_hecs(self) -> bool {
matches!(self, StatWeaponType::MainHe | StatWeaponType::MainCs)
}
pub fn is_csap(self) -> bool {
matches!(
self,
StatWeaponType::AtbaCs | StatWeaponType::AtbaAp | StatWeaponType::MainAp | StatWeaponType::MainCs
)
}
pub fn to_atba(self) -> StatWeaponType {
match self {
StatWeaponType::MainAp => StatWeaponType::AtbaAp,
StatWeaponType::MainHe => StatWeaponType::AtbaHe,
StatWeaponType::MainCs => StatWeaponType::AtbaCs,
other => other,
}
}
}
pub fn ammo_to_stat_weapon_table(ammo: &str) -> StatWeaponType {
match ammo {
"AP" => StatWeaponType::MainAp,
"HE" => StatWeaponType::MainHe,
"CS" => StatWeaponType::MainCs,
_ => StatWeaponType::Other,
}
}
pub fn alpha_damage_coeff(
weapon: StatWeaponType,
modifier: &ModifierBundle,
is_alt_mode: bool,
) -> (f32, Vec<AppliedModifier>) {
let mut applied = Vec::new();
let mut coeff = modifier.coef("allAlphaMultiplier");
applied.push(AppliedModifier { name: "allAlphaMultiplier", op: Op::Mul });
if weapon.is_main() {
coeff *= modifier.coef("GMAlphaFactor");
applied.push(AppliedModifier { name: "GMAlphaFactor", op: Op::Mul });
} else if weapon.is_atba() {
coeff *= modifier.coef("GSAlphaFactor");
applied.push(AppliedModifier { name: "GSAlphaFactor", op: Op::Mul });
if is_alt_mode {
coeff *= modifier.coef("GSMAlphaFactor");
applied.push(AppliedModifier { name: "GSMAlphaFactor", op: Op::Mul });
}
}
(coeff, applied)
}
pub fn artillery_damage_coeff(
caliber_m: f32,
weapon: StatWeaponType,
modifier: &ModifierBundle,
heavy_cruiser_shell_diameter_m: f32,
) -> (f32, Vec<AppliedModifier>) {
let mut applied = Vec::new();
if weapon.is_main() {
let mut y = modifier.coef("GMDamageCoeff");
applied.push(AppliedModifier { name: "GMDamageCoeff", op: Op::Mul });
match weapon {
StatWeaponType::MainAp => {
y *= modifier.coef("GMAPDamageCoeff");
applied.push(AppliedModifier { name: "GMAPDamageCoeff", op: Op::Mul });
if caliber_m >= heavy_cruiser_shell_diameter_m {
y *= modifier.coef("GMHeavyCruiserCaliberDamageCoeff");
applied.push(AppliedModifier { name: "GMHeavyCruiserCaliberDamageCoeff", op: Op::Mul });
}
}
StatWeaponType::MainCs => {
y *= modifier.coef("GMCSDamageCoeff");
applied.push(AppliedModifier { name: "GMCSDamageCoeff", op: Op::Mul });
}
_ => {}
}
if weapon.is_hecs() {
y *= modifier.coef("GMHECSDamageCoeff");
applied.push(AppliedModifier { name: "GMHECSDamageCoeff", op: Op::Mul });
}
return (y, applied);
}
if weapon.is_atba() {
let mut y = modifier.coef("GSDamageCoeff");
applied.push(AppliedModifier { name: "GSDamageCoeff", op: Op::Mul });
match weapon {
StatWeaponType::AtbaCs => {
y *= modifier.coef("GSCSDamageCoeff");
applied.push(AppliedModifier { name: "GSCSDamageCoeff", op: Op::Mul });
}
StatWeaponType::AtbaAp => {
y *= modifier.coef("GSAPDamageCoeff");
applied.push(AppliedModifier { name: "GSAPDamageCoeff", op: Op::Mul });
}
_ => {}
}
return (y, applied);
}
(1.0, applied)
}
pub fn is_small_projectile(bullet_diametr_m: f32, small_projectile_max_diameter_m: f32) -> bool {
bullet_diametr_m <= small_projectile_max_diameter_m
}
pub fn calculate_burn_chance(
ammo_owner_level: u32,
initial_burn_prob: f32,
modifier: &ModifierBundle,
is_small: bool,
) -> (f32, Vec<AppliedModifier>) {
let mut applied = Vec::new();
let mut prob = initial_burn_prob;
if ammo_owner_level > MAX_SMALL_CATEGORY_LEVEL {
prob *= modifier.coef("burnChanceFactorHighLevel");
applied.push(AppliedModifier { name: "burnChanceFactorHighLevel", op: Op::Mul });
} else {
prob *= modifier.coef("burnChanceFactorLowLevel");
applied.push(AppliedModifier { name: "burnChanceFactorLowLevel", op: Op::Mul });
}
prob *= modifier.coef("burnChanceGMGSMultiplier");
applied.push(AppliedModifier { name: "burnChanceGMGSMultiplier", op: Op::Mul });
prob += modifier.bonus("artilleryBurnChanceBonus");
applied.push(AppliedModifier { name: "artilleryBurnChanceBonus", op: Op::Add });
prob *= modifier.coef("burnChanceMultiplier");
applied.push(AppliedModifier { name: "burnChanceMultiplier", op: Op::Mul });
prob += modifier.bonus("burnChanceBonus");
applied.push(AppliedModifier { name: "burnChanceBonus", op: Op::Add });
if is_small {
prob += modifier.bonus("burnChanceFactorSmall");
applied.push(AppliedModifier { name: "burnChanceFactorSmall", op: Op::Add });
} else {
prob += modifier.bonus("burnChanceFactorBig");
applied.push(AppliedModifier { name: "burnChanceFactorBig", op: Op::Add });
}
(prob, applied)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_params::types::CrewSkillModifier;
use crate::game_params::types::Species;
const VERSION: crate::data::Version = crate::data::Version::base(15, 0, 0);
fn stock() -> ModifierBundle {
ModifierBundle::empty(Species::Cruiser)
}
fn modifier(name: &str, value: f32) -> CrewSkillModifier {
CrewSkillModifier::builder()
.name(name.to_string())
.aircraft_carrier(value)
.auxiliary(value)
.battleship(value)
.cruiser(value)
.destroyer(value)
.submarine(value)
.excluded_consumables(Vec::new())
.build()
}
#[test]
fn ammo_strings_map_to_main_weapon() {
assert_eq!(ammo_to_stat_weapon_table("HE"), StatWeaponType::MainHe);
assert_eq!(ammo_to_stat_weapon_table("AP"), StatWeaponType::MainAp);
assert_eq!(ammo_to_stat_weapon_table("CS"), StatWeaponType::MainCs);
assert_eq!(ammo_to_stat_weapon_table("torpedo"), StatWeaponType::Other);
}
#[test]
fn csap_set_membership() {
assert!(StatWeaponType::MainAp.is_csap());
assert!(StatWeaponType::MainCs.is_csap());
assert!(StatWeaponType::AtbaAp.is_csap());
assert!(StatWeaponType::AtbaCs.is_csap());
assert!(!StatWeaponType::MainHe.is_csap());
assert!(!StatWeaponType::AtbaHe.is_csap());
}
#[test]
fn hecs_set_membership() {
assert!(StatWeaponType::MainHe.is_hecs());
assert!(StatWeaponType::MainCs.is_hecs());
assert!(!StatWeaponType::MainAp.is_hecs());
assert!(!StatWeaponType::AtbaHe.is_hecs());
}
#[test]
fn main_to_atba_mapping() {
assert_eq!(StatWeaponType::MainAp.to_atba(), StatWeaponType::AtbaAp);
assert_eq!(StatWeaponType::MainHe.to_atba(), StatWeaponType::AtbaHe);
assert_eq!(StatWeaponType::MainCs.to_atba(), StatWeaponType::AtbaCs);
assert_eq!(StatWeaponType::AtbaAp.to_atba(), StatWeaponType::AtbaAp);
}
#[test]
fn stock_alpha_damage_coeff_is_one() {
assert_eq!(alpha_damage_coeff(StatWeaponType::MainHe, &stock(), false).0, 1.0);
assert_eq!(alpha_damage_coeff(StatWeaponType::AtbaHe, &stock(), false).0, 1.0);
}
#[test]
fn alpha_damage_coeff_folds_main_factors() {
let mods = [modifier("allAlphaMultiplier", 1.1), modifier("GMAlphaFactor", 1.2)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (got, applied) = alpha_damage_coeff(StatWeaponType::MainAp, &bundle, false);
assert!((got - 1.32).abs() < 1e-5, "got {got}");
assert_eq!(applied.len(), 2);
assert_eq!(applied[0].name, "allAlphaMultiplier");
assert_eq!(applied[1].name, "GMAlphaFactor");
assert!(applied.iter().all(|m| m.op == Op::Mul));
}
#[test]
fn alpha_damage_coeff_atba_reports_gs_factor() {
let mods = [modifier("allAlphaMultiplier", 1.0), modifier("GSAlphaFactor", 1.05)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (got, applied) = alpha_damage_coeff(StatWeaponType::AtbaHe, &bundle, false);
assert!((got - 1.05).abs() < 1e-5, "got {got}");
assert!(applied.iter().any(|m| m.name == "GSAlphaFactor"), "GSAlphaFactor must be reported");
assert!(!applied.iter().any(|m| m.name == "GMAlphaFactor"), "GMAlphaFactor must not be reported for ATBA");
}
#[test]
fn stock_artillery_damage_coeff_he_is_one() {
let (got, _) = artillery_damage_coeff(0.152, StatWeaponType::MainHe, &stock(), 0.149);
assert_eq!(got, 1.0);
}
#[test]
fn artillery_damage_coeff_ap_heavy_cruiser_gate() {
let mods = [modifier("GMAPDamageCoeff", 1.5), modifier("GMHeavyCruiserCaliberDamageCoeff", 2.0)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (below, below_applied) = artillery_damage_coeff(0.152, StatWeaponType::MainAp, &bundle, 0.190);
assert!((below - 1.5).abs() < 1e-5, "got {below}");
assert!(!below_applied.iter().any(|m| m.name == "GMHeavyCruiserCaliberDamageCoeff"));
let (above, above_applied) = artillery_damage_coeff(0.203, StatWeaponType::MainAp, &bundle, 0.190);
assert!((above - 3.0).abs() < 1e-5, "got {above}");
assert!(above_applied.iter().any(|m| m.name == "GMHeavyCruiserCaliberDamageCoeff"));
}
#[test]
fn stock_burn_chance_high_level_is_burn_prob() {
let (got, _) = calculate_burn_chance(10, 0.12, &stock(), false);
assert!((got.max(0.0) - 0.12).abs() < 1e-6, "got {got}");
}
#[test]
fn stock_burn_chance_low_level_is_burn_prob() {
let (got, _) = calculate_burn_chance(5, 0.08, &stock(), true);
assert!((got.max(0.0) - 0.08).abs() < 1e-6, "got {got}");
}
#[test]
fn burn_chance_full_formula_high_level() {
let mods = [
modifier("burnChanceFactorHighLevel", 0.9),
modifier("burnChanceGMGSMultiplier", 0.95),
modifier("artilleryBurnChanceBonus", 0.02),
modifier("burnChanceMultiplier", 1.1),
modifier("burnChanceBonus", 0.01),
modifier("burnChanceFactorBig", -0.03),
];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (got, applied) = calculate_burn_chance(10, 0.12, &bundle, false);
let expected = ((0.12f32 * 0.9 * 0.95) + 0.02) * 1.1 + 0.01 - 0.03;
assert!((got - expected).abs() < 1e-6, "got {got} expected {expected}");
assert_eq!(applied.len(), 6);
assert_eq!(applied[0].name, "burnChanceFactorHighLevel");
assert_eq!(applied[0].op, Op::Mul);
assert_eq!(applied[1].name, "burnChanceGMGSMultiplier");
assert_eq!(applied[1].op, Op::Mul);
assert_eq!(applied[2].name, "artilleryBurnChanceBonus");
assert_eq!(applied[2].op, Op::Add);
assert_eq!(applied[3].name, "burnChanceMultiplier");
assert_eq!(applied[3].op, Op::Mul);
assert_eq!(applied[4].name, "burnChanceBonus");
assert_eq!(applied[4].op, Op::Add);
assert_eq!(applied[5].name, "burnChanceFactorBig");
assert_eq!(applied[5].op, Op::Add);
}
#[test]
fn burn_chance_clamps_to_zero() {
let mods = [modifier("burnChanceBonus", -1.0)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (got, _) = calculate_burn_chance(10, 0.12, &bundle, false);
assert_eq!(got.max(0.0), 0.0);
}
#[test]
fn burn_chance_ap_na_clamps_to_zero() {
let (got, _) = calculate_burn_chance(10, -0.5, &stock(), false);
assert_eq!(got.max(0.0), 0.0);
}
#[test]
fn small_projectile_threshold() {
assert!(is_small_projectile(0.1, 0.149));
assert!(!is_small_projectile(0.152, 0.149));
}
}