Skip to main content

wowlab_types/types/data/
assisted.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4pub struct AssistedConditionFlat {
5    #[serde(rename = "type")]
6    pub r#type: i32,
7    pub name: String,
8    pub v1: i32,
9    pub v2: i32,
10    pub v3: i32,
11    #[serde(default, skip_serializing_if = "is_zero")]
12    pub flags: i32,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub predicate: Option<String>,
15    #[serde(skip)]
16    pub condition_type: Option<ConditionType>,
17}
18
19#[expect(
20    clippy::trivially_copy_pass_by_ref,
21    reason = "serde skip_serializing_if callbacks receive a reference"
22)]
23const fn is_zero(v: &i32) -> bool {
24    *v == 0
25}
26
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
28pub struct AssistedActionFlat {
29    pub step_id: i32,
30    pub order_index: i32,
31    pub spell_id: i32,
32    pub allow_casting_success: bool,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub predicate: Option<String>,
35    pub conditions: Vec<AssistedConditionFlat>,
36}
37
38#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
39pub struct AssistedRotationFlat {
40    pub spec_id: i32,
41    pub actions: Vec<AssistedActionFlat>,
42}
43
44/// Rule predicate IDs for Blizzard's assisted combat rotations.
45#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::IntoStaticStr)]
46#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
47#[repr(i32)]
48// #t(rust_non_exhaustive_on_public) fixed condition type IDs matching the assisted rotation protocol
49pub enum ConditionType {
50    SpellLearned = 0,
51    SpellOnCooldown = 1,
52    SpellOffCooldown = 2,
53    TargetDistanceLess = 3,
54    TargetDistanceGreater = 4,
55    HostileTarget = 5,
56    FriendlyTarget = 6,
57    HealthPctGreater = 7,
58    HealthPctLess = 8,
59    AuraOnPlayer = 9,
60    AuraOnTarget = 10,
61    TargetCountNearTargetGreater = 11,
62    TargetCountNearPlayerGreater = 12,
63    AuraCountNearPlayerGreater = 13,
64    AffordCost = 14,
65    AuraMissingTarget = 15,
66    AuraMissingPlayer = 16,
67    AuraDurationPlayer = 17,
68    AuraDurationTarget = 18,
69    ManaGreater = 19,
70    ManaLess = 20,
71    RageGreater = 21,
72    RageLess = 22,
73    FocusGreater = 23,
74    FocusLess = 24,
75    EnergyGreater = 25,
76    EnergyLess = 26,
77    ComboPointsGreater = 27,
78    ComboPointsLess = 28,
79    RunesGreater = 29,
80    RunesLess = 30,
81    RunicPowerGreater = 31,
82    RunicPowerLess = 32,
83    SoulShardsGreater = 33,
84    SoulShardsLess = 34,
85    LunarPowerGreater = 35,
86    LunarPowerLess = 36,
87    HolyPowerGreater = 37,
88    HolyPowerLess = 38,
89    MaelstromGreater = 39,
90    MaelstromLess = 40,
91    ChiGreater = 41,
92    ChiLess = 42,
93    InsanityGreater = 43,
94    InsanityLess = 44,
95    EssenceGreater = 45,
96    EssenceLess = 46,
97    ArcaneChargesGreater = 47,
98    ArcaneChargesLess = 48,
99    TargetCountNearTargetLess = 49,
100    TargetCountNearPlayerLess = 50,
101    AuraCountNearPlayerLess = 51,
102    TargetAuraApplicationGreater = 52,
103    TargetAuraApplicationLess = 53,
104    PlayerAuraApplicationGreater = 54,
105    PlayerAuraApplicationLess = 55,
106    SpellInRange = 56,
107    HasPet = 57,
108    HasNoPet = 58,
109    FuryGreater = 59,
110    FuryLess = 60,
111    PainGreater = 61,
112    PainLess = 62,
113    SpellChargesGreater = 63,
114    SpellChargesLess = 64,
115    CooldownRemainingGreater = 65,
116    CooldownRemainingLess = 66,
117    CooldownAllowCastingSuccess = 67,
118    PlayerHealthPctGreater = 68,
119    PlayerHealthPctLess = 69,
120    AutomationOnly = 70,
121}
122
123impl ConditionType {
124    // #t(fn: rust_cyclomatic_complexity) 1:1 integer-to-enum dispatch for all 71 condition types
125    #[must_use]
126    pub fn from_raw(value: i32) -> Option<Self> {
127        Some(match value {
128            0 => Self::SpellLearned,
129            1 => Self::SpellOnCooldown,
130            2 => Self::SpellOffCooldown,
131            3 => Self::TargetDistanceLess,
132            4 => Self::TargetDistanceGreater,
133            5 => Self::HostileTarget,
134            6 => Self::FriendlyTarget,
135            7 => Self::HealthPctGreater,
136            8 => Self::HealthPctLess,
137            9 => Self::AuraOnPlayer,
138            10 => Self::AuraOnTarget,
139            11 => Self::TargetCountNearTargetGreater,
140            12 => Self::TargetCountNearPlayerGreater,
141            13 => Self::AuraCountNearPlayerGreater,
142            14 => Self::AffordCost,
143            15 => Self::AuraMissingTarget,
144            16 => Self::AuraMissingPlayer,
145            17 => Self::AuraDurationPlayer,
146            18 => Self::AuraDurationTarget,
147            19 => Self::ManaGreater,
148            20 => Self::ManaLess,
149            21 => Self::RageGreater,
150            22 => Self::RageLess,
151            23 => Self::FocusGreater,
152            24 => Self::FocusLess,
153            25 => Self::EnergyGreater,
154            26 => Self::EnergyLess,
155            27 => Self::ComboPointsGreater,
156            28 => Self::ComboPointsLess,
157            29 => Self::RunesGreater,
158            30 => Self::RunesLess,
159            31 => Self::RunicPowerGreater,
160            32 => Self::RunicPowerLess,
161            33 => Self::SoulShardsGreater,
162            34 => Self::SoulShardsLess,
163            35 => Self::LunarPowerGreater,
164            36 => Self::LunarPowerLess,
165            37 => Self::HolyPowerGreater,
166            38 => Self::HolyPowerLess,
167            39 => Self::MaelstromGreater,
168            40 => Self::MaelstromLess,
169            41 => Self::ChiGreater,
170            42 => Self::ChiLess,
171            43 => Self::InsanityGreater,
172            44 => Self::InsanityLess,
173            45 => Self::EssenceGreater,
174            46 => Self::EssenceLess,
175            47 => Self::ArcaneChargesGreater,
176            48 => Self::ArcaneChargesLess,
177            49 => Self::TargetCountNearTargetLess,
178            50 => Self::TargetCountNearPlayerLess,
179            51 => Self::AuraCountNearPlayerLess,
180            52 => Self::TargetAuraApplicationGreater,
181            53 => Self::TargetAuraApplicationLess,
182            54 => Self::PlayerAuraApplicationGreater,
183            55 => Self::PlayerAuraApplicationLess,
184            56 => Self::SpellInRange,
185            57 => Self::HasPet,
186            58 => Self::HasNoPet,
187            59 => Self::FuryGreater,
188            60 => Self::FuryLess,
189            61 => Self::PainGreater,
190            62 => Self::PainLess,
191            63 => Self::SpellChargesGreater,
192            64 => Self::SpellChargesLess,
193            65 => Self::CooldownRemainingGreater,
194            66 => Self::CooldownRemainingLess,
195            67 => Self::CooldownAllowCastingSuccess,
196            68 => Self::PlayerHealthPctGreater,
197            69 => Self::PlayerHealthPctLess,
198            70 => Self::AutomationOnly,
199            _ => return None,
200        })
201    }
202
203    #[must_use]
204    pub fn name(self) -> &'static str {
205        self.into()
206    }
207
208    /// True when `value_2` encodes milliseconds.
209    #[must_use]
210    pub fn uses_milliseconds(self) -> bool {
211        matches!(
212            self,
213            Self::AuraDurationPlayer
214                | Self::AuraDurationTarget
215                | Self::CooldownRemainingGreater
216                | Self::CooldownRemainingLess
217        )
218    }
219
220    /// True when `value_1` encodes a tenths-precision value.
221    #[must_use]
222    pub fn uses_tenths(self) -> bool {
223        matches!(
224            self,
225            Self::ManaGreater
226                | Self::ManaLess
227                | Self::RageGreater
228                | Self::RageLess
229                | Self::RunicPowerGreater
230                | Self::RunicPowerLess
231                | Self::SoulShardsGreater
232                | Self::SoulShardsLess
233                | Self::LunarPowerGreater
234                | Self::LunarPowerLess
235        )
236    }
237
238    /// True when `value_1` encodes a hundredths-precision value.
239    #[must_use]
240    pub fn uses_hundredths(self) -> bool {
241        matches!(self, Self::InsanityGreater | Self::InsanityLess)
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use googletest::prelude::*;
248    use rstest::rstest;
249
250    use super::*;
251
252    #[gtest]
253    #[rstest]
254    #[case::first(0, Some(ConditionType::SpellLearned))]
255    #[case::mid(35, Some(ConditionType::LunarPowerGreater))]
256    #[case::last_valid(70, Some(ConditionType::AutomationOnly))]
257    #[case::past_last(71, None)]
258    #[case::negative(-1, None)]
259    fn from_raw_boundaries(
260        #[case] input: i32,
261        #[case] expected: Option<ConditionType>,
262    ) -> Result<()> {
263        verify_that!(ConditionType::from_raw(input), eq(expected))
264    }
265
266    #[gtest]
267    #[rstest]
268    #[case::first(0)]
269    #[case::mid(35)]
270    #[case::last(70)]
271    fn from_raw_roundtrip(#[case] value: i32) -> Result<()> {
272        verify_that!(
273            ConditionType::from_raw(value).map(|c| c as i32),
274            some(eq(value))
275        )
276    }
277
278    #[gtest]
279    #[rstest]
280    #[case::aura_duration_player(ConditionType::AuraDurationPlayer, true)]
281    #[case::aura_duration_target(ConditionType::AuraDurationTarget, true)]
282    #[case::cooldown_remaining_greater(ConditionType::CooldownRemainingGreater, true)]
283    #[case::cooldown_remaining_less(ConditionType::CooldownRemainingLess, true)]
284    #[case::mana_greater(ConditionType::ManaGreater, false)]
285    #[case::spell_learned(ConditionType::SpellLearned, false)]
286    fn uses_milliseconds(#[case] cond: ConditionType, #[case] expected: bool) -> Result<()> {
287        verify_that!(cond.uses_milliseconds(), eq(expected))
288    }
289
290    #[gtest]
291    #[rstest]
292    #[case::mana_greater(ConditionType::ManaGreater, true)]
293    #[case::rage_less(ConditionType::RageLess, true)]
294    #[case::runic_power_greater(ConditionType::RunicPowerGreater, true)]
295    #[case::soul_shards_less(ConditionType::SoulShardsLess, true)]
296    #[case::lunar_power_greater(ConditionType::LunarPowerGreater, true)]
297    #[case::insanity_greater(ConditionType::InsanityGreater, false)]
298    #[case::aura_duration_player(ConditionType::AuraDurationPlayer, false)]
299    fn uses_tenths(#[case] cond: ConditionType, #[case] expected: bool) -> Result<()> {
300        verify_that!(cond.uses_tenths(), eq(expected))
301    }
302
303    #[gtest]
304    #[rstest]
305    #[case::insanity_greater(ConditionType::InsanityGreater, true)]
306    #[case::insanity_less(ConditionType::InsanityLess, true)]
307    #[case::mana_greater(ConditionType::ManaGreater, false)]
308    fn uses_hundredths(#[case] cond: ConditionType, #[case] expected: bool) -> Result<()> {
309        verify_that!(cond.uses_hundredths(), eq(expected))
310    }
311
312    #[gtest]
313    #[rstest]
314    #[case::spell_learned(ConditionType::SpellLearned, "SPELL_LEARNED")]
315    #[case::target_distance_less(ConditionType::TargetDistanceLess, "TARGET_DISTANCE_LESS")]
316    #[case::has_no_pet(ConditionType::HasNoPet, "HAS_NO_PET")]
317    fn name_screaming_snake(#[case] cond: ConditionType, #[case] expected: &str) -> Result<()> {
318        verify_that!(cond.name(), eq(expected))
319    }
320}