Skip to main content

wowlab_engine_combat/state/combat_state/
runtime.rs

1use super::{
2    AccumulatedRng, ActorId, AttackPosition, AuraKey, CooldownPool, DamagePool, EncounterRuntime,
3    EnemyIdx, EnemyState, Event, ExpiryPool, FastMap, GuardianInstance, HealthPool,
4    ImpactEffectProc, IncomingImmunityWindow, IntMap, LocalSpellIdx, PendingHookTimer,
5    PendingSpellImpact, PetActionState, RollingTickPool, RuneState, ScratchBuffers, ShuffledRng,
6    SimTime, SpatialTransform, SpecRuntime, SpecRuntimeError, SpellGatePool,
7};
8use crate::state::{
9    ActiveCast, ControlDeadline, CrowdControlKind, DiminishingGroup, DiminishingState,
10    ImpactChanceScale, MovementState, SchoolLockout,
11};
12
13/// Mutable non-buffer state that changes within a single iteration and is cleared on reset.
14#[derive(Debug)]
15pub struct CombatRuntime {
16    pub(super) spec_runtime: Box<dyn SpecRuntime>,
17    pub(super) enemies: Vec<EnemyState>,
18    pub(crate) encounter: EncounterRuntime,
19    pub player_transform: SpatialTransform,
20    pub attack_position: AttackPosition,
21    pub current_target: Option<EnemyIdx>,
22    pub(crate) health: RuntimeHealth,
23    pub(super) runtime_error: Option<SpecRuntimeError>,
24    pub total_damage: f64,
25    pub pending_events: Vec<Event>,
26    pub(crate) resources: RuntimeResources,
27    pub(crate) casting: RuntimeCasting,
28    pub(crate) control: RuntimeControl,
29    pub scratch: ScratchBuffers,
30    pub(crate) deferred_work: crate::state::DeferredWorkQueue,
31    pub(crate) procs: RuntimeProcs,
32    /// Last reason each spell was masked out of rotation evaluation, keyed by spell id.
33    ///
34    /// Only the discriminant is kept, so the trace fires once per change rather than per tick.
35    pub(crate) mask_reasons:
36        IntMap<u32, std::mem::Discriminant<crate::handler::can_cast::CastReject>>,
37    pub recharge_rate_mults: Vec<f64>,
38    pub(crate) pools: RuntimePools,
39    pub(crate) pending: RuntimePending,
40    pub(crate) companions: RuntimeCompanions,
41}
42
43#[derive(Debug)]
44pub(crate) struct RuntimeHealth {
45    pub(crate) player_health: HealthPool,
46    pub(crate) pet_health: Vec<Option<HealthPool>>,
47    pub(crate) immunity_windows: Vec<IncomingImmunityWindow>,
48    pub(crate) active_absorbs: Vec<crate::state::ActiveAbsorb>,
49    pub(crate) next_absorb_application_order: u64,
50    pub(crate) enemy_swing_deadlines: Vec<Option<SimTime>>,
51    pub(crate) enemy_invulnerable_until: Vec<SimTime>,
52}
53
54#[derive(Debug)]
55pub(crate) struct RuntimeResources {
56    pub(crate) last_primary_spent: f64,
57    pub(crate) last_optional_primary_spent: f64,
58    pub(crate) last_secondary_spent: f64,
59    pub(crate) primary_base_regen_override: Option<f64>,
60    pub(crate) runes: RuneState,
61}
62
63#[derive(Debug)]
64pub(crate) struct RuntimeCasting {
65    pub(crate) instant_cast_pending_at: Option<SimTime>,
66    pub(crate) instant_off_gcd_casts_at: Option<SimTime>,
67    pub(crate) instant_off_gcd_spell_ids: Vec<u32>,
68    pub(crate) active_channel_target: Option<EnemyIdx>,
69    pub(crate) channel_generation: u64,
70    pub(crate) active_channel_damage_mult: f64,
71    pub(crate) active_channel_spell_id: Option<u32>,
72    pub(crate) active_channel_last_tick: SimTime,
73    pub(crate) active_channel_tick_interval_ms: u32,
74    pub(crate) active_channel_ticks_remaining: u8,
75    pub(crate) active_channel_on_last_tick: bool,
76    pub(crate) active_channel_pushback_count: u8,
77    pub(crate) free_action_dispatch_depth: u8,
78    pub(crate) channel_chain_spell_id: Option<u32>,
79    pub(crate) active_casts: FastMap<ActorId, ActiveCast>,
80}
81
82#[derive(Debug)]
83pub(crate) struct RuntimeControl {
84    pub(crate) school_lockouts: FastMap<ActorId, SchoolLockout>,
85    pub(crate) movement: FastMap<ActorId, MovementState>,
86    pub(crate) movement_generation: FastMap<ActorId, u64>,
87    pub(crate) crowd_control: FastMap<(ActorId, CrowdControlKind), ControlDeadline>,
88    pub(crate) diminishing_returns: FastMap<(ActorId, DiminishingGroup), DiminishingState>,
89}
90
91#[derive(Debug)]
92pub(crate) struct RuntimeProcs {
93    pub(crate) impact_effect_ready_at: Vec<f64>,
94    pub(crate) impact_effect_target_ready_at: Vec<FastMap<EnemyIdx, SimTime>>,
95    pub(crate) impact_effect_charges: Vec<u8>,
96    pub(crate) impact_effect_rng: Vec<ImpactEffectRngState>,
97    pub(crate) proc_category_ready_at: IntMap<u32, SimTime>,
98    pub(crate) proc_attempts: IntMap<u32, u32>,
99    pub(crate) proc_counters: IntMap<u32, u32>,
100    pub(crate) proc_heartbeat_scheduled_at: Option<SimTime>,
101}
102
103#[derive(Debug)]
104pub(crate) struct RuntimePools {
105    pub(crate) residual_damage_pools: DamagePool<AuraKey>,
106    pub(crate) accumulated_damage_pools: DamagePool<AuraKey>,
107    pub(crate) rolling_tick_mult: RollingTickPool<AuraKey>,
108    pub(crate) async_stack_expiries: ExpiryPool<AuraKey>,
109    pub(crate) overkill_absorb_ready_at: CooldownPool<AuraKey>,
110    pub(crate) cooldown_ready_at: CooldownPool<LocalSpellIdx>,
111    pub(crate) start_recovery_ready_at: CooldownPool<wowlab_types::data::CooldownCategoryId>,
112    pub(crate) cooldown_category_ready_at: CooldownPool<wowlab_types::data::CooldownCategoryId>,
113    pub(crate) charge_category_ready_at: CooldownPool<wowlab_types::data::CooldownCategoryId>,
114    pub(crate) spell_gates: SpellGatePool<LocalSpellIdx>,
115}
116
117#[derive(Debug)]
118pub(crate) struct RuntimePending {
119    pub(crate) pending_hook_timers: Vec<Option<PendingHookTimer>>,
120    pub(crate) pending_spell_impacts: Vec<Option<PendingSpellImpact>>,
121}
122
123#[derive(Debug)]
124pub(crate) struct RuntimeCompanions {
125    pub(crate) guardians: Vec<Option<GuardianInstance>>,
126    pub(crate) guardian_generations: Vec<u32>,
127    pub(crate) pet_actions: Vec<Option<PetActionState>>,
128}
129
130impl CombatRuntime {
131    pub(crate) fn new(enemies: Vec<EnemyState>, init: CombatRuntimeInit) -> Self {
132        let enemy_count = enemies.len();
133        let CombatRuntimeInit {
134            encounter,
135            preferred_target,
136            player_transform,
137            player_max_health,
138            has_pet,
139        } = init;
140        let current_target = enemies
141            .get(preferred_target.as_usize())
142            .filter(|enemy| enemy.id() == preferred_target && enemy.is_active() && enemy.is_alive())
143            .map(EnemyState::id)
144            .or_else(|| {
145                enemies
146                    .iter()
147                    .find(|enemy| enemy.is_active() && enemy.is_alive())
148                    .map(EnemyState::id)
149            });
150
151        Self {
152            spec_runtime: Box::new(()),
153            enemies,
154            encounter,
155            player_transform,
156            attack_position: AttackPosition::Behind,
157            current_target,
158            health: RuntimeHealth {
159                player_health: HealthPool::full(player_max_health),
160                pet_health: if has_pet {
161                    vec![Some(HealthPool::full(player_max_health))]
162                } else {
163                    Vec::new()
164                },
165                immunity_windows: Vec::new(),
166                active_absorbs: Vec::new(),
167                next_absorb_application_order: 0,
168                enemy_swing_deadlines: vec![None; enemy_count],
169                enemy_invulnerable_until: vec![SimTime::ZERO; enemy_count],
170            },
171            runtime_error: None,
172            total_damage: 0.0,
173            pending_events: Vec::new(),
174            resources: RuntimeResources {
175                last_primary_spent: 0.0,
176                last_optional_primary_spent: 0.0,
177                last_secondary_spent: 0.0,
178                primary_base_regen_override: None,
179                runes: RuneState::default(),
180            },
181            casting: RuntimeCasting {
182                instant_cast_pending_at: None,
183                instant_off_gcd_casts_at: None,
184                instant_off_gcd_spell_ids: Vec::new(),
185                active_channel_target: None,
186                channel_generation: 0,
187                active_channel_damage_mult: 1.0,
188                active_channel_spell_id: None,
189                active_channel_last_tick: SimTime::ZERO,
190                active_channel_tick_interval_ms: 0,
191                active_channel_ticks_remaining: 0,
192                active_channel_on_last_tick: false,
193                active_channel_pushback_count: 0,
194                free_action_dispatch_depth: 0,
195                channel_chain_spell_id: None,
196                active_casts: FastMap::default(),
197            },
198            control: RuntimeControl {
199                school_lockouts: FastMap::default(),
200                movement: FastMap::default(),
201                movement_generation: FastMap::default(),
202                crowd_control: FastMap::default(),
203                diminishing_returns: FastMap::default(),
204            },
205            scratch: ScratchBuffers::default(),
206            deferred_work: crate::state::DeferredWorkQueue::default(),
207            procs: RuntimeProcs {
208                impact_effect_ready_at: Vec::new(),
209                impact_effect_target_ready_at: Vec::new(),
210                impact_effect_charges: Vec::new(),
211                impact_effect_rng: Vec::new(),
212                proc_category_ready_at: IntMap::default(),
213                proc_attempts: IntMap::default(),
214                proc_counters: IntMap::default(),
215                proc_heartbeat_scheduled_at: None,
216            },
217            mask_reasons: IntMap::default(),
218            recharge_rate_mults: Vec::new(),
219            pools: RuntimePools {
220                residual_damage_pools: DamagePool::default(),
221                accumulated_damage_pools: DamagePool::default(),
222                rolling_tick_mult: RollingTickPool::default(),
223                async_stack_expiries: ExpiryPool::default(),
224                overkill_absorb_ready_at: CooldownPool::default(),
225                cooldown_ready_at: CooldownPool::default(),
226                start_recovery_ready_at: CooldownPool::default(),
227                cooldown_category_ready_at: CooldownPool::default(),
228                charge_category_ready_at: CooldownPool::default(),
229                spell_gates: SpellGatePool::default(),
230            },
231            pending: RuntimePending {
232                pending_hook_timers: Vec::new(),
233                pending_spell_impacts: Vec::new(),
234            },
235            companions: RuntimeCompanions {
236                guardians: Vec::new(),
237                guardian_generations: Vec::new(),
238                pet_actions: Vec::new(),
239            },
240        }
241    }
242
243    #[inline]
244    pub(crate) fn reset_spec_runtime(&mut self) {
245        self.spec_runtime.reset();
246    }
247}
248
249#[derive(Debug)]
250pub(crate) enum ImpactEffectRngState {
251    Stateless,
252    Shuffled(ShuffledRng),
253    Accumulated(AccumulatedRng),
254}
255
256impl ImpactEffectRngState {
257    pub(crate) fn from_proc(proc: &ImpactEffectProc) -> Self {
258        match proc.chance_scale {
259            ImpactChanceScale::Shuffled {
260                success_entries,
261                total_entries,
262            } => Self::Shuffled(ShuffledRng::from_valid_counts(
263                success_entries,
264                total_entries,
265            )),
266            ImpactChanceScale::Accumulated { cap, initial_count } => {
267                Self::Accumulated(AccumulatedRng::new(proc.chance, cap, initial_count))
268            }
269            _ => Self::Stateless,
270        }
271    }
272
273    pub(crate) fn reset(&mut self) {
274        match self {
275            Self::Stateless => {}
276            Self::Shuffled(tracker) => tracker.reset(),
277            Self::Accumulated(tracker) => tracker.reset(),
278        }
279    }
280}
281
282pub(crate) struct CombatRuntimeInit {
283    pub(crate) encounter: EncounterRuntime,
284    pub(crate) preferred_target: EnemyIdx,
285    pub(crate) player_transform: SpatialTransform,
286    pub(crate) player_max_health: f64,
287    pub(crate) has_pet: bool,
288}