wowlab_engine_domain/targeting/
overlay.rs1use super::{TargetPlanError, TargetPlanInput, TargetPlanOperations};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct TargetEffectCoordinate {
9 pub spell_id: u32,
10 pub effect_index: u8,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct TargetOverlayMetadata {
16 pub name: &'static str,
17 pub reason: &'static str,
18}
19
20#[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#[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
114pub 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
135pub 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}