wowlab_engine_combat/state/runtime/
mod.rs1use wowlab_engine_domain::{
2 encounter::{BecameDead, EnemyActorDefinition, EnemyState, HealthModel},
3 rotation::BoundDenseBuffer,
4};
5use wowlab_engine_ports::{
6 EnemyDespawnOutcome, EngineError, Event, SpatialActor, SpatialQuery, SpatialQueryError,
7 SpecRuntime, SpecRuntimeError,
8};
9use wowlab_engine_rng::{AccumulatedRng, ShuffledRng};
10#[cfg(test)]
11use wowlab_types::sim::SpellIdx;
12use wowlab_types::sim::{
13 ActorId, EnemyIdx, IntMap, PositionedActorRef, PullId, SimTime, SpatialTransform,
14};
15
16use super::{
17 EncounterRuntime, GuardianInstance, HealthPool, ImpactEffectProc, IncomingImmunityWindow,
18 LocalSpellIdx, PendingHookTimer, PendingSpellImpact, PetActionState, RppmTracker, RuneState,
19 ScratchBuffers, ThresholdTracker,
20};
21
22mod auras;
23mod buff_cache;
24mod config_access;
25mod dense_pools;
26mod enemies;
27mod events;
28mod health;
29mod projection;
30mod spatial;
31mod state;
32
33pub(crate) use auras::{
34 AuraActivation, AuraMutation, AuraRefresh, AuraSchedule, AuraSnapshot, AuraTransferState,
35};
36#[cfg(all(
37 feature = "rotation-optimizations",
38 any(test, feature = "test-support")
39))]
40pub(crate) use buff_cache::DamageProjectionSheetStats;
41#[cfg(feature = "rotation-optimizations")]
42pub(crate) use buff_cache::{DamageBuffProjection, DamageProjectionKey};
43pub(crate) use dense_pools::{
44 DenseAuraCooldownPool, DenseDamagePool, DenseExpiryPool, DenseRollingTickPool,
45 DenseSpellGatePool,
46};
47pub(crate) use projection::ProjectionDirty;
48pub(crate) use state::{
49 ActiveChannelIdentity, CombatRuntime, CombatRuntimeInit, ImpactEffectRngState,
50};
51#[cfg(feature = "rotation-optimizations")]
52pub(crate) use state::{
53 RotationMutationAccumulator, RotationMutationDirection, RotationSemanticMutation,
54};
55
56#[derive(Debug)]
58pub struct CombatInstance {
60 #[cfg(feature = "test-support")]
61 #[doc(hidden)]
62 pub runtime: CombatRuntime,
63 #[cfg(not(feature = "test-support"))]
64 pub(crate) runtime: CombatRuntime,
65 #[cfg(feature = "test-support")]
66 #[doc(hidden)]
67 pub buffer: BoundDenseBuffer,
68 #[cfg(not(feature = "test-support"))]
69 pub(crate) buffer: BoundDenseBuffer,
70 spatial: SpatialQueryHandle,
71 aura_totals_revisions: auras::AuraTotalsRevisions,
72 #[cfg(feature = "rotation-optimizations")]
73 active_aura_bindings: auras::ActiveAuraBindings,
74 pub(super) buff_totals_caches: buff_cache::BuffTotalsCaches,
75 projection_dirty: ProjectionDirty,
76 projection_dirty_at: SimTime,
77 aura_projection_dirty: projection::AuraProjectionDirty,
78 #[cfg(test)]
79 projection_flush_count: u32,
80}
81
82#[derive(Debug)]
83struct SpatialQueryHandle(Box<dyn SpatialQuery>);
84
85impl std::ops::Deref for SpatialQueryHandle {
86 type Target = dyn SpatialQuery;
87
88 fn deref(&self) -> &Self::Target {
89 &*self.0
90 }
91}
92
93impl std::ops::DerefMut for SpatialQueryHandle {
94 fn deref_mut(&mut self) -> &mut Self::Target {
95 &mut *self.0
96 }
97}
98
99fn parry_hasted_deadline(base_ms: u32, current: SimTime, now: SimTime) -> Option<SimTime> {
100 const MINIMUM_REMAINING_FRACTION: f64 = 0.2;
101 const HASTE_REDUCTION_FRACTION: f64 = 0.4;
102 const FULL_REDUCTION_THRESHOLD: f64 = 0.6;
103
104 let base = SimTime::from_millis(base_ms);
105 let remaining = current.saturating_sub(now);
106 let scaled = |fraction| {
107 SimTime::from_millis(wowlab_types::numeric::f64_to_u32_saturating_ceil(
108 f64::from(base_ms) * fraction,
109 ))
110 };
111 let minimum = scaled(MINIMUM_REMAINING_FRACTION);
112 let accelerated_remaining = if remaining > scaled(FULL_REDUCTION_THRESHOLD) {
113 remaining.saturating_sub(scaled(HASTE_REDUCTION_FRACTION))
114 } else if remaining > minimum {
115 minimum
116 } else {
117 return None;
118 };
119
120 Some(now.saturating_add(accelerated_remaining.min(base)))
121}
122
123impl CombatInstance {
124 pub(crate) fn new(
125 program: &crate::state::CombatProgram,
126 runtime: CombatRuntime,
127 buffer: BoundDenseBuffer,
128 ) -> Result<Self, EngineError> {
129 if program.enemies.len() != runtime.enemies.len() {
130 return Err(EngineError::spec_construction(format!(
131 "enemy definition/runtime length mismatch: definitions={}, runtime={}",
132 program.enemies.len(),
133 runtime.enemies.len()
134 )));
135 }
136
137 for (raw_index, (definition, state)) in
138 program.enemies.iter().zip(&runtime.enemies).enumerate()
139 {
140 let index = u16::try_from(raw_index).map_err(|source| {
141 EngineError::spec_construction(format!(
143 "enemy index {raw_index} exceeds EnemyIdx range: {source}"
144 ))
145 })?;
146 let expected = EnemyIdx(index);
147
148 if definition.id() != expected || state.id() != expected {
149 return Err(EngineError::spec_construction(format!(
151 "enemy identity mismatch at dense index {raw_index}: definition={:?}, runtime={:?}",
152 definition.id(),
153 state.id()
154 )));
155 }
156 }
157
158 let initial_actors: Vec<_> = runtime
159 .enemies
160 .iter()
161 .filter(|enemy| enemy.is_active() && enemy.is_alive())
162 .map(|enemy| SpatialActor {
163 id: enemy.id(),
164 transform: enemy.current_transform(),
165 })
166 .collect();
167 let spatial = wowlab_engine_spatial::build_spatial_query(
168 &program.config.encounter.definition().spatial_scene,
169 &initial_actors,
170 )
171 .map_err(|error| {
172 EngineError::spec_construction(format!("failed to build spatial index: {error}"))
173 })?;
174
175 Ok(Self {
176 runtime,
177 buffer,
178 spatial: SpatialQueryHandle(Box::new(spatial)),
179 aura_totals_revisions: auras::AuraTotalsRevisions::new(program.enemies.len()),
180 #[cfg(feature = "rotation-optimizations")]
181 active_aura_bindings: auras::ActiveAuraBindings::new(
182 &program.buffer_bindings,
183 program.effects.static_total_aura_plan(),
184 program.enemies.len(),
185 ),
186 buff_totals_caches: buff_cache::BuffTotalsCaches::new(program.enemies.len()),
187 projection_dirty: ProjectionDirty::empty(),
188 projection_dirty_at: SimTime::ZERO,
189 aura_projection_dirty: projection::AuraProjectionDirty::new(
190 program.aura_dependencies.projection_count(),
191 ),
192 #[cfg(test)]
193 projection_flush_count: 0,
194 })
195 }
196
197 #[must_use]
199 pub fn player_crit(&self) -> f64 {
200 self.buffer.player().crit
201 }
202
203 pub fn rppm_tracker_count(&self) -> usize {
204 self.runtime.procs.rppm_trackers.len()
205 }
206 pub fn rppm_tracker(&self, tracker: super::LocalRppmIdx) -> Option<&RppmTracker> {
207 self.runtime.procs.rppm_trackers.get(tracker.as_usize())
208 }
209}
210
211#[cfg(test)]
212#[path = "tests.rs"]
213mod tests;