Skip to main content

wowlab_engine_combat/targeting/
program.rs

1//! DBC target-program lowering into runtime target shapes.
2
3use wowlab_engine_domain::{
4    dbc::{EffectLookup, ModifierPropertyKind, SpellAttributeKind, spell_attribute_is},
5    rotation::DenseBuffer,
6    targeting::{
7        CandidateFilter, ChainTargetOrigin, ExecutableCandidateDomain, ExecutableLocation,
8        ExecutableLocationAlgorithm, ExecutableSelection, ExecutableTargetAlgorithm,
9        ExecutableTargetDirection, ExecutableTargetFallback, ExecutableTargetOperation,
10        ExecutableTargetReference, ResolvedTargetShape, SingleTargetSelector, TargetPlanInput,
11        TargetShapeError, compile_target_plan, lower_target_plan,
12        registered_target_geometry_overlay,
13    },
14};
15use wowlab_engine_gamedata::ResolvedGameData;
16use wowlab_types::sim::{ActorId, EffectRef, EnemyIdx, Position2, SimTime, SpatialTransform};
17
18use super::{
19    ConeGeometry, DIRECTIONAL_SUMMON_DEFAULT_DISTANCE, EffectGeometry, EffectGeometryInput,
20    LineGeometry, LineOfSightPolicy, RuntimeTargetAnchor, SpellTargetRequest,
21    SpellTargetShapeInput, TargetProgramPositions, TargetProgramState, direction_angle,
22    resolve_targets,
23};
24use crate::state::CombatState;
25
26/// Resolve a spell effect's hostile targets from its compiled DBC target program.
27///
28/// An unauthored cast destination defaults to the explicit target's position.
29pub(crate) fn resolve_spell_targets(
30    state: &CombatState,
31    buf: &DenseBuffer,
32    request: SpellTargetRequest,
33    now: SimTime,
34    rng: Option<&mut dyn FnMut() -> f64>,
35) -> Result<Vec<EnemyIdx>, TargetShapeError> {
36    let data_radius = state
37        .config
38        .game_data
39        .effect_radius(request.geometry_spell, request.geometry_effect)
40        .or_else(|| state.config.game_data.radius(request.geometry_spell))
41        .unwrap_or(0.0);
42    let radius = crate::systems::active_targeting_value(
43        crate::context::ActorView::new(state, buf, request.source),
44        request.geometry_spell,
45        ModifierPropertyKind::Radius,
46        data_radius,
47    );
48    let range = crate::systems::active_targeting_value(
49        crate::context::ActorView::new(state, buf, request.source),
50        request.geometry_spell,
51        ModifierPropertyKind::Range,
52        state
53            .config
54            .game_data
55            .hostile_max_range(request.geometry_spell)
56            .unwrap_or(data_radius),
57    );
58    let explicit_target = state.actor_transform(ActorId::Enemy(request.anchor));
59    let shapes = spell_target_shapes_with_resolved_geometry(
60        EffectLookup::new(
61            &state.config.game_data,
62            EffectRef::new(request.geometry_spell, request.geometry_effect),
63        ),
64        SpellTargetShapeInput {
65            anchor: request.anchor,
66            destination: request.destination.or(explicit_target),
67            positions: TargetProgramPositions {
68                source: request
69                    .source_position
70                    .or_else(|| state.actor_transform(request.source)),
71                explicit_target,
72            },
73            geometry: EffectGeometryInput { radius, range },
74        },
75        rng,
76    )?;
77    let line_of_sight = spell_line_of_sight_policy(&state.config.game_data, request.impact_spell);
78    let mut targets = Vec::new();
79
80    for shape in shapes {
81        for target in resolve_targets(
82            state,
83            request.source,
84            &shape,
85            &CandidateFilter::default(),
86            line_of_sight,
87            now,
88        )? {
89            if !targets.contains(&target) {
90                targets.push(target);
91            }
92        }
93    }
94
95    let data_cap = crate::systems::active_targeting_value(
96        crate::context::ActorView::new(state, buf, request.source),
97        request.geometry_spell,
98        ModifierPropertyKind::MaxTargets,
99        f64::from(
100            state
101                .config
102                .game_data
103                .max_affected_targets(request.geometry_spell)
104                .unwrap_or(0),
105        ),
106    )
107    .round()
108    .clamp(0.0, f64::from(u8::MAX));
109    let data_cap = wowlab_types::numeric::f64_to_u8_saturating_trunc(data_cap);
110    let cap = match (request.max_targets.filter(|cap| *cap > 0), data_cap) {
111        (Some(requested), 0) => Some(requested),
112        (Some(requested), data) => Some(requested.min(data)),
113        (None, 0) => None,
114        (None, data) => Some(data),
115    };
116
117    if let Some(cap) = cap {
118        targets.truncate(usize::from(cap));
119    }
120
121    Ok(targets)
122}
123
124pub(crate) fn spell_line_of_sight_policy(
125    game_data: &ResolvedGameData,
126    spell: wowlab_types::sim::SpellIdx,
127) -> LineOfSightPolicy {
128    LineOfSightPolicy::from_ignores_line_of_sight(
129        game_data.ignores_line_of_sight(spell).unwrap_or(false),
130    )
131}
132
133pub(crate) fn spell_chain_target_origin(
134    game_data: &ResolvedGameData,
135    spell: wowlab_types::sim::SpellIdx,
136) -> ChainTargetOrigin {
137    if game_data.spell_attributes(spell).is_some_and(|attributes| {
138        spell_attribute_is(attributes, SpellAttributeKind::ChainFromCaster)
139    }) {
140        ChainTargetOrigin::Source
141    } else {
142        ChainTargetOrigin::PreviousTarget
143    }
144}
145
146#[cfg(test)]
147pub(crate) fn spell_target_shapes(
148    lookup: EffectLookup<'_>,
149    anchor: EnemyIdx,
150) -> Result<Vec<ResolvedTargetShape>, TargetShapeError> {
151    spell_target_shapes_with_destination(lookup, anchor, None)
152}
153
154#[cfg(test)]
155pub(crate) fn spell_target_shapes_with_destination(
156    lookup: EffectLookup<'_>,
157    anchor: EnemyIdx,
158    destination: Option<SpatialTransform>,
159) -> Result<Vec<ResolvedTargetShape>, TargetShapeError> {
160    let game_data = lookup.data;
161    let spell = lookup.effect.spell;
162    let data_radius = game_data
163        .effect_radius(spell, lookup.effect.effect_index)
164        .or_else(|| game_data.radius(spell))
165        .unwrap_or(0.0);
166
167    spell_target_shapes_with_resolved_geometry(
168        lookup,
169        SpellTargetShapeInput {
170            anchor,
171            destination,
172            positions: TargetProgramPositions::default(),
173            geometry: EffectGeometryInput {
174                radius: data_radius,
175                range: game_data.hostile_max_range(spell).unwrap_or(data_radius),
176            },
177        },
178        None,
179    )
180}
181
182pub(super) fn spell_target_shapes_with_resolved_geometry(
183    lookup: EffectLookup<'_>,
184    input: SpellTargetShapeInput,
185    mut rng: Option<&mut dyn FnMut() -> f64>,
186) -> Result<Vec<ResolvedTargetShape>, TargetShapeError> {
187    let game_data = lookup.data;
188    let spell = lookup.effect.spell;
189    let effect_index = lookup.effect.effect_index;
190    let plan = compile_target_plan(TargetPlanInput {
191        spell_id: spell.as_u32(),
192        effect_index,
193        effect_type: game_data.effect_type(spell, effect_index),
194        target_a: game_data.implicit_target_a(spell, effect_index),
195        target_b: game_data.implicit_target_b(spell, effect_index),
196    })?;
197    let program = lower_target_plan(&plan)?;
198    let overlay = registered_target_geometry_overlay(spell.as_u32(), effect_index)?;
199    let geometry = EffectGeometry {
200        radius: overlay
201            .and_then(|value| value.radius)
202            .unwrap_or(input.geometry.radius),
203        range: overlay
204            .and_then(|value| value.range)
205            .unwrap_or(input.geometry.range),
206        cone_half_angle: overlay
207            .and_then(|value| value.cone_half_angle)
208            .unwrap_or_else(|| game_data.cone_half_angle(spell).unwrap_or(0.0)),
209        face_explicit_target: overlay.is_some_and(|value| value.face_explicit_target)
210            || game_data.spell_attributes(spell).is_some_and(|attributes| {
211                spell_attribute_is(
212                    attributes,
213                    SpellAttributeKind::EnforceFacingPrimaryTargetOnly,
214                )
215            }),
216    };
217
218    tracing::trace!(
219        spell_id = spell.as_u32(),
220        effect_index,
221        target_a = game_data.implicit_target_a(spell, effect_index),
222        target_b = game_data.implicit_target_b(spell, effect_index),
223        radius = geometry.radius,
224        range = geometry.range,
225        cone_half_angle = geometry.cone_half_angle,
226        face_explicit_target = geometry.face_explicit_target,
227        overlaid = overlay.is_some(),
228        "spell target geometry resolved"
229    );
230    let mut state = TargetProgramState::with_destination(input.destination);
231    let mut shapes = Vec::new();
232
233    for operation in program.operations.into_array().into_iter().flatten() {
234        match operation {
235            ExecutableTargetOperation::AssignSource(location) => {
236                state.source = resolve_location(
237                    location,
238                    state,
239                    input.positions,
240                    geometry.radius,
241                    reborrow_rng(&mut rng),
242                )?;
243            }
244            ExecutableTargetOperation::AssignDestination(location) => {
245                state.destination = Some(resolve_location(
246                    location,
247                    state,
248                    input.positions,
249                    geometry.radius,
250                    reborrow_rng(&mut rng),
251                )?);
252            }
253            ExecutableTargetOperation::Select(selection) => shapes.push(resolve_selection(
254                selection,
255                state,
256                input.anchor,
257                geometry,
258                reborrow_rng(&mut rng),
259            )?),
260            ExecutableTargetOperation::SelectAndAssignDestination(selection) => {
261                shapes.push(resolve_selection(
262                    selection,
263                    state,
264                    input.anchor,
265                    geometry,
266                    reborrow_rng(&mut rng),
267                )?);
268                state.destination = Some(resolve_reference(selection.reference, state)?);
269            }
270        }
271    }
272
273    if shapes.is_empty() {
274        resolve_effect_fallback(program.fallback, input.anchor, &mut shapes)?;
275    }
276
277    Ok(shapes)
278}
279
280fn reborrow_rng<'short, 'long: 'short>(
281    rng: &'short mut Option<&'long mut (dyn FnMut() -> f64 + 'long)>,
282) -> Option<&'short mut (dyn FnMut() -> f64 + 'short)> {
283    match rng {
284        Some(rng) => Some(&mut **rng),
285        None => None,
286    }
287}
288
289fn resolve_location(
290    location: ExecutableLocation,
291    state: TargetProgramState,
292    positions: TargetProgramPositions,
293    distance: f64,
294    rng: Option<&mut dyn FnMut() -> f64>,
295) -> Result<RuntimeTargetAnchor, TargetShapeError> {
296    let reference = match location.algorithm {
297        ExecutableLocationAlgorithm::Reference => resolve_reference(location.reference, state),
298        ExecutableLocationAlgorithm::ChannelTarget => Ok(RuntimeTargetAnchor::ExplicitTarget),
299    }?;
300
301    resolve_directional_location(
302        reference,
303        location.reference,
304        location.direction,
305        positions,
306        distance,
307        rng,
308    )
309}
310
311fn resolve_directional_location(
312    reference: RuntimeTargetAnchor,
313    reference_kind: ExecutableTargetReference,
314    direction: ExecutableTargetDirection,
315    positions: TargetProgramPositions,
316    distance: f64,
317    rng: Option<&mut dyn FnMut() -> f64>,
318) -> Result<RuntimeTargetAnchor, TargetShapeError> {
319    if direction == ExecutableTargetDirection::None {
320        return Ok(reference);
321    }
322
323    let origin = match reference {
324        RuntimeTargetAnchor::Source => positions.source,
325        RuntimeTargetAnchor::ExplicitTarget => positions.explicit_target,
326        RuntimeTargetAnchor::Location(location) => Some(location),
327    }
328    .ok_or_else(|| TargetShapeError::missing_reference_position(reference_kind))?;
329    let distance = if distance.abs() <= f64::EPSILON
330        && matches!(
331            direction,
332            ExecutableTargetDirection::FrontRight
333                | ExecutableTargetDirection::BackRight
334                | ExecutableTargetDirection::BackLeft
335                | ExecutableTargetDirection::FrontLeft
336        ) {
337        DIRECTIONAL_SUMMON_DEFAULT_DISTANCE
338    } else {
339        distance
340    };
341    let angle = origin.heading + direction_angle(direction, rng)?;
342
343    Ok(RuntimeTargetAnchor::Location(SpatialTransform {
344        position: Position2 {
345            x: origin.position.x + distance * angle.cos(),
346            y: origin.position.y + distance * angle.sin(),
347        },
348        ..origin
349    }))
350}
351
352fn resolve_reference(
353    reference: ExecutableTargetReference,
354    state: TargetProgramState,
355) -> Result<RuntimeTargetAnchor, TargetShapeError> {
356    match reference {
357        ExecutableTargetReference::Caster => Ok(RuntimeTargetAnchor::Source),
358        ExecutableTargetReference::ExplicitTarget => Ok(RuntimeTargetAnchor::ExplicitTarget),
359        ExecutableTargetReference::Source => Ok(state.source),
360        ExecutableTargetReference::Destination => state
361            .destination
362            .ok_or_else(TargetShapeError::missing_cast_destination),
363    }
364}
365
366fn resolve_selection(
367    selection: ExecutableSelection,
368    state: TargetProgramState,
369    explicit_target: EnemyIdx,
370    geometry: EffectGeometry,
371    rng: Option<&mut dyn FnMut() -> f64>,
372) -> Result<ResolvedTargetShape, TargetShapeError> {
373    match selection.candidates {
374        ExecutableCandidateDomain::EnemyUnits => {
375            resolve_enemy_selection(selection, state, explicit_target, geometry, rng)
376        }
377    }
378}
379
380fn resolve_enemy_selection(
381    selection: ExecutableSelection,
382    state: TargetProgramState,
383    explicit_target: EnemyIdx,
384    geometry: EffectGeometry,
385    rng: Option<&mut dyn FnMut() -> f64>,
386) -> Result<ResolvedTargetShape, TargetShapeError> {
387    let origin = resolve_reference(selection.reference, state)?;
388
389    Ok(match selection.algorithm {
390        ExecutableTargetAlgorithm::Single => resolve_single(selection, origin, explicit_target)?,
391        ExecutableTargetAlgorithm::ChannelTarget => {
392            ResolvedTargetShape::Single(SingleTargetSelector::Explicit(explicit_target))
393        }
394        ExecutableTargetAlgorithm::Radius => {
395            resolve_radius(origin, explicit_target, geometry.radius)
396        }
397        ExecutableTargetAlgorithm::Cone => resolve_cone(
398            origin,
399            explicit_target,
400            ConeGeometry {
401                range: geometry.range,
402                half_angle: geometry.cone_half_angle,
403                heading_offset: 0.0,
404            },
405            selection.direction,
406            geometry.face_explicit_target,
407            rng,
408        )?,
409        ExecutableTargetAlgorithm::Line => resolve_line(
410            origin,
411            explicit_target,
412            LineGeometry {
413                range: geometry.range,
414                half_width: geometry.radius,
415                heading_offset: 0.0,
416            },
417            selection.direction,
418            geometry.face_explicit_target,
419            rng,
420        )?,
421    })
422}
423
424fn resolve_single(
425    selection: ExecutableSelection,
426    origin: RuntimeTargetAnchor,
427    explicit_target: EnemyIdx,
428) -> Result<ResolvedTargetShape, TargetShapeError> {
429    match origin {
430        RuntimeTargetAnchor::ExplicitTarget => Ok(ResolvedTargetShape::Single(
431            SingleTargetSelector::Explicit(explicit_target),
432        )),
433        RuntimeTargetAnchor::Source | RuntimeTargetAnchor::Location(_) => {
434            Err(TargetShapeError::non_hostile_selection(selection.selector))
435        }
436    }
437}
438
439fn resolve_radius(
440    origin: RuntimeTargetAnchor,
441    explicit_target: EnemyIdx,
442    radius: f64,
443) -> ResolvedTargetShape {
444    match origin {
445        RuntimeTargetAnchor::Source => ResolvedTargetShape::RadiusFromSource { radius },
446        RuntimeTargetAnchor::ExplicitTarget => ResolvedTargetShape::RadiusFromTarget {
447            anchor: SingleTargetSelector::Explicit(explicit_target),
448            radius,
449        },
450        RuntimeTargetAnchor::Location(location) => {
451            ResolvedTargetShape::RadiusAtLocation { location, radius }
452        }
453    }
454}
455
456fn resolve_cone(
457    origin: RuntimeTargetAnchor,
458    explicit_target: EnemyIdx,
459    geometry: ConeGeometry,
460    direction: ExecutableTargetDirection,
461    face_explicit_target: bool,
462    rng: Option<&mut dyn FnMut() -> f64>,
463) -> Result<ResolvedTargetShape, TargetShapeError> {
464    let heading_offset = geometry.heading_offset + direction_angle(direction, rng)?;
465
466    Ok(match origin {
467        RuntimeTargetAnchor::Source if face_explicit_target => {
468            ResolvedTargetShape::ConeTowardTarget {
469                anchor: SingleTargetSelector::Explicit(explicit_target),
470                range: geometry.range,
471                half_angle: geometry.half_angle,
472                heading_offset,
473            }
474        }
475        RuntimeTargetAnchor::Source => ResolvedTargetShape::Cone {
476            range: geometry.range,
477            half_angle: geometry.half_angle,
478            heading_offset,
479        },
480        RuntimeTargetAnchor::ExplicitTarget => ResolvedTargetShape::ConeFromTarget {
481            anchor: SingleTargetSelector::Explicit(explicit_target),
482            range: geometry.range,
483            half_angle: geometry.half_angle,
484            heading_offset,
485        },
486        RuntimeTargetAnchor::Location(location) => ResolvedTargetShape::ConeAtLocation {
487            location,
488            range: geometry.range,
489            half_angle: geometry.half_angle,
490            heading_offset,
491        },
492    })
493}
494
495fn resolve_line(
496    origin: RuntimeTargetAnchor,
497    explicit_target: EnemyIdx,
498    geometry: LineGeometry,
499    direction: ExecutableTargetDirection,
500    face_explicit_target: bool,
501    rng: Option<&mut dyn FnMut() -> f64>,
502) -> Result<ResolvedTargetShape, TargetShapeError> {
503    let heading_offset = geometry.heading_offset + direction_angle(direction, rng)?;
504
505    Ok(match origin {
506        RuntimeTargetAnchor::Source if face_explicit_target => {
507            ResolvedTargetShape::LineTowardTarget {
508                anchor: SingleTargetSelector::Explicit(explicit_target),
509                range: geometry.range,
510                half_width: geometry.half_width,
511                heading_offset,
512            }
513        }
514        RuntimeTargetAnchor::Source => ResolvedTargetShape::Line {
515            range: geometry.range,
516            half_width: geometry.half_width,
517            heading_offset,
518        },
519        RuntimeTargetAnchor::ExplicitTarget => ResolvedTargetShape::LineTowardTarget {
520            anchor: SingleTargetSelector::Explicit(explicit_target),
521            range: geometry.range,
522            half_width: geometry.half_width,
523            heading_offset,
524        },
525        RuntimeTargetAnchor::Location(location) => ResolvedTargetShape::LineAtLocation {
526            location,
527            range: geometry.range,
528            half_width: geometry.half_width,
529            heading_offset,
530        },
531    })
532}
533
534fn resolve_effect_fallback(
535    fallback: ExecutableTargetFallback,
536    explicit_target: EnemyIdx,
537    shapes: &mut Vec<ResolvedTargetShape>,
538) -> Result<(), TargetShapeError> {
539    match fallback {
540        ExecutableTargetFallback::ExplicitUnit => {
541            shapes.push(ResolvedTargetShape::Single(SingleTargetSelector::Explicit(
542                explicit_target,
543            )));
544
545            Ok(())
546        }
547        ExecutableTargetFallback::NoSelection => Ok(()),
548        ExecutableTargetFallback::ExplicitDestination => {
549            Err(TargetShapeError::missing_cast_destination())
550        }
551    }
552}