wowlab_engine_combat/systems/buffs/
costs.rs1use super::{
2 ActorId, ActorView, BuffEffect, CombatInstance, CombatView, HUNDRED, ModifierOperation,
3 ResourcePool, dbc_effect_points, for_each_active_cost_modifier_for_action,
4};
5
6pub(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}