Skip to main content

wowlab_types/types/game/
introspection.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
6#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
7#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
8pub struct SpecIntrospection {
9    pub resource_primary: ResourceInfo,
10    pub resource_secondary: Option<ResourceInfo>,
11    pub spells: Vec<SpellInfo>,
12    pub auras: Vec<AuraInfo>,
13    pub auto_attacks: Vec<AutoAttackInfo>,
14    pub hero_talents: Vec<HeroTalentTree>,
15}
16
17#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
18#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
19#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
20pub struct ResourceInfo {
21    pub name: String,
22    pub max: f64,
23    pub regen: f64,
24    pub starts_at: f64,
25    pub type_id: u8,
26}
27
28#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
29#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
30#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
31pub struct SpellInfo {
32    pub spell_id: u32,
33    pub name: String,
34    pub slug: String,
35    pub cast_time_ms: u32,
36    pub start_recovery_category: Option<crate::data::CooldownCategoryId>,
37    pub off_gcd: bool,
38    pub gcd_ms: u32,
39    pub is_pet: bool,
40    pub breaks_stealth: bool,
41    pub resource_cost: f64,
42    pub resource_gain: f64,
43    pub secondary_resource_cost: f64,
44    pub secondary_resource_gain: f64,
45    pub damage: DamageInfo,
46    pub applies_aura_id: Option<u32>,
47    pub cooldown: Option<CooldownInfo>,
48    pub cdr_effects: Vec<CdrEffect>,
49}
50
51#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
52#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
53#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
54pub struct CooldownInfo {
55    pub duration_secs: f64,
56    pub max_charges: u8,
57    pub recharge_secs: f64,
58}
59
60#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
61#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
62#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
63pub struct CdrEffect {
64    pub target_spell_local: u8,
65    pub amount_ms: u32,
66    pub condition: CdrCondition,
67}
68
69#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
70#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
71#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
72#[serde(tag = "type")]
73#[non_exhaustive]
74pub enum CdrCondition {
75    Always,
76    ProcChance { chance: f64 },
77    WhileAuraActive { aura_local: u8 },
78    ResetWhileAura { aura_local: u8 },
79}
80
81impl fmt::Display for CdrCondition {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Always => write!(f, "Always"),
85            Self::ProcChance { chance } => write!(f, "ProcChance({chance:.0}%)"),
86            Self::WhileAuraActive { aura_local } => {
87                write!(f, "WhileAuraActive({aura_local})")
88            }
89            Self::ResetWhileAura { aura_local } => write!(f, "ResetWhileAura({aura_local})"),
90        }
91    }
92}
93
94#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
95#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
96#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
97pub struct DamageInfo {
98    pub kind: DamageKind,
99}
100
101#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
102#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
103#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
104#[serde(tag = "type")]
105#[non_exhaustive]
106pub enum DamageKind {
107    None,
108    Flat {
109        amount: f64,
110    },
111    ApCoefficient {
112        coef: f64,
113        is_physical: bool,
114    },
115    SpCoefficient {
116        coef: f64,
117        is_physical: bool,
118    },
119    Weapon {
120        multiplier: f64,
121        flat_bonus: f64,
122        normalized: bool,
123        is_physical: bool,
124    },
125}
126
127impl fmt::Display for DamageKind {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Self::None => Ok(()),
131            Self::Flat { amount } => write!(f, "dmg=flat({amount:.1})"),
132            Self::ApCoefficient { coef, is_physical } => {
133                write!(f, "dmg=ap_coef({coef:.4}) physical={is_physical}")
134            }
135            Self::SpCoefficient { coef, is_physical } => {
136                write!(f, "dmg=sp_coef({coef:.4}) physical={is_physical}")
137            }
138            Self::Weapon {
139                multiplier,
140                flat_bonus,
141                normalized,
142                is_physical,
143            } => write!(
144                f,
145                "dmg=weapon(multiplier={multiplier:.4} flat_bonus={flat_bonus:.1}) normalized={normalized} physical={is_physical}"
146            ),
147        }
148    }
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
152#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
153#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
154pub struct AuraInfo {
155    pub aura_id: u32,
156    pub name: String,
157    pub slug: String,
158    pub on: String,
159    pub base_duration_ms: u32,
160    pub max_stacks: u8,
161    pub pandemic: bool,
162    pub haste_buff_pct: f64,
163    pub damage_mult_pct: f64,
164    pub periodic: Option<PeriodicInfo>,
165}
166
167#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
168#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
169#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
170pub struct PeriodicInfo {
171    pub tick_ms: u32,
172    pub effect: PeriodicEffect,
173}
174
175#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
176#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
177#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
178#[serde(tag = "type")]
179#[non_exhaustive]
180pub enum PeriodicEffect {
181    FlatDamage { amount: f64, is_physical: bool },
182    Damage { ap_coef: f64, is_physical: bool },
183    SpDamage { sp_coef: f64, is_physical: bool },
184    MaxHealthDamage { percent: f64, is_physical: bool },
185    ResourceGain { amount: f64 },
186    ResourceDrain { amount: f64 },
187    ApplyAura { target_aura_local: u8 },
188    ResidualDamage,
189    RemoveStack,
190    Hook,
191}
192
193impl fmt::Display for PeriodicEffect {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            Self::FlatDamage {
197                amount,
198                is_physical,
199            } => write!(f, "amount={amount:.1} physical={is_physical}"),
200            Self::Damage {
201                ap_coef,
202                is_physical,
203            } => write!(f, "ap_coef={ap_coef:.4} physical={is_physical}"),
204            Self::SpDamage {
205                sp_coef,
206                is_physical,
207            } => write!(f, "sp_coef={sp_coef:.4} physical={is_physical}"),
208            Self::MaxHealthDamage {
209                percent,
210                is_physical,
211            } => write!(f, "max_health={percent:.4}% physical={is_physical}"),
212            Self::ResourceGain { amount } => write!(f, "resource_gain={amount:.1}"),
213            Self::ResourceDrain { amount } => write!(f, "resource_drain={amount:.1}"),
214            Self::ApplyAura { target_aura_local } => {
215                write!(f, "apply_aura={target_aura_local}")
216            }
217            Self::ResidualDamage => f.write_str("residual_damage"),
218            Self::RemoveStack => f.write_str("remove_stack"),
219            Self::Hook => f.write_str("hook"),
220        }
221    }
222}
223
224#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
225#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
226#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
227pub struct AutoAttackInfo {
228    pub spell_id: u32,
229    pub swing_ms: u32,
230    pub ap_coef: f64,
231    pub is_pet: bool,
232}
233
234#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
235#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
236#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
237pub struct HeroTalentTree {
238    pub name: String,
239    pub spells: Vec<(String, u32)>,
240    pub auras: Vec<(String, u32)>,
241}
242
243#[cfg(test)]
244mod tests {
245    use googletest::prelude::*;
246    use rstest::rstest;
247
248    use super::*;
249
250    #[gtest]
251    #[rstest]
252    #[case::always(CdrCondition::Always, "Always")]
253    #[case::proc_chance(CdrCondition::ProcChance { chance: 50.0 }, "ProcChance(50%)")]
254    #[case::proc_chance_round(CdrCondition::ProcChance { chance: 49.6 }, "ProcChance(50%)")]
255    #[case::while_aura(CdrCondition::WhileAuraActive { aura_local: 3 }, "WhileAuraActive(3)")]
256    #[case::reset_aura(CdrCondition::ResetWhileAura { aura_local: 7 }, "ResetWhileAura(7)")]
257    fn cdr_condition_display(#[case] cond: CdrCondition, #[case] expected: &str) -> Result<()> {
258        verify_that!(format!("{cond}"), eq(expected))
259    }
260
261    #[gtest]
262    fn cdr_effect_serde_shape_preserves_wasm_contract() -> Result<()> {
263        let effect = CdrEffect {
264            target_spell_local: 4,
265            amount_ms: 750,
266            condition: CdrCondition::WhileAuraActive { aura_local: 2 },
267        };
268        let json = serde_json::to_value(effect).or_fail()?;
269
270        verify_that!(
271            json,
272            eq(&serde_json::json!({
273                "target_spell_local": 4,
274                "amount_ms": 750,
275                "condition": {
276                    "type": "WhileAuraActive",
277                    "aura_local": 2,
278                },
279            }))
280        )?;
281        let round_trip: CdrEffect = serde_json::from_value(json).or_fail()?;
282
283        verify_that!(round_trip, eq(effect))
284    }
285
286    #[gtest]
287    #[rstest]
288    #[case::none(DamageKind::None, "")]
289    #[case::flat(DamageKind::Flat { amount: 12.5 }, "dmg=flat(12.5)")]
290    #[case::ap(
291        DamageKind::ApCoefficient { coef: 1.25, is_physical: true },
292        "dmg=ap_coef(1.2500) physical=true"
293    )]
294    #[case::sp(
295        DamageKind::SpCoefficient { coef: 0.75, is_physical: false },
296        "dmg=sp_coef(0.7500) physical=false"
297    )]
298    #[case::weapon(
299        DamageKind::Weapon {
300            multiplier: 1.5,
301            flat_bonus: 20.0,
302            normalized: true,
303            is_physical: true,
304        },
305        "dmg=weapon(multiplier=1.5000 flat_bonus=20.0) normalized=true physical=true"
306    )]
307    fn damage_kind_display(#[case] kind: DamageKind, #[case] expected: &str) -> Result<()> {
308        verify_that!(kind.to_string(), eq(expected))
309    }
310
311    #[gtest]
312    #[rstest]
313    #[case::flat(
314        PeriodicEffect::FlatDamage { amount: 12.5, is_physical: true },
315        "amount=12.5 physical=true"
316    )]
317    #[case::ap(
318        PeriodicEffect::Damage { ap_coef: 1.25, is_physical: false },
319        "ap_coef=1.2500 physical=false"
320    )]
321    #[case::sp(
322        PeriodicEffect::SpDamage { sp_coef: 0.75, is_physical: false },
323        "sp_coef=0.7500 physical=false"
324    )]
325    #[case::max_health(
326        PeriodicEffect::MaxHealthDamage { percent: 2.5, is_physical: true },
327        "max_health=2.5000% physical=true"
328    )]
329    #[case::gain(PeriodicEffect::ResourceGain { amount: 3.0 }, "resource_gain=3.0")]
330    #[case::drain(PeriodicEffect::ResourceDrain { amount: 4.0 }, "resource_drain=4.0")]
331    #[case::apply(PeriodicEffect::ApplyAura { target_aura_local: 7 }, "apply_aura=7")]
332    #[case::residual(PeriodicEffect::ResidualDamage, "residual_damage")]
333    #[case::remove_stack(PeriodicEffect::RemoveStack, "remove_stack")]
334    #[case::hook(PeriodicEffect::Hook, "hook")]
335    fn periodic_effect_display(
336        #[case] effect: PeriodicEffect,
337        #[case] expected: &str,
338    ) -> Result<()> {
339        verify_that!(effect.to_string(), eq(expected))
340    }
341}