wowlab_engine_combat/systems/cooldowns/
charges.rs1use super::{
2 BuffEffect, CombatState, DenseBuffer, Event, LocalSpellIdx, MS_PER_SECOND, SimTime, SpellData,
3 effective_recharge_ms, for_each_active_aura_effect, project_members,
4};
5
6pub(super) fn spell_at(state: &CombatState, local: LocalSpellIdx) -> SpellData {
7 state
8 .defs
9 .spells
10 .get(local.as_usize())
11 .copied()
12 .expect("local spell index validated during combat construction")
13}
14
15pub(super) fn charge_pool_members(
16 state: &CombatState,
17 spell_local: LocalSpellIdx,
18) -> Vec<LocalSpellIdx> {
19 let spell = spell_at(state, spell_local);
20
21 spell.cooldown.charge_category.map_or_else(
22 || vec![spell_local],
23 |category| state.index.charge_categories.members(&category).to_vec(),
24 )
25}
26
27pub(super) fn charge_pool_owner(state: &CombatState, spell_local: LocalSpellIdx) -> LocalSpellIdx {
28 let spell = spell_at(state, spell_local);
29
30 spell
31 .cooldown
32 .charge_category
33 .and_then(|category| state.index.charge_categories.owner(&category).copied())
34 .unwrap_or(spell_local)
35}
36
37pub(super) fn sync_charge_pool(
38 state: &CombatState,
39 buf: &mut DenseBuffer,
40 owner: LocalSpellIdx,
41 members: &[LocalSpellIdx],
42) {
43 let owner_spell = spell_at(state, owner);
44 let Some(source) = buf.cooldown(owner_spell.idx()).copied() else {
45 return;
46 };
47
48 for member in members.iter().copied().filter(|member| *member != owner) {
49 let member_spell = spell_at(state, member);
50
51 if let Some(target) = buf.cooldown_mut(member_spell.idx()) {
52 target.current_charges = source.current_charges;
53 target.max_charges = source.max_charges;
54 target.next_charge_at = source.next_charge_at;
55 target.recharge_time = source.recharge_time;
56 target.duration = source.duration;
57 }
58 }
59}
60
61pub(super) fn consume_charge(
62 state: &mut CombatState,
63 cd: &mut wowlab_engine_domain::rotation::CooldownSlot,
64 spell: &SpellData,
65 recharge_ms: u32,
66 now: SimTime,
67) {
68 cd.duration = f64::from(recharge_ms) / MS_PER_SECOND;
69 cd.recharge_time = cd.duration;
70
71 if cd.current_charges <= 0 {
72 return;
73 }
74
75 let new_charges = cd.current_charges - 1;
76
77 if new_charges == cd.max_charges - 1 {
78 let ready_at = now.saturating_add(SimTime::from_millis(recharge_ms));
79
80 state.schedule(Event::CooldownReady {
81 t: ready_at,
82 cooldown_key: spell.idx(),
83 });
84 cd.next_charge_at = ready_at.as_secs_f64();
85 }
86
87 cd.current_charges = new_charges;
88}
89
90pub(crate) fn grant_charges(
92 state: &CombatState,
93 buf: &mut DenseBuffer,
94 spell_local: LocalSpellIdx,
95 amount: u8,
96) {
97 let spell = spell_at(state, spell_local);
98
99 if !spell.is_charged() || amount == 0 {
100 return;
101 }
102
103 let members = charge_pool_members(state, spell_local);
104 let owner = charge_pool_owner(state, spell_local);
105 let owner_spell = spell_at(state, owner);
106 let Some(cd) = buf.cooldown_mut(owner_spell.idx()) else {
107 return;
108 };
109
110 cd.current_charges = (cd.current_charges + i32::from(amount)).min(cd.max_charges);
111
112 if cd.current_charges >= cd.max_charges {
113 cd.next_charge_at = 0.0;
114 }
115
116 sync_charge_pool(state, buf, owner, &members);
117 project_members(state, buf, &members);
118}
119
120pub(super) fn reduce_charged_cooldown(
121 state: &mut CombatState,
122 cd: &mut wowlab_engine_domain::rotation::CooldownSlot,
123 cooldown_key: wowlab_types::sim::SpellIdx,
124 recharge_ms: u32,
125 mut reduction_s: f64,
126 now: SimTime,
127) {
128 let now_s = now.as_secs_f64();
129 let interval_s = f64::from(recharge_ms) / MS_PER_SECOND;
130
131 while reduction_s > 0.0 && cd.current_charges < cd.max_charges {
132 if cd.next_charge_at <= 0.0 {
133 cd.next_charge_at = now_s + interval_s;
134 }
135
136 let remaining_s = (cd.next_charge_at - now_s).max(0.0);
137
138 if reduction_s < remaining_s {
139 cd.next_charge_at -= reduction_s;
140 state.schedule(Event::CooldownReady {
141 t: SimTime::from_secs_f64(cd.next_charge_at),
142 cooldown_key,
143 });
144
145 return;
146 }
147
148 reduction_s -= remaining_s;
149 cd.current_charges += 1;
150
151 if cd.current_charges >= cd.max_charges {
152 cd.next_charge_at = 0.0;
153
154 return;
155 }
156
157 cd.next_charge_at = now_s + interval_s;
158 }
159
160 if cd.current_charges < cd.max_charges && cd.next_charge_at > now_s {
161 state.schedule(Event::CooldownReady {
162 t: SimTime::from_secs_f64(cd.next_charge_at),
163 cooldown_key,
164 });
165 }
166}
167
168pub(crate) fn sync_max_charges(state: &mut CombatState, buf: &mut DenseBuffer, now: SimTime) {
170 for i in 0..state.defs.spells.len() {
171 let Some(spell) = state.defs.spells.get(i).copied() else {
172 continue;
173 };
174
175 if !spell.is_charged() {
176 continue;
177 }
178
179 let local = LocalSpellIdx::new(u8::try_from(i).expect("spell count fits in u8"));
180
181 if charge_pool_owner(state, local) != local {
182 continue;
183 }
184
185 let Some(category) = spell.cooldown.charge_category else {
186 continue;
187 };
188 let mut delta = 0.0;
189
190 for_each_active_aura_effect(state, buf, |stacks, effect| {
191 if let BuffEffect::MaxChargesCategory(value, effect_category) = effect {
192 if *effect_category == category.raw() {
193 delta += value * stacks;
194 }
195 }
196 });
197 let new_max = wowlab_types::numeric::f64_to_i32_saturating_round(
198 (f64::from(spell.cooldown.max_charges) + delta).max(1.0),
199 );
200 let (recharge_ms, _) = effective_recharge_ms(state, buf, &spell);
201 let mut schedule_recharge_at = None;
202
203 if let Some(cd) = buf.cooldown_mut(spell.idx()) {
204 let old_max = cd.max_charges;
205
206 if new_max == old_max {
207 continue;
208 }
209
210 if new_max > old_max {
211 cd.current_charges += new_max - old_max;
212 } else {
213 cd.current_charges = (cd.current_charges - (old_max - new_max)).max(0);
214 }
215
216 cd.max_charges = new_max;
217 let now_s = now.as_secs_f64();
218
219 if cd.current_charges < new_max && cd.next_charge_at <= now_s && recharge_ms > 0 {
220 let next = now.saturating_add(SimTime::from_millis(recharge_ms));
221
222 cd.next_charge_at = next.as_secs_f64();
223 schedule_recharge_at = Some(next);
224 }
225 }
226
227 if let Some(t) = schedule_recharge_at {
228 state.schedule(Event::CooldownReady {
229 t,
230 cooldown_key: spell.idx(),
231 });
232 }
233
234 let members = charge_pool_members(state, local);
235
236 sync_charge_pool(state, buf, local, &members);
237 project_members(state, buf, &members);
238 }
239}