wowlab_engine_combat/targeting/
chain.rs1use wowlab_engine_domain::targeting::{ChainTargetOrigin, SingleTargetSelector};
4use wowlab_engine_ports::SpatialEnvelope;
5use wowlab_types::sim::{ActorId, GEOMETRY_EPSILON, Position2};
6
7use super::{
8 ChainGeometry, ResolutionContext, ResolutionDiagnostics, fill_spatial_candidates, select_one,
9 visible_counted,
10};
11use crate::state::{CombatInstance, TargetWorkspace};
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(
28 state: &CombatInstance,
29 source: ActorId,
30 anchor: SingleTargetSelector,
31 geometry: ChainGeometry,
32 context: ResolutionContext<'_>,
33 diagnostics: &mut ResolutionDiagnostics,
34 workspace: &mut TargetWorkspace,
35) {
36 if geometry.max_hits == 0 {
37 return;
38 }
39
40 let Some(anchor) = select_one(state, source, anchor, context, diagnostics, workspace) else {
41 return;
42 };
43
44 workspace.push_target(anchor);
45
46 while workspace.targets().len() < usize::from(geometry.max_hits) {
47 let origin = match geometry.origin {
48 ChainTargetOrigin::PreviousTarget => {
49 let Some(last) = workspace.targets().last().copied() else {
50 break;
51 };
52
53 state.actor_transform(ActorId::Enemy(last))
54 }
55 ChainTargetOrigin::Source => state.actor_transform(source),
56 };
57 let Some(origin) = origin else {
58 break;
59 };
60
61 fill_spatial_candidates(
62 state,
63 origin.layer,
64 radius_envelope(origin.position, geometry.jump_radius),
65 diagnostics,
66 workspace,
67 );
68 workspace.retain_candidates_excluding_targets(|enemy| {
69 super::metadata_eligible_counted(
70 context.program,
71 state,
72 enemy,
73 context.filter,
74 diagnostics,
75 )
76 });
77
78 diagnostics.record_exact_geometry(workspace.candidate_count());
79 let next = workspace
81 .candidates()
82 .iter()
83 .copied()
84 .filter(|enemy| {
85 state
86 .actor_transform(ActorId::Enemy(*enemy))
87 .is_some_and(|target| {
88 target.layer == origin.layer
89 && origin.position.distance(target.position)
90 <= geometry.jump_radius + GEOMETRY_EPSILON
91 })
92 })
93 .filter(|enemy| {
94 visible_counted(
95 context.program,
96 state,
97 source,
98 *enemy,
99 context.line_of_sight,
100 diagnostics,
101 )
102 })
103 .min_by(|left, right| {
104 let left_distance = state
105 .actor_transform(ActorId::Enemy(*left))
106 .map(|transform| origin.position.distance(transform.position))
107 .unwrap_or_default();
108 let right_distance = state
109 .actor_transform(ActorId::Enemy(*right))
110 .map(|transform| origin.position.distance(transform.position))
111 .unwrap_or_default();
112
113 left_distance
114 .total_cmp(&right_distance)
115 .then_with(|| left.cmp(right))
116 });
117 let Some(next) = next else {
118 break;
119 };
120
121 workspace.push_target(next);
122 }
123}