1#[cfg(feature = "rotation-optimizations")]
8use wowlab_types::combat::ResourceType;
9use wowlab_types::sim::PetIdx;
10
11use super::{
12 AccumulatedRng, ActorId, DenseAuraCooldownPool, DenseDamagePool, DenseExpiryPool,
13 DenseRollingTickPool, DenseSpellGatePool, EncounterRuntime, EnemyIdx, EnemyState, Event,
14 GuardianInstance, HealthPool, ImpactEffectProc, IncomingImmunityWindow, IntMap, LocalSpellIdx,
15 PendingHookTimer, PendingSpellImpact, PetActionState, RppmTracker, RuneState, ScratchBuffers,
16 ShuffledRng, SimTime, SpatialTransform, SpecRuntime, SpecRuntimeError, ThresholdTracker,
17};
18use crate::state::{
19 ActiveCast, AttackPosition, ControlDeadline, CrowdControlKind, DiminishingGroup,
20 DiminishingState, ImpactChanceScale, MovementState, SchoolLockout,
21};
22
23#[derive(Clone, Copy, Debug)]
24pub(crate) struct ActiveChannelIdentity {
25 pub(crate) local: LocalSpellIdx,
26 pub(crate) spell: wowlab_types::sim::SpellIdx,
27 pub(crate) handle: wowlab_engine_domain::rotation::SpellSlotHandle,
28 pub(crate) tick_cost_aura: Option<crate::state::CastAuraRequirement>,
29 pub(crate) tick_damage_aura: Option<crate::state::CastAuraRequirement>,
30}
31
32#[derive(Debug)]
34#[cfg_attr(
35 not(feature = "test-support"),
36 expect(
37 unreachable_pub,
38 reason = "the type is named only by the explicit test-support capability"
39 )
40)]
41pub struct CombatRuntime {
42 pub(super) spec_runtime: Box<dyn SpecRuntime>,
43 pub(super) enemies: Vec<EnemyState>,
44 pub(crate) encounter: EncounterRuntime,
45 pub(crate) player_transform: SpatialTransform,
46 pub(crate) attack_position: AttackPosition,
47 #[cfg(feature = "test-support")]
48 #[doc(hidden)]
49 pub current_target: Option<EnemyIdx>,
50 #[cfg(not(feature = "test-support"))]
51 pub(crate) current_target: Option<EnemyIdx>,
52 pub(crate) health: RuntimeHealth,
53 pub(super) runtime_error: Option<SpecRuntimeError>,
54 pub(crate) total_damage: f64,
55 pub(crate) pending_events: Vec<Event>,
56 #[cfg(feature = "rotation-optimizations")]
57 pub(crate) rotation_mutations: RotationMutationAccumulator,
58 pub(crate) resources: RuntimeResources,
59 pub(crate) casting: RuntimeCasting,
60 pub(crate) control: RuntimeControl,
61 pub(crate) scratch: ScratchBuffers,
62 pub(crate) deferred_work: crate::state::DeferredWorkQueue,
63 pub(crate) procs: RuntimeProcs,
64 pub(crate) mask_reasons:
68 Option<IntMap<u32, std::mem::Discriminant<crate::handler::can_cast::CastReject>>>,
69 #[cfg(test)]
70 pub(crate) rotation_spells_scanned: usize,
71 pub(crate) recharge_rate_mults: Vec<f64>,
72 #[cfg(feature = "rotation-optimizations")]
73 pub(crate) cooldown_duration_mults: Vec<f64>,
74 #[cfg(feature = "rotation-optimizations")]
75 pub(crate) initial_recharge_rate_mults: Vec<f64>,
76 #[cfg(feature = "rotation-optimizations")]
77 pub(crate) initial_cooldown_duration_mults: Vec<f64>,
78 pub(crate) pools: RuntimePools,
79 pub(crate) pending: RuntimePending,
80 pub(crate) companions: RuntimeCompanions,
81}
82
83#[derive(Debug)]
84pub(crate) struct RuntimeHealth {
85 pub(crate) player_health: HealthPool,
86 pub(crate) pet_health: Vec<Option<HealthPool>>,
87 pub(crate) immunity_windows: Vec<IncomingImmunityWindow>,
88 pub(crate) active_absorbs: crate::state::BoundedVec<crate::state::ActiveAbsorb>,
89 pub(crate) next_absorb_application_order: u64,
90 pub(crate) enemy_swing_deadlines: Vec<Option<SimTime>>,
91 pub(crate) enemy_invulnerable_until: Vec<SimTime>,
92}
93
94#[derive(Debug)]
95pub(crate) struct RuntimeResources {
96 pub(crate) last_primary_spent: f64,
97 pub(crate) last_optional_primary_spent: f64,
98 pub(crate) last_secondary_spent: f64,
99 pub(crate) primary_base_regen_override: Option<f64>,
100 pub(crate) runes: RuneState,
101}
102
103#[derive(Debug)]
104pub(crate) struct RuntimeCasting {
105 pub(crate) selected_candidate: Option<crate::handler::can_cast::CastCandidate>,
106 pub(crate) active_player_candidate: Option<crate::handler::can_cast::CastCandidate>,
107 pub(crate) instant_cast_pending_at: Option<SimTime>,
108 pub(crate) instant_off_gcd_casts_at: Option<SimTime>,
109 pub(crate) instant_off_gcd_spells: LocalSpellSet,
110 pub(crate) active_channel_target: Option<EnemyIdx>,
111 pub(crate) channel_generation: u64,
112 pub(crate) active_channel_damage_mult: f64,
113 pub(crate) active_channel: Option<ActiveChannelIdentity>,
114 pub(crate) active_channel_last_tick: SimTime,
115 pub(crate) active_channel_tick_interval_ms: u32,
116 pub(crate) active_channel_ticks_remaining: u8,
117 pub(crate) active_channel_on_last_tick: bool,
118 pub(crate) active_channel_pushback_count: u8,
119 pub(crate) free_action_dispatch_depth: u8,
120 pub(crate) channel_chain_spell: Option<LocalSpellIdx>,
121 pub(crate) active_casts: ActorTable<Option<ActiveCast>>,
122}
123
124#[derive(Debug, Default)]
125pub(crate) struct LocalSpellSet {
126 words: [u64; 4],
127}
128
129impl LocalSpellSet {
130 pub(crate) fn clear(&mut self) {
131 self.words.fill(0);
132 }
133
134 pub(crate) fn contains(&self, spell: LocalSpellIdx) -> bool {
135 let index = spell.as_usize();
136
137 self.words[index / u64::BITS as usize] & (1_u64 << (index % u64::BITS as usize)) != 0
138 }
139
140 pub(crate) fn insert(&mut self, spell: LocalSpellIdx) {
141 let index = spell.as_usize();
142
143 self.words[index / u64::BITS as usize] |= 1_u64 << (index % u64::BITS as usize);
144 }
145
146 #[cfg(feature = "rotation-optimizations")]
147 pub(super) fn is_empty(&self) -> bool {
148 self.words.iter().all(|word| *word == 0)
149 }
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154#[cfg(feature = "rotation-optimizations")]
155pub(crate) enum RotationMutationDirection {
156 Rise,
157 Fall,
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162#[cfg(feature = "rotation-optimizations")]
163pub(crate) enum RotationSemanticMutation {
164 AuraState,
165 TargetHealth,
166 Spatial,
167 EncounterPopulation,
168 PlayerHealth,
169 ActorState,
170 SpellState,
171 EquipmentState,
172 HistoryState,
173 AuraTiming,
174 TargetIdentity,
175}
176
177#[cfg(feature = "rotation-optimizations")]
178impl RotationSemanticMutation {
179 const fn bit(self) -> u16 {
180 match self {
181 Self::AuraState => 1 << 0,
182 Self::TargetHealth => 1 << 1,
183 Self::Spatial => 1 << 2,
184 Self::EncounterPopulation => 1 << 3,
185 Self::PlayerHealth => 1 << 5,
186 Self::ActorState => 1 << 6,
187 Self::SpellState => 1 << 7,
188 Self::EquipmentState => 1 << 8,
189 Self::HistoryState => 1 << 9,
190 Self::AuraTiming => 1 << 10,
191 Self::TargetIdentity => 1 << 11,
192 }
193 }
194}
195
196#[derive(Debug, Default)]
198#[cfg(feature = "rotation-optimizations")]
199pub(crate) struct RotationMutationAccumulator {
200 cooldown_rises: LocalSpellSet,
201 cooldown_falls: LocalSpellSet,
202 resource_rises: u32,
203 resource_falls: u32,
204 semantic_rises: u16,
205 semantic_falls: u16,
206 at: SimTime,
207}
208
209#[cfg(feature = "rotation-optimizations")]
210#[cfg_attr(
211 not(test),
212 expect(
213 dead_code,
214 reason = "the accumulator API is consumed by the gated event-wake integration"
215 )
216)]
217impl RotationMutationAccumulator {
218 pub(crate) fn record_cooldown(
219 &mut self,
220 spell: LocalSpellIdx,
221 direction: RotationMutationDirection,
222 at: SimTime,
223 ) {
224 match direction {
225 RotationMutationDirection::Rise => self.cooldown_rises.insert(spell),
226 RotationMutationDirection::Fall => self.cooldown_falls.insert(spell),
227 }
228
229 self.record_time(at);
230 }
231
232 pub(crate) fn record_resource(
233 &mut self,
234 resource: ResourceType,
235 direction: RotationMutationDirection,
236 at: SimTime,
237 ) {
238 let bit = 1_u32 << u8::from(resource);
239
240 match direction {
241 RotationMutationDirection::Rise => self.resource_rises |= bit,
242 RotationMutationDirection::Fall => self.resource_falls |= bit,
243 }
244
245 self.record_time(at);
246 }
247
248 pub(crate) fn contains_cooldown(
249 &self,
250 spell: LocalSpellIdx,
251 direction: RotationMutationDirection,
252 ) -> bool {
253 match direction {
254 RotationMutationDirection::Rise => self.cooldown_rises.contains(spell),
255 RotationMutationDirection::Fall => self.cooldown_falls.contains(spell),
256 }
257 }
258
259 pub(crate) fn contains_resource(
260 &self,
261 resource: ResourceType,
262 direction: RotationMutationDirection,
263 ) -> bool {
264 let bit = 1_u32 << u8::from(resource);
265
266 match direction {
267 RotationMutationDirection::Rise => self.resource_rises & bit != 0,
268 RotationMutationDirection::Fall => self.resource_falls & bit != 0,
269 }
270 }
271
272 pub(crate) fn record_semantic(
273 &mut self,
274 mutation: RotationSemanticMutation,
275 direction: RotationMutationDirection,
276 at: SimTime,
277 ) {
278 match direction {
279 RotationMutationDirection::Rise => self.semantic_rises |= mutation.bit(),
280 RotationMutationDirection::Fall => self.semantic_falls |= mutation.bit(),
281 }
282
283 self.record_time(at);
284 }
285
286 pub(crate) const fn contains_semantic(
287 &self,
288 mutation: RotationSemanticMutation,
289 direction: RotationMutationDirection,
290 ) -> bool {
291 let mutations = match direction {
292 RotationMutationDirection::Rise => self.semantic_rises,
293 RotationMutationDirection::Fall => self.semantic_falls,
294 };
295
296 mutations & mutation.bit() != 0
297 }
298
299 pub(crate) const fn at(&self) -> SimTime {
300 self.at
301 }
302
303 pub(crate) const fn cooldown_rises(&self) -> &[u64; 4] {
304 &self.cooldown_rises.words
305 }
306
307 pub(crate) const fn cooldown_falls(&self) -> &[u64; 4] {
308 &self.cooldown_falls.words
309 }
310
311 pub(crate) const fn resource_rises(&self) -> u32 {
312 self.resource_rises
313 }
314
315 pub(crate) const fn resource_falls(&self) -> u32 {
316 self.resource_falls
317 }
318
319 pub(crate) const fn semantic_rises(&self) -> u16 {
320 self.semantic_rises
321 }
322
323 pub(crate) const fn semantic_falls(&self) -> u16 {
324 self.semantic_falls
325 }
326
327 pub(crate) fn take(&mut self) -> Option<Self> {
328 if self.is_empty() {
329 return None;
330 }
331
332 Some(std::mem::take(self))
333 }
334
335 pub(crate) fn clear(&mut self) {
336 *self = Self::default();
337 }
338
339 fn record_time(&mut self, at: SimTime) {
340 self.at = self.at.max(at);
341 }
342
343 fn is_empty(&self) -> bool {
344 self.cooldown_rises.is_empty()
345 && self.cooldown_falls.is_empty()
346 && self.resource_rises == 0
347 && self.resource_falls == 0
348 && self.semantic_rises == 0
349 && self.semantic_falls == 0
350 }
351}
352
353#[cfg(all(test, feature = "rotation-optimizations"))]
354mod rotation_mutation_tests {
355 use googletest::prelude::*;
356
357 use super::*;
358
359 const FIRST: LocalSpellIdx = LocalSpellIdx::new(1);
360 const SECOND: LocalSpellIdx = LocalSpellIdx::new(65);
361
362 #[gtest]
363 fn repeated_mutations_coalesce_by_identity_and_direction() -> Result<()> {
364 let mut mutations = RotationMutationAccumulator::default();
365
366 mutations.record_cooldown(
367 FIRST,
368 RotationMutationDirection::Rise,
369 SimTime::from_millis(10),
370 );
371 mutations.record_cooldown(
372 FIRST,
373 RotationMutationDirection::Rise,
374 SimTime::from_millis(10),
375 );
376 mutations.record_cooldown(
377 SECOND,
378 RotationMutationDirection::Fall,
379 SimTime::from_millis(10),
380 );
381 mutations.record_resource(
382 ResourceType::Energy,
383 RotationMutationDirection::Rise,
384 SimTime::from_millis(10),
385 );
386 mutations.record_resource(
387 ResourceType::Energy,
388 RotationMutationDirection::Fall,
389 SimTime::from_millis(10),
390 );
391 mutations.record_semantic(
392 RotationSemanticMutation::AuraState,
393 RotationMutationDirection::Rise,
394 SimTime::from_millis(10),
395 );
396
397 verify_that!(
398 mutations.contains_cooldown(FIRST, RotationMutationDirection::Rise),
399 is_true()
400 )?;
401 verify_that!(
402 mutations.contains_cooldown(FIRST, RotationMutationDirection::Fall),
403 is_false()
404 )?;
405 verify_that!(
406 mutations.contains_cooldown(SECOND, RotationMutationDirection::Fall),
407 is_true()
408 )?;
409 verify_that!(
410 mutations.contains_resource(ResourceType::Energy, RotationMutationDirection::Rise),
411 is_true()
412 )?;
413
414 verify_that!(
415 mutations.contains_resource(ResourceType::Energy, RotationMutationDirection::Fall),
416 is_true()
417 )?;
418 verify_that!(
419 mutations.contains_semantic(
420 RotationSemanticMutation::AuraState,
421 RotationMutationDirection::Rise,
422 ),
423 is_true()
424 )?;
425
426 verify_that!(
427 mutations.contains_semantic(
428 RotationSemanticMutation::AuraState,
429 RotationMutationDirection::Fall,
430 ),
431 is_false()
432 )
433 }
434
435 #[gtest]
436 fn mutation_time_is_the_latest_recorded_time() -> Result<()> {
437 let mut mutations = RotationMutationAccumulator::default();
438
439 for at in [90, 40, 120, 110] {
440 mutations.record_resource(
441 ResourceType::ComboPoints,
442 RotationMutationDirection::Rise,
443 SimTime::from_millis(at),
444 );
445 }
446
447 verify_that!(mutations.at(), eq(SimTime::from_millis(120)))
448 }
449
450 #[gtest]
451 fn take_and_clear_restore_the_empty_state() -> Result<()> {
452 let mut mutations = RotationMutationAccumulator::default();
453
454 mutations.record_cooldown(
455 FIRST,
456 RotationMutationDirection::Rise,
457 SimTime::from_millis(15),
458 );
459 let taken = mutations.take().or_fail()?;
460
461 verify_that!(
462 taken.contains_cooldown(FIRST, RotationMutationDirection::Rise),
463 is_true()
464 )?;
465 verify_that!(mutations.take(), none())?;
466 verify_that!(mutations.at(), eq(SimTime::ZERO))?;
467
468 mutations.record_resource(
469 ResourceType::Mana,
470 RotationMutationDirection::Fall,
471 SimTime::from_millis(20),
472 );
473 mutations.clear();
474
475 verify_that!(mutations.take(), none())?;
476
477 verify_that!(mutations.at(), eq(SimTime::ZERO))
478 }
479
480 #[gtest]
481 fn record_take_and_clear_allocate_nothing() -> Result<()> {
482 let mut mutations = RotationMutationAccumulator::default();
483 let (_, allocations) = crate::test_alloc::allocations_during(|| {
484 mutations.record_cooldown(
485 LocalSpellIdx::new(u8::MAX),
486 RotationMutationDirection::Fall,
487 SimTime::from_millis(50),
488 );
489 mutations.record_resource(
490 ResourceType::AlternateMount,
491 RotationMutationDirection::Rise,
492 SimTime::from_millis(60),
493 );
494 mutations.record_semantic(
495 RotationSemanticMutation::EncounterPopulation,
496 RotationMutationDirection::Rise,
497 SimTime::from_millis(70),
498 );
499 let _ = mutations.take();
500
501 mutations.clear();
502 });
503
504 verify_that!(allocations.count, eq(0))?;
505
506 verify_that!(allocations.bytes, eq(0))
507 }
508}
509
510#[derive(Debug)]
511pub(crate) struct RuntimeControl {
512 pub(crate) school_lockouts: ActorTable<Option<SchoolLockout>>,
513 pub(crate) movement: ActorTable<MovementState>,
514 pub(crate) movement_generation: ActorTable<u64>,
515 pub(crate) crowd_control: CrowdControlTable,
516 pub(crate) diminishing_returns: DiminishingTable,
517}
518
519const ACTOR_TABLE_LEN: usize = 2 + PetIdx::MAX + EnemyIdx::MAX;
521
522const fn actor_ordinal(actor: ActorId) -> usize {
523 match actor {
524 ActorId::Player => 0,
525 ActorId::External => 1,
526 ActorId::Pet(pet) => 2 + pet.as_usize(),
527 ActorId::Enemy(enemy) => 2 + PetIdx::MAX + enemy.as_usize(),
528 }
529}
530
531#[derive(Debug)]
532pub(crate) struct ActorTable<T> {
533 values: Box<[T]>,
534 reset: T,
535}
536
537impl<T: Clone> ActorTable<T> {
540 fn new(reset: T) -> Self {
541 Self {
542 values: vec![reset.clone(); ACTOR_TABLE_LEN].into_boxed_slice(),
543 reset,
544 }
545 }
546
547 pub(crate) fn clear(&mut self) {
548 self.values.fill(self.reset.clone());
549 }
550}
551
552impl<T> ActorTable<Option<T>> {
553 pub(crate) fn contains_key(&self, actor: ActorId) -> bool {
554 self.get(actor).is_some()
555 }
556 pub(crate) fn get(&self, actor: ActorId) -> Option<&T> {
557 self.values.get(actor_ordinal(actor))?.as_ref()
558 }
559
560 pub(crate) fn get_mut(&mut self, actor: ActorId) -> Option<&mut T> {
561 self.values.get_mut(actor_ordinal(actor))?.as_mut()
562 }
563
564 pub(crate) fn insert(&mut self, actor: ActorId, value: T) -> Option<T> {
565 self.values
566 .get_mut(actor_ordinal(actor))
567 .and_then(|slot| slot.replace(value))
568 }
569
570 pub(crate) fn remove(&mut self, actor: ActorId) -> Option<T> {
571 self.values.get_mut(actor_ordinal(actor))?.take()
572 }
573}
574
575impl ActorTable<MovementState> {
577 pub(crate) fn value(&self, actor: ActorId) -> MovementState {
578 self.values[actor_ordinal(actor)]
579 }
580
581 pub(crate) fn set(&mut self, actor: ActorId, value: MovementState) {
582 self.values[actor_ordinal(actor)] = value;
583 }
584}
585
586impl ActorTable<u64> {
588 pub(crate) fn value(&self, actor: ActorId) -> u64 {
589 self.values[actor_ordinal(actor)]
590 }
591
592 pub(crate) fn value_mut(&mut self, actor: ActorId) -> &mut u64 {
593 &mut self.values[actor_ordinal(actor)]
594 }
595}
596
597#[derive(Debug)]
598pub(crate) struct CrowdControlTable {
599 values: Box<[ControlDeadline]>,
600}
601
602impl CrowdControlTable {
605 fn new() -> Self {
606 Self {
607 values: vec![ControlDeadline::default(); ACTOR_TABLE_LEN * CrowdControlKind::COUNT]
608 .into_boxed_slice(),
609 }
610 }
611
612 pub(crate) fn get(&self, key: (ActorId, CrowdControlKind)) -> Option<&ControlDeadline> {
613 self.values.get(Self::ordinal(key))
614 }
615
616 pub(crate) fn insert(
617 &mut self,
618 key: (ActorId, CrowdControlKind),
619 value: ControlDeadline,
620 ) -> Option<ControlDeadline> {
621 self.values
622 .get_mut(Self::ordinal(key))
623 .map(|slot| std::mem::replace(slot, value))
624 }
625
626 pub(crate) fn remove(&mut self, key: (ActorId, CrowdControlKind)) -> Option<ControlDeadline> {
627 self.values.get_mut(Self::ordinal(key)).map(std::mem::take)
628 }
629
630 pub(crate) fn clear(&mut self) {
631 self.values.fill(ControlDeadline::default());
632 }
633
634 fn ordinal(key: (ActorId, CrowdControlKind)) -> usize {
635 actor_ordinal(key.0) * CrowdControlKind::COUNT + key.1.ordinal()
636 }
637}
638
639#[derive(Debug)]
640pub(crate) struct DiminishingTable {
641 values: Box<[DiminishingState]>,
642}
643
644impl DiminishingTable {
647 fn new() -> Self {
648 Self {
649 values: vec![DiminishingState::default(); ACTOR_TABLE_LEN * DiminishingGroup::COUNT]
650 .into_boxed_slice(),
651 }
652 }
653
654 #[cfg(test)]
655 pub(crate) fn get(&self, key: (ActorId, DiminishingGroup)) -> Option<&DiminishingState> {
656 self.values.get(Self::ordinal(key))
657 }
658
659 pub(crate) fn get_mut(
660 &mut self,
661 key: (ActorId, DiminishingGroup),
662 ) -> Option<&mut DiminishingState> {
663 self.values.get_mut(Self::ordinal(key))
664 }
665
666 pub(crate) fn state_mut(
667 &mut self,
668 actor: ActorId,
669 group: DiminishingGroup,
670 ) -> &mut DiminishingState {
671 &mut self.values[Self::ordinal((actor, group))]
672 }
673
674 pub(crate) fn clear(&mut self) {
675 self.values.fill(DiminishingState::default());
676 }
677
678 fn ordinal(key: (ActorId, DiminishingGroup)) -> usize {
679 actor_ordinal(key.0) * DiminishingGroup::COUNT + key.1.ordinal()
680 }
681}
682
683#[derive(Debug)]
684#[expect(
685 clippy::struct_excessive_bools,
686 reason = "independent proc-family reentrancy guards prevent recursive dispatch per family"
687)]
688pub(crate) struct RuntimeProcs {
689 pub(crate) player_cast_hooks_dispatching: bool,
690 pub(crate) impact_procs_dispatching: bool,
691 pub(crate) landed_impact_procs_dispatching: bool,
692 pub(crate) accumulating_impact_procs_dispatching: bool,
693 pub(crate) impact_effect_procs_dispatching: bool,
694 pub(crate) resource_gain_procs_dispatching: bool,
695 pub(crate) rppm_trackers: Vec<RppmTracker>,
696 pub(crate) threshold_trackers: Vec<ThresholdTracker>,
697 pub(crate) accumulating_impact_accumulators: Vec<f64>,
698 pub(crate) resource_gain_attempts: Vec<u32>,
699 pub(crate) impact_effect_ready_at: Vec<f64>,
700 pub(crate) impact_effect_target_ready_at: Vec<SimTime>,
701 impact_effect_target_count: usize,
702 pub(crate) impact_effect_charges: Vec<u8>,
703 pub(crate) impact_effect_rng: Vec<ImpactEffectRngState>,
704 pub(crate) system_rppm_ready_at: Vec<SimTime>,
705 pub(crate) proc_category_ready_at_dense: Vec<SimTime>,
706 pub(crate) proc_counters: Vec<u32>,
707 pub(crate) proc_heartbeat_scheduled_at: Option<SimTime>,
708}
709
710#[derive(Debug)]
711pub(crate) struct RuntimePools {
712 pub(crate) residual_damage_pools: DenseDamagePool,
713 pub(crate) accumulated_damage_pools: DenseDamagePool,
714 pub(crate) rolling_tick_mult: DenseRollingTickPool,
715 pub(crate) async_stack_expiries: DenseExpiryPool,
716 pub(crate) overkill_absorb_ready_at: DenseAuraCooldownPool,
717 pub(crate) cooldown_ready_at: DenseCooldownPool,
718 pub(crate) start_recovery_ready_at: DenseCooldownPool,
719 pub(crate) cooldown_category_ready_at: DenseCooldownPool,
720 pub(crate) charge_category_ready_at: DenseCooldownPool,
721 pub(crate) spell_gates: DenseSpellGatePool,
722}
723
724impl RuntimePools {
725 pub(crate) fn resize_finalized(&mut self, aura_count: usize, spell_count: usize) {
726 self.residual_damage_pools.resize(aura_count);
727 self.accumulated_damage_pools.resize(aura_count);
728 self.rolling_tick_mult.resize(aura_count);
729 self.async_stack_expiries.resize(aura_count);
730 self.overkill_absorb_ready_at.resize(aura_count);
731 self.spell_gates.resize(spell_count);
732 }
733}
734
735impl RuntimeProcs {
736 pub(crate) fn resize_target_icds(&mut self, proc_count: usize, target_count: usize) {
737 self.impact_effect_target_count = target_count;
738 self.impact_effect_target_ready_at
739 .resize(proc_count.saturating_mul(target_count), SimTime::ZERO);
740 self.impact_effect_target_ready_at.fill(SimTime::ZERO);
741 }
742
743 pub(crate) fn resize_target_icds_for_known_targets(&mut self, proc_count: usize) {
744 self.resize_target_icds(proc_count, self.impact_effect_target_count);
745 }
746
747 pub(crate) fn target_icd(&self, proc_index: usize, target: EnemyIdx) -> Option<SimTime> {
748 let index = proc_index
749 .checked_mul(self.impact_effect_target_count)?
750 .checked_add(target.as_usize())?;
751
752 self.impact_effect_target_ready_at.get(index).copied()
753 }
754
755 pub(crate) fn set_target_icd(
756 &mut self,
757 proc_index: usize,
758 target: EnemyIdx,
759 ready_at: SimTime,
760 ) {
761 let Some(index) = proc_index
762 .checked_mul(self.impact_effect_target_count)
763 .and_then(|base| base.checked_add(target.as_usize()))
764 else {
765 return;
766 };
767
768 if let Some(slot) = self.impact_effect_target_ready_at.get_mut(index) {
769 *slot = ready_at;
770 }
771 }
772}
773
774#[derive(Debug, Default)]
775pub(crate) struct DenseCooldownPool {
776 ready_at: Vec<SimTime>,
777}
778
779impl DenseCooldownPool {
780 pub(crate) fn resize(&mut self, len: usize) {
781 self.ready_at.resize(len, SimTime::ZERO);
782 }
783
784 pub(crate) fn ready_at(&self, local: LocalSpellIdx) -> SimTime {
785 self.ready_at
786 .get(local.as_usize())
787 .copied()
788 .unwrap_or(SimTime::ZERO)
789 }
790
791 pub(crate) fn is_ready(&self, local: LocalSpellIdx, now: SimTime) -> bool {
792 self.ready_at(local) <= now
793 }
794
795 pub(crate) fn start(&mut self, local: LocalSpellIdx, ready_at: SimTime) -> SimTime {
796 if let Some(slot) = self.ready_at.get_mut(local.as_usize()) {
797 *slot = ready_at;
798 }
799
800 ready_at
801 }
802
803 pub(crate) fn start_for(
804 &mut self,
805 local: LocalSpellIdx,
806 now: SimTime,
807 duration: SimTime,
808 ) -> SimTime {
809 self.start(local, now.saturating_add(duration))
810 }
811
812 pub(crate) fn reduce(
813 &mut self,
814 local: LocalSpellIdx,
815 amount: SimTime,
816 floor: SimTime,
817 ) -> SimTime {
818 let reduced = self.ready_at(local).saturating_sub(amount).max(floor);
819
820 self.start(local, reduced)
821 }
822
823 pub(crate) fn reset(&mut self, local: LocalSpellIdx) {
824 self.start(local, SimTime::ZERO);
825 }
826
827 pub(crate) fn clear(&mut self) {
828 self.ready_at.fill(SimTime::ZERO);
829 }
830}
831
832#[derive(Debug)]
833pub(crate) struct RuntimePending {
834 pub(crate) pending_hook_timers: Vec<Option<PendingHookTimer>>,
835 pub(crate) hook_timer_generations: Vec<u32>,
836 pub(crate) pending_spell_impacts: Vec<Option<PendingSpellImpact>>,
837 pub(crate) spell_impact_generations: Vec<u32>,
838}
839
840#[derive(Debug)]
841pub(crate) struct RuntimeCompanions {
842 pub(crate) guardians: Vec<Option<GuardianInstance>>,
843 pub(crate) guardian_generations: Vec<u32>,
844 pub(crate) active_guardian_slots: Vec<u16>,
845 pub(crate) free_guardian_slots: Vec<u16>,
846 pub(crate) pet_actions: Vec<Option<PetActionState>>,
847}
848
849impl CombatRuntime {
850 pub(crate) fn new(enemies: Vec<EnemyState>, init: CombatRuntimeInit) -> Self {
851 let enemy_count = enemies.len();
852 let CombatRuntimeInit {
853 encounter,
854 preferred_target,
855 player_transform,
856 player_max_health,
857 has_pet,
858 } = init;
859 let current_target = enemies
860 .get(preferred_target.as_usize())
861 .filter(|enemy| enemy.id() == preferred_target && enemy.is_active() && enemy.is_alive())
862 .map(EnemyState::id)
863 .or_else(|| {
864 enemies
865 .iter()
866 .find(|enemy| enemy.is_active() && enemy.is_alive())
867 .map(EnemyState::id)
868 });
869
870 Self {
871 spec_runtime: Box::new(()),
872 enemies,
873 encounter,
874 player_transform,
875 attack_position: AttackPosition::Behind,
876 current_target,
877 health: RuntimeHealth {
878 player_health: HealthPool::full(player_max_health),
879 pet_health: companion_health_slots(has_pet, player_max_health),
880 immunity_windows: Vec::new(),
881 active_absorbs: crate::state::BoundedVec::default(),
882 next_absorb_application_order: 0,
883 enemy_swing_deadlines: vec![None; enemy_count],
884 enemy_invulnerable_until: vec![SimTime::ZERO; enemy_count],
885 },
886 runtime_error: None,
887 total_damage: 0.0,
888 pending_events: Vec::new(),
889 #[cfg(feature = "rotation-optimizations")]
890 rotation_mutations: RotationMutationAccumulator::default(),
891 resources: RuntimeResources {
892 last_primary_spent: 0.0,
893 last_optional_primary_spent: 0.0,
894 last_secondary_spent: 0.0,
895 primary_base_regen_override: None,
896 runes: RuneState::default(),
897 },
898 casting: RuntimeCasting {
899 selected_candidate: None,
900 active_player_candidate: None,
901 instant_cast_pending_at: None,
902 instant_off_gcd_casts_at: None,
903 instant_off_gcd_spells: LocalSpellSet::default(),
904 active_channel_target: None,
905 channel_generation: 0,
906 active_channel_damage_mult: 1.0,
907 active_channel: None,
908 active_channel_last_tick: SimTime::ZERO,
909 active_channel_tick_interval_ms: 0,
910 active_channel_ticks_remaining: 0,
911 active_channel_on_last_tick: false,
912 active_channel_pushback_count: 0,
913 free_action_dispatch_depth: 0,
914 channel_chain_spell: None,
915 active_casts: ActorTable::new(None),
916 },
917 control: RuntimeControl {
918 school_lockouts: ActorTable::new(None),
919 movement: ActorTable::new(MovementState::Stationary),
920 movement_generation: ActorTable::new(0),
921 crowd_control: CrowdControlTable::new(),
922 diminishing_returns: DiminishingTable::new(),
923 },
924 scratch: ScratchBuffers::default(),
925 deferred_work: crate::state::DeferredWorkQueue::default(),
926 procs: RuntimeProcs {
927 player_cast_hooks_dispatching: false,
928 impact_procs_dispatching: false,
929 landed_impact_procs_dispatching: false,
930 accumulating_impact_procs_dispatching: false,
931 impact_effect_procs_dispatching: false,
932 resource_gain_procs_dispatching: false,
933 rppm_trackers: Vec::new(),
934 threshold_trackers: Vec::new(),
935 accumulating_impact_accumulators: Vec::new(),
936 resource_gain_attempts: Vec::new(),
937 impact_effect_ready_at: Vec::new(),
938 impact_effect_target_ready_at: Vec::new(),
939 impact_effect_target_count: enemy_count,
940 impact_effect_charges: Vec::new(),
941 impact_effect_rng: Vec::new(),
942 system_rppm_ready_at: Vec::new(),
943 proc_category_ready_at_dense: Vec::new(),
944 proc_counters: vec![0; crate::generated_identities::proc_counter::COUNT],
945 proc_heartbeat_scheduled_at: None,
946 },
947 mask_reasons: None,
948 #[cfg(test)]
949 rotation_spells_scanned: 0,
950 recharge_rate_mults: Vec::new(),
951 #[cfg(feature = "rotation-optimizations")]
952 cooldown_duration_mults: Vec::new(),
953 #[cfg(feature = "rotation-optimizations")]
954 initial_recharge_rate_mults: Vec::new(),
955 #[cfg(feature = "rotation-optimizations")]
956 initial_cooldown_duration_mults: Vec::new(),
957 pools: RuntimePools {
958 residual_damage_pools: DenseDamagePool::default(),
959 accumulated_damage_pools: DenseDamagePool::default(),
960 rolling_tick_mult: DenseRollingTickPool::default(),
961 async_stack_expiries: DenseExpiryPool::default(),
962 overkill_absorb_ready_at: DenseAuraCooldownPool::default(),
963 cooldown_ready_at: DenseCooldownPool::default(),
964 start_recovery_ready_at: DenseCooldownPool::default(),
965 cooldown_category_ready_at: DenseCooldownPool::default(),
966 charge_category_ready_at: DenseCooldownPool::default(),
967 spell_gates: DenseSpellGatePool::default(),
968 },
969 pending: RuntimePending {
970 pending_hook_timers: Vec::new(),
971 hook_timer_generations: Vec::new(),
972 pending_spell_impacts: Vec::new(),
973 spell_impact_generations: Vec::new(),
974 },
975 companions: RuntimeCompanions {
976 guardians: vec![None; PetIdx::MAX - 1],
977 guardian_generations: vec![0; PetIdx::MAX - 1],
978 active_guardian_slots: Vec::with_capacity(PetIdx::MAX - 1),
979 free_guardian_slots: (0..u16::try_from(PetIdx::MAX - 1)
980 .expect("guardian arena fits u16"))
981 .rev()
982 .collect(),
983 pet_actions: Vec::new(),
984 },
985 }
986 }
987
988 pub(crate) fn set_spec_runtime(&mut self, runtime: Box<dyn SpecRuntime>) {
989 self.spec_runtime = runtime;
990 }
991
992 #[inline]
993 pub(crate) fn reset_spec_runtime(&mut self) {
994 self.spec_runtime.reset();
995 }
996}
997
998fn companion_health_slots(has_pet: bool, player_max_health: f64) -> Vec<Option<HealthPool>> {
999 let mut health = vec![None; PetIdx::MAX];
1000
1001 if has_pet && let Some(primary) = health.get_mut(PetIdx::PRIMARY.as_usize()) {
1002 *primary = Some(HealthPool::full(player_max_health));
1003 }
1004
1005 health
1006}
1007
1008#[derive(Debug)]
1009pub(crate) enum ImpactEffectRngState {
1010 Stateless,
1011 Shuffled(ShuffledRng),
1012 Accumulated(AccumulatedRng),
1013}
1014
1015impl ImpactEffectRngState {
1016 pub(crate) fn from_proc(proc: &ImpactEffectProc) -> Self {
1017 match proc.chance_scale {
1018 ImpactChanceScale::Shuffled {
1019 success_entries,
1020 total_entries,
1021 } => Self::Shuffled(ShuffledRng::from_valid_counts(
1022 success_entries,
1023 total_entries,
1024 )),
1025 ImpactChanceScale::Accumulated { cap, initial_count } => {
1026 Self::Accumulated(AccumulatedRng::new(proc.chance, cap, initial_count))
1027 }
1028 _ => Self::Stateless,
1029 }
1030 }
1031
1032 pub(crate) fn reset(&mut self) {
1033 match self {
1034 Self::Stateless => {}
1035 Self::Shuffled(tracker) => tracker.reset(),
1036 Self::Accumulated(tracker) => tracker.reset(),
1037 }
1038 }
1039}
1040
1041pub(crate) struct CombatRuntimeInit {
1042 pub(crate) encounter: EncounterRuntime,
1043 pub(crate) preferred_target: EnemyIdx,
1044 pub(crate) player_transform: SpatialTransform,
1045 pub(crate) player_max_health: f64,
1046 pub(crate) has_pet: bool,
1047}