1use wowlab_types::sim::{EnemyIdx, GroupId, SpatialTransform};
4
5mod overlay;
6mod plan;
7mod support;
8
9pub use overlay::{
10 TargetEffectCoordinate, TargetGeometryOverlay, TargetOverlayMetadata, TargetPlanOverlay,
11 registered_target_geometry_overlay, registered_target_plan_overlay,
12};
13pub use plan::{
14 TargetPlan, TargetPlanError, TargetPlanInput, TargetPlanOperation, TargetPlanOperations,
15 TargetPlanProvenance, TargetSelector, compile_dbc_target_plan, compile_target_plan,
16};
17pub use support::{
18 ExecutableCandidateDomain, ExecutableLocation, ExecutableLocationAlgorithm,
19 ExecutableSelection, ExecutableTargetAlgorithm, ExecutableTargetDirection,
20 ExecutableTargetFallback, ExecutableTargetOperation, ExecutableTargetOperations,
21 ExecutableTargetPlan, ExecutableTargetReference, TargetPlanAxis, TargetPlanGap,
22 lower_target_plan, target_plan_is_multi_target,
23};
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum SingleTargetSelector {
29 Current,
30 Primary,
31 Explicit(EnemyIdx),
32 Nearest,
33 Farthest,
34 LowestHealth,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ChainTargetOrigin {
41 PreviousTarget,
42 Source,
43}
44
45#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub struct CandidateFilter {
48 pub groups: Vec<GroupId>,
49 pub tags: Vec<String>,
50}
51
52impl CandidateFilter {
53 #[must_use]
55 pub fn matches(
56 &self,
57 group: GroupId,
58 enemy_tags: &[Box<str>],
59 group_tags: &[Box<str>],
60 ) -> bool {
61 (self.groups.is_empty() || self.groups.contains(&group))
62 && self.tags.iter().all(|required| {
63 enemy_tags.iter().any(|tag| tag.as_ref() == required)
64 || group_tags.iter().any(|tag| tag.as_ref() == required)
65 })
66 }
67}
68
69#[derive(Clone, Debug, PartialEq)]
71pub enum ResolvedTargetShape {
73 Single(SingleTargetSelector),
74 RadiusFromSource {
75 radius: f64,
76 },
77 RadiusFromTarget {
78 anchor: SingleTargetSelector,
79 radius: f64,
80 },
81 RadiusAtLocation {
82 location: SpatialTransform,
83 radius: f64,
84 },
85 Cone {
86 range: f64,
87 half_angle: f64,
88 heading_offset: f64,
89 },
90 ConeFromTarget {
91 anchor: SingleTargetSelector,
92 range: f64,
93 half_angle: f64,
94 heading_offset: f64,
95 },
96 ConeAtLocation {
97 location: SpatialTransform,
98 range: f64,
99 half_angle: f64,
100 heading_offset: f64,
101 },
102 ConeTowardTarget {
103 anchor: SingleTargetSelector,
104 range: f64,
105 half_angle: f64,
106 heading_offset: f64,
107 },
108 Line {
109 range: f64,
110 half_width: f64,
111 heading_offset: f64,
112 },
113 LineTowardTarget {
114 anchor: SingleTargetSelector,
115 range: f64,
116 half_width: f64,
117 heading_offset: f64,
118 },
119 LineAtLocation {
120 location: SpatialTransform,
121 range: f64,
122 half_width: f64,
123 heading_offset: f64,
124 },
125 Chain {
126 anchor: SingleTargetSelector,
127 origin: ChainTargetOrigin,
128 jump_radius: f64,
129 max_hits: u8,
130 },
131 Explicit(ExplicitTargets),
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
136pub struct ExplicitTargets(Vec<EnemyIdx>);
137
138impl ExplicitTargets {
139 #[must_use]
141 pub fn as_slice(&self) -> &[EnemyIdx] {
142 &self.0
143 }
144
145 fn validate(&self, enemy_count: usize) -> Result<(), TargetShapeError> {
146 validate_explicit_targets(self.0.iter().copied(), enemy_count)
147 }
148}
149
150wowlab_engine_macros::define_error! {
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct TargetShapeError {
154 #[source]
155 kind: TargetShapeErrorKind,
156}
157
158#[derive(Clone, Copy, Debug, thiserror::Error, Eq, PartialEq)]
159enum TargetShapeErrorKind {
160 #[error("explicit target {enemy:?} does not exist")]
161 MissingExplicitEnemy { enemy: EnemyIdx },
162 #[error(transparent)]
163 InvalidTargetPlan(#[from] TargetPlanError),
164 #[error(transparent)]
165 UnsupportedTargetPlan(#[from] TargetPlanGap),
166 #[error("target selector {selector:?} does not resolve hostile enemy identities")]
167 NonHostileSelection { selector: TargetSelector },
168 #[error("target plan requires an authored cast destination")]
169 MissingCastDestination,
170 #[error("directional target plan requires a spatial position for its {reference:?} reference")]
171 MissingReferencePosition {
172 reference: ExecutableTargetReference,
173 },
174 #[error("random target direction requires the simulation RNG")]
175 MissingDirectionRng,
176}
177}
178
179impl TargetShapeError {
180 #[must_use]
182 pub const fn missing_explicit_enemy(enemy: EnemyIdx) -> Self {
183 Self {
184 kind: TargetShapeErrorKind::MissingExplicitEnemy { enemy },
185 }
186 }
187
188 #[must_use]
190 pub const fn non_hostile_selection(selector: TargetSelector) -> Self {
191 Self {
192 kind: TargetShapeErrorKind::NonHostileSelection { selector },
193 }
194 }
195
196 #[must_use]
198 pub const fn missing_cast_destination() -> Self {
199 Self {
200 kind: TargetShapeErrorKind::MissingCastDestination,
201 }
202 }
203
204 #[must_use]
206 pub const fn missing_reference_position(reference: ExecutableTargetReference) -> Self {
207 Self {
208 kind: TargetShapeErrorKind::MissingReferencePosition { reference },
209 }
210 }
211
212 #[must_use]
214 pub const fn missing_direction_rng() -> Self {
215 Self {
216 kind: TargetShapeErrorKind::MissingDirectionRng,
217 }
218 }
219}
220
221impl From<TargetPlanError> for TargetShapeError {
222 fn from(error: TargetPlanError) -> Self {
223 Self {
224 kind: TargetShapeErrorKind::InvalidTargetPlan(error),
225 }
226 }
227}
228
229impl From<TargetPlanGap> for TargetShapeError {
230 fn from(error: TargetPlanGap) -> Self {
231 Self {
232 kind: TargetShapeErrorKind::UnsupportedTargetPlan(error),
233 }
234 }
235}
236
237impl ResolvedTargetShape {
238 pub fn explicit(
244 targets: impl IntoIterator<Item = EnemyIdx>,
245 enemy_count: usize,
246 ) -> Result<Self, TargetShapeError> {
247 let resolved = deduplicate_explicit_targets(targets, enemy_count)?;
248
249 Ok(Self::Explicit(ExplicitTargets(resolved)))
250 }
251
252 #[must_use]
254 pub fn explicit_targets(&self) -> Option<&[EnemyIdx]> {
255 match self {
256 Self::Explicit(targets) => Some(targets.as_slice()),
257 _ => None,
258 }
259 }
260
261 pub fn validate(&self, enemy_count: usize) -> Result<(), TargetShapeError> {
267 match self {
268 Self::Single(selector) => validate_selector(*selector, enemy_count),
269 Self::RadiusFromTarget { anchor, .. }
270 | Self::ConeFromTarget { anchor, .. }
271 | Self::ConeTowardTarget { anchor, .. }
272 | Self::LineTowardTarget { anchor, .. }
273 | Self::Chain { anchor, .. } => validate_selector(*anchor, enemy_count),
274 Self::Explicit(targets) => targets.validate(enemy_count),
275 Self::RadiusFromSource { .. }
276 | Self::RadiusAtLocation { .. }
277 | Self::Cone { .. }
278 | Self::ConeAtLocation { .. }
279 | Self::Line { .. }
280 | Self::LineAtLocation { .. } => Ok(()),
281 }
282 }
283}
284
285fn validate_selector(
286 selector: SingleTargetSelector,
287 enemy_count: usize,
288) -> Result<(), TargetShapeError> {
289 if let SingleTargetSelector::Explicit(enemy) = selector {
290 validate_explicit_targets([enemy], enemy_count)?;
291 }
292
293 Ok(())
294}
295
296fn validate_explicit_targets(
297 targets: impl IntoIterator<Item = EnemyIdx>,
298 enemy_count: usize,
299) -> Result<(), TargetShapeError> {
300 for enemy in targets {
301 if enemy.as_usize() >= enemy_count {
302 return Err(TargetShapeError::missing_explicit_enemy(enemy));
303 }
304 }
305
306 Ok(())
307}
308
309fn deduplicate_explicit_targets(
310 targets: impl IntoIterator<Item = EnemyIdx>,
311 enemy_count: usize,
312) -> Result<Vec<EnemyIdx>, TargetShapeError> {
313 let mut resolved = Vec::new();
314
315 for enemy in targets {
316 validate_explicit_targets([enemy], enemy_count)?;
317
318 if !resolved.contains(&enemy) {
319 resolved.push(enemy);
320 }
321 }
322
323 Ok(resolved)
324}
325
326#[cfg(test)]
327mod tests {
328 use googletest::prelude::*;
329
330 use super::*;
331
332 #[gtest]
333 fn candidate_filter_uses_or_groups_and_all_tags_from_union() {
334 let filter = CandidateFilter {
335 groups: vec![GroupId(1), GroupId(3)],
336 tags: vec!["caster".into(), "priority".into()],
337 };
338
339 expect_that!(
340 filter.matches(GroupId(3), &["caster".into()], &["priority".into()]),
341 is_true()
342 );
343 expect_that!(
344 !filter.matches(GroupId(2), &["caster".into()], &["priority".into()]),
345 is_true()
346 );
347 expect_that!(
348 !filter.matches(GroupId(3), &["caster".into()], &[]),
349 is_true()
350 );
351 }
352
353 #[gtest]
354 fn explicit_shape_deduplicates_in_first_occurrence_order() {
355 let shape =
356 ResolvedTargetShape::explicit([EnemyIdx(2), EnemyIdx(1), EnemyIdx(2), EnemyIdx(0)], 3)
357 .expect("valid explicit identities");
358
359 expect_that!(
360 shape.explicit_targets(),
361 eq(Some(&[EnemyIdx(2), EnemyIdx(1), EnemyIdx(0)][..]))
362 );
363 expect_that!(
364 ResolvedTargetShape::explicit([EnemyIdx(3)], 3),
365 eq(&Err(TargetShapeError {
366 kind: TargetShapeErrorKind::MissingExplicitEnemy { enemy: EnemyIdx(3) }
367 }))
368 );
369 }
370}