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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct TransformativeInput {
16 pub character_level: u32,
18 pub elemental_mastery: f64,
20 pub reaction: Reaction,
22 pub reaction_bonus: f64,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct TransformativeResult {
29 pub damage: f64,
31 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
60pub 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 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 #[test]
245 fn test_golden_overloaded_em200() {
246 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 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 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 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 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 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}