wowlab_engine_combat/pipeline/
impact.rs1use super::{
2 ActorId, CombatCtx, DamageFlags, EffectExecution, Event, PendingSpellImpact, ProfiledDamageDef,
3 RuntimeDamageDef, SimTime, SpellCastPayload, SpellCastTargetFlags, SpellData, SpellIdx,
4 apply_cast_aura, deal_profiled_damage_def_at, execute_effect_range, fire_expire_hooks,
5 resolve_payload_flags,
6};
7
8pub(super) fn schedule_spell_impact(
9 ctx: &mut CombatCtx<'_>,
10 spell: &SpellData,
11 payload: SpellCastPayload,
12 now: SimTime,
13) {
14 let impact_id = u32::try_from(ctx.state.runtime.pending.pending_spell_impacts.len())
15 .expect("pending impact arena length fits in u32");
16
17 ctx.state
18 .runtime
19 .pending
20 .pending_spell_impacts
21 .push(Some(PendingSpellImpact {
22 spell_id: spell.spell_id,
23 damage_effect: spell.damage_effect,
24 base_points: payload.base_points,
25 damage: payload.damage,
26 is_pet: spell.behavior.is_pet,
27 may_crit: spell.damage_policy.may_crit,
28 attribute_flags: spell.damage_attribute_flags,
29 applies_aura: spell.applies_aura,
30 applies_aura_duration_ms: payload.aura_duration_ms,
31 followup_effects: spell.followup_effects,
32 source: ctx.source,
33 target: ctx.target,
34 cast_complete_at: now,
35 travel: spell.travel,
36 locked_source_transform: None,
37 locked_target_transform: None,
38 cast_destination: None,
39 }));
40
41 if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(spell.spell_id)) {
42 slot.in_flight_count += 1;
43 slot.is_in_flight = 1;
44 }
45
46 if spell.travel.is_fixed() {
47 process_spell_launch(ctx, impact_id);
48 } else {
49 let launch_at = now.saturating_add(SimTime::from_millis(crate::travel::launch_offset_ms(
50 spell.travel,
51 )));
52
53 ctx.state.schedule(Event::SpellLaunch {
54 t: launch_at,
55 impact_id,
56 source: ctx.source,
57 target: ctx.target,
58 });
59 }
60}
61
62pub(crate) fn process_spell_launch(ctx: &mut CombatCtx<'_>, impact_id: u32) {
63 let Some((spell_id, source, target, cast_complete_at, travel)) = ctx
64 .state
65 .runtime
66 .pending
67 .pending_spell_impacts
68 .get(impact_id as usize)
69 .and_then(Option::as_ref)
70 .map(|pending| {
71 (
72 pending.spell_id,
73 pending.source,
74 pending.target,
75 pending.cast_complete_at,
76 pending.travel,
77 )
78 })
79 else {
80 return;
81 };
82
83 if !ctx.state.is_valid_target(target) {
84 cancel_pending_spell(ctx, impact_id);
85
86 return;
87 }
88
89 let Some(source_transform) = ctx.state.actor_transform(source) else {
90 cancel_pending_spell(ctx, impact_id);
91
92 return;
93 };
94 let Some(target_transform) = ctx.state.actor_transform(ActorId::Enemy(target)) else {
95 cancel_pending_spell(ctx, impact_id);
96
97 return;
98 };
99 let distance = source_transform
100 .position
101 .distance(target_transform.position);
102 let impact_at = cast_complete_at.saturating_add(SimTime::from_millis(
103 crate::travel::impact_offset_ms(travel, distance),
104 ));
105 let required_targets = SpellCastTargetFlags::from_bits_retain(
106 ctx.state
107 .config
108 .game_data
109 .required_explicit_target_mask(SpellIdx::from_raw(spell_id))
110 .unwrap_or_default(),
111 );
112 let cast_destination = required_targets
113 .contains(SpellCastTargetFlags::DEST_LOCATION)
114 .then_some(target_transform);
115
116 let Some(pending) = ctx
117 .state
118 .runtime
119 .pending
120 .pending_spell_impacts
121 .get_mut(impact_id as usize)
122 .and_then(Option::as_mut)
123 else {
124 return;
125 };
126
127 pending.locked_source_transform = Some(source_transform);
128 pending.locked_target_transform = Some(target_transform);
129 pending.cast_destination = cast_destination;
130 ctx.state.schedule(Event::SpellImpact {
131 t: impact_at,
132 impact_id,
133 source,
134 target,
135 });
136
137 if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(spell_id)) {
138 slot.in_flight_end = slot.in_flight_end.max(impact_at.as_secs_f64());
139 }
140}
141
142pub(crate) fn process_spell_impact(ctx: &mut CombatCtx<'_>, impact_id: u32) {
143 let pending = ctx
144 .state
145 .runtime
146 .pending
147 .pending_spell_impacts
148 .get_mut(impact_id as usize)
149 .and_then(Option::take);
150 let Some(pending) = pending else {
151 return;
152 };
153
154 if !ctx.state.is_valid_target(pending.target)
155 || pending.locked_source_transform.is_none()
156 || pending.locked_target_transform.is_none()
157 {
158 finish_pending_spell_slot(ctx, &pending);
159
160 return;
161 }
162
163 ctx.source = pending.source;
164 ctx.target = pending.target;
165
166 let base_impact_flags = pending.attribute_flags
167 | (if pending.is_pet {
168 DamageFlags::PET
169 } else {
170 DamageFlags::empty()
171 })
172 | if pending.may_crit {
173 DamageFlags::empty()
174 } else {
175 DamageFlags::NO_CRIT
176 };
177 let Some(impact_flags) = resolve_payload_flags(
178 ctx,
179 pending.spell_id,
180 pending.damage,
181 pending.applies_aura,
182 pending.followup_effects,
183 base_impact_flags,
184 ) else {
185 finish_pending_spell_slot(ctx, &pending);
186
187 return;
188 };
189
190 let aura_applied_before_payload = pending.applies_aura.filter(|aura_local| {
191 pending.followup_effects.len > 0
192 && ctx
193 .state
194 .defs
195 .auras
196 .get(aura_local.as_usize())
197 .is_some_and(|aura| aura.on == wowlab_types::sim::AuraOn::Player)
198 });
199
200 if let Some(aura_local) = aura_applied_before_payload {
201 apply_cast_aura(
202 ctx,
203 aura_local,
204 pending.applies_aura_duration_ms,
205 impact_flags,
206 );
207 }
208
209 if !matches!(pending.damage, RuntimeDamageDef::None) {
210 deal_profiled_damage_def_at(
211 ctx,
212 &ProfiledDamageDef {
213 effect: pending.damage_effect,
214 geometry_effect: pending.damage_effect,
215 profile_spell_id: pending.spell_id,
216 base_points: Some(pending.base_points),
217 def: pending.damage,
218 flags: impact_flags,
219 source_position: pending.locked_source_transform,
220 destination: pending.cast_destination,
221 },
222 );
223 }
224
225 if let Some(aura_local) = pending
226 .applies_aura
227 .filter(|aura_local| Some(*aura_local) != aura_applied_before_payload)
228 {
229 apply_cast_aura(
230 ctx,
231 aura_local,
232 pending.applies_aura_duration_ms,
233 impact_flags,
234 );
235 }
236
237 execute_effect_range(
238 ctx,
239 EffectExecution {
240 range: pending.followup_effects,
241 profile_spell_id: pending.spell_id,
242 flags: impact_flags,
243 },
244 );
245
246 finish_pending_spell_slot(ctx, &pending);
247 ctx.state.schedule(Event::OffGcdReady { t: ctx.now });
249 fire_expire_hooks(ctx);
250}
251
252pub(super) fn cancel_pending_spell(ctx: &mut CombatCtx<'_>, impact_id: u32) {
253 let pending = ctx
254 .state
255 .runtime
256 .pending
257 .pending_spell_impacts
258 .get_mut(impact_id as usize)
259 .and_then(Option::take);
260
261 if let Some(pending) = pending {
262 finish_pending_spell_slot(ctx, &pending);
263 }
264}
265
266pub(super) fn finish_pending_spell_slot(ctx: &mut CombatCtx<'_>, pending: &PendingSpellImpact) {
267 if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(pending.spell_id)) {
268 slot.in_flight_count = (slot.in_flight_count - 1).max(0);
269
270 if slot.in_flight_count == 0 {
271 slot.is_in_flight = 0;
272 slot.in_flight_end = 0.0;
273 }
274 }
275}