wowlab_engine_application/game_data/
spells.rs1mod 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}