Skip to main content

wowlab_engine_domain/dbc/
decode.rs

1//! Per-spell DBC decode arithmetic: power-cost scaling and energize-gain matching.
2
3use 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
12/// Power-type sentinel for a costless spell, paired with a `0.0` amount (no real `power_type_enum` is negative).
13pub(super) const NO_POWER_COST_TYPE: i32 = -1;
14/// `power_type_enum -> display_modifier` divisors used to descale DBC power amounts.
15#[derive(Clone, Debug, Default)]
16pub struct PowerModifiers {
17    by_type: FastMap<i32, f64>,
18}
19
20impl PowerModifiers {
21    /// Creates an empty modifier table.
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Build from the resolved `power_type_enum -> display_modifier` pairs (only positive modifiers).
28    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/// Descale a raw DBC power amount by its `display_modifier` (or leave it as-is when absent).
40#[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/// Primary power cost (descaled) and its power type, matched by resource type; -1 when absent.
69#[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/// Health costs decoded from the active health-power row.
151#[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/// Flat, percentage, and optional health costs from the active health-power row.
173#[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/// Optional primary-resource spend encoded as a Power Burn spell effect.
191#[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/// Minimum secondary power cost from the row matching the spec's secondary resource type.
210#[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/// 1-based DBC power-cost entry index for the resource type's cost; 0 when absent.
229#[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/// Optional secondary power cost above the minimum (for variable-point finishers).
253#[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/// Resource gain from the first ENERGIZE effect granting the primary resource type, descaled.
272#[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/// Resource gain from the first ENERGIZE effect granting the secondary resource type, descaled.
288#[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}