wowlab_engine_domain/dbc/semantics/
spell_targets.rs1use 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#[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#[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}