1use serde::Deserialize;
4
5use crate::{
6 AoeDef, ChannelDef, EmpowerDef, EventEffectDef, ManifestDamageDef, ScalarRef,
7 manifest::default_true,
8};
9
10#[derive(Debug, Deserialize)]
22#[serde(deny_unknown_fields)]
23#[expect(
24 clippy::struct_excessive_bools,
25 reason = "fields are independent switches in the stable flat spell manifest schema"
26)]
27pub struct ManifestSpellDef {
28 pub id: u32,
29 #[serde(default)]
31 pub proc_attributes: Vec<ManifestProcAttribute>,
32 #[serde(default)]
33 pub cooldown: Option<ScalarRef>,
34 #[serde(default)]
35 pub cost: Option<ScalarRef>,
36 #[serde(default)]
37 pub gain: Option<ScalarRef>,
38 #[serde(default)]
39 pub damage: Option<ManifestDamageDef>,
40 #[serde(default = "default_true")]
41 pub infer_damage: bool,
42 #[serde(default = "default_true")]
43 pub infer_primary_damage: bool,
44 #[serde(default)]
45 pub damage_effects: Vec<DamageEffectDef>,
46 #[serde(default)]
47 pub trigger_program_predicates: Vec<TriggerProgramPredicateDef>,
48 #[serde(default)]
49 pub usable_while_casting: bool,
50 #[serde(default)]
51 pub travel_time_ms: Option<ScalarRef>,
52 #[serde(default)]
53 pub cast_time_ms: Option<ScalarRef>,
54 #[serde(default)]
55 pub gcd_ms: Option<ScalarRef>,
56 #[serde(default)]
57 pub charges: Option<ScalarRef>,
58 #[serde(default)]
59 pub charge_cd: Option<ScalarRef>,
60 #[serde(default)]
61 pub cooldown_hasted: Option<bool>,
62 #[serde(default)]
63 pub applies_aura: Option<String>,
64 #[serde(default = "default_true")]
65 pub infer_applies_aura: bool,
66 #[serde(default)]
67 pub applies_auras: Vec<String>,
68 #[serde(default)]
69 pub extends_auras: Vec<ExtendAuraDef>,
70 #[serde(default)]
71 pub reduces_cd: Option<Vec<ReducesCd>>,
72 #[serde(default)]
73 pub reduces_cd_chance: Option<ReducesCdChance>,
74 #[serde(default)]
75 pub resets_cd_while: Option<ResetsCdWhile>,
76 #[serde(default)]
77 pub hook: Option<String>,
78 #[serde(default = "default_breaks_stealth")]
79 pub breaks_stealth: bool,
80 #[serde(default)]
81 pub aoe: Option<AoeDef>,
82 #[serde(default)]
83 pub channel: Option<ChannelDef>,
84 #[serde(default)]
85 pub empower: Option<EmpowerDef>,
86 #[serde(default)]
87 pub requires_aura_id: Option<u32>,
88 #[serde(default)]
89 pub requires_aura_min_stacks: Option<u8>,
90 #[serde(default)]
91 pub cost_free_when_aura: Option<u32>,
92 #[serde(default)]
93 pub consume_aura_on_cast: Option<u32>,
94 #[serde(default)]
95 pub cooldown_bypass_when_aura: Option<String>,
96 #[serde(default)]
97 pub execute_below_pct: Option<f64>,
98 #[serde(default)]
99 pub instant_when_aura: Option<String>,
100 #[serde(default)]
101 pub override_when_aura: Option<String>,
102 #[serde(default)]
103 pub override_with_spell: Option<String>,
104 #[serde(default)]
105 pub override_aura_min_stacks: Option<u8>,
106 #[serde(default)]
107 pub override_shares_base_cooldown: bool,
108 #[serde(default)]
109 pub override_preserves_aura: bool,
110 #[serde(default)]
111 pub guaranteed_crit: bool,
112}
113#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
117#[serde(rename_all = "snake_case")]
118#[non_exhaustive]
119pub enum ManifestProcAttribute {
120 RequireExperienceOrHonor,
121 TriggeredCanProc,
122 RequirePowerCost,
123 RequireSpellModifier,
124 UseStacksForCharges,
125 ReduceProcAboveLevel60,
126 CannotProcFromItemCast,
127}
128
129impl ManifestSpellDef {
130 pub fn scalar_refs(&self) -> impl Iterator<Item = &ScalarRef> {
132 [
133 self.cooldown.as_ref(),
134 self.cost.as_ref(),
135 self.gain.as_ref(),
136 self.travel_time_ms.as_ref(),
137 self.cast_time_ms.as_ref(),
138 self.gcd_ms.as_ref(),
139 self.charges.as_ref(),
140 self.charge_cd.as_ref(),
141 self.channel
142 .as_ref()
143 .and_then(|channel| channel.tick_count.as_ref()),
144 self.channel
145 .as_ref()
146 .and_then(|channel| channel.tick_interval_ms.as_ref()),
147 ]
148 .into_iter()
149 .flatten()
150 .chain(
151 self.channel
152 .iter()
153 .flat_map(|channel| {
154 channel
155 .tick_effects
156 .iter()
157 .chain(channel.complete_effects.iter())
158 })
159 .map(|effect| match effect {
160 EventEffectDef::Energize { amount, .. } => amount,
161 EventEffectDef::ExtendAura { amount_ms, .. } => amount_ms,
162 }),
163 )
164 .chain(
165 self.reduces_cd
166 .iter()
167 .flatten()
168 .map(|cdr| &cdr.amount_ms)
169 .chain(self.reduces_cd_chance.iter().map(|cdr| &cdr.amount_ms)),
170 )
171 }
172}
173
174#[derive(Debug, Deserialize)]
176pub struct DamageEffectDef {
177 pub spell_id: u32,
178 pub effect: u8,
179 pub kind: String,
180 #[serde(default)]
181 pub ap_type: Option<String>,
182 #[serde(default = "default_one")]
183 pub multiplier: f64,
184}
185
186#[derive(Debug, Deserialize)]
188pub struct TriggerProgramPredicateDef {
189 pub effects: Vec<u8>,
190 pub predicate: String,
191}
192
193const fn default_one() -> f64 {
194 1.0
195}
196
197#[derive(Debug, Deserialize)]
198pub struct ExtendAuraDef {
199 pub aura: String,
200 pub amount_ms: u32,
201}
202
203const fn default_breaks_stealth() -> bool {
204 true
205}
206
207#[derive(Debug, Deserialize)]
209pub struct ReducesCd {
210 pub target: String,
211 pub amount_ms: ScalarRef,
212}
213
214#[derive(Debug, Deserialize)]
216pub struct ReducesCdChance {
217 pub target: String,
218 pub amount_ms: ScalarRef,
219 pub chance: f64,
220}
221
222#[derive(Debug, Deserialize)]
224pub struct ResetsCdWhile {
225 pub target: String,
226 pub aura: String,
227}
228
229#[cfg(test)]
230mod tests {
231 use googletest::prelude::*;
232
233 use super::*;
234
235 #[gtest]
236 fn server_proc_attributes_parse_as_typed_manifest_identities() -> Result<()> {
237 let spell: ManifestSpellDef = toml::from_str(
238 r#"
239id = 44544
240proc_attributes = ["triggered_can_proc", "use_stacks_for_charges"]
241"#,
242 )
243 .or_fail()?;
244
245 verify_that!(
246 spell.proc_attributes.as_slice(),
247 elements_are![
248 eq(&ManifestProcAttribute::TriggeredCanProc),
249 eq(&ManifestProcAttribute::UseStacksForCharges),
250 ]
251 )
252 }
253
254 #[gtest]
255 fn a_channel_tick_count_accepts_a_game_data_reference_and_a_literal() -> Result<()> {
256 let referenced: ManifestSpellDef = toml::from_str(
257 r#"
258id = 257044
259
260[channel]
261tick_count = { effect = 1, field = "base_points", spell_id = 257044 }
262"#,
263 )
264 .or_fail()?;
265 let literal: ManifestSpellDef = toml::from_str(
266 r"
267id = 113656
268
269[channel]
270tick_count = 5
271",
272 )
273 .or_fail()?;
274 let referenced_count = referenced
275 .channel
276 .as_ref()
277 .and_then(|channel| channel.tick_count.as_ref());
278 let literal_count = literal
279 .channel
280 .as_ref()
281 .and_then(|channel| channel.tick_count.as_ref());
282
283 verify_that!(
284 referenced_count,
285 some(pat!(&ScalarRef::EffectRef {
286 spell_id: eq(257_044),
287 effect: eq(1),
288 ..
289 }))
290 )?;
291 verify_that!(literal_count, some(pat!(&ScalarRef::Literal(eq(5.0)))))?;
292
293 verify_that!(
296 referenced
297 .scalar_refs()
298 .filter_map(ScalarRef::effect_spell_id)
299 .collect::<Vec<_>>(),
300 eq(&vec![257_044])
301 )
302 }
303}
304
305#[derive(Debug, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct AutoAttackDef {
309 pub spell_id: u32,
310 pub swing_ms: u32,
311 #[serde(default)]
313 pub npc_id: Option<u32>,
314 #[serde(default)]
315 pub ap_coef: Option<f64>,
316 #[serde(default)]
318 pub pet_power_from_owner: Option<f64>,
319 #[serde(default)]
321 pub pet_weapon_speed_ms: Option<u32>,
322 #[serde(default)]
323 pub pet_weapon_multiplier: Option<f64>,
324 #[serde(default)]
325 pub is_pet: bool,
326 #[serde(default)]
328 pub uses_spell_power: bool,
329 #[serde(default)]
330 pub on_crit_reduce_charge: Option<OnCritReduceCharge>,
331 #[serde(default)]
333 pub hook: Option<String>,
334}
335
336#[derive(Debug, Deserialize)]
338pub struct OnCritReduceCharge {
339 pub target: String,
340 pub chance: f64,
341}