1use wowlab_engine_domain::rotation::BoundDenseBuffer;
4use wowlab_engine_ports::{Event, SpecConfig, SpecRuntime};
5use wowlab_engine_telemetry::TelemetrySink;
6use wowlab_types::sim::{ActorId, EnemyIdx, SimTime};
7
8use crate::{
9 state::{CombatInstance, CombatProgram},
10 systems,
11};
12
13mod aura;
14mod channel;
15pub(crate) mod combat_ctx;
16pub(crate) mod damage;
17pub(crate) mod hook;
18
19pub(crate) use combat_ctx::{ActorView, CombatCtx, CombatCtxRequest, CombatView, HookView};
20pub(crate) use damage::DamageOutcome;
21pub(crate) use hook::{AuraOps, DamageOps};
22
23#[cfg(test)]
24mod tests;
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28#[non_exhaustive]
29pub enum FreeActionSource {
30 ChannelChild,
32}
33
34#[must_use]
36pub struct HookCtx<'a> {
37 pub(crate) program: &'a CombatProgram,
38 pub(crate) instance: &'a mut CombatInstance,
39 pub(crate) sink: &'a mut TelemetrySink,
40 pub(crate) now: SimTime,
41 pub(crate) source_damage_flags: systems::DamageFlags,
42 pub(crate) source_npc_id: Option<u32>,
43 pub(crate) pet_stat_inheritance: crate::state::PetStatInheritance,
44 pub(crate) source: ActorId,
45 pub(crate) target: Option<EnemyIdx>,
46 pub(crate) effect_target: Option<ActorId>,
47 pub(crate) rng: &'a mut dyn FnMut() -> f64,
48 driver_spell_id: u32,
50 empower_rank: u8,
51 free_action_source: Option<FreeActionSource>,
52}
53
54#[must_use]
56pub struct HookCtxServices<'a> {
58 #[cfg(feature = "test-support")]
59 #[doc(hidden)]
60 pub program: &'a CombatProgram,
61 #[cfg(not(feature = "test-support"))]
62 pub(crate) program: &'a CombatProgram,
63 #[cfg(feature = "test-support")]
64 #[doc(hidden)]
65 pub instance: &'a mut CombatInstance,
66 #[cfg(not(feature = "test-support"))]
67 pub(crate) instance: &'a mut CombatInstance,
68 #[cfg(feature = "test-support")]
69 #[doc(hidden)]
70 pub sink: &'a mut TelemetrySink,
71 #[cfg(not(feature = "test-support"))]
72 pub(crate) sink: &'a mut TelemetrySink,
73 #[cfg(feature = "test-support")]
74 #[doc(hidden)]
75 pub rng: &'a mut dyn FnMut() -> f64,
76 #[cfg(not(feature = "test-support"))]
77 pub(crate) rng: &'a mut dyn FnMut() -> f64,
78}
79
80impl std::fmt::Debug for HookCtxServices<'_> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("HookCtxServices")
83 .field("program", self.program)
84 .field("instance", self.instance)
85 .field("sink", self.sink)
86 .field("rng", &"<callback>")
87 .finish()
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq)]
93#[must_use]
94pub struct HookCtxRequest {
95 pub now: SimTime,
96 pub source: ActorId,
97 pub target: Option<EnemyIdx>,
98 source_damage_flags: systems::DamageFlags,
99 source_npc_id: Option<u32>,
100 pet_stat_inheritance: crate::state::PetStatInheritance,
101 effect_target: Option<ActorId>,
102}
103
104impl HookCtxRequest {
105 pub const fn targetless(now: SimTime) -> Self {
106 Self {
107 now,
108 source: ActorId::Player,
109 target: None,
110 source_damage_flags: systems::DamageFlags::empty(),
111 source_npc_id: None,
112 pet_stat_inheritance: crate::state::PetStatInheritance::IDENTITY,
113 effect_target: None,
114 }
115 }
116
117 pub const fn for_target(now: SimTime, target: EnemyIdx) -> Self {
118 Self {
119 now,
120 source: ActorId::Player,
121 target: Some(target),
122 source_damage_flags: systems::DamageFlags::empty(),
123 source_npc_id: None,
124 pet_stat_inheritance: crate::state::PetStatInheritance::IDENTITY,
125 effect_target: None,
126 }
127 }
128
129 pub const fn for_optional_target(now: SimTime, target: Option<EnemyIdx>) -> Self {
130 Self {
131 now,
132 source: ActorId::Player,
133 target,
134 source_damage_flags: systems::DamageFlags::empty(),
135 source_npc_id: None,
136 pet_stat_inheritance: crate::state::PetStatInheritance::IDENTITY,
137 effect_target: None,
138 }
139 }
140
141 pub const fn with_source(mut self, source: ActorId) -> Self {
142 self.source = source;
143
144 self
145 }
146
147 pub(crate) const fn with_source_damage_flags(mut self, flags: systems::DamageFlags) -> Self {
148 self.source_damage_flags = flags;
149
150 self
151 }
152
153 pub(crate) const fn with_source_npc_id(mut self, npc_id: Option<u32>) -> Self {
154 self.source_npc_id = npc_id;
155
156 self
157 }
158
159 pub(crate) const fn with_pet_stat_inheritance(
160 mut self,
161 inheritance: crate::state::PetStatInheritance,
162 ) -> Self {
163 self.pet_stat_inheritance = inheritance;
164
165 self
166 }
167
168 pub(crate) const fn with_effect_target(mut self, target: ActorId) -> Self {
169 self.effect_target = Some(target);
170
171 self
172 }
173}
174
175impl<'a> HookCtx<'a> {
176 pub fn new(services: HookCtxServices<'a>, request: HookCtxRequest) -> Self {
177 let HookCtxServices {
178 program,
179 instance,
180 sink,
181 rng,
182 } = services;
183
184 Self {
185 program,
186 instance,
187 sink,
188 now: request.now,
189 source_damage_flags: request.source_damage_flags,
190 source_npc_id: request.source_npc_id,
191 pet_stat_inheritance: request.pet_stat_inheritance,
192 source: request.source,
193 target: request.target,
194 effect_target: request.effect_target,
195 rng,
196 driver_spell_id: 0,
197 empower_rank: 0,
198 free_action_source: None,
199 }
200 }
201
202 #[cfg(feature = "test-support")]
204 #[doc(hidden)]
205 #[must_use]
206 pub fn __test_impact_event(
207 &self,
208 action: crate::DamageActionId,
209 data: crate::ImpactEventData,
210 ) -> Option<crate::ImpactEvent> {
211 let local = self.program.damage.generated_action(action)?;
212
213 Some(crate::ImpactEvent::bound(action, local, data))
214 }
215
216 pub const fn view(&self) -> HookView<'_> {
217 HookView {
218 program: self.program,
219 instance: self.instance,
220 target: self.target,
221 now: self.now,
222 }
223 }
224
225 #[must_use]
227 pub const fn free_action_source(&self) -> Option<FreeActionSource> {
228 self.free_action_source
229 }
230
231 #[must_use]
232 pub const fn source(&self) -> ActorId {
233 self.source
234 }
235
236 #[must_use]
237 pub const fn target(&self) -> Option<EnemyIdx> {
238 self.target
239 }
240
241 #[must_use]
243 pub const fn empower_rank(&self) -> Option<u8> {
244 if self.empower_rank == 0 {
245 None
246 } else {
247 Some(self.empower_rank)
248 }
249 }
250
251 pub fn set_health_fraction(&mut self, fraction: f64) {
253 let actor = self.source;
254 let Some(before) = self
255 .instance
256 .set_friendly_actor_health_fraction(actor, fraction)
257 else {
258 return;
259 };
260 let after = self
261 .instance
262 .actor_health_fraction(self.program, actor, self.now)
263 .unwrap_or(before);
264
265 systems::process_actor_health_change(
266 self.program,
267 self.instance,
268 actor,
269 before,
270 after,
271 self.now,
272 );
273 }
274
275 pub fn with_resolved_spell_effect_targets<T>(
279 &mut self,
280 geometry_effect: crate::DamageEffectId,
281 impact_action: crate::DamageActionId,
282 max_targets: Option<u8>,
283 use_targets: impl FnOnce(&mut Self, &[EnemyIdx]) -> T,
284 ) -> T {
285 let Some(anchor) = self.target else {
286 return use_targets(self, &[]);
287 };
288 let geometry_token = geometry_effect;
289 let Some((geometry_effect, planned_geometry)) =
290 self.program.damage.effect_id_plan(geometry_token)
291 else {
292 self.instance.record_runtime_error(
293 wowlab_engine_ports::SpecRuntimeError::unresolved_damage_identity(
294 geometry_token.action().spell_id(),
295 geometry_token.effect_index(),
296 ),
297 );
298
299 return use_targets(self, &[]);
300 };
301 let Some(impact_action) = self
302 .program
303 .damage
304 .action_id_request(impact_action)
305 .map(crate::state::program::DamageRequestBinding::effect_action)
306 else {
307 self.instance.record_runtime_error(
308 wowlab_engine_ports::SpecRuntimeError::unresolved_damage_identity(
309 impact_action.spell_id(),
310 0,
311 ),
312 );
313
314 return use_targets(self, &[]);
315 };
316 let Some(target) = planned_geometry.target else {
317 self.instance.record_runtime_error(
318 wowlab_engine_ports::SpecRuntimeError::unresolved_damage_identity(
319 geometry_token.action().spell_id(),
320 geometry_token.effect_index(),
321 ),
322 );
323
324 return use_targets(self, &[]);
325 };
326 let Some(mut workspace) = crate::targeting::take_target_workspace(self.instance) else {
327 self.instance.record_runtime_error(
328 wowlab_engine_ports::SpecRuntimeError::damage_target_resolution(
329 geometry_token.action().spell_id(),
330 geometry_token.effect_index(),
331 crate::targeting::TARGET_WORKSPACE_EXHAUSTED.to_string(),
332 ),
333 );
334
335 return use_targets(self, &[]);
336 };
337
338 let resolution = crate::targeting::resolve_spell_targets_into(
339 self.program,
340 self.instance,
341 crate::targeting::SpellTargetRequest {
342 source: self.source,
343 source_position: None,
344 anchor,
345 geometry_effect,
346 impact_action,
347 target,
348 max_targets,
349 destination: None,
350 },
351 self.now,
352 Some(&mut *self.rng),
353 &mut workspace,
354 );
355
356 if let Err(error) = resolution {
357 workspace.clear();
358 self.instance.record_runtime_error(
359 wowlab_engine_ports::SpecRuntimeError::damage_target_resolution(
360 geometry_token.action().spell_id(),
361 geometry_token.effect_index(),
362 error.to_string(),
363 ),
364 );
365 }
366
367 let value = use_targets(self, workspace.targets());
368
369 crate::targeting::restore_target_workspace(self.instance, workspace);
370
371 value
372 }
373
374 #[inline]
375 #[must_use]
376 pub fn program(&self) -> combat_ctx::HookProgramView<'_> {
377 combat_ctx::HookProgramView {
378 program: self.program,
379 }
380 }
381
382 #[inline]
384 #[must_use]
385 pub fn buff_totals(&self) -> crate::BuffTotals {
386 systems::buff_totals(self.program, self.instance, self.target)
387 }
388
389 #[inline]
390 #[must_use]
391 pub fn instance(&self) -> &CombatInstance {
392 self.instance
393 }
394
395 #[inline]
397 #[must_use]
398 pub fn talent_rank(&self, talent: crate::DamageActionId) -> u8 {
399 self.program.talent_rank(talent)
400 }
401
402 #[inline]
404 #[must_use]
405 pub fn talent_selected(&self, talent: crate::DamageActionId) -> bool {
406 self.talent_rank(talent) > 0
407 }
408
409 #[inline]
410 #[must_use]
411 pub fn spec_config<T>(&self) -> Option<&T>
412 where
413 T: SpecConfig,
414 {
415 self.program.spec_config::<T>()
416 }
417
418 #[inline]
419 #[must_use]
420 pub fn spec_runtime<T>(&self) -> Option<&T>
421 where
422 T: SpecRuntime,
423 {
424 self.instance.spec_runtime::<T>()
425 }
426
427 #[inline]
428 pub fn spec_runtime_mut<T>(&mut self) -> Option<&mut T>
429 where
430 T: SpecRuntime,
431 {
432 self.instance.spec_runtime_mut::<T>()
433 }
434
435 #[inline]
436 #[must_use]
437 pub fn total_damage(&self) -> f64 {
438 self.instance.runtime.total_damage
439 }
440
441 #[inline]
442 #[must_use]
443 pub fn buf(&self) -> &BoundDenseBuffer {
444 &self.instance.buffer
445 }
446
447 #[must_use]
449 pub const fn driver_spell_id(&self) -> u32 {
450 self.driver_spell_id
451 }
452
453 pub fn rng(&mut self) -> &mut dyn FnMut() -> f64 {
455 self.rng
456 }
457
458 pub(crate) const fn set_source(&mut self, source: ActorId) {
459 self.source = source;
460 }
461
462 pub(crate) const fn with_empower_rank(mut self, rank: u8) -> Self {
463 self.empower_rank = rank;
464
465 self
466 }
467
468 pub(crate) const fn with_free_action_source(mut self, source: FreeActionSource) -> Self {
469 self.free_action_source = Some(source);
470
471 self
472 }
473
474 pub(crate) const fn with_driver_spell(mut self, driver_spell_id: u32) -> Self {
476 self.driver_spell_id = driver_spell_id;
477
478 self
479 }
480
481 pub(crate) fn combat_ctx(&mut self, target: EnemyIdx) -> CombatCtx<'_> {
482 self.combat_ctx_with_source(self.source, target)
483 }
484
485 pub(crate) fn combat_ctx_with_source(
486 &mut self,
487 source: ActorId,
488 target: EnemyIdx,
489 ) -> CombatCtx<'_> {
490 CombatCtx {
491 program: self.program,
492 instance: self.instance,
493 sink: self.sink,
494 now: self.now,
495 rng: self.rng,
496 source,
497 target,
498 }
499 }
500
501 fn spell_identity(&self, local: crate::LocalSpellIdx) -> crate::LocalSpellIdx {
502 self.program
503 .resolve_spell_identity(local)
504 .expect("foreign specialization spell binding")
505 }
506
507 fn aura_identity(&self, local: crate::LocalAuraIdx) -> crate::LocalAuraIdx {
508 self.program
509 .resolve_aura_identity(local)
510 .expect("foreign specialization aura binding")
511 }
512
513 const fn driver_resource_gain_source(&self) -> crate::state::ResourceGainSource {
514 if self.driver_spell_id == 0 {
515 crate::state::ResourceGainSource::Unattributed
516 } else {
517 crate::state::ResourceGainSource::Spell(self.driver_spell_id)
518 }
519 }
520
521 fn schedule_pending_timer(&mut self, delay_ms: u32, pending: crate::state::PendingHookTimer) {
522 let timers = &mut self.instance.runtime.pending.pending_hook_timers;
523 let timer_id = if let Some(free) = timers.iter().position(Option::is_none) {
524 timers[free] = Some(pending);
526
527 free
528 } else {
529 timers.push(Some(pending));
530 self.instance.runtime.pending.hook_timer_generations.push(0);
531
532 timers.len() - 1
533 };
534 let generation = self
535 .instance
536 .runtime
537 .pending
538 .hook_timer_generations
539 .get_mut(timer_id)
540 .expect("hook timer generation parallels its arena slot");
541
542 *generation = generation.wrapping_add(1).max(1);
543 let timer = self.program.runtime_events.hook_timer(
544 u32::try_from(timer_id).expect("timer arena length fits in u32"),
545 *generation,
546 );
547 let t = self.now.saturating_add(SimTime::from_millis(delay_ms));
548
549 self.instance.schedule(Event::HookTimer {
550 t,
551 timer,
552 source: pending.source,
553 target: pending.target,
554 });
555 }
556}
557impl std::fmt::Debug for HookCtx<'_> {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559 f.debug_struct("HookCtx")
560 .field("now", &self.now)
561 .field("source_damage_flags", &self.source_damage_flags)
562 .field("source_npc_id", &self.source_npc_id)
563 .field("source", &self.source)
564 .field("target", &self.target)
565 .finish_non_exhaustive()
566 }
567}