wowlab_engine_application/game_data/spells/
effects.rs1use wowlab_engine_domain::dbc::{PassiveQuery, TriggerPayload, effect_redirects_to_trigger_spell};
2use wowlab_engine_ports::{DataResolver, EngineError, SpellId, TalentSelection};
3use wowlab_types::{
4 data::{SpellDataFlat, SpellEffect},
5 sim::SpellIdx,
6};
7
8use super::{
9 context::SpellResolutionContext,
10 descriptions::{described_effect_copy, resolve_trigger_chain_payload},
11};
12
13pub(super) async fn resolve_spell_effects(
14 context: &mut SpellResolutionContext<'_, '_>,
15 spell: &SpellDataFlat,
16 spell_idx: SpellIdx,
17 spell_id: SpellId,
18 spell_id_raw: u32,
19) -> Result<(), EngineError> {
20 let mut effects = context
21 .resolver
22 .get_spell_effects(spell_id)
23 .await
24 .map_err(|error| {
25 EngineError::spec_construction(format!(
26 "failed to resolve effects for spell {spell_id}: {error}"
27 ))
28 })?;
29
30 effects.sort_unstable_by_key(|effect| effect.index);
31
32 for mut effect in effects {
33 apply_runtime_talent_effect(context.talents, spell_id_raw, &mut effect);
34 let effect_index = runtime_effect_index(spell_id, &effect)?;
35 let payload = resolve_effect_payload(context, spell, &effect).await?;
36
37 insert_effect(context, spell, spell_idx, effect_index, &effect, &payload);
38 }
39
40 Ok(())
41}
42
43pub(crate) fn apply_runtime_talent_effect(
44 talents: &[TalentSelection],
45 spell_id_raw: u32,
46 effect: &mut SpellEffect,
47) {
48 let Some(talent) = talents
49 .iter()
50 .find(|talent| talent.spell_id == spell_id_raw)
51 else {
52 return;
53 };
54
55 if let Some(adjustment) = talent
56 .effect_overrides
57 .iter()
58 .find(|adjustment| adjustment.effect_index == effect.index)
59 {
60 effect.base_points = adjustment
61 .operation
62 .apply(effect.base_points, adjustment.value);
63 }
64}
65
66fn runtime_effect_index(spell_id: SpellId, effect: &SpellEffect) -> Result<u8, EngineError> {
67 let zero_based = u8::try_from(effect.index).map_err(|error| {
68 EngineError::spec_construction(format!(
69 "spell {spell_id} has invalid effect index {}: {error}",
70 effect.index
71 ))
72 })?;
73
74 zero_based.checked_add(1).ok_or_else(|| {
75 EngineError::spec_construction(format!(
76 "spell {spell_id} effect index {} exceeds the runtime index range",
77 effect.index
78 ))
79 })
80}
81
82pub(crate) async fn resolve_effect_payload(
83 context: &mut SpellResolutionContext<'_, '_>,
84 spell: &SpellDataFlat,
85 effect: &SpellEffect,
86) -> Result<TriggerPayload, EngineError> {
87 let mut payload = TriggerPayload::from_effect(effect);
88 let passive_query = PassiveQuery {
89 passives: context.passives,
90 spell,
91 };
92
93 if payload.has_no_coef()
94 && effect_redirects_to_trigger_spell(effect)
95 && let Some(child) =
96 resolve_trigger_chain_payload(context.resolver, SpellId::new(effect.trigger_spell))
97 .await?
98 {
99 payload = child;
100 }
101
102 payload.base_points = wowlab_engine_domain::dbc::modified_effect_base_points(
103 passive_query,
104 effect.index,
105 payload.base_points,
106 );
107 let declared = context
108 .effect_overrides
109 .iter()
110 .find(|((spell_id, index), _)| {
111 i64::from(*spell_id) == i64::from(spell.id) && i32::from(*index) - 1 == effect.index
112 });
113
114 if let Some((_, replacement)) = declared {
115 payload.base_points = *replacement;
116 } else if let Some(copied) = described_effect_copy(context, spell, effect).await? {
117 payload.base_points = copied;
118 }
119
120 payload.amplitude =
121 wowlab_engine_domain::dbc::modified_effect_amplitude(passive_query, payload.amplitude);
122 let power_coefficients = wowlab_engine_domain::dbc::modified_power_coefficients(
123 passive_query,
124 effect.index,
125 payload.sp_coef,
126 payload.ap_coef,
127 );
128
129 payload.sp_coef = power_coefficients.spell_power;
130 payload.ap_coef = power_coefficients.attack_power;
131 payload.power_coefficient_mastery_add = power_coefficients.mastery_add;
132 payload.coefficient = wowlab_engine_domain::dbc::modified_effect_coefficient(
133 passive_query,
134 effect.index,
135 payload.coefficient,
136 );
137
138 Ok(payload)
139}
140
141pub(crate) fn insert_effect(
142 context: &mut SpellResolutionContext<'_, '_>,
143 spell: &SpellDataFlat,
144 spell_idx: SpellIdx,
145 effect_index: u8,
146 effect: &SpellEffect,
147 payload: &TriggerPayload,
148) {
149 let passive_query = PassiveQuery {
150 passives: context.passives,
151 spell,
152 };
153
154 context.builder.insert_trigger_spell(
155 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
156 effect.trigger_spell,
157 );
158
159 if let Ok(child) = u32::try_from(effect.trigger_spell)
160 && child != 0
161 && !context.resolved.contains(&child)
162 {
163 context.pending.push_back(child);
164 }
165
166 context
167 .builder
168 .insert_ap_coef(spell_idx, effect_index, payload.ap_coef);
169 context
170 .builder
171 .insert_sp_coef(spell_idx, effect_index, payload.sp_coef);
172 context.builder.insert_power_coefficient_mastery_add(
173 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
174 payload.power_coefficient_mastery_add,
175 );
176 context
177 .builder
178 .insert_base_points(spell_idx, effect_index, payload.base_points);
179 context
180 .builder
181 .insert_amplitude(spell_idx, effect_index, payload.amplitude);
182 context.builder.insert_period(
183 spell_idx,
184 effect_index,
185 f64::from(wowlab_engine_domain::dbc::passive_period_ms(
186 passive_query,
187 effect.period,
188 )),
189 );
190 context
191 .builder
192 .insert_coefficient(spell_idx, effect_index, payload.coefficient);
193 context.builder.insert_scaling_class(
194 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
195 effect.scaling_class,
196 );
197 context.builder.insert_chain_targets(
198 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
199 wowlab_engine_domain::dbc::passive_chain_targets(passive_query, effect.chain_targets),
200 );
201 context.builder.insert_chain_multiplier(
202 spell_idx,
203 effect_index,
204 wowlab_engine_domain::dbc::passive_chain_multiplier(passive_query, effect.chain_multiplier),
205 );
206 context.builder.insert_effect_radius(
207 spell_idx,
208 effect_index,
209 wowlab_engine_domain::dbc::passive_radius(
210 passive_query,
211 f64::from(effect.radius_max.max(effect.radius_min)),
212 ),
213 );
214 context.builder.insert_effect_misc_value_0(
215 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
216 effect.misc_value_0,
217 );
218 context.builder.insert_effect_misc_value_1(
219 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
220 effect.misc_value_1,
221 );
222 context.builder.insert_shapeshift_form_flags(
223 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
224 effect.shapeshift_form_flags,
225 );
226 context.builder.insert_shapeshift_combat_round_time_ms(
227 spell_idx,
228 effect_index,
229 effect.shapeshift_combat_round_time_ms,
230 );
231 context.builder.insert_effect_type(
232 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
233 effect.effect,
234 );
235 context.builder.insert_effect_mechanic(
236 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
237 effect.mechanic,
238 );
239 context.builder.insert_effect_attributes(
240 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
241 effect.effect_attributes,
242 );
243 context.builder.insert_implicit_targets(
244 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
245 effect.implicit_target_a,
246 effect.implicit_target_b,
247 );
248 context.builder.insert_effect_aura(
249 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
250 effect.aura,
251 );
252 context.builder.insert_effect_class_mask(
253 spell_idx,
254 effect_index,
255 [
256 effect.effect_class_mask_1,
257 effect.effect_class_mask_2,
258 effect.effect_class_mask_3,
259 effect.effect_class_mask_4,
260 ],
261 );
262 context.builder.insert_points_per_resource(
263 spell_idx,
264 effect_index,
265 f64::from(effect.points_per_resource),
266 );
267}