Skip to main content

wowlab_engine_application/game_data/
spells.rs

1//! Recursive spell, learned-spell, trigger, resource, and effect resolution.
2
3mod context;
4mod descriptions;
5mod effects;
6mod properties;
7mod resources;
8
9use wowlab_engine_ports::{DataResolver, EngineError, SpellId};
10use wowlab_types::{
11    data::SpellDataFlat,
12    sim::{FastSet, SpellIdx},
13};
14
15use self::effects::resolve_spell_effects;
16use super::passives::apply_talent_effect_overrides;
17
18#[rustfmt::skip]
19pub(super) use context::SpellResolutionContext;
20#[rustfmt::skip]
21#[cfg(test)]
22pub(super) use descriptions::resolve_trigger_chain_payload;
23
24#[rustfmt::skip]
25#[cfg(test)]
26pub(super) use effects::{apply_runtime_talent_effect, insert_effect, resolve_effect_payload};
27#[rustfmt::skip]
28pub(super) use properties::insert_spell_properties;
29
30pub(super) async fn resolve_spells(
31    context: &mut SpellResolutionContext<'_, '_>,
32) -> Result<(), EngineError> {
33    while let Some(spell_id_raw) = context.pending.pop_front() {
34        if !context.resolved.insert(spell_id_raw) {
35            continue;
36        }
37
38        let spell_idx = SpellIdx::from_raw(spell_id_raw);
39        let spell_id = SpellId::try_from(spell_idx).map_err(|source| {
40            EngineError::spell_id_conversion("failed to convert resolved spell id", source)
41        })?;
42        let Some(mut spell) = load_spell(
43            context.resolver,
44            spell_id,
45            spell_id_raw,
46            context.required_spell_ids,
47        )
48        .await?
49        else {
50            continue;
51        };
52
53        if let Some(talent) = context
54            .talents
55            .iter()
56            .find(|talent| talent.spell_id == spell_id_raw)
57        {
58            apply_talent_effect_overrides(&mut spell, talent, false);
59        }
60
61        insert_learned_spells(context, &spell, spell_idx, spell_id)?;
62        insert_spell_properties(context, &spell, spell_idx);
63        resolve_spell_effects(context, &spell, spell_idx, spell_id, spell_id_raw).await?;
64    }
65
66    Ok(())
67}
68
69async fn load_spell(
70    resolver: &wowlab_engine_ports::DynDataResolver<'_>,
71    spell_id: SpellId,
72    spell_id_raw: u32,
73    required_spell_ids: &FastSet<u32>,
74) -> Result<Option<SpellDataFlat>, EngineError> {
75    match resolver.get_spell(spell_id).await {
76        Ok(spell) => Ok(Some(spell)),
77        Err(error) if !required_spell_ids.contains(&spell_id_raw) => {
78            tracing::warn!(
79                spell_id = spell_id_raw,
80                %error,
81                "trigger child spell not found; skipping unresolved edge target"
82            );
83
84            Ok(None)
85        }
86        Err(error) => Err(EngineError::spec_construction(format!(
87            "failed to resolve spell {spell_id}: {error}"
88        ))),
89    }
90}
91
92fn insert_learned_spells(
93    context: &mut SpellResolutionContext<'_, '_>,
94    spell: &SpellDataFlat,
95    spell_idx: SpellIdx,
96    spell_id: SpellId,
97) -> Result<(), EngineError> {
98    for learned in &spell.learn_spells {
99        if learned.learn_spell_id <= 0 {
100            continue;
101        }
102
103        let learned_id = learned_spell_id(learned.learn_spell_id, spell_id)?;
104        let learned_idx = SpellIdx::from_raw(learned_id);
105
106        context.builder.insert_spell_learn(spell_idx, learned_idx);
107
108        if !context.resolved.contains(&learned_id) {
109            context.pending.push_back(learned_id);
110        }
111    }
112
113    Ok(())
114}
115
116fn learned_spell_id(raw_spell_id: i32, source_spell_id: SpellId) -> Result<u32, EngineError> {
117    u32::try_from(raw_spell_id).map_err(|error| {
118        EngineError::spec_construction(format!(
119            "invalid learned spell id {raw_spell_id} from {source_spell_id}: {error}"
120        ))
121    })
122}