use std::collections::HashMap;
use crate::game_params::modifier_settings_data::modifier_setting;
use crate::game_params::types::CrewSkillModifier;
use crate::game_params::types::Species;
const KNOWN_ADDITIVE: &[&str] = &["yawSpeedBonus", "buffsStartPool"];
const KNOWN_MULTIPLICATIVE: &[&str] =
&["uwCoeffMultiplier", "burnChanceFactorHighLevel", "burnChanceGMGSMultiplier", "burnChanceMultiplier"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Combine {
Multiply,
Add,
}
impl Combine {
fn classify(build: u32, name: &str) -> Combine {
if KNOWN_ADDITIVE.contains(&name) {
return Combine::Add;
}
if KNOWN_MULTIPLICATIVE.contains(&name) {
return Combine::Multiply;
}
match modifier_setting(build, name) {
Some(s) if s.base_value == 0.0 => Combine::Add,
Some(_) => Combine::Multiply,
None => {
debug_assert!(
false,
"modifier name {name:?} is absent from MODIFIER_SETTINGS and KNOWN_ADDITIVE; defaulting to Multiply (add it to KNOWN_ADDITIVE if it is a bonus)"
);
Combine::Multiply
}
}
}
fn identity(self) -> f32 {
match self {
Combine::Multiply => 1.0,
Combine::Add => 0.0,
}
}
fn fold(self, acc: f32, value: f32) -> f32 {
match self {
Combine::Multiply => acc * value,
Combine::Add => acc + value,
}
}
}
#[derive(Clone, Debug)]
pub struct ModifierBundle {
species: Species,
values: HashMap<String, f32>,
rules: HashMap<String, Combine>,
}
impl ModifierBundle {
pub fn from_modifiers(mods: &[CrewSkillModifier], species: Species, build: u32) -> ModifierBundle {
let mut values: HashMap<String, f32> = HashMap::new();
let mut rules: HashMap<String, Combine> = HashMap::new();
for m in mods {
let name = m.name();
let combine = *rules.entry(name.to_string()).or_insert_with(|| Combine::classify(build, name));
let entry = values.entry(name.to_string()).or_insert_with(|| combine.identity());
*entry = combine.fold(*entry, m.get_for_species(&species));
}
ModifierBundle { species, values, rules }
}
pub fn empty(species: Species) -> ModifierBundle {
ModifierBundle { species, values: HashMap::new(), rules: HashMap::new() }
}
pub fn species(&self) -> Species {
self.species
}
pub fn coef(&self, name: &str) -> f32 {
match self.values.get(name).copied() {
Some(v) => {
debug_assert_eq!(
self.rules.get(name),
Some(&Combine::Multiply),
"coef({name:?}) reads an additively-folded modifier; use bonus() instead"
);
v
}
None => 1.0,
}
}
pub fn bonus(&self, name: &str) -> f32 {
match self.values.get(name).copied() {
Some(v) => {
debug_assert_eq!(
self.rules.get(name),
Some(&Combine::Add),
"bonus({name:?}) reads a multiplicatively-folded modifier; use coef() instead"
);
v
}
None => 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_params::types::CrewSkillModifier;
const BUILD: u32 = 11791718;
fn modifier(name: &str, battleship: f32) -> CrewSkillModifier {
CrewSkillModifier::builder()
.name(name.to_string())
.aircraft_carrier(1.0)
.auxiliary(1.0)
.battleship(battleship)
.cruiser(1.0)
.destroyer(1.0)
.submarine(1.0)
.excluded_consumables(Vec::new())
.build()
}
fn modifier_per_species(name: &str, battleship: f32, cruiser: f32) -> CrewSkillModifier {
CrewSkillModifier::builder()
.name(name.to_string())
.aircraft_carrier(1.0)
.auxiliary(1.0)
.battleship(battleship)
.cruiser(cruiser)
.destroyer(1.0)
.submarine(1.0)
.excluded_consumables(Vec::new())
.build()
}
#[test]
fn same_multiplicative_name_multiplies() {
let mods = [modifier("speedCoef", 0.9), modifier("speedCoef", 1.05)];
let bundle = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
assert!((bundle.coef("speedCoef") - 0.945).abs() < 1e-6, "got {}", bundle.coef("speedCoef"));
}
#[test]
fn same_additive_name_adds() {
let mods = [modifier("torpedoSpeedBonus", 5.0), modifier("torpedoSpeedBonus", 3.0)];
let bundle = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
assert!((bundle.bonus("torpedoSpeedBonus") - 8.0).abs() < 1e-6, "got {}", bundle.bonus("torpedoSpeedBonus"));
}
#[test]
fn second_additive_name_adds() {
let mods = [modifier("buffsShiftMaxLevel", 2.0), modifier("buffsShiftMaxLevel", 1.0)];
let bundle = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
assert!((bundle.bonus("buffsShiftMaxLevel") - 3.0).abs() < 1e-6);
}
#[test]
fn absent_name_is_identity() {
let bundle = ModifierBundle::from_modifiers(&[], Species::Battleship, BUILD);
assert_eq!(bundle.coef("speedCoef"), 1.0);
assert_eq!(bundle.bonus("torpedoSpeedBonus"), 0.0);
}
#[test]
fn per_species_resolution() {
let mods = [modifier_per_species("speedCoef", 0.9, 1.2)];
let bb = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
let ca = ModifierBundle::from_modifiers(&mods, Species::Cruiser, BUILD);
assert!((bb.coef("speedCoef") - 0.9).abs() < 1e-6);
assert!((ca.coef("speedCoef") - 1.2).abs() < 1e-6);
}
#[test]
fn empty_bundle_is_identity() {
let bundle = ModifierBundle::empty(Species::Battleship);
assert_eq!(bundle.species(), Species::Battleship);
assert_eq!(bundle.coef("speedCoef"), 1.0);
assert_eq!(bundle.coef("torpedoSpeedMultiplier"), 1.0);
assert_eq!(bundle.bonus("torpedoSpeedBonus"), 0.0);
}
#[test]
fn known_additive_absent_name_adds() {
let mods = [modifier("yawSpeedBonus", 4.0), modifier("yawSpeedBonus", 2.0)];
let bundle = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
assert!((bundle.bonus("yawSpeedBonus") - 6.0).abs() < 1e-6, "got {}", bundle.bonus("yawSpeedBonus"));
}
#[test]
fn absent_name_returns_identity_per_accessor() {
let bundle = ModifierBundle::from_modifiers(&[], Species::Battleship, BUILD);
assert_eq!(bundle.coef("yawSpeedBonus"), 1.0);
assert_eq!(bundle.bonus("speedCoef"), 0.0);
assert_eq!(bundle.coef("nonexistentName"), 1.0);
assert_eq!(bundle.bonus("nonexistentName"), 0.0);
}
#[test]
#[should_panic(expected = "additively-folded")]
#[cfg(debug_assertions)]
fn coef_on_additive_name_trips_assert() {
let mods = [modifier("yawSpeedBonus", 4.0)];
let bundle = ModifierBundle::from_modifiers(&mods, Species::Battleship, BUILD);
let _ = bundle.coef("yawSpeedBonus");
}
}