use std::collections::BTreeMap;
use crate::game_params::ttx::constants::BW_TO_BALLISTIC;
use crate::game_params::ttx::constants::KM_TO_M;
use crate::game_params::ttx::model::AmmoCount;
use crate::game_params::ttx::model::Seconds;
use crate::game_params::ttx::modifiers::ModifierBundle;
use crate::game_params::types::AbilityCategory;
use crate::game_params::types::Km;
use crate::game_params::types::Meters;
const GROUP_SHIP: &str = "ship";
const GROUP_SQUADRON: &str = "squadron";
const LIFECYCLE_COUNT_BASED: f32 = 0.0;
const LIFECYCLE_TIME_BASED: f32 = 1.0;
const TYPE_REGEN_CREW: &str = "regenCrew";
const TYPE_SMOKE_GENERATOR: &str = "smokeGenerator";
const TYPE_PLANE_SMOKE_GENERATOR: &str = "planeSmokeGenerator";
const TYPE_FIGHTER: &str = "fighter";
const TYPE_REGENERATE_HEALTH: &str = "regenerateHealth";
const CALL_FIGHTERS_CONSUMABLES: &[&str] = &["callFighters", "planeTacticalFighters"];
const CONSUMABLES_WITH_RELOAD_COEFFICIENTS: &[&str] = &[
"artilleryBoosters",
"torpedoReloader",
"crashCrew",
"airDefenseDisp",
"scout",
"fighter",
"sonar",
"rls",
"smokeGenerator",
"speedBoosters",
"regenCrew",
"healForsage",
"regenerateHealth",
"planeSmokeGenerator",
"hydrophone",
"submarineLocator",
"activeManeuvering",
];
const CONSUMABLES_WITH_WORK_TIME_COEFFICIENTS: &[&str] = &[
"speedBoosters",
"smokeGenerator",
"scout",
"crashCrew",
"regenCrew",
"airDefenseDisp",
"sonar",
"rls",
"fighter",
"regenerateHealth",
"callFighters",
"planeSmokeGenerator",
"subsEnergyFreeze",
"fastRudders",
"submarineLocator",
"planeTacticalFighters",
"activeManeuvering",
];
const CONSUMABLES_WITH_CAPACITY_COEFFICIENTS: &[&str] = &[
"crashCrew",
"regenCrew",
"smokeGenerator",
"planeSmokeGenerator",
"speedBoosters",
"sonar",
"rls",
"artilleryBoosters",
"regenerateHealth",
"subsEnergyFreeze",
"scout",
"fighter",
"callFighters",
];
const ADDITIONAL_CONSUMABLES_COUNT: &[&str] = &[
"crashCrew",
"regenCrew",
"regenerateHealth",
"callFighters",
"scout",
"torpedoReloader",
"smokeGenerator",
"planeTacticalFighters",
"activeManeuvering",
];
#[derive(Clone, Debug, PartialEq)]
pub struct EffectiveConsumable {
pub reload_time: Seconds,
pub work_time: Option<Seconds>,
pub preparation_time: Seconds,
pub charges: AmmoCount,
pub max_capacity: Option<f32>,
pub detection_radius: Option<Meters>,
pub regeneration_hp_speed: Option<f32>,
pub smoke_radius: Option<Km>,
pub smoke_lifetime: Option<Seconds>,
pub fighters_count: Option<f32>,
pub call_fighters_radius: Option<Km>,
pub call_fighters_time_delay: Option<Seconds>,
pub call_fighters_time_from_heaven: Option<Seconds>,
pub plane_regeneration_rate: Option<f32>,
}
fn gated_type_coef(bundle: &ModifierBundle, base: f32, type_name: &str, gate: &[&str], suffix: &str) -> f32 {
if gate.contains(&type_name) { base * bundle.coef(&format!("{type_name}{suffix}")) } else { base }
}
fn consumable_reload_coeff(bundle: &ModifierBundle, type_name: &str, group: &str) -> f32 {
let mut coeff = bundle.coef("allConsumableReloadTime");
match group {
GROUP_SHIP => coeff *= bundle.coef("ConsumableReloadTime"), GROUP_SQUADRON => coeff *= bundle.coef("planeConsumableReloadTime"), _ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_RELOAD_COEFFICIENTS, "ReloadCoeff")
}
fn consumable_work_time_coeff(bundle: &ModifierBundle, type_name: &str, group: &str) -> f32 {
let mut coeff = 1.0;
match group {
GROUP_SHIP => coeff *= bundle.coef("ConsumablesWorkTime"), GROUP_SQUADRON => coeff *= bundle.coef("planeConsumablesWorkTime"), _ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_WORK_TIME_COEFFICIENTS, "WorkTimeCoeff")
}
fn consumable_capacity_coeff(bundle: &ModifierBundle, type_name: &str, group: &str) -> f32 {
let mut coeff = bundle.coef("consumableCapacityCoeff"); match group {
GROUP_SHIP => coeff *= bundle.coef("shipConsumableCapacityCoeff"), GROUP_SQUADRON => coeff *= bundle.coef("squadronConsumableCapacityCoeff"), _ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_CAPACITY_COEFFICIENTS, "CapacityCoeff")
}
fn additional_consumables_count(bundle: &ModifierBundle, type_name: &str) -> f32 {
if ADDITIONAL_CONSUMABLES_COUNT.contains(&type_name) {
bundle.apply(0.0, &format!("{type_name}AdditionalConsumables"))
} else {
0.0
}
}
fn additional_consumables_for_group(bundle: &ModifierBundle, group: &str) -> f32 {
match group {
GROUP_SHIP => bundle.apply(0.0, "additionalConsumables"), GROUP_SQUADRON => bundle.apply(0.0, "planeAdditionalConsumables"), _ => 0.0,
}
}
fn field(fields: &BTreeMap<String, f32>, name: &str) -> Option<f32> {
fields.get(name).copied()
}
fn km_from_ballistic(radius: f32) -> Km {
Km::from(radius * (BW_TO_BALLISTIC / KM_TO_M))
}
pub fn effective_consumable(category: &AbilityCategory, modifiers: &ModifierBundle) -> EffectiveConsumable {
let type_name = category.consumable_type_raw();
let group = category.group();
let fields = category.effect_fields();
let reload_coeff = consumable_reload_coeff(modifiers, type_name, group);
let reload_time = Seconds::from(category.reload_time() * reload_coeff);
let preparation_time = Seconds::from(category.preparation_time() * reload_coeff);
let lifecycle = field(fields, "lifeCycleType").unwrap_or(LIFECYCLE_COUNT_BASED);
let work_time = if lifecycle == LIFECYCLE_COUNT_BASED {
Some(Seconds::from(category.work_time() * consumable_work_time_coeff(modifiers, type_name, group)))
} else {
None
};
let base_count = category.num_consumables();
let charges = if base_count < 0 {
AmmoCount::Infinite
} else if lifecycle == LIFECYCLE_COUNT_BASED {
let added =
additional_consumables_count(modifiers, type_name) + additional_consumables_for_group(modifiers, group);
let total = (base_count as f32 + added).max(0.0);
AmmoCount::Finite(total.round() as u32)
} else {
AmmoCount::Finite(base_count as u32)
};
let max_capacity = if lifecycle == LIFECYCLE_TIME_BASED {
field(fields, "maxCapacity")
.filter(|&c| c >= 0.0)
.map(|c| c * consumable_capacity_coeff(modifiers, type_name, group))
} else {
None
};
let regeneration_hp_speed = if type_name == TYPE_REGEN_CREW {
category.regeneration_hp_speed().map(|v| v * modifiers.coef("regenerationHPSpeed"))
} else {
None
};
let detection_radius = category.detection_radius();
let mut smoke_radius = None;
let mut smoke_lifetime = None;
let mut fighters_count = None;
let mut call_fighters_radius = None;
let mut call_fighters_time_delay = None;
let mut call_fighters_time_from_heaven = None;
let mut plane_regeneration_rate = None;
if type_name == TYPE_SMOKE_GENERATOR {
smoke_lifetime = field(fields, "lifeTime").map(|v| Seconds::from(modifiers.apply(v, "smokeGeneratorLifeTime")));
smoke_radius =
field(fields, "radius").map(|v| km_from_ballistic(modifiers.apply(v, "smokeScreenRadiusCoefficient")));
} else if type_name == TYPE_PLANE_SMOKE_GENERATOR {
smoke_lifetime =
field(fields, "lifeTime").map(|v| Seconds::from(modifiers.apply(v, "planeSmokeGeneratorLifeTime")));
} else if type_name == TYPE_REGENERATE_HEALTH {
plane_regeneration_rate =
field(fields, "regenerationRate").map(|v| modifiers.apply(v, "planeRegenerationRate"));
} else if type_name == TYPE_FIGHTER {
fighters_count = field(fields, "fightersNum").map(|v| modifiers.apply(v, "extraFighterCount"));
} else if CALL_FIGHTERS_CONSUMABLES.contains(&type_name) {
call_fighters_radius =
field(fields, "radius").map(|v| km_from_ballistic(modifiers.apply(v, "callFightersRadiusCoeff")));
call_fighters_time_delay =
field(fields, "timeDelayAttack").map(|v| Seconds::from(modifiers.apply(v, "callFightersTimeDelayAttack")));
call_fighters_time_from_heaven =
field(fields, "timeFromHeaven").map(|v| Seconds::from(modifiers.apply(v, "callFightersAppearDelay")));
}
EffectiveConsumable {
reload_time,
work_time,
preparation_time,
charges,
max_capacity,
detection_radius,
regeneration_hp_speed,
smoke_radius,
smoke_lifetime,
fighters_count,
call_fighters_radius,
call_fighters_time_delay,
call_fighters_time_from_heaven,
plane_regeneration_rate,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_params::types::AbilityCategory;
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 crash_crew() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("reloadTime".to_string(), 120.0);
fields.insert("workTime".to_string(), 15.0);
AbilityCategory::builder()
.consumable_type("crashCrew".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(-1)
.preparation_time(0.0)
.reload_time(120.0)
.work_time(15.0)
.effect_fields(fields)
.build()
}
fn finite_sonar() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
AbilityCategory::builder()
.consumable_type("sonar".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(3)
.preparation_time(0.0)
.reload_time(90.0)
.work_time(100.0)
.effect_fields(fields)
.build()
}
fn smoke_generator() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("radius".to_string(), 15.0);
fields.insert("lifeTime".to_string(), 77.0);
AbilityCategory::builder()
.consumable_type("smokeGenerator".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(2)
.preparation_time(0.0)
.reload_time(240.0)
.work_time(20.0)
.effect_fields(fields)
.build()
}
fn call_fighters() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("radius".to_string(), 116.667);
fields.insert("timeDelayAttack".to_string(), 5.0);
fields.insert("timeFromHeaven".to_string(), 3.0);
AbilityCategory::builder()
.consumable_type("callFighters".to_string())
.group("squadron".to_string())
.icon_id(String::new())
.num_consumables(-1)
.preparation_time(0.0)
.reload_time(60.0)
.work_time(60.0)
.effect_fields(fields)
.build()
}
fn fighter() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("fightersNum".to_string(), 1.0);
AbilityCategory::builder()
.consumable_type("fighter".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(3)
.preparation_time(0.0)
.reload_time(90.0)
.work_time(60.0)
.effect_fields(fields)
.build()
}
fn regenerate_health() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("regenerationRate".to_string(), 0.1);
AbilityCategory::builder()
.consumable_type("regenerateHealth".to_string())
.group("squadron".to_string())
.icon_id(String::new())
.num_consumables(-1)
.preparation_time(0.0)
.reload_time(60.0)
.work_time(60.0)
.effect_fields(fields)
.build()
}
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 empty_bundle_yields_base_values() {
let cat = crash_crew();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::Battleship));
assert_eq!(eff.reload_time, Seconds::from(120.0));
assert_eq!(eff.preparation_time, Seconds::from(0.0));
assert_eq!(eff.work_time, Some(Seconds::from(15.0)));
assert_eq!(eff.charges, AmmoCount::Infinite);
assert_eq!(eff.max_capacity, None);
}
#[test]
fn reload_modifier_scales_reload_time() {
let cat = crash_crew();
let mods = [modifier("ConsumableReloadTime", 0.9)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert!((eff.reload_time.value() - 108.0).abs() < 1e-3, "got {}", eff.reload_time.value());
}
#[test]
fn reload_coefficients_compound() {
let cat = crash_crew();
let mods = [modifier("ConsumableReloadTime", 0.9), modifier("allConsumableReloadTime", 0.95)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert!((eff.reload_time.value() - 102.6).abs() < 1e-3, "got {}", eff.reload_time.value());
}
#[test]
fn infinite_charges_stay_infinite() {
let cat = crash_crew();
let mods = [modifier("crashCrewAdditionalConsumables", 1.0)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert_eq!(eff.charges, AmmoCount::Infinite);
}
#[test]
fn finite_charges_add_group_bonus() {
let cat = finite_sonar();
let mods = [modifier("additionalConsumables", 1.0)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert_eq!(eff.charges, AmmoCount::Finite(4));
}
#[test]
fn finite_charges_base_unchanged() {
let cat = finite_sonar();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.charges, AmmoCount::Finite(3));
}
#[test]
fn per_type_work_time_coeff_applies() {
let cat = finite_sonar();
let mods = [modifier("sonarWorkTimeCoeff", 0.8)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert_eq!(eff.work_time, Some(Seconds::from(80.0)));
}
#[test]
fn smoke_effects_base() {
let cat = smoke_generator();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.smoke_radius, Some(Km::from(0.45)));
assert_eq!(eff.smoke_lifetime, Some(Seconds::from(77.0)));
assert_eq!(eff.call_fighters_radius, None);
assert_eq!(eff.fighters_count, None);
assert_eq!(eff.plane_regeneration_rate, None);
}
#[test]
fn smoke_radius_modifier_applies() {
let cat = smoke_generator();
let mods = [modifier("smokeScreenRadiusCoefficient", 1.2)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert!((eff.smoke_radius.unwrap().value() - 0.54).abs() < 1e-4, "got {}", eff.smoke_radius.unwrap().value());
}
#[test]
fn call_fighters_effects_base() {
let cat = call_fighters();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::AirCarrier));
assert!(
(eff.call_fighters_radius.unwrap().value() - 3.5).abs() < 1e-3,
"got {}",
eff.call_fighters_radius.unwrap().value()
);
assert_eq!(eff.call_fighters_time_delay, Some(Seconds::from(5.0)));
assert_eq!(eff.call_fighters_time_from_heaven, Some(Seconds::from(3.0)));
assert_eq!(eff.smoke_radius, None);
assert_eq!(eff.fighters_count, None);
}
#[test]
fn call_fighters_time_modifier_applies() {
let cat = call_fighters();
let mods = [modifier("callFightersTimeDelayAttack", 0.8)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::AirCarrier, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert_eq!(eff.call_fighters_time_delay, Some(Seconds::from(4.0)));
}
#[test]
fn fighter_count_additive_modifier() {
let cat = fighter();
let mods = [modifier("extraFighterCount", 1.0)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert_eq!(eff.fighters_count, Some(2.0));
assert_eq!(eff.smoke_radius, None);
}
#[test]
fn fighter_count_base() {
let cat = fighter();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.fighters_count, Some(1.0));
}
#[test]
fn plane_regen_rate_modifier_applies() {
let cat = regenerate_health();
let mods = [modifier("planeRegenerationRate", 1.5)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::AirCarrier, VERSION).expect("test modifiers are all known");
let eff = effective_consumable(&cat, &bundle);
assert!(
(eff.plane_regeneration_rate.unwrap() - 0.15).abs() < 1e-6,
"got {}",
eff.plane_regeneration_rate.unwrap()
);
}
#[test]
fn non_matching_consumable_has_no_effect_fields() {
let cat = crash_crew();
let eff = effective_consumable(&cat, &ModifierBundle::empty(Species::Battleship));
assert_eq!(eff.smoke_radius, None);
assert_eq!(eff.smoke_lifetime, None);
assert_eq!(eff.fighters_count, None);
assert_eq!(eff.call_fighters_radius, None);
assert_eq!(eff.call_fighters_time_delay, None);
assert_eq!(eff.call_fighters_time_from_heaven, None);
assert_eq!(eff.plane_regeneration_rate, None);
}
}