Skip to main content

wowlab_engine_combat/handler/
reset.rs

1use wowlab_types::sim::{ActorId, SimTime};
2
3use crate::{
4    builder::buffer_init::project_initial_encounter,
5    state::{CombatInstance, LocalAuraIdx, LocalSpellIdx},
6    systems::{
7        aura_keys_for_application, initialize_all_spell_costs, reset_cooldown,
8        reset_cooldown_for_lifecycle,
9    },
10};
11
12// #t(fn: rust_max_fn_lines) reset is one auditable exhaustive state transaction.
13pub(super) fn reset_combat(
14    program: &crate::state::CombatProgram,
15    state: &mut CombatInstance,
16    last_sync: &mut SimTime,
17) {
18    state.runtime.total_damage = 0.0;
19    state.runtime.resources.last_primary_spent = 0.0;
20    state.runtime.resources.last_optional_primary_spent = 0.0;
21    state.runtime.resources.last_secondary_spent = 0.0;
22    state.runtime.casting.selected_candidate = None;
23    state.runtime.casting.active_player_candidate = None;
24    state.runtime.casting.instant_cast_pending_at = None;
25    state.runtime.casting.instant_off_gcd_casts_at = None;
26    state.runtime.casting.instant_off_gcd_spells.clear();
27    state.runtime.casting.active_channel_target = None;
28    state.runtime.casting.channel_generation = 0;
29    state.runtime.casting.active_channel_damage_mult = 1.0;
30    state.runtime.casting.active_channel = None;
31    state.runtime.casting.active_channel_last_tick = SimTime::ZERO;
32    state.runtime.casting.active_channel_tick_interval_ms = 0;
33    state.runtime.casting.active_channel_ticks_remaining = 0;
34    state.runtime.casting.active_channel_on_last_tick = false;
35    state.runtime.casting.active_channel_pushback_count = 0;
36    state.runtime.casting.free_action_dispatch_depth = 0;
37    state.runtime.casting.channel_chain_spell = None;
38    state.runtime.casting.active_casts.clear();
39    state.runtime.control.school_lockouts.clear();
40    state.runtime.control.movement.clear();
41    state.runtime.control.movement_generation.clear();
42    state.runtime.control.crowd_control.clear();
43    state.runtime.control.diminishing_returns.clear();
44    state.runtime.health.player_health.reset();
45
46    for health in state.runtime.health.pet_health.iter_mut().flatten() {
47        health.reset();
48    }
49
50    state.runtime.health.immunity_windows.clear();
51    state.runtime.health.active_absorbs.clear();
52    state.runtime.health.next_absorb_application_order = 0;
53    state.take_runtime_error();
54    *last_sync = SimTime::ZERO;
55    state.runtime.pending_events.clear();
56    state.runtime.procs.proc_heartbeat_scheduled_at = None;
57    state.runtime.deferred_work.clear();
58    state.runtime.procs.impact_effect_ready_at.fill(0.0);
59
60    state
61        .runtime
62        .procs
63        .impact_effect_target_ready_at
64        .fill(SimTime::ZERO);
65
66    for (remaining, proc) in state
67        .runtime
68        .procs
69        .impact_effect_charges
70        .iter_mut()
71        .zip(&program.impact_effect_procs)
72    {
73        *remaining = if proc.uses_stacks_for_charges {
74            0
75        } else {
76            proc.charges
77        };
78    }
79
80    for rng in &mut state.runtime.procs.impact_effect_rng {
81        rng.reset();
82    }
83
84    state.runtime.procs.system_rppm_ready_at.fill(SimTime::ZERO);
85    state
86        .runtime
87        .procs
88        .proc_category_ready_at_dense
89        .fill(SimTime::ZERO);
90    state.runtime.procs.proc_counters.fill(0);
91    reset_rotation_scratch(state);
92    #[cfg(not(feature = "rotation-optimizations"))]
93    state.runtime.recharge_rate_mults.fill(1.0);
94    state.runtime.pools.residual_damage_pools.clear();
95    state.runtime.pools.accumulated_damage_pools.clear();
96    state.runtime.pools.rolling_tick_mult.clear();
97    state.runtime.pools.async_stack_expiries.clear();
98    state.runtime.pools.overkill_absorb_ready_at.clear();
99    state.runtime.pools.cooldown_ready_at.clear();
100    state.runtime.pools.start_recovery_ready_at.clear();
101    state.runtime.pools.cooldown_category_ready_at.clear();
102    state.runtime.pools.charge_category_ready_at.clear();
103    crate::systems::reset_spell_gates(program, state);
104    state.runtime.pending.pending_hook_timers.fill(None);
105    state.runtime.pending.pending_spell_impacts.fill(None);
106
107    clear_active_guardians(state);
108
109    state.runtime.companions.guardian_generations.fill(0);
110    reset_free_guardian_slots(state);
111    state.runtime.companions.pet_actions.fill(None);
112    state.runtime.reset_spec_runtime();
113    state.reset_enemies(program);
114    state.buffer.player_mut().gcd_end = 0.0;
115    state.buffer.player_mut().cast_end = 0.0;
116    state.buffer.player_mut().channel_end = 0.0;
117
118    for tracker in &mut state.runtime.procs.rppm_trackers {
119        tracker.last_attempt_time = 0.0;
120        tracker.last_proc_time = 0.0;
121        tracker.accumulated_blp = 0.0;
122    }
123
124    for tracker in &mut state.runtime.procs.threshold_trackers {
125        // NaN sentinel: the first roll randomizes the initial state.
126        tracker.accumulated = f64::NAN;
127    }
128
129    for accumulator in &mut state.runtime.procs.accumulating_impact_accumulators {
130        *accumulator = f64::NAN;
131    }
132
133    for attempts in &mut state.runtime.procs.resource_gain_attempts {
134        *attempts = 0;
135    }
136
137    state.runtime.procs.player_cast_hooks_dispatching = false;
138    state.runtime.procs.impact_procs_dispatching = false;
139    state.runtime.procs.landed_impact_procs_dispatching = false;
140    state.runtime.procs.accumulating_impact_procs_dispatching = false;
141    state.runtime.procs.impact_effect_procs_dispatching = false;
142    state.runtime.procs.resource_gain_procs_dispatching = false;
143
144    reset_resources(program, state);
145    reset_cooldowns_for_lifecycle(program, state);
146    reset_channels(program, state);
147    program.reset_aura_slots(state, super::AuraResetToken(super::AuraResetScope::All));
148    initialize_all_spell_costs(program, state);
149    reset_auto_attacks(program, state);
150    state.buffer.clear_history();
151    state
152        .buffer
153        .player_mut()
154        .set_haste(program.config.base_stats.stats.haste);
155
156    #[cfg(feature = "rotation-optimizations")]
157    crate::systems::reset_cooldown_projections(state);
158
159    project_initial_encounter(program, state, SimTime::ZERO);
160
161    #[cfg(feature = "rotation-optimizations")]
162    state.runtime.rotation_mutations.clear();
163
164    #[cfg(test)]
165    state.reset_projection_flush_count();
166}
167
168fn reset_rotation_scratch(state: &mut CombatInstance) {
169    if let Some(reasons) = &mut state.runtime.mask_reasons {
170        reasons.clear();
171    }
172
173    state.runtime.scratch.rotation_masks.clear();
174    state.runtime.scratch.locked_action_masks.clear();
175    state.runtime.scratch.rotation_candidates.clear();
176
177    if let Some(reasons) = &mut state.runtime.scratch.rotation_trace_reasons {
178        reasons.clear();
179    }
180
181    #[cfg(test)]
182    {
183        state.runtime.rotation_spells_scanned = 0;
184    }
185}
186
187pub(super) fn reset_resources(program: &crate::state::CombatProgram, state: &mut CombatInstance) {
188    let resource_type = program.config.base_stats.resource_type;
189    let resource_start = program.config.base_stats.resource_start;
190    let base_regen = program.config.base_stats.base_regen;
191    let secondary_resource_type = program.config.base_stats.secondary_resource_type;
192
193    state.runtime.resources.primary_base_regen_override = None;
194
195    if let Some(rt) = resource_type
196        && let Some(handle) = program.resource_binding(rt)
197    {
198        let res = state.buffer.resource_mut_at(handle);
199
200        if resource_start >= 0.0 {
201            res.current = resource_start;
202        } else {
203            res.current = res.max;
204        }
205
206        res.regen_per_sec = base_regen;
207
208        if rt == wowlab_types::combat::ResourceType::Runes {
209            state.runtime.resources.runes.reset_to(
210                wowlab_types::numeric::f64_to_u32_saturating_round(res.current) as usize,
211                SimTime::ZERO,
212            );
213        }
214    }
215
216    if let Some(rt) = secondary_resource_type
217        && let Some(res) = program.buffer_resource_mut(state, rt)
218    {
219        res.current = 0.0;
220    }
221}
222
223fn reset_channels(program: &crate::state::CombatProgram, state: &mut CombatInstance) {
224    for (index, spell) in program.spells.iter().enumerate() {
225        if !spell.channel.behavior.is_channel {
226            continue;
227        }
228
229        let local = LocalSpellIdx::new(
230            u8::try_from(index).expect("spell count is finalized below local u8 capacity"),
231        );
232        let slot = state
233            .buffer
234            .spell_mut_at(program.buffer_bindings.spell(local));
235
236        slot.is_channeling = 0;
237        slot.channel_end = 0.0;
238    }
239}
240
241fn for_each_resettable_cooldown(
242    program: &crate::state::CombatProgram,
243    mut reset: impl FnMut(LocalSpellIdx),
244) {
245    for i in 0..program.spells.len() {
246        let Some(spell) = program.spells.get(i).copied() else {
247            continue;
248        };
249        // Draft-time charge registration is frozen in the finalized program.
250
251        if !spell.cooldown.has_cooldown && !spell.is_charged() {
252            continue;
253        }
254
255        let local = LocalSpellIdx::new(u8::try_from(i).expect("spell count validated below 256"));
256
257        reset(local);
258    }
259}
260
261pub(super) fn reset_cooldowns(
262    program: &crate::state::CombatProgram,
263    state: &mut CombatInstance,
264    now: SimTime,
265) {
266    for_each_resettable_cooldown(program, |local| {
267        reset_cooldown(program, state, local, now);
268    });
269}
270
271fn reset_cooldowns_for_lifecycle(
272    program: &crate::state::CombatProgram,
273    state: &mut CombatInstance,
274) {
275    for_each_resettable_cooldown(program, |local| {
276        reset_cooldown_for_lifecycle(program, state, local);
277    });
278}
279
280pub(super) fn reset_player_auras(
281    program: &crate::state::CombatProgram,
282    state: &mut CombatInstance,
283) {
284    program.reset_aura_slots(state, super::AuraResetToken(super::AuraResetScope::Player));
285}
286
287pub(super) fn reset_guardians_between_pulls(
288    _program: &crate::state::CombatProgram,
289    state: &mut CombatInstance,
290) {
291    clear_active_guardians(state);
292}
293
294fn clear_active_guardians(state: &mut CombatInstance) {
295    while let Some(slot) = state.runtime.companions.active_guardian_slots.pop() {
296        let index = usize::from(slot);
297
298        // BOUNDS: active slots are inserted only from the finalized guardian arena's free-slot stack.
299
300        if let Some(guardian) = state.runtime.companions.guardians[index].take() {
301            let _ = state.deactivate_pet_health(guardian.source);
302        }
303
304        state.runtime.companions.free_guardian_slots.push(slot);
305    }
306}
307
308fn reset_free_guardian_slots(state: &mut CombatInstance) {
309    state.runtime.companions.free_guardian_slots.clear();
310
311    for slot in (0..state.runtime.companions.guardians.len()).rev() {
312        state
313            .runtime
314            .companions
315            .free_guardian_slots
316            .push(u16::try_from(slot).expect("guardian arena fits u16"));
317    }
318}
319
320fn reset_auto_attacks(program: &crate::state::CombatProgram, state: &mut CombatInstance) {
321    for (i, attack) in program.auto_attacks.iter().enumerate() {
322        if let Some(sw) = program.buffer_swing_mut(state, i) {
323            sw.next_swing_at = if attack.starts_active {
324                0.0
325            } else {
326                f64::INFINITY
327            };
328        }
329    }
330}
331
332pub(super) fn set_precombat_aura_stacks(
333    program: &crate::state::CombatProgram,
334    state: &mut CombatInstance,
335    local_idx: LocalAuraIdx,
336    stacks: u8,
337    target: Option<wowlab_types::sim::EnemyIdx>,
338) {
339    // Precombat is an explicit application boundary: lock the selected concrete target now.
340    let max_stacks = i32::from(program.aura(local_idx).max_stacks);
341
342    for key in aura_keys_for_application(program, state, local_idx, ActorId::Player, target)
343        .into_iter()
344        .flatten()
345    {
346        if let Some(binding) = program.aura_binding(local_idx, key) {
347            program.mutate_aura(
348                state,
349                binding,
350                crate::state::AuraMutation::SetStacks(i32::from(stacks).min(max_stacks)),
351                SimTime::ZERO,
352            );
353        }
354    }
355
356    initialize_all_spell_costs(program, state);
357}