1use 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
26pub(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}