Skip to main content

wowlab_engine_combat/targeting/
chain.rs

1//! Deterministic chain-target traversal.
2
3use 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
26// #t(fn: rust_large_fn_params) chain traversal keeps geometry, policy, diagnostics, and caller-owned scratch explicit.
27pub(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        // #t(rust_closure_dense_method_chain) geometry, visibility, and deterministic tie-breaking are one ordered candidate pipeline.
80        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}