wowlab_engine_combat/handler/
reset.rs1use 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
12pub(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 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 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 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 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}