Skip to main content

wowlab_engine_domain/targeting/support/
lower.rs

1//! Lowering from canonical target semantics into the components the combat runtime executes.
2
3use super::algebra::{
4    ExecutableCandidateDomain, ExecutableLocation, ExecutableLocationAlgorithm,
5    ExecutableSelection, ExecutableTargetAlgorithm, ExecutableTargetDirection,
6    ExecutableTargetFallback, ExecutableTargetOperation, ExecutableTargetOperations,
7    ExecutableTargetPlan, ExecutableTargetReference, TargetPlanAxis, TargetPlanGap,
8};
9use crate::{
10    dbc::{
11        ImplicitTargetCheck, ImplicitTargetDirection, ImplicitTargetObject,
12        ImplicitTargetReference, ImplicitTargetSelection, SpellEffectImplicitTarget,
13    },
14    targeting::{TargetPlan, TargetPlanOperation, TargetSelector},
15};
16
17/// Lower every semantic axis independently into the runtime target algebra.
18///
19/// # Errors
20///
21/// Returns a gap when the runtime cannot represent a semantic operation or fallback.
22pub fn lower_target_plan(plan: &TargetPlan) -> Result<ExecutableTargetPlan, TargetPlanGap> {
23    let mut operations = ExecutableTargetOperations::default();
24
25    for operation in plan.operations.into_array().into_iter().flatten() {
26        operations.push(lower_operation(operation)?);
27    }
28
29    Ok(ExecutableTargetPlan {
30        operations,
31        fallback: lower_fallback(plan)?,
32    })
33}
34
35/// Whether the canonical program emits a spatially multi-target actor selection.
36#[must_use]
37pub fn target_plan_is_multi_target(plan: &TargetPlan) -> bool {
38    plan.operations
39        .into_array()
40        .into_iter()
41        .flatten()
42        .any(|operation| {
43            let selector = match operation {
44                TargetPlanOperation::Select(selector)
45                | TargetPlanOperation::SelectAndAssignDestination(selector) => selector,
46                TargetPlanOperation::AssignSource(_)
47                | TargetPlanOperation::AssignDestination(_) => return false,
48            };
49
50            matches!(
51                selector.algorithm,
52                ImplicitTargetSelection::Area
53                    | ImplicitTargetSelection::Cone
54                    | ImplicitTargetSelection::Line
55                    | ImplicitTargetSelection::Trajectory
56            )
57        })
58}
59
60fn lower_operation(
61    operation: TargetPlanOperation,
62) -> Result<ExecutableTargetOperation, TargetPlanGap> {
63    match operation {
64        TargetPlanOperation::AssignSource(selector) => Ok(ExecutableTargetOperation::AssignSource(
65            lower_location(selector)?,
66        )),
67        TargetPlanOperation::AssignDestination(selector) => Ok(
68            ExecutableTargetOperation::AssignDestination(lower_location(selector)?),
69        ),
70        TargetPlanOperation::Select(selector) => Ok(ExecutableTargetOperation::Select(
71            lower_selection(selector)?,
72        )),
73        TargetPlanOperation::SelectAndAssignDestination(selector) => Ok(
74            ExecutableTargetOperation::SelectAndAssignDestination(lower_selection(selector)?),
75        ),
76    }
77}
78
79fn lower_location(selector: TargetSelector) -> Result<ExecutableLocation, TargetPlanGap> {
80    lower_check(selector)?;
81
82    Ok(ExecutableLocation {
83        reference: lower_reference(selector)?,
84        algorithm: lower_location_algorithm(selector)?,
85        direction: lower_direction(selector)?,
86    })
87}
88
89fn lower_selection(selector: TargetSelector) -> Result<ExecutableSelection, TargetPlanGap> {
90    Ok(ExecutableSelection {
91        candidates: lower_candidate_domain(selector)?,
92        reference: lower_reference(selector)?,
93        algorithm: lower_selection_algorithm(selector)?,
94        direction: lower_direction(selector)?,
95        selector,
96    })
97}
98
99fn lower_candidate_domain(
100    selector: TargetSelector,
101) -> Result<ExecutableCandidateDomain, TargetPlanGap> {
102    lower_object(selector)?;
103    lower_check(selector)?;
104
105    Ok(ExecutableCandidateDomain::EnemyUnits)
106}
107
108fn lower_object(selector: TargetSelector) -> Result<(), TargetPlanGap> {
109    match selector.object {
110        ImplicitTargetObject::Unit | ImplicitTargetObject::UnitAndDestination => Ok(()),
111        value => Err(selector_gap(
112            selector,
113            TargetPlanAxis::Object,
114            value.as_str(),
115        )),
116    }
117}
118
119fn lower_reference(selector: TargetSelector) -> Result<ExecutableTargetReference, TargetPlanGap> {
120    match selector.reference {
121        ImplicitTargetReference::Caster => Ok(ExecutableTargetReference::Caster),
122        ImplicitTargetReference::Target => Ok(ExecutableTargetReference::ExplicitTarget),
123        ImplicitTargetReference::Source => Ok(ExecutableTargetReference::Source),
124        ImplicitTargetReference::Destination => Ok(ExecutableTargetReference::Destination),
125        value => Err(selector_gap(
126            selector,
127            TargetPlanAxis::Reference,
128            value.as_str(),
129        )),
130    }
131}
132
133fn lower_location_algorithm(
134    selector: TargetSelector,
135) -> Result<ExecutableLocationAlgorithm, TargetPlanGap> {
136    match selector.algorithm {
137        ImplicitTargetSelection::Default => Ok(ExecutableLocationAlgorithm::Reference),
138        ImplicitTargetSelection::Channel => Ok(ExecutableLocationAlgorithm::ChannelTarget),
139        value => Err(selector_gap(
140            selector,
141            TargetPlanAxis::Algorithm,
142            value.as_str(),
143        )),
144    }
145}
146
147fn lower_selection_algorithm(
148    selector: TargetSelector,
149) -> Result<ExecutableTargetAlgorithm, TargetPlanGap> {
150    match selector.algorithm {
151        ImplicitTargetSelection::Default => Ok(ExecutableTargetAlgorithm::Single),
152        ImplicitTargetSelection::Channel => Ok(ExecutableTargetAlgorithm::ChannelTarget),
153        ImplicitTargetSelection::Area => Ok(ExecutableTargetAlgorithm::Radius),
154        ImplicitTargetSelection::Cone => Ok(ExecutableTargetAlgorithm::Cone),
155        ImplicitTargetSelection::Line => Ok(ExecutableTargetAlgorithm::Line),
156        value => Err(selector_gap(
157            selector,
158            TargetPlanAxis::Algorithm,
159            value.as_str(),
160        )),
161    }
162}
163
164fn lower_check(selector: TargetSelector) -> Result<(), TargetPlanGap> {
165    match selector.check {
166        ImplicitTargetCheck::Default | ImplicitTargetCheck::Enemy => Ok(()),
167        value => Err(selector_gap(
168            selector,
169            TargetPlanAxis::Check,
170            value.as_str(),
171        )),
172    }
173}
174
175fn lower_direction(selector: TargetSelector) -> Result<ExecutableTargetDirection, TargetPlanGap> {
176    match selector.direction {
177        ImplicitTargetDirection::None => Ok(ExecutableTargetDirection::None),
178        ImplicitTargetDirection::Front => Ok(ExecutableTargetDirection::Front),
179        ImplicitTargetDirection::Back => Ok(ExecutableTargetDirection::Back),
180        ImplicitTargetDirection::Right => Ok(ExecutableTargetDirection::Right),
181        ImplicitTargetDirection::Left => Ok(ExecutableTargetDirection::Left),
182        ImplicitTargetDirection::FrontRight => Ok(ExecutableTargetDirection::FrontRight),
183        ImplicitTargetDirection::BackRight => Ok(ExecutableTargetDirection::BackRight),
184        ImplicitTargetDirection::BackLeft => Ok(ExecutableTargetDirection::BackLeft),
185        ImplicitTargetDirection::FrontLeft => Ok(ExecutableTargetDirection::FrontLeft),
186        ImplicitTargetDirection::Random => Ok(ExecutableTargetDirection::Random),
187        value => Err(selector_gap(
188            selector,
189            TargetPlanAxis::Direction,
190            value.as_str(),
191        )),
192    }
193}
194
195fn lower_fallback(plan: &TargetPlan) -> Result<ExecutableTargetFallback, TargetPlanGap> {
196    use ImplicitTargetObject as Object;
197    use SpellEffectImplicitTarget as Implicit;
198
199    match (plan.fallback.implicit, plan.fallback.object) {
200        (Implicit::Explicit, Object::Unit | Object::UnitAndDestination) => {
201            Ok(ExecutableTargetFallback::ExplicitUnit)
202        }
203        (Implicit::None, Object::None | Object::Unit | Object::Destination)
204        | (Implicit::Caster, Object::Unit)
205        | (Implicit::Explicit, Object::None) => Ok(ExecutableTargetFallback::NoSelection),
206        (Implicit::Explicit, Object::Destination) => {
207            Ok(ExecutableTargetFallback::ExplicitDestination)
208        }
209        (_, object) => Err(gap(None, TargetPlanAxis::EffectFallback, object.as_str())),
210    }
211}
212
213const fn selector_gap(
214    selector: TargetSelector,
215    axis: TargetPlanAxis,
216    value: &'static str,
217) -> TargetPlanGap {
218    gap(selector.raw, axis, value)
219}
220
221const fn gap(selector: Option<i32>, axis: TargetPlanAxis, value: &'static str) -> TargetPlanGap {
222    TargetPlanGap {
223        selector,
224        axis,
225        value,
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use googletest::prelude::*;
232    use rstest::rstest;
233
234    use super::*;
235    use crate::{
236        dbc::SpellEffectTargetSemantic,
237        targeting::{TargetPlanOperations, TargetPlanProvenance, compile_dbc_target_plan},
238    };
239
240    #[gtest]
241    #[rstest]
242    #[case::blade_flurry(18, 16)]
243    #[case::blade_rush(53, 16)]
244    #[case::front_cone(24, 0)]
245    #[case::channel_destination_area(76, 16)]
246    #[case::channel_unit(77, 0)]
247    fn composed_supported_axes_lower(#[case] target_a: i32, #[case] target_b: i32) {
248        let plan = compile_dbc_target_plan(2, target_a, target_b).unwrap();
249
250        lower_target_plan(&plan).unwrap();
251    }
252
253    #[gtest]
254    #[rstest]
255    #[case::front_right(41, ExecutableTargetDirection::FrontRight)]
256    #[case::back_right(42, ExecutableTargetDirection::BackRight)]
257    #[case::back_left(43, ExecutableTargetDirection::BackLeft)]
258    #[case::front_left(44, ExecutableTargetDirection::FrontLeft)]
259    #[case::front(47, ExecutableTargetDirection::Front)]
260    #[case::back(48, ExecutableTargetDirection::Back)]
261    #[case::right(49, ExecutableTargetDirection::Right)]
262    #[case::left(50, ExecutableTargetDirection::Left)]
263    #[case::random(72, ExecutableTargetDirection::Random)]
264    fn directional_locations_lower_to_typed_runtime_axes(
265        #[case] raw: i32,
266        #[case] expected: ExecutableTargetDirection,
267    ) {
268        let plan = compile_dbc_target_plan(2, raw, 0).unwrap();
269        let [
270            Some(ExecutableTargetOperation::AssignDestination(location)),
271            None,
272        ] = lower_target_plan(&plan).unwrap().operations.into_array()
273        else {
274            panic!("expected one destination assignment");
275        };
276
277        expect_that!(location.direction, eq(expected));
278    }
279
280    #[gtest]
281    #[rstest]
282    #[case::raid(57, TargetPlanAxis::Check, "raid")]
283    #[case::trajectory(89, TargetPlanAxis::Algorithm, "trajectory")]
284    fn one_axis_reports_one_precise_gap(
285        #[case] raw: i32,
286        #[case] axis: TargetPlanAxis,
287        #[case] value: &'static str,
288    ) {
289        let plan = compile_dbc_target_plan(2, raw, 0).unwrap();
290
291        expect_that!(
292            lower_target_plan(&plan),
293            eq(Err(TargetPlanGap {
294                selector: Some(raw),
295                axis,
296                value,
297            }))
298        );
299    }
300
301    #[gtest]
302    #[rstest]
303    #[case::blade_flurry(18, 16, true)]
304    #[case::blade_rush(53, 16, true)]
305    #[case::explicit_enemy(6, 0, false)]
306    #[case::destination_only(53, 0, false)]
307    fn multi_target_classification_comes_from_selection_operations(
308        #[case] target_a: i32,
309        #[case] target_b: i32,
310        #[case] expected: bool,
311    ) {
312        let plan = compile_dbc_target_plan(2, target_a, target_b).unwrap();
313
314        expect_that!(target_plan_is_multi_target(&plan), eq(expected));
315    }
316
317    #[gtest]
318    #[rstest]
319    #[case::no_actor_or_location(SpellEffectImplicitTarget::None, ImplicitTargetObject::None)]
320    #[case::intrinsic_unit(SpellEffectImplicitTarget::None, ImplicitTargetObject::Unit)]
321    #[case::intrinsic_destination(
322        SpellEffectImplicitTarget::None,
323        ImplicitTargetObject::Destination
324    )]
325    fn intrinsic_effect_fallbacks_do_not_require_an_explicit_actor(
326        #[case] implicit: SpellEffectImplicitTarget,
327        #[case] object: ImplicitTargetObject,
328    ) {
329        let plan = TargetPlan {
330            operations: TargetPlanOperations::default(),
331            fallback: SpellEffectTargetSemantic { implicit, object },
332            provenance: TargetPlanProvenance::Dbc {
333                target_a: 0,
334                target_b: 0,
335            },
336        };
337
338        expect_that!(
339            lower_target_plan(&plan).unwrap().fallback,
340            eq(ExecutableTargetFallback::NoSelection)
341        );
342    }
343}