1use std::{collections::HashSet, hash::BuildHasher};
4
5use wowlab_types::{
6 data::{PowerCostEntry, ResourceCostPercent, SpellDataFlat},
7 sim::FastMap,
8};
9
10use super::{PowerType, SpellEffectKind, SpellEffectSemanticExt};
11
12pub(super) const NO_POWER_COST_TYPE: i32 = -1;
14#[derive(Clone, Debug, Default)]
16pub struct PowerModifiers {
17 by_type: FastMap<i32, f64>,
18}
19
20impl PowerModifiers {
21 #[must_use]
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn from_pairs(pairs: impl IntoIterator<Item = (i32, f64)>) -> Self {
29 Self {
30 by_type: pairs.into_iter().filter(|(_, m)| *m > 0.0).collect(),
31 }
32 }
33
34 fn divisor(&self, power_type: i32) -> f64 {
35 self.by_type.get(&power_type).copied().unwrap_or(1.0)
36 }
37}
38
39#[must_use]
41pub fn scale_power_amount(raw: f64, power_type: i32, modifiers: &PowerModifiers) -> f64 {
42 raw / modifiers.divisor(power_type)
43}
44
45pub(super) fn power_cost_is_available<S>(
46 cost: &PowerCostEntry,
47 active_aura_spell_ids: &HashSet<i32, S>,
48) -> bool
49where
50 S: BuildHasher,
51{
52 cost.required_aura_spell_id == 0 || active_aura_spell_ids.contains(&cost.required_aura_spell_id)
53}
54
55fn power_cost_row<'a, S>(
56 spell: &'a SpellDataFlat,
57 resource_type: i32,
58 active_aura_spell_ids: &HashSet<i32, S>,
59) -> Option<&'a PowerCostEntry>
60where
61 S: BuildHasher,
62{
63 spell.power_costs.iter().find(|cost| {
64 cost.power_type == resource_type && power_cost_is_available(cost, active_aura_spell_ids)
65 })
66}
67
68#[must_use]
70pub fn primary_cost<S>(
71 spell: &SpellDataFlat,
72 primary_resource_type: i32,
73 modifiers: &PowerModifiers,
74 active_aura_spell_ids: &HashSet<i32, S>,
75) -> (f64, i32)
76where
77 S: BuildHasher,
78{
79 power_cost_row(spell, primary_resource_type, active_aura_spell_ids).map_or(
80 (0.0, NO_POWER_COST_TYPE),
81 |p| {
82 (
83 scale_power_amount(f64::from(p.cost), p.power_type, modifiers),
84 p.power_type,
85 )
86 },
87 )
88}
89
90#[must_use]
91pub fn primary_cost_pct<S>(
92 spell: &SpellDataFlat,
93 primary_resource_type: i32,
94 active_aura_spell_ids: &HashSet<i32, S>,
95) -> ResourceCostPercent
96where
97 S: BuildHasher,
98{
99 ResourceCostPercent::base(
100 power_cost_row(spell, primary_resource_type, active_aura_spell_ids)
101 .map_or(0.0, |p| p.cost_pct),
102 )
103}
104
105#[must_use]
106pub fn primary_max_cost_pct<S>(
107 spell: &SpellDataFlat,
108 primary_resource_type: i32,
109 active_aura_spell_ids: &HashSet<i32, S>,
110) -> ResourceCostPercent
111where
112 S: BuildHasher,
113{
114 ResourceCostPercent::maximum(
115 power_cost_row(spell, primary_resource_type, active_aura_spell_ids)
116 .map_or(0.0, |p| p.max_cost_pct),
117 )
118}
119
120#[must_use]
121pub fn primary_optional_cost_pct<S>(
122 spell: &SpellDataFlat,
123 primary_resource_type: i32,
124 active_aura_spell_ids: &HashSet<i32, S>,
125) -> ResourceCostPercent
126where
127 S: BuildHasher,
128{
129 ResourceCostPercent::base(
130 power_cost_row(spell, primary_resource_type, active_aura_spell_ids)
131 .map_or(0.0, |p| p.optional_cost_pct),
132 )
133}
134
135#[must_use]
136pub fn primary_optional_cost<S>(
137 spell: &SpellDataFlat,
138 primary_resource_type: i32,
139 modifiers: &PowerModifiers,
140 active_aura_spell_ids: &HashSet<i32, S>,
141) -> f64
142where
143 S: BuildHasher,
144{
145 power_cost_row(spell, primary_resource_type, active_aura_spell_ids).map_or(0.0, |p| {
146 scale_power_amount(f64::from(p.optional_cost), p.power_type, modifiers)
147 })
148}
149
150#[derive(Clone, Copy, Debug, PartialEq)]
152pub struct HealthCosts {
153 pub cost: f64,
154 pub cost_pct: f64,
155 pub max_cost_pct: f64,
156 pub optional_cost: f64,
157 pub optional_cost_pct: ResourceCostPercent,
158}
159
160impl Default for HealthCosts {
161 fn default() -> Self {
162 Self {
163 cost: 0.0,
164 cost_pct: 0.0,
165 max_cost_pct: 0.0,
166 optional_cost: 0.0,
167 optional_cost_pct: ResourceCostPercent::maximum(0.0),
168 }
169 }
170}
171
172#[must_use]
174pub fn health_cost<S>(spell: &SpellDataFlat, active_aura_spell_ids: &HashSet<i32, S>) -> HealthCosts
175where
176 S: BuildHasher,
177{
178 power_cost_row(spell, PowerType::Health as i32, active_aura_spell_ids).map_or_else(
179 HealthCosts::default,
180 |cost| HealthCosts {
181 cost: f64::from(cost.cost).max(0.0),
182 cost_pct: cost.cost_pct.max(0.0),
183 max_cost_pct: cost.max_cost_pct.max(0.0),
184 optional_cost: f64::from(cost.optional_cost).max(0.0),
185 optional_cost_pct: ResourceCostPercent::maximum(cost.optional_cost_pct.max(0.0)),
186 },
187 )
188}
189
190#[must_use]
192pub fn primary_power_burn_cost(
193 spell: &SpellDataFlat,
194 primary_resource_type: i32,
195 modifiers: &PowerModifiers,
196) -> f64 {
197 spell
198 .effects
199 .iter()
200 .find(|effect| {
201 effect.effect_is(SpellEffectKind::PowerBurn)
202 && effect.misc_value_0 == primary_resource_type
203 })
204 .map_or(0.0, |effect| {
205 scale_power_amount(effect.base_points, effect.misc_value_0, modifiers).max(0.0)
206 })
207}
208
209#[must_use]
211pub fn secondary_cost<S>(
212 spell: &SpellDataFlat,
213 secondary_resource_type: Option<i32>,
214 modifiers: &PowerModifiers,
215 active_aura_spell_ids: &HashSet<i32, S>,
216) -> f64
217where
218 S: BuildHasher,
219{
220 secondary_resource_type
221 .and_then(|st| {
222 power_cost_row(spell, st, active_aura_spell_ids)
223 .map(|p| scale_power_amount(f64::from(p.cost), p.power_type, modifiers))
224 })
225 .unwrap_or(0.0)
226}
227
228#[must_use]
230pub fn cost_entry_index<S>(
231 spell: &SpellDataFlat,
232 resource_type: Option<i32>,
233 active_aura_spell_ids: &HashSet<i32, S>,
234) -> u8
235where
236 S: BuildHasher,
237{
238 let Some(resource_type) = resource_type else {
239 return 0;
240 };
241
242 spell
243 .power_costs
244 .iter()
245 .position(|cost| {
246 cost.power_type == resource_type && power_cost_is_available(cost, active_aura_spell_ids)
247 })
248 .and_then(|index| u8::try_from(index + 1).ok())
249 .unwrap_or(0)
250}
251
252#[must_use]
254pub fn secondary_optional_cost<S>(
255 spell: &SpellDataFlat,
256 secondary_resource_type: Option<i32>,
257 modifiers: &PowerModifiers,
258 active_aura_spell_ids: &HashSet<i32, S>,
259) -> f64
260where
261 S: BuildHasher,
262{
263 secondary_resource_type
264 .and_then(|st| {
265 power_cost_row(spell, st, active_aura_spell_ids)
266 .map(|p| scale_power_amount(f64::from(p.optional_cost), p.power_type, modifiers))
267 })
268 .unwrap_or(0.0)
269}
270
271#[must_use]
273pub fn primary_resource_gain(
274 spell: &SpellDataFlat,
275 primary_resource_type: i32,
276 modifiers: &PowerModifiers,
277) -> f64 {
278 spell
279 .effects
280 .iter()
281 .find(|e| e.effect_is(SpellEffectKind::Energize) && e.misc_value_0 == primary_resource_type)
282 .map_or(0.0, |e| {
283 scale_power_amount(e.base_points, e.misc_value_0, modifiers)
284 })
285}
286
287#[must_use]
289pub fn secondary_resource_gain(
290 spell: &SpellDataFlat,
291 secondary_resource_type: Option<i32>,
292 modifiers: &PowerModifiers,
293) -> f64 {
294 secondary_resource_type
295 .and_then(|st| {
296 spell
297 .effects
298 .iter()
299 .find(|e| e.effect_is(SpellEffectKind::Energize) && e.misc_value_0 == st)
300 .map(|e| scale_power_amount(e.base_points, e.misc_value_0, modifiers))
301 })
302 .unwrap_or(0.0)
303}
304
305#[cfg(test)]
306mod tests {
307 use googletest::prelude::*;
308 use wowlab_types::{
309 data::{PowerCostEntry, SpellDataFlat, SpellEffect},
310 sim::FastSet,
311 };
312
313 use super::{
314 NO_POWER_COST_TYPE, PowerModifiers, primary_cost, primary_power_burn_cost,
315 scale_power_amount, secondary_cost, secondary_optional_cost,
316 };
317
318 #[gtest]
319 fn power_cost_divided_by_display_modifier() {
320 let modifiers = PowerModifiers::from_pairs([(8_i32, 10.0_f64), (13_i32, 100.0_f64)]);
321
322 expect_that!(scale_power_amount(400.0, 8, &modifiers), eq(40.0));
323 expect_that!(scale_power_amount(5000.0, 13, &modifiers), eq(50.0));
324 }
325
326 #[gtest]
327 fn required_aura_filters_other_specializations_power_rows() {
328 let spell = SpellDataFlat {
329 power_costs: vec![
330 PowerCostEntry {
331 power_type: 12,
332 cost: 2,
333 cost_pct: 0.0,
334 max_cost_pct: 0.0,
335 optional_cost: 0,
336 optional_cost_pct: 0.0,
337 required_aura_spell_id: 137_025,
338 },
339 PowerCostEntry {
340 power_type: 3,
341 cost: 40,
342 cost_pct: 0.0,
343 max_cost_pct: 0.0,
344 optional_cost: 0,
345 optional_cost_pct: 0.0,
346 required_aura_spell_id: 137_023,
347 },
348 ],
349 ..Default::default()
350 };
351 let active = FastSet::from_iter([137_025]);
352 let modifiers = PowerModifiers::default();
353
354 expect_that!(primary_cost(&spell, 3, &modifiers, &active).0, eq(0.0));
355 expect_that!(
356 secondary_cost(&spell, Some(12), &modifiers, &active),
357 eq(2.0)
358 );
359 }
360
361 #[gtest]
362 fn secondary_cost_keeps_optional_spend_separate() {
363 let spell = SpellDataFlat {
364 power_costs: vec![PowerCostEntry {
365 power_type: 4,
366 cost: 1,
367 cost_pct: 0.0,
368 max_cost_pct: 0.0,
369 optional_cost: 4,
370 optional_cost_pct: 0.0,
371 required_aura_spell_id: 0,
372 }],
373 ..Default::default()
374 };
375 let active = FastSet::default();
376 let modifiers = PowerModifiers::default();
377
378 expect_that!(
379 secondary_cost(&spell, Some(4), &modifiers, &active),
380 eq(1.0)
381 );
382 expect_that!(
383 secondary_optional_cost(&spell, Some(4), &modifiers, &active),
384 eq(4.0)
385 );
386 }
387
388 #[gtest]
389 fn power_burn_is_optional_primary_resource_spend() {
390 let spell = SpellDataFlat {
391 effects: vec![SpellEffect {
392 effect: 62,
393 base_points: 25.0,
394 misc_value_0: 3,
395 ..Default::default()
396 }],
397 ..Default::default()
398 };
399 let modifiers = PowerModifiers::default();
400
401 expect_that!(primary_power_burn_cost(&spell, 3, &modifiers), eq(25.0));
402 expect_that!(primary_power_burn_cost(&spell, 0, &modifiers), eq(0.0));
403 }
404
405 #[gtest]
406 fn power_amount_unscaled_when_modifier_absent() {
407 let modifiers = PowerModifiers::from_pairs([(8_i32, 10.0_f64)]);
408
409 expect_that!(scale_power_amount(40.0, 3, &modifiers), eq(40.0));
410 expect_that!(
411 scale_power_amount(0.0, NO_POWER_COST_TYPE, &modifiers),
412 eq(0.0)
413 );
414 }
415
416 #[gtest]
417 fn primary_gain_data_scaled_for_match_literal_preserved_for_absent() {
418 use wowlab_engine_gamedata::SpellProps;
419 use wowlab_types::{data::PowerTypeFlat, sim::SpellIdx};
420
421 let power_types = [PowerTypeFlat {
422 power_type_enum: 8,
423 display_modifier: 10.0,
424 ..Default::default()
425 }];
426 let modifiers = PowerModifiers::from_pairs(
427 power_types
428 .iter()
429 .map(|p| (p.power_type_enum, f64::from(p.display_modifier))),
430 );
431
432 let starfire_gain = scale_power_amount(80.0, 8, &modifiers);
433
434 expect_that!(starfire_gain, eq(8.0));
435
436 let mut builder = wowlab_engine_gamedata::ResolvedGameData::builder();
437
438 builder.insert_spell_props(
439 SpellIdx::from_raw(194_153),
440 SpellProps {
441 gain: starfire_gain,
442 ..Default::default()
443 },
444 );
445 builder.insert_spell_props(
446 SpellIdx::from_raw(190_984),
447 SpellProps {
448 gain: 8.0,
449 ..Default::default()
450 },
451 );
452 let data = builder.build();
453
454 expect_that!(data.gain(SpellIdx::from_raw(194_153)), eq(Some(8.0)));
455 expect_that!(data.gain(SpellIdx::from_raw(190_984)), eq(Some(8.0)));
456 }
457}