use crate::context::StatContext;
use crate::error::StatError;
use crate::numeric::{StatNumeric, StatValue};
use crate::stat_id::StatId;
use crate::transform::core::{ClampBounds, StatTransform, TransformPhase};
use rustc_hash::FxHashMap;
#[derive(Debug, Clone)]
pub struct MultiplicativeTransform {
multiplier: f64,
}
impl MultiplicativeTransform {
pub fn new(multiplier: f64) -> Self {
Self { multiplier }
}
pub fn multiplier(&self) -> f64 {
self.multiplier
}
}
impl StatTransform for MultiplicativeTransform {
fn depends_on(&self) -> Vec<StatId> {
Vec::new()
}
fn apply(
&self,
input: StatValue,
_dependencies: &FxHashMap<StatId, StatValue>,
_context: &StatContext,
) -> Result<StatValue, StatError> {
Ok(input * StatValue::from_f64(self.multiplier))
}
fn description(&self) -> String {
format!("×{:.2}", self.multiplier)
}
}
#[derive(Debug, Clone)]
pub struct AdditiveTransform {
bonus: f64,
}
impl AdditiveTransform {
pub fn new(bonus: f64) -> Self {
Self { bonus }
}
pub fn bonus(&self) -> f64 {
self.bonus
}
}
impl StatTransform for AdditiveTransform {
fn depends_on(&self) -> Vec<StatId> {
Vec::new()
}
fn phase(&self) -> TransformPhase {
TransformPhase::Additive
}
fn apply(
&self,
input: StatValue,
_dependencies: &FxHashMap<StatId, StatValue>,
_context: &StatContext,
) -> Result<StatValue, StatError> {
Ok(input + StatValue::from_f64(self.bonus))
}
fn description(&self) -> String {
format!("+{:.2}", self.bonus)
}
}
#[derive(Debug, Clone)]
pub struct ClampTransform {
pub min: Option<StatValue>,
pub max: Option<StatValue>,
}
impl ClampTransform {
pub fn new(min: f64, max: f64) -> Self {
Self {
min: Some(StatValue::from_f64(min)),
max: Some(StatValue::from_f64(max)),
}
}
pub fn with_bounds(min: Option<StatValue>, max: Option<StatValue>) -> Self {
Self { min, max }
}
pub fn with_min(min: StatValue) -> Self {
Self {
min: Some(min),
max: None,
}
}
pub fn with_max(max: StatValue) -> Self {
Self {
min: None,
max: Some(max),
}
}
pub fn min(&self) -> Option<StatValue> {
self.min
}
pub fn max(&self) -> Option<StatValue> {
self.max
}
}
impl StatTransform for ClampTransform {
fn depends_on(&self) -> Vec<StatId> {
Vec::new()
}
fn phase(&self) -> TransformPhase {
TransformPhase::Final
}
fn apply(
&self,
input: StatValue,
_dependencies: &FxHashMap<StatId, StatValue>,
_context: &StatContext,
) -> Result<StatValue, StatError> {
let mut result = input;
if let Some(min) = self.min {
result = result.max(min);
}
if let Some(max) = self.max {
result = result.min(max);
}
Ok(result)
}
fn description(&self) -> String {
match (self.min, self.max) {
(Some(min), Some(max)) => format!("clamp({:.2}, {:.2})", min.to_f64(), max.to_f64()),
(Some(min), None) => format!("clamp_min({:.2})", min.to_f64()),
(None, Some(max)) => format!("clamp_max({:.2})", max.to_f64()),
(None, None) => "clamp(none)".to_string(),
}
}
}
impl ClampBounds for ClampTransform {
fn min_bound(&self) -> Option<StatValue> {
self.min
}
fn max_bound(&self) -> Option<StatValue> {
self.max
}
}
#[derive(Debug, Clone)]
pub struct ScalingTransform {
dependency: StatId,
scale_factor: f64,
}
impl ScalingTransform {
pub fn new(dependency: StatId, scale_factor: f64) -> Self {
Self {
dependency,
scale_factor,
}
}
}
impl StatTransform for ScalingTransform {
fn depends_on(&self) -> Vec<StatId> {
vec![self.dependency.clone()]
}
fn phase(&self) -> TransformPhase {
TransformPhase::Additive
}
fn apply(
&self,
input: StatValue,
dependencies: &FxHashMap<StatId, StatValue>,
_context: &StatContext,
) -> Result<StatValue, StatError> {
let dep_value = dependencies
.get(&self.dependency)
.ok_or_else(|| StatError::MissingDependency(self.dependency.clone()))?;
Ok(input + (*dep_value * StatValue::from_f64(self.scale_factor)))
}
fn description(&self) -> String {
format!("scale({}, {:.2})", self.dependency, self.scale_factor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::numeric::StatNumeric;
#[test]
fn test_scaling_transform() {
let str_id = StatId::from("STR");
let transform = ScalingTransform::new(str_id.clone(), 2.5);
let mut deps = FxHashMap::default();
deps.insert(str_id.clone(), StatValue::from_f64(10.0));
let context = StatContext::new();
let result = transform
.apply(StatValue::from_f64(100.0), &deps, &context)
.unwrap();
assert_eq!(result.to_f64(), 125.0);
}
#[test]
fn test_scaling_transform_missing_dep() {
let str_id = StatId::from("STR");
let transform = ScalingTransform::new(str_id.clone(), 2.5);
let deps = FxHashMap::default(); let context = StatContext::new();
let err = transform.apply(StatValue::from_f64(100.0), &deps, &context);
assert!(matches!(err, Err(StatError::MissingDependency(_))));
}
}