1#[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
67pub(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
247fn 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}