Skip to main content

wowlab_engine_combat/state/
combat_state.rs

1use wowlab_engine_domain::{
2    encounter::{BecameDead, EnemyActorDefinition, EnemyState, HealthModel},
3    pool::{CooldownPool, DamagePool, ExpiryPool, PoolIndex, RollingTickPool, SpellGatePool},
4};
5use wowlab_engine_gamedata::ResolvedGameData;
6use wowlab_engine_ports::{
7    EnemyDespawnOutcome, EngineError, Event, ResolvedEncounter, SpatialActor, SpatialQuery,
8    SpatialQueryError, SpecConfig, SpecRuntime, SpecRuntimeError,
9};
10use wowlab_engine_rng::{AccumulatedRng, ShuffledRng};
11use wowlab_engine_telemetry::{AuraTelemetryScope, TargetMetadata, TelemetryScope};
12use wowlab_types::{
13    game::GearSlot,
14    sim::{
15        ActorId, AuraKey, EnemyIdx, FastMap, IntMap, PositionedActorRef, PullId, SimTime,
16        SpatialTransform, SpellIdx,
17    },
18};
19
20use super::{
21    AccumulatingImpactProc, AuraData, AutoAttackData, BaseStats, CastHookFn, DriverSpellModifier,
22    EmpowerReleaseHookFn, EncounterRuntime, GuardianInstance, HealthPool, ImpactEffectProc,
23    ImpactProc, IncomingImmunityWindow, LandedImpactProc, LocalAuraIdx, LocalRppmIdx,
24    LocalSpellIdx, PassiveDriverEffect, PendingHookTimer, PendingSpellImpact, PeriodicDriver,
25    PetActionState, ResourceGainProc, ResourceThresholdTrigger, RppmTracker, RuneState,
26    ScratchBuffers, SpellData, SpellEffectData, ThresholdTracker, WeaponEnchantBinding,
27    WeaponImbueBinding,
28};
29use crate::systems::MasteryFn;
30
31pub(crate) mod config;
32mod config_access;
33mod defs;
34mod enemies;
35mod health;
36mod index;
37mod lookups;
38mod runtime;
39mod runtime_events;
40mod spatial;
41mod telemetry;
42
43pub(crate) use config::{AttackPosition, CombatConfig};
44pub(crate) use defs::CombatDefs;
45pub(crate) use index::CombatIndex;
46pub(crate) use runtime::{CombatRuntime, CombatRuntimeInit, ImpactEffectRngState};
47
48/// All static combat data plus mutable non-buffer state, grouped into sub-structs.
49#[derive(Debug)]
50pub struct CombatState {
51    pub defs: CombatDefs,
52    pub index: CombatIndex,
53    pub runtime: CombatRuntime,
54    pub config: CombatConfig,
55    spatial: SpatialQueryHandle,
56}
57
58#[derive(Debug)]
59struct SpatialQueryHandle(Box<dyn SpatialQuery>);
60
61impl std::ops::Deref for SpatialQueryHandle {
62    type Target = dyn SpatialQuery;
63
64    fn deref(&self) -> &Self::Target {
65        &*self.0
66    }
67}
68
69impl std::ops::DerefMut for SpatialQueryHandle {
70    fn deref_mut(&mut self) -> &mut Self::Target {
71        &mut *self.0
72    }
73}
74
75fn parry_hasted_deadline(base_ms: u32, current: SimTime, now: SimTime) -> Option<SimTime> {
76    const MINIMUM_REMAINING_FRACTION: f64 = 0.2;
77    const HASTE_REDUCTION_FRACTION: f64 = 0.4;
78    const FULL_REDUCTION_THRESHOLD: f64 = 0.6;
79
80    let base = SimTime::from_millis(base_ms);
81    let remaining = current.saturating_sub(now);
82    let scaled = |fraction| {
83        SimTime::from_millis(wowlab_types::numeric::f64_to_u32_saturating_ceil(
84            f64::from(base_ms) * fraction,
85        ))
86    };
87    let minimum = scaled(MINIMUM_REMAINING_FRACTION);
88    let accelerated_remaining = if remaining > scaled(FULL_REDUCTION_THRESHOLD) {
89        remaining.saturating_sub(scaled(HASTE_REDUCTION_FRACTION))
90    } else if remaining > minimum {
91        minimum
92    } else {
93        return None;
94    };
95
96    Some(now.saturating_add(accelerated_remaining.min(base)))
97}
98
99impl CombatState {
100    pub(crate) fn new(
101        defs: CombatDefs,
102        index: CombatIndex,
103        runtime: CombatRuntime,
104        config: CombatConfig,
105    ) -> Result<Self, EngineError> {
106        if defs.enemies.len() != runtime.enemies.len() {
107            return Err(EngineError::spec_construction(format!(
108                "enemy definition/runtime length mismatch: definitions={}, runtime={}",
109                defs.enemies.len(),
110                runtime.enemies.len()
111            )));
112        }
113
114        for (raw_index, (definition, state)) in
115            defs.enemies.iter().zip(&runtime.enemies).enumerate()
116        {
117            let index = u16::try_from(raw_index).map_err(|source| {
118                // #t(rust_alloc_in_loop) only allocates while rejecting invalid construction
119                EngineError::spec_construction(format!(
120                    "enemy index {raw_index} exceeds EnemyIdx range: {source}"
121                ))
122            })?;
123            let expected = EnemyIdx(index);
124
125            if definition.id() != expected || state.id() != expected {
126                // #t(rust_alloc_in_loop) only allocates while rejecting invalid construction
127                return Err(EngineError::spec_construction(format!(
128                    "enemy identity mismatch at dense index {raw_index}: definition={:?}, runtime={:?}",
129                    definition.id(),
130                    state.id()
131                )));
132            }
133        }
134
135        let initial_actors: Vec<_> = runtime
136            .enemies
137            .iter()
138            .filter(|enemy| enemy.is_active() && enemy.is_alive())
139            .map(|enemy| SpatialActor {
140                id: enemy.id(),
141                transform: enemy.current_transform(),
142            })
143            .collect();
144        let spatial = wowlab_engine_spatial::build_spatial_query(
145            &config.encounter.definition().spatial_scene,
146            &initial_actors,
147        )
148        .map_err(|error| {
149            EngineError::spec_construction(format!("failed to build spatial index: {error}"))
150        })?;
151
152        Ok(Self {
153            defs,
154            index,
155            runtime,
156            config,
157            spatial: SpatialQueryHandle(Box::new(spatial)),
158        })
159    }
160}
161
162#[cfg(test)]
163#[path = "combat_state/tests.rs"]
164mod tests;