Skip to main content

genshin_calc_core/
transformative.rs

1use serde::{Deserialize, Serialize};
2
3use crate::damage::resistance_multiplier;
4use crate::em::transformative_em_bonus;
5use crate::enemy::Enemy;
6use crate::error::CalcError;
7use crate::level_table::reaction_base_value;
8use crate::reaction::{
9    Reaction, ReactionCategory, transformative_element, transformative_multiplier,
10};
11use crate::types::Element;
12
13/// Input for transformative reaction damage calculation.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct TransformativeInput {
16    /// Character level (1-100).
17    pub character_level: u32,
18    /// Elemental mastery.
19    pub elemental_mastery: f64,
20    /// Transformative reaction type.
21    pub reaction: Reaction,
22    /// Reaction DMG bonus in decimal form.
23    pub reaction_bonus: f64,
24}
25
26/// Result of transformative reaction damage calculation.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct TransformativeResult {
29    /// Total reaction damage.
30    pub damage: f64,
31    /// Element of the reaction damage. `None` for physical (e.g. shattered).
32    pub damage_element: Option<Element>,
33}
34
35fn validate(input: &TransformativeInput, enemy: &Enemy) -> Result<(), CalcError> {
36    if !(1..=100).contains(&input.character_level) {
37        return Err(CalcError::InvalidReactionLevel(input.character_level));
38    }
39    if !(1..=200).contains(&enemy.level) {
40        return Err(CalcError::InvalidEnemyLevel(enemy.level));
41    }
42    if input.elemental_mastery < 0.0 {
43        return Err(CalcError::InvalidElementalMastery(input.elemental_mastery));
44    }
45    if input.reaction_bonus < 0.0 {
46        return Err(CalcError::InvalidReactionBonus(input.reaction_bonus));
47    }
48    if input.reaction.category() != ReactionCategory::Transformative {
49        return Err(CalcError::NotTransformative(input.reaction));
50    }
51    if let Reaction::Swirl(elem) = input.reaction {
52        match elem {
53            Element::Pyro | Element::Hydro | Element::Electro | Element::Cryo => {}
54            _ => return Err(CalcError::InvalidSwirlElement(elem)),
55        }
56    }
57    Ok(())
58}
59
60/// Calculates transformative reaction damage.
61///
62/// Transformative reactions deal fixed damage based on character level and
63/// elemental mastery. They ignore ATK, talent multipliers, crit, and defense.
64///
65/// # Errors
66///
67/// Returns [`CalcError`] if the reaction is not transformative or inputs are invalid.
68///
69/// # Examples
70///
71/// ```
72/// use genshin_calc_core::*;
73///
74/// let input = TransformativeInput {
75///     character_level: 90,
76///     elemental_mastery: 200.0,
77///     reaction: Reaction::Overloaded,
78///     reaction_bonus: 0.0,
79/// };
80/// let enemy = Enemy { level: 90, resistance: 0.10, def_reduction: 0.0, def_ignore: 0.0 };
81/// let result = calculate_transformative(&input, &enemy).unwrap();
82/// assert!(result.damage > 0.0);
83/// ```
84pub fn calculate_transformative(
85    input: &TransformativeInput,
86    enemy: &Enemy,
87) -> Result<TransformativeResult, CalcError> {
88    validate(input, enemy)?;
89
90    let level_base = reaction_base_value(input.character_level).expect("validated: level 1..=100");
91    let reaction_mult =
92        transformative_multiplier(input.reaction).expect("validated: Transformative reaction");
93    let em_bonus = transformative_em_bonus(input.elemental_mastery);
94    let res_mult = resistance_multiplier(enemy);
95    let damage_elem =
96        transformative_element(input.reaction).expect("validated: Transformative reaction");
97
98    let damage = level_base * reaction_mult * (1.0 + em_bonus + input.reaction_bonus) * res_mult;
99
100    Ok(TransformativeResult {
101        damage,
102        damage_element: damage_elem,
103    })
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    const EPSILON: f64 = 1e-6;
111
112    fn default_enemy() -> Enemy {
113        Enemy {
114            level: 90,
115            resistance: 0.1,
116            def_reduction: 0.0,
117            def_ignore: 0.0,
118        }
119    }
120
121    #[test]
122    fn test_overloaded_lv90_no_em() {
123        let input = TransformativeInput {
124            character_level: 90,
125            elemental_mastery: 0.0,
126            reaction: Reaction::Overloaded,
127            reaction_bonus: 0.0,
128        };
129        // 1446.8535 * 2.75 * 1.0 * 0.9 = 3577.9122...
130        let result = calculate_transformative(&input, &default_enemy()).unwrap();
131        assert!((result.damage - 1446.8535 * 2.75 * 0.9).abs() < 0.01);
132        assert_eq!(result.damage_element, Some(Element::Pyro));
133    }
134
135    #[test]
136    fn test_overloaded_lv90_em800() {
137        let input = TransformativeInput {
138            character_level: 90,
139            elemental_mastery: 800.0,
140            reaction: Reaction::Overloaded,
141            reaction_bonus: 0.0,
142        };
143        let em_bonus = 16.0 * 800.0 / (800.0 + 2000.0);
144        let expected = 1446.8535 * 2.75 * (1.0 + em_bonus) * 0.9;
145        let result = calculate_transformative(&input, &default_enemy()).unwrap();
146        assert!((result.damage - expected).abs() < 0.01);
147    }
148
149    #[test]
150    fn test_superconduct() {
151        let input = TransformativeInput {
152            character_level: 90,
153            elemental_mastery: 0.0,
154            reaction: Reaction::Superconduct,
155            reaction_bonus: 0.0,
156        };
157        let result = calculate_transformative(&input, &default_enemy()).unwrap();
158        assert!((result.damage - 1446.8535 * 1.5 * 0.9).abs() < 0.01);
159        assert_eq!(result.damage_element, Some(Element::Cryo));
160    }
161
162    #[test]
163    fn test_swirl_pyro() {
164        let input = TransformativeInput {
165            character_level: 90,
166            elemental_mastery: 0.0,
167            reaction: Reaction::Swirl(Element::Pyro),
168            reaction_bonus: 0.0,
169        };
170        let result = calculate_transformative(&input, &default_enemy()).unwrap();
171        assert!((result.damage - 1446.8535 * 0.6 * 0.9).abs() < 0.01);
172        assert_eq!(result.damage_element, Some(Element::Pyro));
173    }
174
175    #[test]
176    fn test_swirl_invalid_element() {
177        let input = TransformativeInput {
178            character_level: 90,
179            elemental_mastery: 0.0,
180            reaction: Reaction::Swirl(Element::Dendro),
181            reaction_bonus: 0.0,
182        };
183        assert!(matches!(
184            calculate_transformative(&input, &default_enemy()),
185            Err(CalcError::InvalidSwirlElement(Element::Dendro))
186        ));
187    }
188
189    #[test]
190    fn test_shattered_physical() {
191        let input = TransformativeInput {
192            character_level: 90,
193            elemental_mastery: 0.0,
194            reaction: Reaction::Shattered,
195            reaction_bonus: 0.0,
196        };
197        let result = calculate_transformative(&input, &default_enemy()).unwrap();
198        assert_eq!(result.damage_element, None);
199    }
200
201    #[test]
202    fn test_bloom() {
203        let input = TransformativeInput {
204            character_level: 90,
205            elemental_mastery: 0.0,
206            reaction: Reaction::Bloom,
207            reaction_bonus: 0.0,
208        };
209        let result = calculate_transformative(&input, &default_enemy()).unwrap();
210        assert!((result.damage - 1446.8535 * 2.0 * 0.9).abs() < 0.01);
211        assert_eq!(result.damage_element, Some(Element::Dendro));
212    }
213
214    #[test]
215    fn test_not_transformative_error() {
216        let input = TransformativeInput {
217            character_level: 90,
218            elemental_mastery: 0.0,
219            reaction: Reaction::Vaporize,
220            reaction_bonus: 0.0,
221        };
222        assert!(matches!(
223            calculate_transformative(&input, &default_enemy()),
224            Err(CalcError::NotTransformative(_))
225        ));
226    }
227
228    #[test]
229    fn test_level_100_valid() {
230        let input = TransformativeInput {
231            character_level: 100,
232            elemental_mastery: 0.0,
233            reaction: Reaction::Overloaded,
234            reaction_bonus: 0.0,
235        };
236        let result = calculate_transformative(&input, &default_enemy());
237        assert!(result.is_ok());
238    }
239
240    // =====================================================================
241    // Golden tests: hand-calculated reaction values
242    // =====================================================================
243
244    #[test]
245    fn test_golden_overloaded_em200() {
246        // Lv90, EM 200, Overloaded (2.75), vs 10% Pyro RES
247        // em_bonus = 16 * 200 / (200 + 2000) = 1.454545
248        // damage = 1446.8535 * 2.75 * (1 + 1.454545) * 0.9 = 8789.635
249        let input = TransformativeInput {
250            character_level: 90,
251            elemental_mastery: 200.0,
252            reaction: Reaction::Overloaded,
253            reaction_bonus: 0.0,
254        };
255        let result = calculate_transformative(&input, &default_enemy()).unwrap();
256        assert!((result.damage - 8789.635).abs() < 0.1);
257        assert_eq!(result.damage_element, Some(Element::Pyro));
258    }
259
260    #[test]
261    fn test_golden_swirl_pyro_em800() {
262        // Lv90, EM 800, Swirl Pyro (0.6), vs 10% Pyro RES
263        // em_bonus = 16 * 800 / (800 + 2000) = 4.571428
264        // damage = 1446.8535 * 0.6 * (1 + 4.571428) * 0.9 = 4352.962
265        let input = TransformativeInput {
266            character_level: 90,
267            elemental_mastery: 800.0,
268            reaction: Reaction::Swirl(Element::Pyro),
269            reaction_bonus: 0.0,
270        };
271        let result = calculate_transformative(&input, &default_enemy()).unwrap();
272        assert!((result.damage - 4352.962).abs() < 0.1);
273        assert_eq!(result.damage_element, Some(Element::Pyro));
274    }
275
276    #[test]
277    fn test_golden_superconduct_em150() {
278        // Lv90, EM 150, Superconduct (1.5), vs 10% Cryo RES
279        // em_bonus = 16 * 150 / (150 + 2000) = 2400/2150 = 1.11628
280        // damage = 1446.8535 * 1.5 * (1 + 2400/2150) * 0.9
281        //        = 2170.280 * (4550/2150) * 0.9 = 4133.627
282        let input = TransformativeInput {
283            character_level: 90,
284            elemental_mastery: 150.0,
285            reaction: Reaction::Superconduct,
286            reaction_bonus: 0.0,
287        };
288        let result = calculate_transformative(&input, &default_enemy()).unwrap();
289        assert!((result.damage - 4133.627).abs() < 0.1);
290        assert_eq!(result.damage_element, Some(Element::Cryo));
291    }
292
293    #[test]
294    fn test_golden_electro_charged_em300() {
295        // Lv90, EM 300, ElectroCharged (2.0), vs 10% Electro RES
296        // em_bonus = 16 * 300 / (300 + 2000) = 4800/2300 = 2.08696
297        // damage = 1446.8535 * 2.0 * (1 + 4800/2300) * 0.9
298        //        = 2893.707 * (7100/2300) * 0.9 = 8039.473
299        let input = TransformativeInput {
300            character_level: 90,
301            elemental_mastery: 300.0,
302            reaction: Reaction::ElectroCharged,
303            reaction_bonus: 0.0,
304        };
305        let result = calculate_transformative(&input, &default_enemy()).unwrap();
306        assert!((result.damage - 8039.473).abs() < 0.1);
307        assert_eq!(result.damage_element, Some(Element::Electro));
308    }
309
310    #[test]
311    fn test_golden_hyperbloom_em800() {
312        // Lv90, EM 800, Hyperbloom (3.0), vs 10% Dendro RES
313        // em_bonus = 16 * 800 / (800 + 2000) = 12800/2800 = 4.57143
314        // damage = 1446.8535 * 3.0 * (1 + 12800/2800) * 0.9
315        //        = 4340.561 * (15600/2800) * 0.9 = 21764.811
316        let input = TransformativeInput {
317            character_level: 90,
318            elemental_mastery: 800.0,
319            reaction: Reaction::Hyperbloom,
320            reaction_bonus: 0.0,
321        };
322        let result = calculate_transformative(&input, &default_enemy()).unwrap();
323        assert!((result.damage - 21764.811).abs() < 0.1);
324        assert_eq!(result.damage_element, Some(Element::Dendro));
325    }
326
327    #[test]
328    fn test_golden_kazuha_swirl_with_vv() {
329        // Kazuha full EM (960), Lv90, Swirl Pyro, 4pc VV (+60% Swirl DMG)
330        // em_bonus = 16 * 960 / (960 + 2000) = 15360/2960 = 5.18919
331        // damage = 1446.8535 * 0.6 * (1 + 5.18919 + 0.60) * 0.9
332        //        = 868.112 * 6.78919 * 0.9 = 5304.306
333        let input = TransformativeInput {
334            character_level: 90,
335            elemental_mastery: 960.0,
336            reaction: Reaction::Swirl(Element::Pyro),
337            reaction_bonus: 0.60,
338        };
339        let result = calculate_transformative(&input, &default_enemy()).unwrap();
340        assert!((result.damage - 5304.306).abs() < 0.5);
341        assert_eq!(result.damage_element, Some(Element::Pyro));
342    }
343
344    #[test]
345    fn test_reaction_bonus_applied() {
346        let base = TransformativeInput {
347            character_level: 90,
348            elemental_mastery: 0.0,
349            reaction: Reaction::Overloaded,
350            reaction_bonus: 0.0,
351        };
352        let with_bonus = TransformativeInput {
353            reaction_bonus: 0.4,
354            ..base.clone()
355        };
356        let r1 = calculate_transformative(&base, &default_enemy()).unwrap();
357        let r2 = calculate_transformative(&with_bonus, &default_enemy()).unwrap();
358        assert!((r2.damage / r1.damage - 1.4).abs() < EPSILON);
359    }
360}