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() && effect_redirects_to_trigger_spell(effect) {
94 if let Some(child) =
95 resolve_trigger_chain_payload(context.resolver, SpellId::new(effect.trigger_spell))
96 .await?
97 {
98 payload = child;
99 }
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 if child != 0 && !context.resolved.contains(&child) {
161 context.pending.push_back(child);
162 }
163 }
164
165 context
166 .builder
167 .insert_ap_coef(spell_idx, effect_index, payload.ap_coef);
168 context
169 .builder
170 .insert_sp_coef(spell_idx, effect_index, payload.sp_coef);
171 context.builder.insert_power_coefficient_mastery_add(
172 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
173 payload.power_coefficient_mastery_add,
174 );
175 context
176 .builder
177 .insert_base_points(spell_idx, effect_index, payload.base_points);
178 context
179 .builder
180 .insert_amplitude(spell_idx, effect_index, payload.amplitude);
181 context.builder.insert_period(
182 spell_idx,
183 effect_index,
184 f64::from(wowlab_engine_domain::dbc::passive_period_ms(
185 passive_query,
186 effect.period,
187 )),
188 );
189 context
190 .builder
191 .insert_coefficient(spell_idx, effect_index, payload.coefficient);
192 context.builder.insert_scaling_class(
193 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
194 effect.scaling_class,
195 );
196 context.builder.insert_chain_targets(
197 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
198 wowlab_engine_domain::dbc::passive_chain_targets(passive_query, effect.chain_targets),
199 );
200 context.builder.insert_chain_multiplier(
201 spell_idx,
202 effect_index,
203 wowlab_engine_domain::dbc::passive_chain_multiplier(passive_query, effect.chain_multiplier),
204 );
205 context.builder.insert_effect_radius(
206 spell_idx,
207 effect_index,
208 wowlab_engine_domain::dbc::passive_radius(
209 passive_query,
210 f64::from(effect.radius_max.max(effect.radius_min)),
211 ),
212 );
213 context.builder.insert_effect_misc_value_0(
214 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
215 effect.misc_value_0,
216 );
217 context.builder.insert_effect_misc_value_1(
218 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
219 effect.misc_value_1,
220 );
221 context.builder.insert_shapeshift_form_flags(
222 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
223 effect.shapeshift_form_flags,
224 );
225 context.builder.insert_shapeshift_combat_round_time_ms(
226 spell_idx,
227 effect_index,
228 effect.shapeshift_combat_round_time_ms,
229 );
230 context.builder.insert_effect_type(
231 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
232 effect.effect,
233 );
234 context.builder.insert_effect_mechanic(
235 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
236 effect.mechanic,
237 );
238 context.builder.insert_effect_attributes(
239 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
240 effect.effect_attributes,
241 );
242 context.builder.insert_implicit_targets(
243 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
244 effect.implicit_target_a,
245 effect.implicit_target_b,
246 );
247 context.builder.insert_effect_aura(
248 wowlab_types::sim::EffectRef::new(spell_idx, effect_index),
249 effect.aura,
250 );
251 context.builder.insert_effect_class_mask(
252 spell_idx,
253 effect_index,
254 [
255 effect.effect_class_mask_1,
256 effect.effect_class_mask_2,
257 effect.effect_class_mask_3,
258 effect.effect_class_mask_4,
259 ],
260 );
261 context.builder.insert_points_per_resource(
262 spell_idx,
263 effect_index,
264 f64::from(effect.points_per_resource),
265 );
266}