wowlab_engine_combat/systems/cooldowns/
charges.rs1#[cfg(feature = "rotation-optimizations")]
2use super::CooldownReadinessSnapshot;
3use super::{
4 BuffEffect, CombatInstance, Event, LocalSpellIdx, MS_PER_SECOND, SimTime, SpellData,
5 effective_recharge_ms, for_each_planned_player_effect, project_members,
6};
7
8pub(super) fn spell_at(program: &crate::state::CombatProgram, local: LocalSpellIdx) -> SpellData {
9 program
10 .spells
11 .get(local.as_usize())
12 .copied()
13 .expect("local spell index validated during combat construction")
14}
15
16pub(super) struct ChargePoolMembers<'a> {
17 single: [LocalSpellIdx; 1],
18 shared: Option<&'a [LocalSpellIdx]>,
19}
20
21impl ChargePoolMembers<'_> {
22 pub(super) fn as_slice(&self) -> &[LocalSpellIdx] {
23 self.shared.unwrap_or(&self.single)
24 }
25}
26
27pub(super) fn charge_pool_members(
28 program: &crate::state::CombatProgram,
29 spell_local: LocalSpellIdx,
30) -> ChargePoolMembers<'_> {
31 let shared = program
32 .charge_category_members
33 .get(spell_local.as_usize())
34 .map(Box::as_ref)
35 .filter(|members| !members.is_empty());
36
37 ChargePoolMembers {
38 single: [spell_local],
39 shared,
40 }
41}
42
43pub(super) fn charge_pool_owner(
44 program: &crate::state::CombatProgram,
45 spell_local: LocalSpellIdx,
46) -> LocalSpellIdx {
47 program
48 .charge_category_owners
49 .get(spell_local.as_usize())
50 .copied()
51 .unwrap_or(spell_local)
52}
53
54pub(super) fn sync_charge_pool(
55 program: &crate::state::CombatProgram,
56 state: &mut CombatInstance,
57 owner: LocalSpellIdx,
58 members: &[LocalSpellIdx],
59) {
60 let Some(source) = program.buffer_cooldown(state, owner).copied() else {
61 return;
62 };
63
64 for member in members.iter().copied().filter(|member| *member != owner) {
65 if let Some(target) = program.buffer_cooldown_mut(state, member) {
66 target.current_charges = source.current_charges;
67 target.max_charges = source.max_charges;
68 target.next_charge_at = source.next_charge_at;
69 target.recharge_time = source.recharge_time;
70 target.duration = source.duration;
71 }
72 }
73}
74
75pub(super) fn consume_charge(
76 pending_events: &mut Vec<Event>,
77 cd: &mut wowlab_engine_domain::rotation::CooldownSlot,
78 spell: wowlab_engine_ports::ScheduledSpellBinding,
79 recharge_ms: u32,
80 now: SimTime,
81) {
82 cd.duration = f64::from(recharge_ms) / MS_PER_SECOND;
83 cd.recharge_time = cd.duration;
84
85 if cd.current_charges <= 0 {
86 return;
87 }
88
89 let new_charges = cd.current_charges - 1;
90
91 if new_charges == cd.max_charges - 1 {
92 let ready_at = now.saturating_add(SimTime::from_millis(recharge_ms));
93
94 pending_events.push(Event::CooldownReady { t: ready_at, spell });
95 cd.next_charge_at = ready_at.as_secs_f64();
96 }
97
98 cd.current_charges = new_charges;
99}
100
101pub(crate) fn grant_charges(
103 program: &crate::state::CombatProgram,
104 state: &mut CombatInstance,
105 spell_local: LocalSpellIdx,
106 amount: u8,
107 _now: SimTime,
108) {
109 #[cfg(feature = "rotation-optimizations")]
110 let members = charge_pool_members(program, spell_local);
111 #[cfg(feature = "rotation-optimizations")]
112 let readiness_before =
113 CooldownReadinessSnapshot::capture(program, state, members.as_slice(), _now);
114
115 grant_charges_state(program, state, spell_local, amount);
116
117 #[cfg(feature = "rotation-optimizations")]
118 readiness_before.record(program, state, members.as_slice(), _now);
119}
120
121pub(super) fn grant_charges_state(
122 program: &crate::state::CombatProgram,
123 state: &mut CombatInstance,
124 spell_local: LocalSpellIdx,
125 amount: u8,
126) {
127 let spell = spell_at(program, spell_local);
128
129 if !spell.is_charged() || amount == 0 {
130 return;
131 }
132
133 let members = charge_pool_members(program, spell_local);
134 let owner = charge_pool_owner(program, spell_local);
135 let Some(cd) = program.buffer_cooldown_mut(state, owner) else {
136 return;
137 };
138
139 cd.current_charges = (cd.current_charges + i32::from(amount)).min(cd.max_charges);
140
141 if cd.current_charges >= cd.max_charges {
142 cd.next_charge_at = 0.0;
143 }
144
145 sync_charge_pool(program, state, owner, members.as_slice());
146 project_members(program, state, members.as_slice());
147}
148
149pub(super) fn reduce_charged_cooldown(
150 pending_events: &mut Vec<Event>,
151 cd: &mut wowlab_engine_domain::rotation::CooldownSlot,
152 spell: wowlab_engine_ports::ScheduledSpellBinding,
153 recharge_ms: u32,
154 mut reduction_s: f64,
155 now: SimTime,
156) {
157 let now_s = now.as_secs_f64();
158 let interval_s = f64::from(recharge_ms) / MS_PER_SECOND;
159
160 while reduction_s > 0.0 && cd.current_charges < cd.max_charges {
161 if cd.next_charge_at <= 0.0 {
162 cd.next_charge_at = now_s + interval_s;
163 }
164
165 let remaining_s = (cd.next_charge_at - now_s).max(0.0);
166
167 if reduction_s < remaining_s {
168 cd.next_charge_at -= reduction_s;
169 pending_events.push(Event::CooldownReady {
170 t: SimTime::from_secs_f64(cd.next_charge_at),
171 spell,
172 });
173
174 return;
175 }
176
177 reduction_s -= remaining_s;
178 cd.current_charges += 1;
179
180 if cd.current_charges >= cd.max_charges {
181 cd.next_charge_at = 0.0;
182
183 return;
184 }
185
186 cd.next_charge_at = now_s + interval_s;
187 }
188
189 if cd.current_charges < cd.max_charges && cd.next_charge_at > now_s {
190 pending_events.push(Event::CooldownReady {
191 t: SimTime::from_secs_f64(cd.next_charge_at),
192 spell,
193 });
194 }
195}
196
197pub(crate) fn refresh_maximum_charges_for(
199 program: &crate::state::CombatProgram,
200 state: &mut CombatInstance,
201 now: SimTime,
202 locals: &[LocalSpellIdx],
203) {
204 for &local in locals {
205 let spell = *program.spell(local);
206
207 if !spell.is_charged() {
208 continue;
209 }
210
211 if charge_pool_owner(program, local) != local {
212 continue;
213 }
214
215 #[cfg(feature = "rotation-optimizations")]
216 let readiness_members = charge_pool_members(program, local);
217 #[cfg(feature = "rotation-optimizations")]
218 let readiness_before =
219 CooldownReadinessSnapshot::capture(program, state, readiness_members.as_slice(), now);
220
221 let Some(category) = spell.cooldown.charge_category else {
222 continue;
223 };
224 let mut delta = 0.0;
225
226 for_each_planned_player_effect(
227 program,
228 state,
229 crate::state::EffectFamily::MAXIMUM_CHARGES,
230 |stacks, effect| {
231 if let BuffEffect::MaxChargesCategory(value, effect_category) = effect
232 && *effect_category == category.raw()
233 {
234 delta += value * stacks;
235 }
236 },
237 );
238 let new_max = wowlab_types::numeric::f64_to_i32_saturating_round(
239 (f64::from(spell.cooldown.max_charges) + delta).max(1.0),
240 );
241 let (recharge_ms, _) = effective_recharge_ms(program, state, local, &spell);
242 let mut schedule_recharge_at = None;
243
244 if let Some(cd) = program.buffer_cooldown_mut(state, local) {
245 let old_max = cd.max_charges;
246
247 if new_max == old_max {
248 continue;
249 }
250
251 if new_max > old_max {
252 cd.current_charges += new_max - old_max;
253 } else {
254 cd.current_charges = (cd.current_charges - (old_max - new_max)).max(0);
255 }
256
257 cd.max_charges = new_max;
258 let now_s = now.as_secs_f64();
259
260 if cd.current_charges < new_max && cd.next_charge_at <= now_s && recharge_ms > 0 {
261 let next = now.saturating_add(SimTime::from_millis(recharge_ms));
262
263 cd.next_charge_at = next.as_secs_f64();
264 schedule_recharge_at = Some(next);
265 }
266 }
267
268 if let Some(t) = schedule_recharge_at {
269 state.schedule(Event::CooldownReady {
270 t,
271 spell: program.scheduled_spell(local),
272 });
273 }
274
275 let members = charge_pool_members(program, local);
276
277 sync_charge_pool(program, state, local, members.as_slice());
278 project_members(program, state, members.as_slice());
279
280 #[cfg(feature = "rotation-optimizations")]
281 readiness_before.record(program, state, readiness_members.as_slice(), now);
282 }
283}