wowlab_engine_combat/context/
mod.rs1use wowlab_engine_domain::rotation::DenseBuffer;
4use wowlab_engine_gamedata::ResolvedGameData;
5use wowlab_engine_ports::{Event, SpecConfig, SpecRuntime};
6use wowlab_engine_telemetry::TelemetrySink;
7use wowlab_types::sim::{ActorId, EnemyIdx, SimTime, SpellIdx};
8
9use crate::{state::CombatState, systems};
10
11mod aura;
12mod channel;
13pub(crate) mod combat_ctx;
14pub(crate) mod damage;
15pub(crate) mod hook;
16
17pub(crate) use combat_ctx::{ActorView, CombatCtx, CombatCtxRequest, CombatView, HookView};
18pub(crate) use damage::DamageOutcome;
19pub(crate) use hook::{AuraOps, DamageOps};
20
21#[cfg(test)]
22mod tests;
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26#[non_exhaustive]
27pub enum FreeActionSource {
28 ChannelChild,
30}
31
32#[must_use]
34pub struct HookCtx<'a> {
35 pub(crate) state: &'a mut CombatState,
36 pub(crate) buf: &'a mut DenseBuffer,
37 pub(crate) sink: &'a mut TelemetrySink,
38 pub(crate) now: SimTime,
39 pub(crate) source_damage_flags: systems::DamageFlags,
40 pub(crate) source_npc_id: Option<u32>,
41 pub(crate) source: ActorId,
42 pub(crate) target: Option<EnemyIdx>,
43 pub(crate) effect_target: Option<ActorId>,
44 pub(crate) rng: &'a mut dyn FnMut() -> f64,
45 driver_spell_id: u32,
47 empower_rank: u8,
48 free_action_source: Option<FreeActionSource>,
49}
50
51#[must_use]
53pub struct HookCtxServices<'a> {
54 pub state: &'a mut CombatState,
55 pub buf: &'a mut DenseBuffer,
56 pub sink: &'a mut TelemetrySink,
57 pub rng: &'a mut dyn FnMut() -> f64,
58}
59
60impl std::fmt::Debug for HookCtxServices<'_> {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("HookCtxServices")
63 .field("state", self.state)
64 .field("buf", self.buf)
65 .field("sink", self.sink)
66 .field("rng", &"<callback>")
67 .finish()
68 }
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73#[must_use]
74pub struct HookCtxRequest {
75 pub now: SimTime,
76 pub source: ActorId,
77 pub target: Option<EnemyIdx>,
78 source_damage_flags: systems::DamageFlags,
79 source_npc_id: Option<u32>,
80 effect_target: Option<ActorId>,
81}
82
83impl HookCtxRequest {
84 pub const fn targetless(now: SimTime) -> Self {
85 Self {
86 now,
87 source: ActorId::Player,
88 target: None,
89 source_damage_flags: systems::DamageFlags::empty(),
90 source_npc_id: None,
91 effect_target: None,
92 }
93 }
94
95 pub const fn for_target(now: SimTime, target: EnemyIdx) -> Self {
96 Self {
97 now,
98 source: ActorId::Player,
99 target: Some(target),
100 source_damage_flags: systems::DamageFlags::empty(),
101 source_npc_id: None,
102 effect_target: None,
103 }
104 }
105
106 pub const fn for_optional_target(now: SimTime, target: Option<EnemyIdx>) -> Self {
107 Self {
108 now,
109 source: ActorId::Player,
110 target,
111 source_damage_flags: systems::DamageFlags::empty(),
112 source_npc_id: None,
113 effect_target: None,
114 }
115 }
116
117 pub const fn with_source(mut self, source: ActorId) -> Self {
118 self.source = source;
119
120 self
121 }
122
123 pub(crate) const fn with_source_damage_flags(mut self, flags: systems::DamageFlags) -> Self {
124 self.source_damage_flags = flags;
125
126 self
127 }
128
129 pub(crate) const fn with_source_npc_id(mut self, npc_id: Option<u32>) -> Self {
130 self.source_npc_id = npc_id;
131
132 self
133 }
134
135 pub(crate) const fn with_effect_target(mut self, target: ActorId) -> Self {
136 self.effect_target = Some(target);
137
138 self
139 }
140}
141
142impl<'a> HookCtx<'a> {
143 pub fn new(services: HookCtxServices<'a>, request: HookCtxRequest) -> Self {
144 let HookCtxServices {
145 state,
146 buf,
147 sink,
148 rng,
149 } = services;
150
151 Self {
152 state,
153 buf,
154 sink,
155 now: request.now,
156 source_damage_flags: request.source_damage_flags,
157 source_npc_id: request.source_npc_id,
158 source: request.source,
159 target: request.target,
160 effect_target: request.effect_target,
161 rng,
162 driver_spell_id: 0,
163 empower_rank: 0,
164 free_action_source: None,
165 }
166 }
167
168 pub const fn view(&self) -> HookView<'_> {
169 HookView {
170 state: self.state,
171 buf: self.buf,
172 target: self.target,
173 now: self.now,
174 }
175 }
176
177 #[must_use]
179 pub const fn free_action_source(&self) -> Option<FreeActionSource> {
180 self.free_action_source
181 }
182
183 #[must_use]
184 pub const fn source(&self) -> ActorId {
185 self.source
186 }
187
188 #[must_use]
189 pub const fn target(&self) -> Option<EnemyIdx> {
190 self.target
191 }
192
193 #[must_use]
195 pub const fn empower_rank(&self) -> Option<u8> {
196 if self.empower_rank == 0 {
197 None
198 } else {
199 Some(self.empower_rank)
200 }
201 }
202
203 pub fn set_health_fraction(&mut self, fraction: f64) {
205 let actor = self.source;
206 let Some(before) = self
207 .state
208 .set_friendly_actor_health_fraction(actor, fraction)
209 else {
210 return;
211 };
212 let after = self
213 .state
214 .actor_health_fraction(actor, self.now)
215 .unwrap_or(before);
216
217 systems::process_actor_health_change(self.state, self.buf, actor, before, after, self.now);
218 }
219
220 #[must_use]
222 pub fn resolved_spell_effect_targets(
223 &mut self,
224 geometry_effect: (u32, u8),
225 impact_spell_id: u32,
226 max_targets: Option<u8>,
227 ) -> Vec<EnemyIdx> {
228 let Some(anchor) = self.target else {
229 return Vec::new();
230 };
231
232 crate::targeting::resolve_spell_targets(
233 self.state,
234 self.buf,
235 crate::targeting::SpellTargetRequest {
236 source: self.source,
237 source_position: None,
238 anchor,
239 geometry_spell: SpellIdx::from_raw(geometry_effect.0),
240 geometry_effect: geometry_effect.1,
241 impact_spell: SpellIdx::from_raw(impact_spell_id),
242 max_targets,
243 destination: None,
244 },
245 self.now,
246 Some(&mut *self.rng),
247 )
248 .unwrap_or_else(|error| {
249 tracing::error!(
250 geometry_spell_id = geometry_effect.0,
251 geometry_effect = geometry_effect.1,
252 impact_spell_id,
253 %error,
254 "invalid spell target shape in cast hook"
255 );
256
257 Vec::new()
258 })
259 }
260
261 #[inline]
262 #[must_use]
263 pub fn state(&self) -> &CombatState {
264 self.state
265 }
266
267 #[inline]
269 #[must_use]
270 pub fn talent_rank(&self, talent_spell_id: u32) -> u8 {
271 self.state
272 .defs
273 .talent_ranks
274 .get(&talent_spell_id)
275 .copied()
276 .unwrap_or(0)
277 }
278
279 #[inline]
281 #[must_use]
282 pub fn talent_selected(&self, talent_spell_id: u32) -> bool {
283 self.talent_rank(talent_spell_id) > 0
284 }
285
286 #[inline]
288 #[must_use]
289 pub fn selected_talent_proc_chance(&self, talent_spell_id: u32) -> f64 {
290 if !self.talent_selected(talent_spell_id) {
291 return 0.0;
292 }
293
294 self.state
295 .config
296 .game_data
297 .proc_chance(SpellIdx::from_raw(talent_spell_id))
298 .unwrap_or(0.0)
299 }
300
301 #[inline]
302 #[must_use]
303 pub fn spec_config<T>(&self) -> Option<&T>
304 where
305 T: SpecConfig,
306 {
307 self.state.spec_config::<T>()
308 }
309
310 #[inline]
311 #[must_use]
312 pub fn spec_runtime<T>(&self) -> Option<&T>
313 where
314 T: SpecRuntime,
315 {
316 self.state.spec_runtime::<T>()
317 }
318
319 #[inline]
320 pub fn spec_runtime_mut<T>(&mut self) -> Option<&mut T>
321 where
322 T: SpecRuntime,
323 {
324 self.state.spec_runtime_mut::<T>()
325 }
326
327 #[inline]
328 #[must_use]
329 pub fn total_damage(&self) -> f64 {
330 self.state.runtime.total_damage
331 }
332
333 #[inline]
334 #[must_use]
335 pub fn buf(&self) -> &DenseBuffer {
336 self.buf
337 }
338
339 #[inline]
340 pub fn buf_mut(&mut self) -> &mut DenseBuffer {
341 self.buf
342 }
343
344 #[inline]
345 #[must_use]
346 pub fn game_data(&self) -> &ResolvedGameData {
347 &self.state.config.game_data
348 }
349
350 #[must_use]
352 pub const fn driver_spell_id(&self) -> u32 {
353 self.driver_spell_id
354 }
355
356 pub fn rng(&mut self) -> &mut dyn FnMut() -> f64 {
358 self.rng
359 }
360
361 pub(crate) const fn set_source(&mut self, source: ActorId) {
362 self.source = source;
363 }
364
365 pub(crate) const fn with_empower_rank(mut self, rank: u8) -> Self {
366 self.empower_rank = rank;
367
368 self
369 }
370
371 pub(crate) const fn with_free_action_source(mut self, source: FreeActionSource) -> Self {
372 self.free_action_source = Some(source);
373
374 self
375 }
376
377 pub(crate) const fn with_driver_spell(mut self, driver_spell_id: u32) -> Self {
379 self.driver_spell_id = driver_spell_id;
380
381 self
382 }
383
384 pub(crate) fn combat_ctx(&mut self, target: EnemyIdx) -> CombatCtx<'_> {
385 self.combat_ctx_with_source(self.source, target)
386 }
387
388 pub(crate) fn combat_ctx_with_source(
389 &mut self,
390 source: ActorId,
391 target: EnemyIdx,
392 ) -> CombatCtx<'_> {
393 CombatCtx {
394 state: self.state,
395 buf: self.buf,
396 sink: self.sink,
397 now: self.now,
398 rng: self.rng,
399 source,
400 target,
401 }
402 }
403
404 const fn driver_resource_gain_source(&self) -> crate::state::ResourceGainSource {
405 if self.driver_spell_id == 0 {
406 crate::state::ResourceGainSource::Unattributed
407 } else {
408 crate::state::ResourceGainSource::Spell(self.driver_spell_id)
409 }
410 }
411
412 fn schedule_pending_timer(&mut self, delay_ms: u32, pending: crate::state::PendingHookTimer) {
413 let timers = &mut self.state.runtime.pending.pending_hook_timers;
414 let timer_id = if let Some(free) = timers.iter().position(Option::is_none) {
415 timers[free] = Some(pending);
417
418 free
419 } else {
420 timers.push(Some(pending));
421
422 timers.len() - 1
423 };
424 let t = self.now.saturating_add(SimTime::from_millis(delay_ms));
425
426 self.state.schedule(Event::HookTimer {
427 t,
428 timer_id: u32::try_from(timer_id).expect("timer arena length fits in u32"),
429 source: pending.source,
430 target: pending.target,
431 });
432 }
433}
434impl std::fmt::Debug for HookCtx<'_> {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 f.debug_struct("HookCtx")
437 .field("now", &self.now)
438 .field("source_damage_flags", &self.source_damage_flags)
439 .field("source_npc_id", &self.source_npc_id)
440 .field("source", &self.source)
441 .field("target", &self.target)
442 .finish_non_exhaustive()
443 }
444}