Skip to main content

wowlab_engine_domain/dbc/semantics/
spell_targets.rs

1//! Composition of implicit selectors into explicit and required cast-target masks.
2
3use wowlab_types::data::{SpellDataFlat, SpellEffect};
4
5use super::{
6    ImplicitTargetCheck, ImplicitTargetObject, ImplicitTargetReference, ImplicitTargetSemantic,
7    SpellCastTargetFlags, SpellEffectAttributes, SpellEffectImplicitTarget,
8    implicit_target_semantic, spell_effect_target_semantic,
9};
10
11const TRAJECTORY_TARGET: i32 = 89;
12const DO_NOT_FAIL_IF_NO_TARGET_WORD: usize = 13;
13const DO_NOT_FAIL_IF_NO_TARGET_MASK: i32 = 1 << 15;
14
15/// Explicit and required target masks calculated from effect selectors and restrictions.
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct SpellTargetMasks {
18    pub explicit: SpellCastTargetFlags,
19    pub required: SpellCastTargetFlags,
20}
21
22#[derive(Clone, Copy, Debug, Default)]
23struct LocationState {
24    source_set: bool,
25    destination_set: bool,
26}
27
28fn trajectory_mask(locations: LocationState) -> SpellCastTargetFlags {
29    let mut mask = SpellCastTargetFlags::empty();
30
31    if !locations.source_set {
32        mask |= SpellCastTargetFlags::SOURCE_LOCATION;
33    }
34
35    if !locations.destination_set {
36        mask |= SpellCastTargetFlags::DEST_LOCATION;
37    }
38
39    mask
40}
41
42const fn checked_unit_mask(check: ImplicitTargetCheck) -> SpellCastTargetFlags {
43    match check {
44        ImplicitTargetCheck::Enemy => SpellCastTargetFlags::UNIT_ENEMY,
45        ImplicitTargetCheck::Ally => SpellCastTargetFlags::UNIT_ALLY,
46        ImplicitTargetCheck::Party => SpellCastTargetFlags::UNIT_PARTY,
47        ImplicitTargetCheck::Raid => SpellCastTargetFlags::UNIT_RAID,
48        ImplicitTargetCheck::Passenger => SpellCastTargetFlags::UNIT_PASSENGER,
49        _ => SpellCastTargetFlags::UNIT,
50    }
51}
52
53const fn object_mask(object: ImplicitTargetObject) -> SpellCastTargetFlags {
54    match object {
55        ImplicitTargetObject::Destination => SpellCastTargetFlags::DEST_LOCATION,
56        ImplicitTargetObject::UnitAndDestination => SpellCastTargetFlags::from_bits_retain(
57            SpellCastTargetFlags::DEST_LOCATION.bits() | SpellCastTargetFlags::UNIT.bits(),
58        ),
59        ImplicitTargetObject::CorpseAlly => SpellCastTargetFlags::CORPSE_ALLY,
60        ImplicitTargetObject::CorpseEnemy => SpellCastTargetFlags::CORPSE_ENEMY,
61        ImplicitTargetObject::Corpse => SpellCastTargetFlags::CORPSE_MASK,
62        ImplicitTargetObject::Unit => SpellCastTargetFlags::UNIT,
63        ImplicitTargetObject::GameObject => SpellCastTargetFlags::GAMEOBJECT,
64        ImplicitTargetObject::GameObjectItem => SpellCastTargetFlags::GAMEOBJECT_ITEM,
65        ImplicitTargetObject::Item => SpellCastTargetFlags::ITEM,
66        ImplicitTargetObject::Source => SpellCastTargetFlags::SOURCE_LOCATION,
67        ImplicitTargetObject::None => SpellCastTargetFlags::empty(),
68    }
69}
70
71const fn explicit_object_mask(target: ImplicitTargetSemantic) -> SpellCastTargetFlags {
72    match target.object {
73        ImplicitTargetObject::GameObject => SpellCastTargetFlags::GAMEOBJECT,
74        ImplicitTargetObject::GameObjectItem => SpellCastTargetFlags::GAMEOBJECT_ITEM,
75        ImplicitTargetObject::Unit
76        | ImplicitTargetObject::UnitAndDestination
77        | ImplicitTargetObject::Destination => checked_unit_mask(target.check),
78        _ => SpellCastTargetFlags::empty(),
79    }
80}
81
82fn reference_mask(
83    target: ImplicitTargetSemantic,
84    locations: LocationState,
85) -> SpellCastTargetFlags {
86    match target.reference {
87        ImplicitTargetReference::Source if !locations.source_set => {
88            SpellCastTargetFlags::SOURCE_LOCATION
89        }
90        ImplicitTargetReference::Destination if !locations.destination_set => {
91            SpellCastTargetFlags::DEST_LOCATION
92        }
93        ImplicitTargetReference::Target => explicit_object_mask(target),
94        _ => SpellCastTargetFlags::empty(),
95    }
96}
97
98fn record_provided_location(target: ImplicitTargetSemantic, locations: &mut LocationState) {
99    match target.object {
100        ImplicitTargetObject::Source => locations.source_set = true,
101        ImplicitTargetObject::Destination | ImplicitTargetObject::UnitAndDestination => {
102            locations.destination_set = true;
103        }
104        _ => {}
105    }
106}
107
108fn selector_mask(raw: i32, locations: &mut LocationState) -> SpellCastTargetFlags {
109    let Some(target) = implicit_target_semantic(raw).copied() else {
110        return SpellCastTargetFlags::empty();
111    };
112    let mask = if raw == TRAJECTORY_TARGET {
113        trajectory_mask(*locations)
114    } else {
115        reference_mask(target, *locations)
116    };
117
118    record_provided_location(target, locations);
119
120    mask
121}
122
123fn effect_target_mask(
124    target_a: i32,
125    target_b: i32,
126    locations: &mut LocationState,
127) -> SpellCastTargetFlags {
128    selector_mask(target_a, locations) | selector_mask(target_b, locations)
129}
130
131fn selector_object_mask(raw: i32) -> SpellCastTargetFlags {
132    implicit_target_semantic(raw).map_or_else(SpellCastTargetFlags::empty, |target| {
133        object_mask(target.object)
134    })
135}
136
137fn remove_provided_objects(
138    mut missing: SpellCastTargetFlags,
139    provided: SpellCastTargetFlags,
140    locations: LocationState,
141) -> SpellCastTargetFlags {
142    if provided.intersects(SpellCastTargetFlags::UNIT_MASK) {
143        missing.remove(SpellCastTargetFlags::UNIT_MASK);
144    }
145
146    if provided.intersects(SpellCastTargetFlags::CORPSE_MASK) {
147        missing.remove(SpellCastTargetFlags::UNIT_MASK | SpellCastTargetFlags::CORPSE_MASK);
148    }
149
150    if provided.contains(SpellCastTargetFlags::GAMEOBJECT_ITEM) {
151        missing.remove(
152            SpellCastTargetFlags::GAMEOBJECT_ITEM
153                | SpellCastTargetFlags::GAMEOBJECT
154                | SpellCastTargetFlags::ITEM,
155        );
156    }
157
158    if provided.contains(SpellCastTargetFlags::GAMEOBJECT) {
159        missing.remove(SpellCastTargetFlags::GAMEOBJECT_MASK);
160    }
161
162    if provided.contains(SpellCastTargetFlags::ITEM) {
163        missing.remove(SpellCastTargetFlags::ITEM | SpellCastTargetFlags::GAMEOBJECT_ITEM);
164    }
165
166    if locations.destination_set || provided.contains(SpellCastTargetFlags::DEST_LOCATION) {
167        missing.remove(SpellCastTargetFlags::DEST_LOCATION);
168    }
169
170    if locations.source_set || provided.contains(SpellCastTargetFlags::SOURCE_LOCATION) {
171        missing.remove(SpellCastTargetFlags::SOURCE_LOCATION);
172    }
173
174    missing
175}
176
177fn missing_effect_mask(
178    effect: &SpellEffect,
179    target_mask: SpellCastTargetFlags,
180    locations: LocationState,
181) -> SpellCastTargetFlags {
182    let Some(targeting) = spell_effect_target_semantic(effect.effect) else {
183        return SpellCastTargetFlags::empty();
184    };
185
186    if targeting.implicit != SpellEffectImplicitTarget::Explicit {
187        return SpellCastTargetFlags::empty();
188    }
189
190    let provided = target_mask
191        | selector_object_mask(effect.implicit_target_a)
192        | selector_object_mask(effect.implicit_target_b);
193
194    remove_provided_objects(object_mask(targeting.object), provided, locations)
195}
196
197fn strip_unreachable_fallbacks(
198    spell: &SpellDataFlat,
199    mut mask: SpellCastTargetFlags,
200) -> SpellCastTargetFlags {
201    if spell.range_max_0 <= 0.0 && spell.range_max_1 <= 0.0 {
202        mask.remove(
203            SpellCastTargetFlags::UNIT_MASK
204                | SpellCastTargetFlags::GAMEOBJECT
205                | SpellCastTargetFlags::CORPSE_MASK
206                | SpellCastTargetFlags::DEST_LOCATION,
207        );
208    }
209
210    mask
211}
212
213fn authored_masks(spell: &SpellDataFlat) -> SpellTargetMasks {
214    let explicit = SpellCastTargetFlags::from_dbc(spell.target_flags);
215    let optional = spell
216        .attributes
217        .get(DO_NOT_FAIL_IF_NO_TARGET_WORD)
218        .is_some_and(|word| word & DO_NOT_FAIL_IF_NO_TARGET_MASK != 0);
219
220    SpellTargetMasks {
221        explicit,
222        required: if optional {
223            SpellCastTargetFlags::empty()
224        } else {
225            explicit
226        },
227    }
228}
229
230/// Calculate Trinity-style explicit and required masks from effect A/B selectors and authored flags.
231#[must_use]
232pub fn spell_target_masks(spell: &SpellDataFlat) -> SpellTargetMasks {
233    let mut locations = LocationState::default();
234    let mut masks = authored_masks(spell);
235
236    for effect in spell.effects.iter().filter(|effect| effect.effect != 0) {
237        let mut target_mask = effect_target_mask(
238            effect.implicit_target_a,
239            effect.implicit_target_b,
240            &mut locations,
241        );
242        let missing = missing_effect_mask(effect, target_mask, locations);
243
244        target_mask |= strip_unreachable_fallbacks(spell, missing);
245        masks.explicit |= target_mask;
246        let attributes = SpellEffectAttributes::from_dbc(effect.effect_attributes);
247
248        if !attributes.contains(SpellEffectAttributes::DONT_FAIL_SPELL_ON_TARGETING_FAILURE) {
249            masks.required |= target_mask;
250        }
251    }
252
253    masks
254}
255
256#[cfg(test)]
257mod tests {
258    use googletest::prelude::*;
259
260    use super::*;
261
262    fn spell_with_target(effect: i32, target: i32) -> SpellDataFlat {
263        let mut spell = SpellDataFlat::default();
264
265        spell.effects.push(SpellEffect {
266            effect,
267            implicit_target_a: target,
268            ..SpellEffect::default()
269        });
270
271        spell
272    }
273
274    #[gtest]
275    fn caster_selector_does_not_require_an_explicit_target() {
276        expect_that!(
277            spell_target_masks(&spell_with_target(6, 1)),
278            eq(SpellTargetMasks::default())
279        );
280    }
281
282    #[gtest]
283    fn enemy_selector_requires_a_hostile_unit() {
284        let masks = spell_target_masks(&spell_with_target(2, 6));
285
286        expect_that!(masks.explicit, eq(SpellCastTargetFlags::UNIT_ENEMY));
287        expect_that!(masks.required, eq(SpellCastTargetFlags::UNIT_ENEMY));
288    }
289}