1use 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
17pub 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#[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}