Skip to main content

wowlab_engine_combat/systems/buffs/
costs.rs

1use super::*;
2
3/// Multiplicative primary-resource gain factor from active auras (aura 213).
4pub(crate) fn current_resource_gain_mult(
5    view: &CombatView<'_>,
6    source: crate::state::ResourceGainSource,
7) -> f64 {
8    let totals = view.buff_totals();
9    let mut multiplier = totals.resource_gain_mult;
10
11    if view.state.config.base_stats.resource_type == Some(wowlab_types::combat::ResourceType::Rage)
12        && matches!(source, crate::state::ResourceGainSource::AutoAttack)
13    {
14        multiplier *= totals.rage_gain_mult;
15        multiplier *= view
16            .state
17            .config
18            .game_data
19            .power_auto_attack_gain_multiplier(i32::from(u8::from(
20                wowlab_types::combat::ResourceType::Rage,
21            )));
22    }
23
24    multiplier
25}
26
27pub(crate) fn effective_spell_cost(
28    view: &CombatView<'_>,
29    spell_id: u32,
30    base_cost: f64,
31    pool: ResourcePool,
32) -> f64 {
33    let CombatView { state, buf, .. } = view;
34    let mut percent_mult = 1.0;
35    let mut resource_cost_percent = 0.0;
36    let mut flat = 0.0;
37    let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
38    let pool_entry = state
39        .config
40        .game_data
41        .cost_entry(idx, pool == ResourcePool::Secondary);
42    let pool_type = match pool {
43        ResourcePool::Primary => state.config.base_stats.resource_type,
44        ResourcePool::Secondary => state.config.base_stats.secondary_resource_type,
45    };
46    let divisor = pool_type.map_or(1.0, |t| {
47        state
48            .config
49            .game_data
50            .power_display_divisor(i32::from(u8::from(t)))
51    });
52
53    if let Some(resource_type) = pool_type {
54        resource_cost_percent += state
55            .config
56            .game_data
57            .power_cost_percent(i32::from(u8::from(resource_type)));
58    }
59
60    for_each_active_aura_effect(state, buf, |stacks, effect| match effect {
61        BuffEffect::ResourceCostPercent(value, resource_type)
62            if Some(*resource_type) == pool_type =>
63        {
64            resource_cost_percent += value * stacks;
65        }
66        BuffEffect::SpellCostPercent(value, spells, effect_pool)
67            if *effect_pool == pool && spells.contains(&spell_id) =>
68        {
69            percent_mult *= 1.0 + value * stacks / HUNDRED;
70        }
71        BuffEffect::SpellCostFlat(value, spells, effect_pool)
72            if *effect_pool == pool && spells.contains(&spell_id) =>
73        {
74            flat += value * stacks;
75        }
76        BuffEffect::SpellCostFromEffect {
77            value,
78            source_spell_id,
79            effect_index,
80            cost_entry,
81            operation,
82        } if pool_entry != 0
83            && *cost_entry == pool_entry
84            && state.config.game_data.effect_affects_spell(
85                wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
86                *effect_index,
87                idx,
88            ) =>
89        {
90            let points = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
91
92            match operation {
93                ModifierOperation::Flat => flat += value * points / divisor,
94                ModifierOperation::Percent => {
95                    percent_mult *= 1.0 + value * points / HUNDRED;
96                }
97                _ => {}
98            }
99        }
100        _ => {}
101    });
102    percent_mult *= 1.0 + resource_cost_percent / HUNDRED;
103
104    ((base_cost + flat) * percent_mult).max(0.0)
105}
106
107pub(crate) fn base_primary_spell_cost(
108    state: &CombatState,
109    buf: &DenseBuffer,
110    spell: &crate::state::SpellData,
111) -> f64 {
112    let base = state.config.base_stats.resource_max;
113    let maximum = state
114        .config
115        .base_stats
116        .resource_type
117        .and_then(|resource_type| buf.resource(resource_type))
118        .map_or(base, |resource| resource.max);
119
120    spell.cost.resource_cost + spell.cost.resource_cost_pct.resolve(base, maximum)
121}
122
123pub(crate) fn base_optional_primary_spell_cost(
124    state: &CombatState,
125    buf: &DenseBuffer,
126    spell: &crate::state::SpellData,
127) -> f64 {
128    let base = state.config.base_stats.resource_max;
129    let maximum = state
130        .config
131        .base_stats
132        .resource_type
133        .and_then(|resource_type| buf.resource(resource_type))
134        .map_or(base, |resource| resource.max);
135
136    spell.cost.optional_resource_cost
137        + spell.cost.optional_resource_cost_pct.resolve(base, maximum)
138        + spell.cost.maximum_resource_cost_pct.resolve(base, maximum)
139}
140
141pub(crate) fn sync_player_health_from_stamina(state: &mut CombatState, buf: &DenseBuffer) {
142    let view = CombatView::new(state, buf);
143    let totals = *view.buff_totals();
144    let stamina = crate::systems::damage_pipeline::dynamic_stamina(state, &totals);
145    let Some(health_per_stamina) = state
146        .config
147        .game_data
148        .health_per_stamina(state.config.game_data.level())
149    else {
150        return;
151    };
152
153    state
154        .runtime
155        .health
156        .player_health
157        .set_maximum_clamp_current(stamina.floor() * health_per_stamina);
158}
159
160pub(crate) fn effective_optional_spell_cost(
161    view: &CombatView<'_>,
162    spell_id: u32,
163    base_optional_cost: f64,
164    pool: ResourcePool,
165) -> f64 {
166    let CombatView { state, buf, .. } = view;
167    let pool_type = match pool {
168        ResourcePool::Primary => state.config.base_stats.resource_type,
169        ResourcePool::Secondary => state.config.base_stats.secondary_resource_type,
170    };
171    let Some(pool_type) = pool_type else {
172        return base_optional_cost.max(0.0);
173    };
174    let raw_pool_type = i32::from(u8::from(pool_type));
175    let mut adjustment = 0.0;
176
177    let divisor = state.config.game_data.power_display_divisor(raw_pool_type);
178
179    for_each_active_aura_effect(state, buf, |stacks, effect| {
180        let BuffEffect::AdditionalPowerCostFromEffect {
181            value,
182            source_spell_id,
183            effect_index,
184            resource_type,
185        } = effect
186        else {
187            return;
188        };
189
190        if *resource_type == raw_pool_type
191            && state.config.game_data.effect_affects_spell(
192                wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
193                *effect_index,
194                wowlab_types::sim::SpellIdx::from_raw(spell_id),
195            )
196        {
197            adjustment += value * stacks / divisor;
198        }
199    });
200
201    (base_optional_cost + adjustment).max(0.0)
202}
203
204pub(crate) fn sync_spell_costs(state: &CombatState, buf: &mut DenseBuffer) {
205    for spell in &state.defs.spells {
206        let base_primary_cost = base_primary_spell_cost(state, buf, spell);
207        let cost = effective_spell_cost(
208            &CombatView::new(state, buf),
209            spell.spell_id,
210            base_primary_cost,
211            ResourcePool::Primary,
212        );
213        let secondary_cost = effective_spell_cost(
214            &CombatView::new(state, buf),
215            spell.spell_id,
216            spell.cost.secondary_resource_cost,
217            ResourcePool::Secondary,
218        );
219
220        if let Some(slot) = buf.spell_mut(wowlab_types::sim::SpellIdx::from_raw(spell.spell_id)) {
221            slot.cost = cost;
222            slot.secondary_cost = secondary_cost;
223        }
224    }
225}