Skip to main content

wowlab_engine_combat/handler/
reset.rs

1use wowlab_engine_domain::rotation::DenseBuffer;
2use wowlab_types::sim::{ActorId, SimTime};
3
4use crate::{
5    builder::buffer_init::project_encounter,
6    keys::auto_attack_key,
7    state::{CombatState, LocalAuraIdx, LocalSpellIdx},
8    systems::{aura_keys_for_application, reset_cooldown, sync_spell_costs},
9};
10
11pub(super) fn reset_combat(
12    state: &mut CombatState,
13    buf: &mut DenseBuffer,
14    last_sync: &mut SimTime,
15) {
16    state.runtime.total_damage = 0.0;
17    state.runtime.resources.last_primary_spent = 0.0;
18    state.runtime.resources.last_optional_primary_spent = 0.0;
19    state.runtime.resources.last_secondary_spent = 0.0;
20    state.runtime.casting.instant_cast_pending_at = None;
21    state.runtime.casting.instant_off_gcd_casts_at = None;
22    state.runtime.casting.instant_off_gcd_spell_ids.clear();
23    state.runtime.casting.active_channel_target = None;
24    state.runtime.casting.channel_generation = 0;
25    state.runtime.casting.active_channel_damage_mult = 1.0;
26    state.runtime.casting.active_channel_spell_id = None;
27    state.runtime.casting.active_channel_last_tick = SimTime::ZERO;
28    state.runtime.casting.active_channel_tick_interval_ms = 0;
29    state.runtime.casting.active_channel_ticks_remaining = 0;
30    state.runtime.casting.active_channel_on_last_tick = false;
31    state.runtime.casting.active_channel_pushback_count = 0;
32    state.runtime.casting.free_action_dispatch_depth = 0;
33    state.runtime.casting.channel_chain_spell_id = None;
34    state.runtime.casting.active_casts.clear();
35    state.runtime.control.school_lockouts.clear();
36    state.runtime.control.movement.clear();
37    state.runtime.control.movement_generation.clear();
38    state.runtime.control.crowd_control.clear();
39    state.runtime.control.diminishing_returns.clear();
40    state.runtime.health.player_health.reset();
41
42    for health in state.runtime.health.pet_health.iter_mut().flatten() {
43        health.reset();
44    }
45
46    state.runtime.health.immunity_windows.clear();
47    state.runtime.health.active_absorbs.clear();
48    state.runtime.health.next_absorb_application_order = 0;
49    state.take_runtime_error();
50    *last_sync = SimTime::ZERO;
51    state.runtime.pending_events.clear();
52    state.runtime.procs.proc_heartbeat_scheduled_at = None;
53    state.runtime.deferred_work.clear();
54    state.runtime.procs.impact_effect_ready_at.fill(0.0);
55
56    for target_cooldowns in &mut state.runtime.procs.impact_effect_target_ready_at {
57        target_cooldowns.clear();
58    }
59
60    for (remaining, proc) in state
61        .runtime
62        .procs
63        .impact_effect_charges
64        .iter_mut()
65        .zip(&state.defs.impact_effect_procs)
66    {
67        *remaining = if proc.uses_stacks_for_charges {
68            0
69        } else {
70            proc.charges
71        };
72    }
73
74    for rng in &mut state.runtime.procs.impact_effect_rng {
75        rng.reset();
76    }
77
78    state.runtime.procs.proc_category_ready_at.clear();
79    state.runtime.procs.proc_attempts.clear();
80    state.runtime.procs.proc_counters.clear();
81    state.runtime.mask_reasons.clear();
82    state.runtime.recharge_rate_mults.fill(1.0);
83    state.runtime.pools.residual_damage_pools.clear();
84    state.runtime.pools.accumulated_damage_pools.clear();
85    state.runtime.pools.rolling_tick_mult.clear();
86    state.runtime.pools.async_stack_expiries.clear();
87    state.runtime.pools.overkill_absorb_ready_at.clear();
88    state.runtime.pools.cooldown_ready_at.clear();
89    state.runtime.pools.start_recovery_ready_at.clear();
90    state.runtime.pools.cooldown_category_ready_at.clear();
91    state.runtime.pools.charge_category_ready_at.clear();
92    crate::systems::reset_spell_gates(state, buf);
93    state.runtime.pending.pending_hook_timers.clear();
94    state.runtime.pending.pending_spell_impacts.clear();
95    let guardian_sources = state
96        .runtime
97        .companions
98        .guardians
99        .iter_mut()
100        .filter_map(Option::take)
101        .map(|guardian| guardian.source)
102        .collect::<Vec<_>>();
103
104    for source in guardian_sources {
105        let _ = state.deactivate_pet_health(source);
106    }
107
108    state.runtime.companions.guardians.clear();
109    state.runtime.companions.guardian_generations.clear();
110    state.runtime.companions.pet_actions.clear();
111    state.runtime.reset_spec_runtime();
112    state.reset_enemies();
113    buf.player_mut().gcd_end = 0.0;
114    buf.player_mut().cast_end = 0.0;
115    buf.player_mut().channel_end = 0.0;
116
117    for tracker in &mut state.defs.rppm_trackers {
118        tracker.last_attempt_time = 0.0;
119        tracker.last_proc_time = 0.0;
120        tracker.accumulated_blp = 0.0;
121    }
122
123    for tracker in &mut state.defs.threshold_trackers {
124        // NaN sentinel: the first roll randomizes the initial state.
125        tracker.accumulated = f64::NAN;
126    }
127
128    for proc in &mut state.defs.accumulating_impact_procs {
129        proc.accumulator = f64::NAN;
130    }
131
132    for proc in &mut state.defs.resource_gain_procs {
133        proc.attempts = 0;
134    }
135
136    let base_haste = state.config.base_stats.stats.haste;
137
138    reset_resources(state, buf);
139    reset_cooldowns(state, buf);
140    reset_channels(state, buf);
141    reset_auras(buf);
142    sync_spell_costs(state, buf);
143    reset_auto_attacks(state, buf);
144    buf.clear_history();
145    buf.player_mut().set_haste(base_haste);
146
147    project_encounter(state, buf, SimTime::ZERO);
148}
149
150pub(super) fn reset_resources(state: &mut CombatState, buf: &mut DenseBuffer) {
151    let resource_type = state.config.base_stats.resource_type;
152    let resource_start = state.config.base_stats.resource_start;
153    let base_regen = state.config.base_stats.base_regen;
154    let secondary_resource_type = state.config.base_stats.secondary_resource_type;
155
156    state.runtime.resources.primary_base_regen_override = None;
157
158    if let Some(rt) = resource_type {
159        if let Some(res) = buf.resource_mut(rt) {
160            if resource_start >= 0.0 {
161                res.current = resource_start;
162            } else {
163                res.current = res.max;
164            }
165
166            res.regen_per_sec = base_regen;
167
168            if rt == wowlab_types::combat::ResourceType::Runes {
169                state.runtime.resources.runes.reset_to(
170                    wowlab_types::numeric::f64_to_u32_saturating_round(res.current) as usize,
171                    SimTime::ZERO,
172                );
173            }
174        }
175    }
176
177    if let Some(rt) = secondary_resource_type {
178        if let Some(res) = buf.resource_mut(rt) {
179            res.current = 0.0;
180        }
181    }
182}
183
184fn reset_channels(state: &CombatState, buf: &mut DenseBuffer) {
185    for spell in &state.defs.spells {
186        if !spell.channel.behavior.is_channel {
187            continue;
188        }
189
190        if let Some(slot) = buf.spell_mut(wowlab_types::sim::SpellIdx::from_raw(spell.spell_id)) {
191            slot.is_channeling = 0;
192            slot.channel_end = 0.0;
193        }
194    }
195}
196
197pub(super) fn reset_cooldowns(state: &mut CombatState, buf: &mut DenseBuffer) {
198    for i in 0..state.defs.spells.len() {
199        let Some(spell) = state.defs.spells.get(i).copied() else {
200            continue;
201        };
202        // Charge registration may mutate definitions after build.
203
204        if !spell.cooldown.has_cooldown && !spell.is_charged() {
205            continue;
206        }
207
208        let local = LocalSpellIdx::new(u8::try_from(i).expect("spell count validated below 256"));
209
210        reset_cooldown(state, buf, local);
211    }
212}
213
214fn reset_auras(buf: &mut DenseBuffer) {
215    let keys: Vec<_> = buf.aura_keys().collect();
216
217    for key in keys {
218        if let Some(a) = buf.aura_mut(key) {
219            a.remove();
220        }
221    }
222}
223
224pub(super) fn reset_player_auras(buf: &mut DenseBuffer) {
225    let keys: Vec<_> = buf
226        .aura_keys()
227        .filter(|key| key.affected() == ActorId::Player)
228        .collect();
229
230    for key in keys {
231        if let Some(aura) = buf.aura_mut(key) {
232            aura.remove();
233        }
234    }
235}
236
237pub(super) fn reset_guardians_between_pulls(state: &mut CombatState) {
238    let guardian_sources = state
239        .runtime
240        .companions
241        .guardians
242        .iter_mut()
243        .filter_map(Option::take)
244        .map(|guardian| guardian.source)
245        .collect::<Vec<_>>();
246
247    for source in guardian_sources {
248        let _ = state.deactivate_pet_health(source);
249    }
250}
251
252fn reset_auto_attacks(state: &CombatState, buf: &mut DenseBuffer) {
253    for (i, attack) in state.defs.auto_attacks.iter().enumerate() {
254        let key = auto_attack_key(i);
255
256        if let Some(sw) = buf.swing_mut(&key) {
257            sw.next_swing_at = if attack.starts_active {
258                0.0
259            } else {
260                f64::INFINITY
261            };
262        }
263    }
264}
265
266pub(super) fn set_precombat_aura_stacks(
267    state: &CombatState,
268    buf: &mut DenseBuffer,
269    local_idx: LocalAuraIdx,
270    stacks: u8,
271    target: Option<wowlab_types::sim::EnemyIdx>,
272) {
273    // Precombat is an explicit application boundary: lock the selected concrete target now.
274    let max_stacks = i32::from(state.aura(local_idx).max_stacks);
275
276    for key in aura_keys_for_application(state, local_idx, ActorId::Player, target)
277        .into_iter()
278        .flatten()
279    {
280        buf.ensure_aura_slot(key);
281
282        if let Some(aura) = buf.aura_mut(key) {
283            aura.stacks = i32::from(stacks).min(max_stacks);
284        }
285    }
286
287    crate::systems::refresh_aura_projections(buf, target);
288    sync_spell_costs(state, buf);
289}