wowlab_engine_combat/targeting/
chain.rs1use wowlab_engine_domain::targeting::{ChainTargetOrigin, SingleTargetSelector};
4use wowlab_engine_ports::{RadiusQuery, SpatialEnvelope};
5use wowlab_types::sim::{ActorId, EnemyIdx, GEOMETRY_EPSILON, Position2};
6
7use super::{
8 ChainGeometry, ResolutionContext, ResolutionDiagnostics, filtered_spatial_candidates,
9 select_one, visible_counted,
10};
11use crate::state::CombatState;
12
13pub(super) const fn radius_envelope(center: Position2, radius: f64) -> SpatialEnvelope {
14 SpatialEnvelope {
15 min: Position2 {
16 x: center.x - radius - GEOMETRY_EPSILON,
17 y: center.y - radius - GEOMETRY_EPSILON,
18 },
19 max: Position2 {
20 x: center.x + radius + GEOMETRY_EPSILON,
21 y: center.y + radius + GEOMETRY_EPSILON,
22 },
23 }
24}
25
26pub(super) fn resolve_chain(
27 state: &CombatState,
28 source: ActorId,
29 anchor: SingleTargetSelector,
30 geometry: ChainGeometry,
31 context: ResolutionContext<'_>,
32 diagnostics: &mut ResolutionDiagnostics,
33) -> Vec<EnemyIdx> {
34 if geometry.max_hits == 0 {
35 return Vec::new();
36 }
37
38 let Some(anchor) = select_one(state, source, anchor, context, diagnostics) else {
39 return Vec::new();
40 };
41 let mut targets = vec![anchor];
42
43 while targets.len() < usize::from(geometry.max_hits) {
44 let origin = match geometry.origin {
45 ChainTargetOrigin::PreviousTarget => {
46 let last = *targets.last().expect("chain always has an anchor");
47
48 state.actor_transform(ActorId::Enemy(last))
49 }
50 ChainTargetOrigin::Source => state.actor_transform(source),
51 };
52 let Some(origin) = origin else {
53 break;
54 };
55 let allowed = filtered_spatial_candidates(
56 state,
57 origin.layer,
58 radius_envelope(origin.position, geometry.jump_radius),
59 context.filter,
60 diagnostics,
61 );
62 let allowed: Vec<_> = allowed
63 .into_iter()
64 .filter(|enemy| !targets.contains(enemy))
65 .collect();
66
67 diagnostics.record_exact_geometry(allowed.len());
68 let next = state
69 .spatial_query()
70 .actors_in_radius(
71 RadiusQuery {
72 layer: origin.layer,
73 center: origin.position,
74 radius: geometry.jump_radius,
75 },
76 Some(&allowed),
77 )
78 .into_iter()
79 .filter(|enemy| {
80 visible_counted(state, source, *enemy, context.line_of_sight, diagnostics)
81 })
82 .min_by(|left, right| {
83 let left_distance = state
84 .actor_transform(ActorId::Enemy(*left))
85 .map(|transform| origin.position.distance(transform.position))
86 .unwrap_or_default();
87 let right_distance = state
88 .actor_transform(ActorId::Enemy(*right))
89 .map(|transform| origin.position.distance(transform.position))
90 .unwrap_or_default();
91
92 left_distance
93 .total_cmp(&right_distance)
94 .then_with(|| left.cmp(right))
95 });
96 let Some(next) = next else {
97 break;
98 };
99
100 targets.push(next);
101 }
102
103 targets
104}