wowlab_engine_combat/state/runtime/
spatial.rs1#[cfg(test)]
2use super::SpatialActor;
3#[cfg(test)]
4use super::SpellIdx;
5use super::{
6 ActorId, CombatInstance, EnemyIdx, EnemyState, PositionedActorRef, SimTime, SpatialQuery,
7 SpatialQueryError, SpatialTransform,
8};
9#[cfg(feature = "rotation-optimizations")]
10use super::{RotationMutationDirection, RotationSemanticMutation};
11use crate::state::LocalSpellIdx;
12
13impl CombatInstance {
14 #[must_use]
16 pub fn actor_transform(&self, actor: ActorId) -> Option<SpatialTransform> {
17 match actor {
18 ActorId::Player | ActorId::External | ActorId::Pet(_) => {
19 Some(self.runtime.player_transform)
20 }
21 ActorId::Enemy(enemy) => self.enemy(enemy).map(EnemyState::current_transform),
22 }
23 }
24
25 #[must_use]
27 pub fn has_line_of_sight(
28 &self,
29 program: &crate::state::CombatProgram,
30 source: ActorId,
31 target: EnemyIdx,
32 ) -> bool {
33 let Some(source_transform) = self.actor_transform(source) else {
34 return false;
35 };
36 let Some(target_transform) = self.actor_transform(ActorId::Enemy(target)) else {
37 return false;
38 };
39
40 if source_transform.layer != target_transform.layer {
41 return false;
42 }
43
44 let source_ref = match source {
45 ActorId::Enemy(enemy) => PositionedActorRef::Enemy(enemy),
46 ActorId::Player | ActorId::External | ActorId::Pet(_) => PositionedActorRef::Player,
47 };
48 let target_ref = PositionedActorRef::Enemy(target);
49 let (a, b) = if source_ref <= target_ref {
50 (source_ref, target_ref)
51 } else {
52 (target_ref, source_ref)
53 };
54
55 if program
56 .config
57 .encounter
58 .definition()
59 .blocked_los_pairs
60 .iter()
61 .any(|pair| pair.a == a && pair.b == b)
62 {
63 return false;
64 }
65
66 self.spatial.segment_visible(
67 source_transform.layer,
68 source_transform.position,
69 target_transform.position,
70 )
71 }
72
73 pub fn relocate_actor(
79 &mut self,
80 actor: PositionedActorRef,
81 transform: SpatialTransform,
82 at: SimTime,
83 ) -> Result<(), SpatialQueryError> {
84 self.spatial.validate_transform(transform)?;
85
86 #[cfg(feature = "rotation-optimizations")]
87 let before = self.actor_transform(match actor {
88 PositionedActorRef::Player => ActorId::Player,
89 PositionedActorRef::Enemy(enemy) => ActorId::Enemy(enemy),
90 });
91
92 let result = match actor {
93 PositionedActorRef::Player => {
94 self.runtime.player_transform = transform;
95 self.mark_projection_dirty(
96 super::ProjectionDirty::TARGET_IDENTITY_RANGE_TRAVEL,
97 at,
98 );
99
100 Ok(())
101 }
102 PositionedActorRef::Enemy(enemy) => {
103 let current = self
104 .enemy(enemy)
105 .filter(|enemy| enemy.is_active() && enemy.is_alive())
106 .map(EnemyState::current_transform)
107 .ok_or_else(|| SpatialQueryError::missing_actor(enemy))?;
108
109 self.spatial.relocate_actor(enemy, current, transform)?;
110 let Some(actor_state) = self.enemy_mut(enemy) else {
111 let _ = self.spatial.relocate_actor(enemy, transform, current);
112
113 return Err(SpatialQueryError::missing_actor(enemy));
114 };
115
116 actor_state.commit_relocation(transform);
117
118 if self.current_target() == Some(enemy) {
119 self.mark_projection_dirty(
120 super::ProjectionDirty::TARGET_IDENTITY_RANGE_TRAVEL,
121 at,
122 );
123 }
124
125 Ok(())
126 }
127 };
128
129 #[cfg(feature = "rotation-optimizations")]
130 if result.is_ok() && before != Some(transform) {
131 self.runtime.rotation_mutations.record_semantic(
132 RotationSemanticMutation::Spatial,
133 RotationMutationDirection::Rise,
134 at,
135 );
136 self.runtime.rotation_mutations.record_semantic(
137 RotationSemanticMutation::Spatial,
138 RotationMutationDirection::Fall,
139 at,
140 );
141 }
142
143 result
144 }
145
146 #[must_use]
148 pub fn spell_target_in_range(
149 &self,
150 program: &crate::state::CombatProgram,
151 spell: LocalSpellIdx,
152 target: EnemyIdx,
153 ) -> bool {
154 let Some(spell) = program.resolve_spell_identity(spell) else {
155 return false;
156 };
157
158 self.spell_target_in_range_profile(
159 program,
160 program.rotation.cast_profile_ref(spell),
161 target,
162 )
163 }
164
165 #[must_use]
166 pub(crate) fn spatial_query(&self) -> &dyn SpatialQuery {
167 &*self.spatial
168 }
169
170 #[cfg(test)]
171 pub(crate) fn remove_spatial_actor_for_test(
172 &mut self,
173 enemy: EnemyIdx,
174 ) -> Result<(), SpatialQueryError> {
175 let transform = self
176 .enemy(enemy)
177 .map(EnemyState::current_transform)
178 .ok_or_else(|| SpatialQueryError::missing_actor(enemy))?;
179
180 self.spatial.remove_actor(SpatialActor {
181 id: enemy,
182 transform,
183 })
184 }
185
186 #[cfg(test)]
187 pub(crate) fn spell_target_in_range_with_buffs(
188 &self,
189 program: &crate::state::CombatProgram,
190 spell_id: SpellIdx,
191 target: EnemyIdx,
192 ) -> bool {
193 let Some(local) = program.spell_local(spell_id.as_u32()) else {
194 return false;
195 };
196
197 self.spell_target_in_range_profile_with_buffs(
198 program,
199 program.rotation.cast_profile_ref(local),
200 target,
201 )
202 }
203
204 pub(crate) fn spell_target_in_range_profile(
205 &self,
206 program: &crate::state::CombatProgram,
207 profile: &crate::state::CastProfile,
208 target: EnemyIdx,
209 ) -> bool {
210 self.spell_target_in_range_profile_with_maximum(
211 program,
212 profile,
213 target,
214 profile.hostile_max_range,
215 )
216 }
217
218 pub(crate) fn spell_target_in_range_profile_with_buffs(
219 &self,
220 program: &crate::state::CombatProgram,
221 profile: &crate::state::CastProfile,
222 target: EnemyIdx,
223 ) -> bool {
224 let maximum = crate::systems::active_targeting_value(
225 crate::context::ActorView::new(program, self, ActorId::Player),
226 program.damage.spell_action(profile.local),
227 wowlab_engine_domain::dbc::ModifierPropertyKind::Range,
228 profile.hostile_max_range,
229 );
230
231 self.spell_target_in_range_profile_with_maximum(program, profile, target, maximum)
232 }
233
234 pub(crate) fn spell_target_in_range_profile_with_maximum(
235 &self,
236 program: &crate::state::CombatProgram,
237 profile: &crate::state::CastProfile,
238 target: EnemyIdx,
239 maximum: f64,
240 ) -> bool {
241 if !self.is_valid_target(target) {
242 return false;
243 }
244
245 let Some(source) = self.actor_transform(ActorId::Player) else {
246 return false;
247 };
248 let Some(target_transform) = self.actor_transform(ActorId::Enemy(target)) else {
249 return false;
250 };
251
252 if source.layer != target_transform.layer {
253 return false;
254 }
255
256 let distance = source.position.distance(target_transform.position);
257 let minimum = profile.hostile_min_range;
258 let in_distance = distance + wowlab_types::sim::GEOMETRY_EPSILON >= minimum
259 && distance <= maximum + wowlab_types::sim::GEOMETRY_EPSILON;
260
261 in_distance
262 && (profile.ignores_line_of_sight
263 || self.has_line_of_sight(program, ActorId::Player, target))
264 }
265}