use serde::{Deserialize, Serialize};
use crate::damage::resistance_multiplier;
use crate::em::transformative_em_bonus;
use crate::enemy::Enemy;
use crate::error::CalcError;
use crate::level_table::reaction_base_value;
use crate::reaction::{
Reaction, ReactionCategory, transformative_element, transformative_multiplier,
};
use crate::types::Element;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransformativeInput {
pub character_level: u32,
pub elemental_mastery: f64,
pub reaction: Reaction,
pub reaction_bonus: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransformativeResult {
pub damage: f64,
pub damage_element: Option<Element>,
}
fn validate(input: &TransformativeInput, enemy: &Enemy) -> Result<(), CalcError> {
if !(1..=100).contains(&input.character_level) {
return Err(CalcError::InvalidReactionLevel(input.character_level));
}
if !(1..=200).contains(&enemy.level) {
return Err(CalcError::InvalidEnemyLevel(enemy.level));
}
if input.elemental_mastery < 0.0 {
return Err(CalcError::InvalidElementalMastery(input.elemental_mastery));
}
if input.reaction_bonus < 0.0 {
return Err(CalcError::InvalidReactionBonus(input.reaction_bonus));
}
if input.reaction.category() != ReactionCategory::Transformative {
return Err(CalcError::NotTransformative(input.reaction));
}
if let Reaction::Swirl(elem) = input.reaction {
match elem {
Element::Pyro | Element::Hydro | Element::Electro | Element::Cryo => {}
_ => return Err(CalcError::InvalidSwirlElement(elem)),
}
}
Ok(())
}
pub fn calculate_transformative(
input: &TransformativeInput,
enemy: &Enemy,
) -> Result<TransformativeResult, CalcError> {
validate(input, enemy)?;
let level_base = reaction_base_value(input.character_level).expect("validated: level 1..=100");
let reaction_mult =
transformative_multiplier(input.reaction).expect("validated: Transformative reaction");
let em_bonus = transformative_em_bonus(input.elemental_mastery);
let res_mult = resistance_multiplier(enemy);
let damage_elem =
transformative_element(input.reaction).expect("validated: Transformative reaction");
let damage = level_base * reaction_mult * (1.0 + em_bonus + input.reaction_bonus) * res_mult;
Ok(TransformativeResult {
damage,
damage_element: damage_elem,
})
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-6;
fn default_enemy() -> Enemy {
Enemy {
level: 90,
resistance: 0.1,
def_reduction: 0.0,
def_ignore: 0.0,
}
}
#[test]
fn test_overloaded_lv90_no_em() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Overloaded,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 1446.8535 * 2.75 * 0.9).abs() < 0.01);
assert_eq!(result.damage_element, Some(Element::Pyro));
}
#[test]
fn test_overloaded_lv90_em800() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 800.0,
reaction: Reaction::Overloaded,
reaction_bonus: 0.0,
};
let em_bonus = 16.0 * 800.0 / (800.0 + 2000.0);
let expected = 1446.8535 * 2.75 * (1.0 + em_bonus) * 0.9;
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - expected).abs() < 0.01);
}
#[test]
fn test_superconduct() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Superconduct,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 1446.8535 * 1.5 * 0.9).abs() < 0.01);
assert_eq!(result.damage_element, Some(Element::Cryo));
}
#[test]
fn test_swirl_pyro() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Swirl(Element::Pyro),
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 1446.8535 * 0.6 * 0.9).abs() < 0.01);
assert_eq!(result.damage_element, Some(Element::Pyro));
}
#[test]
fn test_swirl_invalid_element() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Swirl(Element::Dendro),
reaction_bonus: 0.0,
};
assert!(matches!(
calculate_transformative(&input, &default_enemy()),
Err(CalcError::InvalidSwirlElement(Element::Dendro))
));
}
#[test]
fn test_shattered_physical() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Shattered,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert_eq!(result.damage_element, None);
}
#[test]
fn test_bloom() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Bloom,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 1446.8535 * 2.0 * 0.9).abs() < 0.01);
assert_eq!(result.damage_element, Some(Element::Dendro));
}
#[test]
fn test_not_transformative_error() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Vaporize,
reaction_bonus: 0.0,
};
assert!(matches!(
calculate_transformative(&input, &default_enemy()),
Err(CalcError::NotTransformative(_))
));
}
#[test]
fn test_level_100_valid() {
let input = TransformativeInput {
character_level: 100,
elemental_mastery: 0.0,
reaction: Reaction::Overloaded,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy());
assert!(result.is_ok());
}
#[test]
fn test_golden_overloaded_em200() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 200.0,
reaction: Reaction::Overloaded,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 8789.635).abs() < 0.1);
assert_eq!(result.damage_element, Some(Element::Pyro));
}
#[test]
fn test_golden_swirl_pyro_em800() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 800.0,
reaction: Reaction::Swirl(Element::Pyro),
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 4352.962).abs() < 0.1);
assert_eq!(result.damage_element, Some(Element::Pyro));
}
#[test]
fn test_golden_superconduct_em150() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 150.0,
reaction: Reaction::Superconduct,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 4133.627).abs() < 0.1);
assert_eq!(result.damage_element, Some(Element::Cryo));
}
#[test]
fn test_golden_electro_charged_em300() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 300.0,
reaction: Reaction::ElectroCharged,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 8039.473).abs() < 0.1);
assert_eq!(result.damage_element, Some(Element::Electro));
}
#[test]
fn test_golden_hyperbloom_em800() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 800.0,
reaction: Reaction::Hyperbloom,
reaction_bonus: 0.0,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 21764.811).abs() < 0.1);
assert_eq!(result.damage_element, Some(Element::Dendro));
}
#[test]
fn test_golden_kazuha_swirl_with_vv() {
let input = TransformativeInput {
character_level: 90,
elemental_mastery: 960.0,
reaction: Reaction::Swirl(Element::Pyro),
reaction_bonus: 0.60,
};
let result = calculate_transformative(&input, &default_enemy()).unwrap();
assert!((result.damage - 5304.306).abs() < 0.5);
assert_eq!(result.damage_element, Some(Element::Pyro));
}
#[test]
fn test_reaction_bonus_applied() {
let base = TransformativeInput {
character_level: 90,
elemental_mastery: 0.0,
reaction: Reaction::Overloaded,
reaction_bonus: 0.0,
};
let with_bonus = TransformativeInput {
reaction_bonus: 0.4,
..base.clone()
};
let r1 = calculate_transformative(&base, &default_enemy()).unwrap();
let r2 = calculate_transformative(&with_bonus, &default_enemy()).unwrap();
assert!((r2.damage / r1.damage - 1.4).abs() < EPSILON);
}
}