wowlab_engine_combat/state/
combat_state.rs1use 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#[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 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 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;