1use wowlab_engine_domain::rotation::DenseBuffer;
4use wowlab_engine_telemetry::{RESOURCE_SOURCE_PASSIVE_REGEN, ResourceEventKind, TelemetrySink};
5use wowlab_types::{
6 combat::ResourceType,
7 constants::{HUNDRED, MS_PER_SECOND},
8 sim::{ActorId, SimTime},
9};
10
11use super::buffs::current_stat_haste_mult;
12use crate::state::{CombatState, ResourceGain, ResourceGainScaling};
13
14#[must_use]
15pub(crate) fn spell_health_cost(
16 state: &CombatState,
17 actor: ActorId,
18 flat: f64,
19 percent_of_maximum: f64,
20) -> f64 {
21 let maximum = state.friendly_actor_max_health(actor).unwrap_or(0.0);
22
23 flat.max(0.0) + maximum * percent_of_maximum.max(0.0) / HUNDRED
24}
25
26pub(crate) fn spend_health_cost(
27 state: &mut CombatState,
28 actor: ActorId,
29 flat: f64,
30 percent_of_maximum: f64,
31) -> f64 {
32 let cost = spell_health_cost(state, actor, flat, percent_of_maximum);
33
34 state
35 .apply_friendly_actor_damage(actor, cost)
36 .unwrap_or(0.0)
37}
38
39pub fn set_primary_resource_base_regen(
41 state: &mut CombatState,
42 buf: &mut DenseBuffer,
43 base_regen: f64,
44) {
45 let (effective_regen, _) = effective_primary_regen(state, buf, base_regen, true);
46
47 state.runtime.resources.primary_base_regen_override = Some(base_regen);
48
49 if let Some(rt) = state.config.base_stats.resource_type {
50 if let Some(res) = buf.resource_mut(rt) {
51 res.regen_per_sec = effective_regen;
52 }
53 }
54}
55
56fn effective_primary_regen(
57 state: &CombatState,
58 buf: &DenseBuffer,
59 base_regen: f64,
60 haste_scaled: bool,
61) -> (f64, f64) {
62 let view = crate::CombatView::new(state, buf);
63 let totals = view.buff_totals();
64 let mut flat_regen_per_second = 0.0;
65 let primary_type = state.config.base_stats.resource_type;
66 let mut interrupted = false;
67 let mut interrupted_percent = 0.0;
68 let mut prevented_mask = 0_u64;
69
70 super::buffs::for_each_active_aura_effect(state, buf, |stacks, effect| match effect {
71 crate::BuffEffect::ResourceRegenPerFiveSeconds(value, resource_type) => {
72 if Some(*resource_type) == primary_type {
73 flat_regen_per_second +=
74 value * stacks / wowlab_engine_domain::dbc::POWER_REGEN_PERIOD_SECONDS;
75 }
76 }
77 crate::BuffEffect::InterruptRegen => interrupted = true,
78 crate::BuffEffect::InterruptedRegenPercent(value) => {
79 interrupted_percent += value * stacks;
80 }
81 crate::BuffEffect::PreventResourceRegen(mask) => prevented_mask |= mask,
82 _ => {}
83 });
84
85 let haste_multiplier = if haste_scaled {
86 current_stat_haste_mult(&view)
87 } else {
88 1.0
89 };
90 let scaled_base = base_regen * haste_multiplier * (1.0 + totals.regen_pct / HUNDRED);
91
92 let effective = apply_regen_interruptions(
93 scaled_base + flat_regen_per_second,
94 primary_type,
95 interrupted,
96 interrupted_percent,
97 prevented_mask,
98 );
99
100 (effective, totals.max_resource_flat)
101}
102
103fn apply_regen_interruptions(
104 rate: f64,
105 primary_type: Option<ResourceType>,
106 interrupted: bool,
107 interrupted_percent: f64,
108 prevented_mask: u64,
109) -> f64 {
110 let resource_prevented = primary_type.is_some_and(|resource_type| {
111 let bit = u32::from(u8::from(resource_type));
112
113 1_u64
114 .checked_shl(bit)
115 .is_some_and(|mask| prevented_mask & mask != 0)
116 });
117
118 if resource_prevented {
119 0.0
120 } else if interrupted {
121 let continuation = if primary_type == Some(ResourceType::Mana) {
122 interrupted_percent.max(0.0) / HUNDRED
123 } else {
124 0.0
125 };
126
127 rate * continuation
128 } else {
129 rate
130 }
131}
132
133pub(crate) fn sync_resource(
135 state: &mut CombatState,
136 buf: &mut DenseBuffer,
137 now: SimTime,
138 last_sync: &mut SimTime,
139 sink: &mut TelemetrySink,
140) {
141 if now <= *last_sync {
142 return;
143 }
144 if state.config.base_stats.resource_type == Some(ResourceType::Runes) {
147 sync_runes(state, buf, now, last_sync, sink);
148
149 return;
150 }
151
152 let elapsed_s = f64::from(now.as_millis() - last_sync.as_millis()) / MS_PER_SECOND;
153
154 let dynamic_base_regen = state.runtime.resources.primary_base_regen_override;
155 let base_regen = dynamic_base_regen.unwrap_or(state.config.base_stats.base_regen);
156 let (effective_regen, max_resource_flat) = effective_primary_regen(
157 state,
158 buf,
159 base_regen,
160 state.config.haste_regen || dynamic_base_regen.is_some(),
161 );
162 let base_max = state.config.base_stats.resource_max;
163 let mut gained = 0.0;
164 let mut wasted = 0.0;
165
166 if let Some(rt) = state.config.base_stats.resource_type {
167 if let Some(res) = buf.resource_mut(rt) {
168 res.max = base_max + max_resource_flat;
169
170 let uncapped = res.current + effective_regen * elapsed_s;
171 let capped = uncapped.min(res.max);
172
173 gained = capped - res.current;
174 wasted = uncapped - capped;
175 res.current = capped;
176 res.regen_per_sec = effective_regen;
177 }
178 }
179
180 emit_passive_regen(state, buf, now, sink, gained, wasted);
181
182 *last_sync = now;
183}
184
185fn emit_passive_regen(
187 state: &CombatState,
188 buf: &DenseBuffer,
189 now: SimTime,
190 sink: &mut TelemetrySink,
191 gained: f64,
192 wasted: f64,
193) {
194 if gained <= 0.0 && wasted <= 0.0 {
195 return;
196 }
197
198 super::procs::emit_resource(
199 state,
200 buf,
201 sink,
202 super::procs::ResourceTelemetry {
203 kind: ResourceEventKind::Gain { wasted },
204 amount: gained,
205 secondary: false,
206 now,
207 source_spell_id: RESOURCE_SOURCE_PASSIVE_REGEN,
208 },
209 );
210}
211
212fn sync_runes(
213 state: &mut CombatState,
214 buf: &mut DenseBuffer,
215 now: SimTime,
216 last_sync: &mut SimTime,
217 sink: &mut TelemetrySink,
218) {
219 let current = buf
220 .resource(ResourceType::Runes)
221 .map_or(0.0, |resource| resource.current);
222 let rune_count = u32::try_from(crate::state::RUNE_COUNT).expect("rune count fits u32");
223 let represented = u32::try_from(state.runtime.resources.runes.full_count())
224 .expect("full rune count fits u32");
225
226 if (current - f64::from(represented)).abs() > f64::EPSILON {
227 let requested = wowlab_types::numeric::f64_to_u32_saturating_round(current).min(rune_count);
228
229 state.runtime.resources.runes.reset_to(
230 usize::try_from(requested).expect("requested rune count fits usize"),
231 *last_sync,
232 );
233 }
234
235 let elapsed_s = f64::from(now.as_millis() - last_sync.as_millis()) / MS_PER_SECOND;
236 let (rate_multiplier, _) = effective_primary_regen(state, buf, 1.0, true);
237
238 let banked_before = u32::try_from(state.runtime.resources.runes.full_count())
239 .expect("full rune count fits u32");
240
241 state
242 .runtime
243 .resources
244 .runes
245 .advance(elapsed_s * rate_multiplier);
246 state.runtime.resources.runes.updated_at = now;
247
248 let mut banked = 0.0;
249
250 if let Some(resource) = buf.resource_mut(ResourceType::Runes) {
251 let full_count = u32::try_from(state.runtime.resources.runes.full_count())
252 .expect("full rune count fits u32");
253 let regenerating_count = u32::try_from(state.runtime.resources.runes.regenerating_count())
254 .expect("regenerating rune count fits u32");
255
256 resource.max = f64::from(rune_count);
257 resource.current = f64::from(full_count);
258 resource.regen_per_sec =
259 f64::from(regenerating_count) * rate_multiplier / crate::state::RUNE_RECHARGE_SECONDS;
260 banked = f64::from(full_count.saturating_sub(banked_before));
261 }
262
263 emit_passive_regen(state, buf, now, sink, banked, 0.0);
264
265 *last_sync = now;
266}
267
268pub(crate) fn rune_time_to_regen(
269 state: &CombatState,
270 buf: &DenseBuffer,
271 needed: usize,
272) -> Option<f64> {
273 let (rate_multiplier, _) = effective_primary_regen(state, buf, 1.0, true);
274
275 state
276 .runtime
277 .resources
278 .runes
279 .time_to_regen(needed, rate_multiplier)
280}
281
282#[inline]
283const fn resource_type(state: &CombatState, secondary: bool) -> Option<ResourceType> {
284 if secondary {
285 state.config.base_stats.secondary_resource_type
286 } else {
287 state.config.base_stats.resource_type
288 }
289}
290
291pub(crate) fn resource_pool_for_type(
292 state: &CombatState,
293 resource_type: ResourceType,
294) -> Option<bool> {
295 if state.config.base_stats.resource_type == Some(resource_type) {
296 Some(false)
297 } else if state.config.base_stats.secondary_resource_type == Some(resource_type) {
298 Some(true)
299 } else {
300 None
301 }
302}
303
304fn spend_resource_impl(
305 state: &mut CombatState,
306 buf: &mut DenseBuffer,
307 amount: f64,
308 secondary: bool,
309) -> bool {
310 if let Some(rt) = resource_type(state, secondary) {
311 if let Some(res) = buf.resource_mut(rt) {
312 if !res.spend(amount) {
313 return false;
314 }
315
316 if rt == ResourceType::Runes {
317 return state.runtime.resources.runes.consume(
318 wowlab_types::numeric::f64_to_u32_saturating_round(amount.max(0.0)) as usize,
319 );
320 }
321
322 return true;
323 }
324 }
325
326 false
327}
328
329fn gain_resource_impl(
330 state: &mut CombatState,
331 buf: &mut DenseBuffer,
332 gain: ResourceGain,
333 secondary: bool,
334) -> (f64, f64) {
335 let amount = if secondary || gain.scaling == ResourceGainScaling::Unmodified {
336 gain.amount
337 } else {
338 gain.amount
339 * super::buffs::current_resource_gain_mult(
340 &crate::CombatView::new(state, buf),
341 gain.source,
342 )
343 };
344
345 tracing::trace!(?gain.source, base_amount = gain.amount, amount, secondary, "RESOURCE_GAIN");
346
347 if let Some(rt) = resource_type(state, secondary) {
348 if let Some(res) = buf.resource_mut(rt) {
349 let wasted = res.gain(amount);
350 let actual = amount - wasted;
351
352 if rt == ResourceType::Runes {
353 let gained = wowlab_types::numeric::f64_to_u32_saturating_round(actual.max(0.0));
354
355 state
356 .runtime
357 .resources
358 .runes
359 .fill(usize::try_from(gained).expect("gained rune count fits usize"));
360 let full_count = u32::try_from(state.runtime.resources.runes.full_count())
361 .expect("full rune count fits u32");
362
363 res.current = f64::from(full_count);
364 }
365
366 return (actual, wasted);
367 }
368 }
369
370 (0.0, amount)
371}
372
373pub(crate) fn spend_resource(state: &mut CombatState, buf: &mut DenseBuffer, amount: f64) -> bool {
374 spend_resource_impl(state, buf, amount, false)
375}
376
377pub(crate) fn gain_resource_from(
378 state: &mut CombatState,
379 buf: &mut DenseBuffer,
380 gain: ResourceGain,
381) -> (f64, f64) {
382 gain_resource_impl(state, buf, gain, false)
383}
384
385pub(crate) fn spend_secondary_resource(
386 state: &mut CombatState,
387 buf: &mut DenseBuffer,
388 amount: f64,
389) -> bool {
390 spend_resource_impl(state, buf, amount, true)
391}
392
393pub(crate) fn secondary_spends_all_points(state: &CombatState) -> bool {
394 matches!(resource_type(state, true), Some(ResourceType::ComboPoints))
395}
396
397pub(crate) fn spend_all_secondary_resource(
398 state: &mut CombatState,
399 buf: &mut DenseBuffer,
400 cost: f64,
401) -> f64 {
402 if let Some(rt) = resource_type(state, true) {
403 if let Some(res) = buf.resource_mut(rt) {
404 let available = res.current;
405
406 if available >= cost && res.spend(available) {
407 return available;
408 }
409 }
410 }
411
412 0.0
413}
414
415pub(crate) fn gain_secondary_resource_from(
416 state: &mut CombatState,
417 buf: &mut DenseBuffer,
418 gain: ResourceGain,
419) -> (f64, f64) {
420 gain_resource_impl(state, buf, gain, true)
421}
422
423#[cfg(test)]
424#[path = "resources/tests.rs"]
425mod tests;