use std::collections::HashMap;
use crate::game_params::ttx::labels::TtxStat;
use crate::game_params::ttx::module_options::ModuleSlot;
use crate::game_params::types::CrewSkillName;
use crate::game_types::Consumable;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum InputId {
Module { slot: ModuleSlot, name: String },
Upgrade { name: String },
Skill { name: CrewSkillName },
Consumable(Consumable),
Innate { skill_type: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
Mul,
Add,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Contribution {
pub input: InputId,
pub modifier_name: String,
pub op: Op,
pub operand: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StatAttribution {
pub stat: TtxStat,
pub qualifier: Option<String>,
pub base_value: f32,
pub base_source: InputId,
pub steps: Vec<Contribution>,
pub value: f32,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ShipStatsProvenance {
pub attributions: Vec<StatAttribution>,
}
impl ShipStatsProvenance {
pub fn replay(attr: &StatAttribution) -> f32 {
attr.steps.iter().fold(attr.base_value, |acc, c| match c.op {
Op::Mul => acc * c.operand,
Op::Add => acc + c.operand,
})
}
}
#[derive(Clone, Debug, Default)]
pub struct ModifierSources {
by_name: HashMap<String, Vec<(InputId, f32)>>,
}
impl ModifierSources {
pub fn record(&mut self, name: &str, input: InputId, raw: f32) {
self.by_name.entry(name.to_string()).or_default().push((input, raw));
}
pub fn get(&self, name: &str) -> &[(InputId, f32)] {
self.by_name.get(name).map(Vec::as_slice).unwrap_or(&[])
}
}
pub struct StepBuilder<'a> {
steps: &'a mut Vec<Contribution>,
}
impl StepBuilder<'_> {
pub fn coef(&mut self, sources: &ModifierSources, name: &str) {
for (input, raw) in sources.get(name) {
self.steps.push(Contribution {
input: input.clone(),
modifier_name: name.to_string(),
op: Op::Mul,
operand: *raw,
});
}
}
pub fn bonus(&mut self, sources: &ModifierSources, name: &str, scale: f32) {
for (input, raw) in sources.get(name) {
self.steps.push(Contribution {
input: input.clone(),
modifier_name: name.to_string(),
op: Op::Add,
operand: *raw * scale,
});
}
}
pub fn module(&mut self, input: InputId, name: &str, value: f32) {
if value == 1.0 {
return;
}
self.steps.push(Contribution { input, modifier_name: name.to_string(), op: Op::Mul, operand: value });
}
pub fn module_add(&mut self, input: InputId, name: &str, value: f32) {
if value == 0.0 {
return;
}
self.steps.push(Contribution { input, modifier_name: name.to_string(), op: Op::Add, operand: value });
}
}
pub trait Recorder {
const ON: bool;
fn record(
&mut self,
stat: TtxStat,
qualifier: Option<&str>,
base_value: f32,
base_source: InputId,
final_value: f32,
build: impl FnOnce(&mut StepBuilder<'_>),
);
fn into_provenance(self) -> ShipStatsProvenance;
}
pub struct Off;
impl Recorder for Off {
const ON: bool = false;
fn record(
&mut self,
_stat: TtxStat,
_qualifier: Option<&str>,
_base_value: f32,
_base_source: InputId,
_final_value: f32,
_build: impl FnOnce(&mut StepBuilder<'_>),
) {
}
fn into_provenance(self) -> ShipStatsProvenance {
ShipStatsProvenance::default()
}
}
#[derive(Default)]
pub struct On {
attributions: Vec<StatAttribution>,
}
impl Recorder for On {
const ON: bool = true;
fn record(
&mut self,
stat: TtxStat,
qualifier: Option<&str>,
base_value: f32,
base_source: InputId,
final_value: f32,
build: impl FnOnce(&mut StepBuilder<'_>),
) {
let mut steps = Vec::new();
build(&mut StepBuilder { steps: &mut steps });
self.attributions.push(StatAttribution {
stat,
qualifier: qualifier.map(str::to_string),
base_value,
base_source,
steps,
value: final_value,
});
}
fn into_provenance(self) -> ShipStatsProvenance {
ShipStatsProvenance { attributions: self.attributions }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_reconstructs_value() {
let attr = StatAttribution {
stat: TtxStat::Health,
qualifier: None,
base_value: 19400.0,
base_source: InputId::Module { slot: ModuleSlot::Hull, name: "H".to_string() },
steps: vec![
Contribution {
input: InputId::Upgrade { name: "U".to_string() },
modifier_name: "healthHullCoeff".to_string(),
op: Op::Mul,
operand: 1.05,
},
Contribution {
input: InputId::Skill { name: CrewSkillName::from("S") },
modifier_name: "healthPerLevel".to_string(),
op: Op::Add,
operand: 3500.0,
},
],
value: 23870.0,
};
assert!((ShipStatsProvenance::replay(&attr) - 23870.0).abs() < 1e-3);
}
}
#[cfg(test)]
mod recorder_tests {
use super::*;
fn sources_with(name: &str, entries: &[(InputId, f32)]) -> ModifierSources {
let mut s = ModifierSources::default();
for (input, raw) in entries {
s.record(name, input.clone(), *raw);
}
s
}
#[test]
fn off_records_nothing() {
let mut rec = Off;
rec.record(
TtxStat::Speed,
None,
36.0,
InputId::Module { slot: ModuleSlot::Hull, name: "H".into() },
37.8,
|b| {
b.coef(&ModifierSources::default(), "speedCoef");
},
);
assert!(rec.into_provenance().attributions.is_empty());
}
#[test]
fn on_records_base_and_per_source_steps() {
let up = InputId::Upgrade { name: "U1".into() };
let sk = InputId::Skill { name: CrewSkillName::from("S1") };
let sources = sources_with("speedCoef", &[(up.clone(), 1.05), (sk.clone(), 1.10)]);
let mut rec = On::default();
rec.record(
TtxStat::Speed,
None,
36.0,
InputId::Module { slot: ModuleSlot::Hull, name: "H".into() },
41.58,
|b| {
b.coef(&sources, "speedCoef");
},
);
let prov = rec.into_provenance();
assert_eq!(prov.attributions.len(), 1);
let a = &prov.attributions[0];
assert_eq!(a.base_value, 36.0);
assert_eq!(a.steps.len(), 2);
assert_eq!(a.steps[0].input, up);
assert_eq!(a.steps[0].op, Op::Mul);
assert!((a.steps[0].operand - 1.05).abs() < 1e-6);
assert_eq!(a.steps[1].input, sk);
assert!((ShipStatsProvenance::replay(a) - 41.58).abs() < 1e-3);
}
#[test]
fn module_identity_factor_is_skipped() {
let mut rec = On::default();
rec.record(
TtxStat::ArtilleryRange,
None,
11.13,
InputId::Module { slot: ModuleSlot::Artillery, name: "A".into() },
11.13,
|b| {
b.module(InputId::Module { slot: ModuleSlot::FireControl, name: "FC".into() }, "maxDistCoef", 1.0);
},
);
assert!(rec.into_provenance().attributions[0].steps.is_empty());
}
#[test]
fn module_add_skips_zero_and_records_add() {
let hull_src = InputId::Module { slot: ModuleSlot::Hull, name: "H".into() };
let mut rec = On::default();
rec.record(TtxStat::SeaDetectionOnFire, None, 7.33, hull_src.clone(), 7.33, |b| {
b.module_add(hull_src.clone(), "visibilityCoefFire", 0.0);
});
assert!(rec.into_provenance().attributions[0].steps.is_empty(), "zero fire should record no step");
let mut rec = On::default();
rec.record(TtxStat::SeaDetectionOnFire, None, 7.33, hull_src.clone(), 9.33, |b| {
b.module_add(hull_src.clone(), "visibilityCoefFire", 2.0);
});
let prov = rec.into_provenance();
let a = &prov.attributions[0];
assert_eq!(a.steps.len(), 1);
assert_eq!(a.steps[0].op, Op::Add);
assert!((a.steps[0].operand - 2.0).abs() < 1e-6);
assert!((ShipStatsProvenance::replay(a) - 9.33).abs() < 1e-4);
}
}