Skip to main content

wowlab_engine_domain/targeting/
overlay.rs

1//! Link-time registration for content-authored target-program replacements.
2
3use super::{TargetPlanError, TargetPlanInput, TargetPlanOperations};
4
5/// Spell-effect coordinate shared by targeting overlays.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7// #t(rust_similar_structs) targeting overlay coordinates are type-separated from combat damage-effect references
8pub struct TargetEffectCoordinate {
9    pub spell_id: u32,
10    pub effect_index: u8,
11}
12
13/// Stable overlay identity and rationale.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct TargetOverlayMetadata {
16    pub name: &'static str,
17    pub reason: &'static str,
18}
19
20/// Declarative replacement for DBC targeting that cannot encode a spell's real behavior.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub struct TargetPlanOverlay {
23    pub spell_id: u32,
24    pub effect_index: u8,
25    pub name: &'static str,
26    pub reason: &'static str,
27    pub operations: TargetPlanOperations,
28}
29
30impl TargetPlanOverlay {
31    #[must_use]
32    pub const fn new(
33        coordinate: TargetEffectCoordinate,
34        metadata: TargetOverlayMetadata,
35        operations: TargetPlanOperations,
36    ) -> Self {
37        Self {
38            spell_id: coordinate.spell_id,
39            effect_index: coordinate.effect_index,
40            name: metadata.name,
41            reason: metadata.reason,
42            operations,
43        }
44    }
45
46    #[must_use]
47    pub const fn input(self, effect_type: i32, target_a: i32, target_b: i32) -> TargetPlanInput {
48        TargetPlanInput {
49            spell_id: self.spell_id,
50            effect_index: self.effect_index,
51            effect_type,
52            target_a,
53            target_b,
54        }
55    }
56}
57
58inventory::collect!(TargetPlanOverlay);
59
60/// Spell-specific metric data that is absent from spell-effect selectors.
61#[derive(Clone, Copy, Debug, Default, PartialEq)]
62pub struct TargetGeometryOverlay {
63    pub spell_id: u32,
64    pub effect_index: u8,
65    pub name: &'static str,
66    pub reason: &'static str,
67    pub range: Option<f64>,
68    pub radius: Option<f64>,
69    pub cone_half_angle: Option<f64>,
70    pub face_explicit_target: bool,
71}
72
73impl TargetGeometryOverlay {
74    #[must_use]
75    pub const fn new(coordinate: TargetEffectCoordinate, metadata: TargetOverlayMetadata) -> Self {
76        Self {
77            spell_id: coordinate.spell_id,
78            effect_index: coordinate.effect_index,
79            name: metadata.name,
80            reason: metadata.reason,
81            range: None,
82            radius: None,
83            cone_half_angle: None,
84            face_explicit_target: false,
85        }
86    }
87
88    #[must_use]
89    pub const fn dimensions(mut self, range: f64, radius: f64) -> Self {
90        self.range = Some(range);
91        self.radius = Some(radius);
92
93        self
94    }
95
96    #[must_use]
97    pub const fn cone(mut self, range: f64, half_angle: f64) -> Self {
98        self.range = Some(range);
99        self.cone_half_angle = Some(half_angle);
100
101        self
102    }
103
104    #[must_use]
105    pub const fn facing_explicit_target(mut self) -> Self {
106        self.face_explicit_target = true;
107
108        self
109    }
110}
111
112inventory::collect!(TargetGeometryOverlay);
113
114/// Find the unique content overlay for a spell effect.
115///
116/// # Errors
117///
118/// Returns an error when multiple overlays register the same spell effect.
119pub fn registered_target_plan_overlay(
120    spell_id: u32,
121    effect_index: u8,
122) -> Result<Option<&'static TargetPlanOverlay>, TargetPlanError> {
123    let mut matches = inventory::iter::<TargetPlanOverlay>
124        .into_iter()
125        .filter(|overlay| overlay.spell_id == spell_id && overlay.effect_index == effect_index);
126    let first = matches.next();
127
128    if matches.next().is_some() {
129        return Err(TargetPlanError::duplicate_overlay(spell_id, effect_index));
130    }
131
132    Ok(first)
133}
134
135/// Find the unique geometry overlay for a spell effect.
136///
137/// # Errors
138///
139/// Returns an error when multiple geometry overlays register the same spell effect.
140pub fn registered_target_geometry_overlay(
141    spell_id: u32,
142    effect_index: u8,
143) -> Result<Option<&'static TargetGeometryOverlay>, TargetPlanError> {
144    let mut matches = inventory::iter::<TargetGeometryOverlay>
145        .into_iter()
146        .filter(|overlay| overlay.spell_id == spell_id && overlay.effect_index == effect_index);
147    let first = matches.next();
148
149    if matches.next().is_some() {
150        return Err(TargetPlanError::duplicate_geometry_overlay(
151            spell_id,
152            effect_index,
153        ));
154    }
155
156    Ok(first)
157}
158#[cfg(test)]
159mod tests {
160    use googletest::prelude::*;
161
162    use super::*;
163    use crate::{
164        dbc::{
165            ImplicitTargetCheck, ImplicitTargetDirection, ImplicitTargetObject,
166            ImplicitTargetReference, ImplicitTargetSelection,
167        },
168        targeting::{TargetPlanOperation, TargetSelector},
169    };
170
171    const TEST_SPELL_ID: u32 = 4_000_000_001;
172
173    inventory::submit! {
174        TargetPlanOverlay::new(
175            TargetEffectCoordinate {
176                spell_id: TEST_SPELL_ID,
177                effect_index: 2,
178            },
179            TargetOverlayMetadata {
180                name: "test area replacement",
181                reason: "fixture proving runtime and audit discovery use the same registry",
182            },
183            TargetPlanOperations::one(TargetPlanOperation::Select(TargetSelector::custom(
184                ImplicitTargetObject::Unit,
185                ImplicitTargetReference::Caster,
186                ImplicitTargetSelection::Area,
187                ImplicitTargetCheck::Enemy,
188                ImplicitTargetDirection::None,
189            ))),
190        )
191    }
192
193    inventory::submit! {
194        TargetGeometryOverlay::new(
195            TargetEffectCoordinate {
196                spell_id: TEST_SPELL_ID,
197                effect_index: 3,
198            },
199            TargetOverlayMetadata {
200                name: "test corridor metrics",
201                reason: "fixture proving geometry discovery is independent",
202            },
203        )
204            .dimensions(40.0, 2.0)
205            .facing_explicit_target()
206    }
207
208    #[gtest]
209    fn registered_overlays_are_discovered_by_spell_effect_coordinate() {
210        let overlay = registered_target_plan_overlay(TEST_SPELL_ID, 2)
211            .expect("test registry is unambiguous")
212            .expect("test overlay is linked");
213
214        expect_that!(overlay.name, eq("test area replacement"));
215        expect_that!(
216            registered_target_plan_overlay(TEST_SPELL_ID, 1),
217            eq(Ok(None))
218        );
219    }
220
221    #[gtest]
222    fn geometry_overlays_are_discovered_independently_from_selector_overlays() {
223        let overlay = registered_target_geometry_overlay(TEST_SPELL_ID, 3)
224            .expect("test registry is unambiguous")
225            .expect("test geometry overlay is linked");
226
227        expect_that!(
228            overlay,
229            matches_pattern!(&TargetGeometryOverlay {
230                range: eq(Some(40.0)),
231                radius: eq(Some(2.0)),
232                face_explicit_target: eq(true),
233                ..
234            })
235        );
236        expect_that!(
237            registered_target_plan_overlay(TEST_SPELL_ID, 3),
238            eq(Ok(None))
239        );
240    }
241}