use super::buffs::BuffSource;
use super::engine::{crunch, CruncherError, EngineInput, EngineOutput, StageName};
use crate::data::Dataset;
#[derive(Clone, Debug)]
pub struct StageLift {
pub source: BuffSource,
pub delta: f64,
}
#[derive(Clone, Debug)]
pub struct AttributedStage {
pub name: StageName,
pub expected: f64,
pub detail: String,
pub baseline: f64,
pub lifts: Vec<StageLift>,
pub residual: f64,
pub intrinsics: Vec<String>,
}
const DEFAULT_EPSILON: f64 = 1e-6;
fn is_groupable(source: &BuffSource) -> bool {
matches!(
source,
BuffSource::Ability { .. } | BuffSource::Manual { .. }
)
}
fn group_key(source: &BuffSource) -> String {
match source {
BuffSource::Ability {
ability_id,
source_unit_id,
..
} => format!(
"a:{}:{}",
ability_id,
source_unit_id.as_deref().unwrap_or("")
),
BuffSource::Manual { label } => format!("m:{label}"),
BuffSource::WeaponKeyword {
weapon_id,
keyword_id,
} => format!("w:{weapon_id}:{keyword_id}"),
}
}
pub fn attribute_stages(
input: &EngineInput,
dataset: Option<&Dataset>,
epsilon: Option<f64>,
) -> Result<Vec<AttributedStage>, CruncherError> {
let eps = epsilon.unwrap_or(DEFAULT_EPSILON);
let full = crunch(input, dataset)?;
let mut order: Vec<String> = Vec::new();
let mut rep_source: std::collections::HashMap<String, BuffSource> =
std::collections::HashMap::new();
for b in &input.buffs {
if !is_groupable(&b.source) {
continue;
}
let key = group_key(&b.source);
if !rep_source.contains_key(&key) {
rep_source.insert(key.clone(), b.source.clone());
order.push(key);
}
}
let baseline_input = EngineInput {
attacker: input.attacker,
target: input.target,
models_firing: input.models_firing,
context: input.context.clone(),
buffs: input
.buffs
.iter()
.filter(|b| !is_groupable(&b.source))
.cloned()
.collect(),
};
let baseline = crunch(&baseline_input, dataset)?;
let mut loo: std::collections::HashMap<String, EngineOutput> = std::collections::HashMap::new();
for key in &order {
let without_input = EngineInput {
attacker: input.attacker,
target: input.target,
models_firing: input.models_firing,
context: input.context.clone(),
buffs: input
.buffs
.iter()
.filter(|b| !is_groupable(&b.source) || &group_key(&b.source) != key)
.cloned()
.collect(),
};
loo.insert(key.clone(), crunch(&without_input, dataset)?);
}
let intrinsics: Vec<String> = full
.resolved
.extra_keywords
.iter()
.map(|e| e.keyword_ref.keyword_id.clone())
.collect();
let out: Vec<AttributedStage> = full
.stages
.iter()
.enumerate()
.map(|(i, s)| {
let expected = s.expected;
let base_expected = baseline.stages[i].expected;
let mut total_lift = 0.0;
let mut lifts: Vec<StageLift> = Vec::new();
for key in &order {
let delta = expected - loo[key].stages[i].expected;
total_lift += delta;
if delta.abs() > eps {
lifts.push(StageLift {
source: rep_source[key].clone(),
delta,
});
}
}
let residual_raw = expected - base_expected - total_lift;
AttributedStage {
name: s.name,
expected,
detail: s.detail.clone(),
baseline: base_expected,
lifts,
residual: if residual_raw.abs() > eps {
residual_raw
} else {
0.0
},
intrinsics: intrinsics.clone(),
}
})
.collect();
Ok(out)
}