Skip to main content

wowlab_engine_combat/systems/buffs/
costs.rs

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