Skip to main content

forge/manifest_ledger/collect/
spell.rs

1// #t(file: rust_alloc_in_loop) spell rows intentionally own stable manifest coordinates.
2
3use wowlab_manifest_schema::{Manifest, ManifestAuraDef, ManifestSpellDef};
4
5use super::{
6    EXPLICIT_AURA_EDGE_EVIDENCE, base_row, damage_row, event_effect, hook_placeholder,
7    scalar_option,
8};
9use crate::manifest_ledger::types::{LedgerDisposition, LedgerRow, OperationCategory};
10
11pub(super) fn collect_spell(
12    rows: &mut Vec<LedgerRow>,
13    owner: &str,
14    path: &str,
15    name: &str,
16    spell: &ManifestSpellDef,
17    auras: &indexmap::IndexMap<String, ManifestAuraDef>,
18) {
19    let root = format!("spells.{name}");
20
21    collect_spell_scalars(rows, owner, path, &root, spell);
22    collect_spell_auras(rows, owner, path, &root, spell, auras);
23    spell_cooldowns(rows, owner, path, &root, spell);
24    spell_gates(rows, owner, path, &root, spell);
25
26    if spell.aoe.is_some() {
27        rows.push(
28            base_row(
29                owner,
30                path,
31                format!("{root}.aoe"),
32                OperationCategory::Targeting,
33                LedgerDisposition::GenericGap,
34                "authored target geometry/cap overrides",
35            )
36            .owner(spell.id),
37        );
38    }
39
40    collect_channel(rows, owner, path, &root, spell);
41
42    if spell.hook.is_some() {
43        hook_placeholder(
44            rows,
45            owner,
46            path,
47            &format!("{root}.hook"),
48            spell.id,
49            OperationCategory::OrderedContent,
50        );
51    }
52}
53
54fn collect_spell_scalars(
55    rows: &mut Vec<LedgerRow>,
56    owner: &str,
57    path: &str,
58    root: &str,
59    spell: &ManifestSpellDef,
60) {
61    for (field, scalar, category, core) in [
62        (
63            "cooldown",
64            spell.cooldown.as_ref(),
65            OperationCategory::Cooldown,
66            true,
67        ),
68        (
69            "cost",
70            spell.cost.as_ref(),
71            OperationCategory::Resource,
72            true,
73        ),
74        (
75            "gain",
76            spell.gain.as_ref(),
77            OperationCategory::Resource,
78            false,
79        ),
80        (
81            "travel_time_ms",
82            spell.travel_time_ms.as_ref(),
83            OperationCategory::OrderedContent,
84            true,
85        ),
86        (
87            "cast_time_ms",
88            spell.cast_time_ms.as_ref(),
89            OperationCategory::Gate,
90            true,
91        ),
92        (
93            "gcd_ms",
94            spell.gcd_ms.as_ref(),
95            OperationCategory::Gate,
96            true,
97        ),
98        (
99            "charges",
100            spell.charges.as_ref(),
101            OperationCategory::Cooldown,
102            true,
103        ),
104        (
105            "charge_cd",
106            spell.charge_cd.as_ref(),
107            OperationCategory::Cooldown,
108            true,
109        ),
110    ] {
111        scalar_option(
112            rows,
113            owner,
114            path,
115            &format!("{root}.{field}"),
116            spell.id,
117            scalar,
118            category,
119            core,
120        );
121    }
122
123    if let Some(damage) = &spell.damage {
124        rows.push(damage_row(
125            owner,
126            path,
127            format!("{root}.damage"),
128            spell.id,
129            damage,
130        ));
131    }
132
133    for (index, damage) in spell.damage_effects.iter().enumerate() {
134        rows.push(
135            base_row(
136                owner,
137                path,
138                format!("{root}.damage_effects[{index}]"),
139                OperationCategory::Damage,
140                LedgerDisposition::GenericGap,
141                "authored DBC child damage effect",
142            )
143            .owner(spell.id)
144            .child(damage.spell_id)
145            .effect(damage.spell_id, damage.effect),
146        );
147    }
148}
149
150fn collect_spell_auras(
151    rows: &mut Vec<LedgerRow>,
152    owner: &str,
153    path: &str,
154    root: &str,
155    spell: &ManifestSpellDef,
156    auras: &indexmap::IndexMap<String, ManifestAuraDef>,
157) {
158    if let Some(aura) = &spell.applies_aura {
159        rows.push(explicit_aura_edge(
160            owner,
161            path,
162            format!("{root}.applies_aura ({aura})"),
163            spell.id,
164            auras.get(aura).map(|definition| definition.id),
165        ));
166    }
167
168    for (index, aura) in spell.applies_auras.iter().enumerate() {
169        rows.push(explicit_aura_edge(
170            owner,
171            path,
172            format!("{root}.applies_auras[{index}] ({aura})"),
173            spell.id,
174            auras.get(aura).map(|definition| definition.id),
175        ));
176    }
177
178    for (index, extend) in spell.extends_auras.iter().enumerate() {
179        rows.push(
180            base_row(
181                owner,
182                path,
183                format!("{root}.extends_auras[{index}] ({})", extend.aura),
184                OperationCategory::AuraApply,
185                LedgerDisposition::ContentOrdering,
186                "ordered aura-duration mutation",
187            )
188            .owner(spell.id),
189        );
190    }
191}
192
193fn collect_channel(
194    rows: &mut Vec<LedgerRow>,
195    owner: &str,
196    path: &str,
197    root: &str,
198    spell: &ManifestSpellDef,
199) {
200    if let Some(channel) = &spell.channel {
201        rows.push(
202            base_row(
203                owner,
204                path,
205                format!("{root}.channel"),
206                OperationCategory::AuraTick,
207                LedgerDisposition::Unresolved,
208                "authored channel cadence and ordered tick program",
209            )
210            .owner(spell.id),
211        );
212
213        if let Some(damage) = &channel.tick_damage {
214            rows.push(damage_row(
215                owner,
216                path,
217                format!("{root}.channel.tick_damage"),
218                spell.id,
219                damage,
220            ));
221        }
222
223        if channel.tick_damage_alt.is_some() {
224            rows.push(
225                base_row(
226                    owner,
227                    path,
228                    format!("{root}.channel.tick_damage_alt"),
229                    OperationCategory::Damage,
230                    LedgerDisposition::ContentOrdering,
231                    "aura-conditional channel damage ordering",
232                )
233                .owner(spell.id),
234            );
235        }
236
237        for (index, effect) in channel.tick_effects.iter().enumerate() {
238            event_effect(
239                rows,
240                owner,
241                path,
242                &format!("{root}.channel.tick_effects[{index}]"),
243                spell.id,
244                effect,
245            );
246        }
247
248        if channel.tick_hook.is_some() {
249            hook_placeholder(
250                rows,
251                owner,
252                path,
253                &format!("{root}.channel.tick_hook"),
254                spell.id,
255                OperationCategory::AuraTick,
256            );
257        }
258    }
259}
260
261fn explicit_aura_edge(
262    owner: &str,
263    path: &str,
264    key: String,
265    spell_id: u32,
266    aura_id: Option<u32>,
267) -> LedgerRow {
268    let row = base_row(
269        owner,
270        path,
271        key,
272        OperationCategory::AuraApply,
273        LedgerDisposition::GenericGap,
274        EXPLICIT_AURA_EDGE_EVIDENCE,
275    )
276    .owner(spell_id);
277
278    if let Some(aura_id) = aura_id {
279        row.child(aura_id)
280    } else {
281        row
282    }
283}
284
285fn spell_cooldowns(
286    rows: &mut Vec<LedgerRow>,
287    owner: &str,
288    path: &str,
289    root: &str,
290    spell: &ManifestSpellDef,
291) {
292    for (index, _) in spell.reduces_cd.iter().flatten().enumerate() {
293        rows.push(
294            base_row(
295                owner,
296                path,
297                format!("{root}.reduces_cd[{index}]"),
298                OperationCategory::Cooldown,
299                LedgerDisposition::GenericGap,
300                "typed cooldown child operation",
301            )
302            .owner(spell.id),
303        );
304    }
305
306    if spell.reduces_cd_chance.is_some() {
307        rows.push(
308            base_row(
309                owner,
310                path,
311                format!("{root}.reduces_cd_chance"),
312                OperationCategory::Cooldown,
313                LedgerDisposition::ContentOrdering,
314                "chance-gated cooldown mutation",
315            )
316            .owner(spell.id),
317        );
318    }
319
320    if spell.resets_cd_while.is_some() {
321        rows.push(
322            base_row(
323                owner,
324                path,
325                format!("{root}.resets_cd_while"),
326                OperationCategory::Cooldown,
327                LedgerDisposition::GenericGap,
328                "aura-gated cooldown reset",
329            )
330            .owner(spell.id),
331        );
332    }
333}
334
335fn spell_gates(
336    rows: &mut Vec<LedgerRow>,
337    owner: &str,
338    path: &str,
339    root: &str,
340    spell: &ManifestSpellDef,
341) {
342    let gates = [
343        ("usable_while_casting", spell.usable_while_casting),
344        ("requires_aura_id", spell.requires_aura_id.is_some()),
345        (
346            "requires_aura_min_stacks",
347            spell.requires_aura_min_stacks.is_some(),
348        ),
349        ("cost_free_when_aura", spell.cost_free_when_aura.is_some()),
350        ("consume_aura_on_cast", spell.consume_aura_on_cast.is_some()),
351        (
352            "cooldown_bypass_when_aura",
353            spell.cooldown_bypass_when_aura.is_some(),
354        ),
355        ("execute_below_pct", spell.execute_below_pct.is_some()),
356        ("instant_when_aura", spell.instant_when_aura.is_some()),
357        ("guaranteed_crit", spell.guaranteed_crit),
358        ("cooldown_hasted", spell.cooldown_hasted.is_some()),
359        ("breaks_stealth", !spell.breaks_stealth),
360    ];
361
362    for (field, present) in gates {
363        if present {
364            rows.push(
365                base_row(
366                    owner,
367                    path,
368                    format!("{root}.{field}"),
369                    OperationCategory::Gate,
370                    LedgerDisposition::GenericGap,
371                    "authored spell gate/property override",
372                )
373                .owner(spell.id),
374            );
375        }
376    }
377
378    let replacements = [
379        ("override_when_aura", spell.override_when_aura.is_some()),
380        ("override_with_spell", spell.override_with_spell.is_some()),
381        (
382            "override_aura_min_stacks",
383            spell.override_aura_min_stacks.is_some(),
384        ),
385        (
386            "override_shares_base_cooldown",
387            spell.override_shares_base_cooldown,
388        ),
389        ("override_preserves_aura", spell.override_preserves_aura),
390    ];
391
392    for (field, present) in replacements {
393        if present {
394            rows.push(
395                base_row(
396                    owner,
397                    path,
398                    format!("{root}.{field}"),
399                    OperationCategory::Replacement,
400                    LedgerDisposition::GenericGap,
401                    "authored action replacement rule",
402                )
403                .owner(spell.id),
404            );
405        }
406    }
407}
408
409pub(super) fn collect_auto_attacks(
410    rows: &mut Vec<LedgerRow>,
411    slug: &str,
412    path: &str,
413    manifest: &Manifest,
414) {
415    for (name, attack) in &manifest.auto_attacks {
416        let root = format!("auto_attacks.{name}");
417
418        rows.push(
419            base_row(
420                slug,
421                path,
422                format!("{root}.damage"),
423                OperationCategory::Damage,
424                LedgerDisposition::GenericGap,
425                "authored swing damage payload",
426            )
427            .owner(attack.spell_id),
428        );
429
430        if attack.is_pet
431            || attack.npc_id.is_some()
432            || attack.uses_spell_power
433            || attack.pet_power_from_owner.is_some()
434            || attack.pet_weapon_speed_ms.is_some()
435            || attack.pet_weapon_multiplier.is_some()
436        {
437            rows.push(
438                base_row(
439                    slug,
440                    path,
441                    format!("{root}.actor"),
442                    OperationCategory::ActorOrPet,
443                    LedgerDisposition::GenericGap,
444                    "authored auto-attack actor/weapon ownership",
445                )
446                .owner(attack.spell_id),
447            );
448        }
449
450        if attack.on_crit_reduce_charge.is_some() {
451            rows.push(
452                base_row(
453                    slug,
454                    path,
455                    format!("{root}.on_crit_reduce_charge"),
456                    OperationCategory::Cooldown,
457                    LedgerDisposition::ContentOrdering,
458                    "critical-swing cooldown mutation",
459                )
460                .owner(attack.spell_id),
461            );
462        }
463
464        if attack.hook.is_some() {
465            hook_placeholder(
466                rows,
467                slug,
468                path,
469                &format!("{root}.hook"),
470                attack.spell_id,
471                OperationCategory::OrderedContent,
472            );
473        }
474    }
475}