wowlab_engine_combat/state/combat_state/
spatial.rs1#[cfg(test)]
2use super::SpatialActor;
3use super::{
4 ActorId, CombatState, EnemyIdx, EnemyState, PositionedActorRef, SpatialQuery,
5 SpatialQueryError, SpatialTransform, SpellIdx,
6};
7
8impl CombatState {
9 #[must_use]
11 pub fn actor_transform(&self, actor: ActorId) -> Option<SpatialTransform> {
12 match actor {
13 ActorId::Player | ActorId::External | ActorId::Pet(_) => {
14 Some(self.runtime.player_transform)
15 }
16 ActorId::Enemy(enemy) => self.enemy(enemy).map(EnemyState::current_transform),
17 }
18 }
19
20 #[must_use]
22 pub fn has_line_of_sight(&self, source: ActorId, target: EnemyIdx) -> bool {
23 let Some(source_transform) = self.actor_transform(source) else {
24 return false;
25 };
26 let Some(target_transform) = self.actor_transform(ActorId::Enemy(target)) else {
27 return false;
28 };
29
30 if source_transform.layer != target_transform.layer {
31 return false;
32 }
33
34 let source_ref = match source {
35 ActorId::Enemy(enemy) => PositionedActorRef::Enemy(enemy),
36 ActorId::Player | ActorId::External | ActorId::Pet(_) => PositionedActorRef::Player,
37 };
38 let target_ref = PositionedActorRef::Enemy(target);
39 let (a, b) = if source_ref <= target_ref {
40 (source_ref, target_ref)
41 } else {
42 (target_ref, source_ref)
43 };
44
45 if self
46 .config
47 .encounter
48 .definition()
49 .blocked_los_pairs
50 .iter()
51 .any(|pair| pair.a == a && pair.b == b)
52 {
53 return false;
54 }
55
56 self.spatial.segment_visible(
57 source_transform.layer,
58 source_transform.position,
59 target_transform.position,
60 )
61 }
62
63 pub fn relocate_actor(
69 &mut self,
70 actor: PositionedActorRef,
71 transform: SpatialTransform,
72 ) -> Result<(), SpatialQueryError> {
73 self.spatial.validate_transform(transform)?;
74
75 match actor {
76 PositionedActorRef::Player => {
77 self.runtime.player_transform = transform;
78
79 Ok(())
80 }
81 PositionedActorRef::Enemy(enemy) => {
82 let current = self
83 .enemy(enemy)
84 .filter(|enemy| enemy.is_active() && enemy.is_alive())
85 .map(EnemyState::current_transform)
86 .ok_or_else(|| SpatialQueryError::missing_actor(enemy))?;
87
88 self.spatial.relocate_actor(enemy, current, transform)?;
89 let Some(actor_state) = self.enemy_mut(enemy) else {
90 let _ = self.spatial.relocate_actor(enemy, transform, current);
91
92 return Err(SpatialQueryError::missing_actor(enemy));
93 };
94
95 actor_state.commit_relocation(transform);
96
97 Ok(())
98 }
99 }
100 }
101
102 #[must_use]
103 pub fn spatial_query(&self) -> &dyn SpatialQuery {
104 &*self.spatial
105 }
106
107 #[must_use]
109 pub fn spell_target_in_range(&self, spell_id: SpellIdx, target: EnemyIdx) -> bool {
110 self.spell_target_in_range_with_maximum(
111 spell_id,
112 target,
113 self.config
114 .game_data
115 .hostile_max_range(spell_id)
116 .unwrap_or(0.0),
117 )
118 }
119
120 #[cfg(test)]
121 pub(crate) fn remove_spatial_actor_for_test(
122 &mut self,
123 enemy: EnemyIdx,
124 ) -> Result<(), SpatialQueryError> {
125 let transform = self
126 .enemy(enemy)
127 .map(EnemyState::current_transform)
128 .ok_or_else(|| SpatialQueryError::missing_actor(enemy))?;
129
130 self.spatial.remove_actor(SpatialActor {
131 id: enemy,
132 transform,
133 })
134 }
135
136 pub(crate) fn spell_target_in_range_with_buffs(
137 &self,
138 buf: &wowlab_engine_domain::rotation::DenseBuffer,
139 spell_id: SpellIdx,
140 target: EnemyIdx,
141 ) -> bool {
142 let maximum = crate::systems::active_targeting_value(
143 crate::context::ActorView::new(self, buf, ActorId::Player),
144 spell_id,
145 wowlab_engine_domain::dbc::ModifierPropertyKind::Range,
146 self.config
147 .game_data
148 .hostile_max_range(spell_id)
149 .unwrap_or(0.0),
150 );
151
152 self.spell_target_in_range_with_maximum(spell_id, target, maximum)
153 }
154
155 fn spell_target_in_range_with_maximum(
156 &self,
157 spell_id: SpellIdx,
158 target: EnemyIdx,
159 maximum: f64,
160 ) -> bool {
161 if !self.is_valid_target(target) {
162 return false;
163 }
164
165 let Some(source) = self.actor_transform(ActorId::Player) else {
166 return false;
167 };
168 let Some(target_transform) = self.actor_transform(ActorId::Enemy(target)) else {
169 return false;
170 };
171
172 if source.layer != target_transform.layer {
173 return false;
174 }
175
176 let distance = source.position.distance(target_transform.position);
177 let minimum = self
178 .config
179 .game_data
180 .hostile_min_range(spell_id)
181 .unwrap_or(0.0);
182 let in_distance = distance + wowlab_types::sim::GEOMETRY_EPSILON >= minimum
183 && distance <= maximum + wowlab_types::sim::GEOMETRY_EPSILON;
184
185 in_distance
186 && (self
187 .config
188 .game_data
189 .ignores_line_of_sight(spell_id)
190 .unwrap_or(false)
191 || self.has_line_of_sight(ActorId::Player, target))
192 }
193}