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::ttx::provenance::Op;
use crate::game_params::types::AbilityCategory;
use crate::game_params::types::Km;
use crate::game_params::types::Meters;
use crate::recognized::Recognized;
#[derive(Clone, Debug, PartialEq)]
pub struct AppliedConsumableModifier {
pub name: String,
pub op: Op,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ConsumableApplied {
pub reload_time: Vec<AppliedConsumableModifier>,
pub work_time: Vec<AppliedConsumableModifier>,
pub preparation_time: Vec<AppliedConsumableModifier>,
pub charges: Vec<AppliedConsumableModifier>,
pub max_capacity: Vec<AppliedConsumableModifier>,
pub regeneration_hp_speed: Vec<AppliedConsumableModifier>,
pub smoke_radius: Vec<AppliedConsumableModifier>,
pub smoke_lifetime: Vec<AppliedConsumableModifier>,
pub fighters_count: Vec<AppliedConsumableModifier>,
pub call_fighters_radius: Vec<AppliedConsumableModifier>,
pub call_fighters_time_delay: Vec<AppliedConsumableModifier>,
pub call_fighters_time_from_heaven: Vec<AppliedConsumableModifier>,
pub plane_regeneration_rate: Vec<AppliedConsumableModifier>,
}
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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConsumableCard {
pub consumable: Recognized<crate::game_types::Consumable>,
pub label: String,
pub stats: EffectiveConsumable,
}
fn gated_type_coef(
bundle: &ModifierBundle,
base: f32,
type_name: &str,
gate: &[&str],
suffix: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
if gate.contains(&type_name) {
let name = format!("{type_name}{suffix}");
out.push(AppliedConsumableModifier { name: name.clone(), op: Op::Mul });
base * bundle.coef(&name)
} else {
base
}
}
fn consumable_reload_coeff(
bundle: &ModifierBundle,
type_name: &str,
group: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
let mut coeff = bundle.coef("allConsumableReloadTime");
out.push(AppliedConsumableModifier { name: "allConsumableReloadTime".into(), op: Op::Mul });
match group {
GROUP_SHIP => {
coeff *= bundle.coef("ConsumableReloadTime"); out.push(AppliedConsumableModifier { name: "ConsumableReloadTime".into(), op: Op::Mul });
}
GROUP_SQUADRON => {
coeff *= bundle.coef("planeConsumableReloadTime"); out.push(AppliedConsumableModifier { name: "planeConsumableReloadTime".into(), op: Op::Mul });
}
_ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_RELOAD_COEFFICIENTS, "ReloadCoeff", out)
}
fn consumable_work_time_coeff(
bundle: &ModifierBundle,
type_name: &str,
group: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
let mut coeff = 1.0;
match group {
GROUP_SHIP => {
coeff *= bundle.coef("ConsumablesWorkTime"); out.push(AppliedConsumableModifier { name: "ConsumablesWorkTime".into(), op: Op::Mul });
}
GROUP_SQUADRON => {
coeff *= bundle.coef("planeConsumablesWorkTime"); out.push(AppliedConsumableModifier { name: "planeConsumablesWorkTime".into(), op: Op::Mul });
}
_ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_WORK_TIME_COEFFICIENTS, "WorkTimeCoeff", out)
}
fn consumable_capacity_coeff(
bundle: &ModifierBundle,
type_name: &str,
group: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
let mut coeff = bundle.coef("consumableCapacityCoeff"); out.push(AppliedConsumableModifier { name: "consumableCapacityCoeff".into(), op: Op::Mul });
match group {
GROUP_SHIP => {
coeff *= bundle.coef("shipConsumableCapacityCoeff"); out.push(AppliedConsumableModifier { name: "shipConsumableCapacityCoeff".into(), op: Op::Mul });
}
GROUP_SQUADRON => {
coeff *= bundle.coef("squadronConsumableCapacityCoeff"); out.push(AppliedConsumableModifier { name: "squadronConsumableCapacityCoeff".into(), op: Op::Mul });
}
_ => {}
}
gated_type_coef(bundle, coeff, type_name, CONSUMABLES_WITH_CAPACITY_COEFFICIENTS, "CapacityCoeff", out)
}
fn additional_consumables_count(
bundle: &ModifierBundle,
type_name: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
if ADDITIONAL_CONSUMABLES_COUNT.contains(&type_name) {
let name = format!("{type_name}AdditionalConsumables");
out.push(AppliedConsumableModifier { name: name.clone(), op: Op::Add });
bundle.apply(0.0, &name)
} else {
0.0
}
}
fn additional_consumables_for_group(
bundle: &ModifierBundle,
group: &str,
out: &mut Vec<AppliedConsumableModifier>,
) -> f32 {
match group {
GROUP_SHIP => {
out.push(AppliedConsumableModifier { name: "additionalConsumables".into(), op: Op::Add });
bundle.apply(0.0, "additionalConsumables") }
GROUP_SQUADRON => {
out.push(AppliedConsumableModifier { name: "planeAdditionalConsumables".into(), op: Op::Add });
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, ConsumableApplied) {
let type_name = category.consumable_type_raw();
let group = category.group();
let fields = category.effect_fields();
let mut applied = ConsumableApplied::default();
let reload_coeff = consumable_reload_coeff(modifiers, type_name, group, &mut applied.reload_time);
applied.preparation_time = applied.reload_time.clone();
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 {
let wt = consumable_work_time_coeff(modifiers, type_name, group, &mut applied.work_time);
Some(Seconds::from(category.work_time() * wt))
} 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, &mut applied.charges)
+ additional_consumables_for_group(modifiers, group, &mut applied.charges);
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, &mut applied.max_capacity))
} else {
None
};
let regeneration_hp_speed = if type_name == TYPE_REGEN_CREW {
category.regeneration_hp_speed().map(|v| {
applied
.regeneration_hp_speed
.push(AppliedConsumableModifier { name: "regenerationHPSpeed".into(), op: Op::Mul });
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| {
applied
.smoke_lifetime
.push(AppliedConsumableModifier { name: "smokeGeneratorLifeTime".into(), op: Op::Mul });
Seconds::from(modifiers.apply(v, "smokeGeneratorLifeTime"))
});
smoke_radius = field(fields, "radius").map(|v| {
applied
.smoke_radius
.push(AppliedConsumableModifier { name: "smokeScreenRadiusCoefficient".into(), op: Op::Mul });
km_from_ballistic(modifiers.apply(v, "smokeScreenRadiusCoefficient"))
});
} else if type_name == TYPE_PLANE_SMOKE_GENERATOR {
smoke_lifetime = field(fields, "lifeTime").map(|v| {
applied
.smoke_lifetime
.push(AppliedConsumableModifier { name: "planeSmokeGeneratorLifeTime".into(), op: Op::Mul });
Seconds::from(modifiers.apply(v, "planeSmokeGeneratorLifeTime"))
});
} else if type_name == TYPE_REGENERATE_HEALTH {
plane_regeneration_rate = field(fields, "regenerationRate").map(|v| {
applied
.plane_regeneration_rate
.push(AppliedConsumableModifier { name: "planeRegenerationRate".into(), op: Op::Mul });
modifiers.apply(v, "planeRegenerationRate")
});
} else if type_name == TYPE_FIGHTER {
fighters_count = field(fields, "fightersNum").map(|v| {
applied.fighters_count.push(AppliedConsumableModifier { name: "extraFighterCount".into(), op: Op::Add });
modifiers.apply(v, "extraFighterCount")
});
} else if CALL_FIGHTERS_CONSUMABLES.contains(&type_name) {
call_fighters_radius = field(fields, "radius").map(|v| {
applied
.call_fighters_radius
.push(AppliedConsumableModifier { name: "callFightersRadiusCoeff".into(), op: Op::Mul });
km_from_ballistic(modifiers.apply(v, "callFightersRadiusCoeff"))
});
call_fighters_time_delay = field(fields, "timeDelayAttack").map(|v| {
applied
.call_fighters_time_delay
.push(AppliedConsumableModifier { name: "callFightersTimeDelayAttack".into(), op: Op::Mul });
Seconds::from(modifiers.apply(v, "callFightersTimeDelayAttack"))
});
call_fighters_time_from_heaven = field(fields, "timeFromHeaven").map(|v| {
applied
.call_fighters_time_from_heaven
.push(AppliedConsumableModifier { name: "callFightersAppearDelay".into(), op: Op::Mul });
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,
},
applied,
)
}
#[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);
const CRASH_CREW_RELOAD_S: f32 = 120.0;
const CRASH_CREW_WORK_TIME_S: f32 = 15.0;
const SONAR_RELOAD_S: f32 = 90.0;
const SONAR_WORK_TIME_S: f32 = 100.0;
const SONAR_BASE_CHARGES: isize = 3;
const SMOKE_RELOAD_S: f32 = 240.0;
const SMOKE_WORK_TIME_S: f32 = 20.0;
const SMOKE_BASE_CHARGES: isize = 2;
const SMOKE_BASE_RADIUS_BW: f32 = 15.0;
const SMOKE_RADIUS_KM: f32 = 0.45;
const SMOKE_LIFETIME_S: f32 = 77.0;
const SMOKE_RADIUS_COEFF: f32 = 1.2;
const SMOKE_RADIUS_AFTER_MOD_KM: f32 = 0.54;
const CF_RADIUS_BW: f32 = 116.667;
const CF_RADIUS_KM: f32 = 3.5;
const CF_TIME_DELAY_S: f32 = 5.0;
const CF_TIME_FROM_HEAVEN_S: f32 = 3.0;
const CF_TIME_DELAY_COEFF: f32 = 0.8;
const CF_TIME_DELAY_AFTER_MOD_S: f32 = 4.0;
const FIGHTER_BASE_COUNT: f32 = 1.0;
const EXTRA_FIGHTER_COUNT: f32 = 1.0;
const FIGHTER_WITH_MOD: f32 = 2.0;
const REGEN_RATE_BASE: f32 = 0.1;
const PLANE_REGEN_COEFF: f32 = 1.5;
const REGEN_RATE_AFTER_MOD: f32 = 0.15;
const RELOAD_COEFF_A: f32 = 0.9;
const RELOAD_COEFF_B: f32 = 0.95;
const CRASH_CREW_RELOAD_AFTER_SINGLE_MOD: f32 = 108.0;
const CRASH_CREW_RELOAD_COMPOUND: f32 = 102.6;
const SONAR_WORK_COEFF: f32 = 0.8;
const SONAR_WORK_AFTER_MOD: f32 = 80.0;
const GROUP_BONUS: f32 = 1.0;
const SONAR_CHARGES_WITH_GROUP: u32 = 4;
fn crash_crew() -> AbilityCategory {
let mut fields = BTreeMap::new();
fields.insert("lifeCycleType".to_string(), LIFECYCLE_COUNT_BASED);
fields.insert("reloadTime".to_string(), CRASH_CREW_RELOAD_S);
fields.insert("workTime".to_string(), CRASH_CREW_WORK_TIME_S);
AbilityCategory::builder()
.consumable_type("crashCrew".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(-1)
.preparation_time(0.0)
.reload_time(CRASH_CREW_RELOAD_S)
.work_time(CRASH_CREW_WORK_TIME_S)
.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(SONAR_BASE_CHARGES)
.preparation_time(0.0)
.reload_time(SONAR_RELOAD_S)
.work_time(SONAR_WORK_TIME_S)
.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(), SMOKE_BASE_RADIUS_BW);
fields.insert("lifeTime".to_string(), SMOKE_LIFETIME_S);
AbilityCategory::builder()
.consumable_type("smokeGenerator".to_string())
.group("ship".to_string())
.icon_id(String::new())
.num_consumables(SMOKE_BASE_CHARGES)
.preparation_time(0.0)
.reload_time(SMOKE_RELOAD_S)
.work_time(SMOKE_WORK_TIME_S)
.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(), CF_RADIUS_BW);
fields.insert("timeDelayAttack".to_string(), CF_TIME_DELAY_S);
fields.insert("timeFromHeaven".to_string(), CF_TIME_FROM_HEAVEN_S);
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(), FIGHTER_BASE_COUNT);
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(), REGEN_RATE_BASE);
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, _applied) = effective_consumable(&cat, &ModifierBundle::empty(Species::Battleship));
assert_eq!(eff.reload_time, Seconds::from(CRASH_CREW_RELOAD_S));
assert_eq!(eff.preparation_time, Seconds::from(0.0));
assert_eq!(eff.work_time, Some(Seconds::from(CRASH_CREW_WORK_TIME_S)));
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", RELOAD_COEFF_A)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert!(
(eff.reload_time.value() - CRASH_CREW_RELOAD_AFTER_SINGLE_MOD).abs() < 1e-3,
"got {}",
eff.reload_time.value()
);
}
#[test]
fn reload_coefficients_compound() {
let cat = crash_crew();
let mods =
[modifier("ConsumableReloadTime", RELOAD_COEFF_A), modifier("allConsumableReloadTime", RELOAD_COEFF_B)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert!((eff.reload_time.value() - CRASH_CREW_RELOAD_COMPOUND).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, _applied) = 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", GROUP_BONUS)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert_eq!(eff.charges, AmmoCount::Finite(SONAR_CHARGES_WITH_GROUP));
}
#[test]
fn finite_charges_base_unchanged() {
let cat = finite_sonar();
let (eff, _applied) = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.charges, AmmoCount::Finite(SONAR_BASE_CHARGES as u32));
}
#[test]
fn per_type_work_time_coeff_applies() {
let cat = finite_sonar();
let mods = [modifier("sonarWorkTimeCoeff", SONAR_WORK_COEFF)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert_eq!(eff.work_time, Some(Seconds::from(SONAR_WORK_AFTER_MOD)));
}
#[test]
fn smoke_effects_base() {
let cat = smoke_generator();
let (eff, _applied) = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.smoke_radius, Some(Km::from(SMOKE_RADIUS_KM)));
assert_eq!(eff.smoke_lifetime, Some(Seconds::from(SMOKE_LIFETIME_S)));
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", SMOKE_RADIUS_COEFF)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert!(
(eff.smoke_radius.unwrap().value() - SMOKE_RADIUS_AFTER_MOD_KM).abs() < 1e-4,
"got {}",
eff.smoke_radius.unwrap().value()
);
}
#[test]
fn call_fighters_effects_base() {
let cat = call_fighters();
let (eff, _applied) = effective_consumable(&cat, &ModifierBundle::empty(Species::AirCarrier));
assert!(
(eff.call_fighters_radius.unwrap().value() - CF_RADIUS_KM).abs() < 1e-3,
"got {}",
eff.call_fighters_radius.unwrap().value()
);
assert_eq!(eff.call_fighters_time_delay, Some(Seconds::from(CF_TIME_DELAY_S)));
assert_eq!(eff.call_fighters_time_from_heaven, Some(Seconds::from(CF_TIME_FROM_HEAVEN_S)));
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", CF_TIME_DELAY_COEFF)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::AirCarrier, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert_eq!(eff.call_fighters_time_delay, Some(Seconds::from(CF_TIME_DELAY_AFTER_MOD_S)));
}
#[test]
fn fighter_count_additive_modifier() {
let cat = fighter();
let mods = [modifier("extraFighterCount", EXTRA_FIGHTER_COUNT)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Cruiser, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert_eq!(eff.fighters_count, Some(FIGHTER_WITH_MOD));
assert_eq!(eff.smoke_radius, None);
}
#[test]
fn fighter_count_base() {
let cat = fighter();
let (eff, _applied) = effective_consumable(&cat, &ModifierBundle::empty(Species::Cruiser));
assert_eq!(eff.fighters_count, Some(FIGHTER_BASE_COUNT));
}
#[test]
fn plane_regen_rate_modifier_applies() {
let cat = regenerate_health();
let mods = [modifier("planeRegenerationRate", PLANE_REGEN_COEFF)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::AirCarrier, VERSION).expect("test modifiers are all known");
let (eff, _applied) = effective_consumable(&cat, &bundle);
assert!(
(eff.plane_regeneration_rate.unwrap() - REGEN_RATE_AFTER_MOD).abs() < 1e-6,
"got {}",
eff.plane_regeneration_rate.unwrap()
);
}
#[test]
fn non_matching_consumable_has_no_effect_fields() {
let cat = crash_crew();
let (eff, _applied) = 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);
}
#[test]
fn applied_reload_time_names_in_order() {
let cat = crash_crew();
let mods =
[modifier("ConsumableReloadTime", RELOAD_COEFF_A), modifier("allConsumableReloadTime", RELOAD_COEFF_B)];
let bundle =
ModifierBundle::from_modifiers(&mods, Species::Battleship, VERSION).expect("test modifiers are all known");
let (_eff, applied) = effective_consumable(&cat, &bundle);
let names: Vec<&str> = applied.reload_time.iter().map(|m| m.name.as_str()).collect();
assert_eq!(names, vec!["allConsumableReloadTime", "ConsumableReloadTime", "crashCrewReloadCoeff"]);
assert!(applied.reload_time.iter().all(|m| m.op == Op::Mul));
}
}