Skip to main content

wowlab_engine_combat/targeting/
program.rs

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