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#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::IntoStaticStr)]
46#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
47#[repr(i32)]
48pub 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 #[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 #[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 #[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 #[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}