wowlab_engine_combat/pipeline/
resources.rs1use super::{
2 CombatCtx, HUNDRED, ResourceEventKind, ResourceGain, ResourceGainSource, ResourceTelemetry,
3 SimTime, SpellData, bypass_aura_active, effective_optional_spell_cost, effective_spell_cost,
4 emit_resource, gain_resource_with_procs, process_cast, secondary_spends_all_points,
5 spend_all_secondary_resource, spend_health_cost, spend_resource, spend_secondary_resource,
6};
7
8pub(super) fn process_resource_threshold_triggers(ctx: &mut CombatCtx<'_>) {
9 let trigger_count = ctx.state.defs.resource_threshold_triggers.len();
10
11 for index in 0..trigger_count {
12 let trigger = ctx.state.defs.resource_threshold_triggers[index];
14 let spent =
15 if Some(trigger.resource_type) == ctx.state.config.base_stats.secondary_resource_type {
16 ctx.state.runtime.resources.last_secondary_spent
17 } else {
18 ctx.state.runtime.resources.last_primary_spent
19 };
20
21 if spent <= 0.0 {
22 continue;
23 }
24
25 let Some(resource) = ctx.buf.resource(trigger.resource_type) else {
26 continue;
27 };
28
29 if resource.current >= trigger.threshold {
30 continue;
31 }
32
33 let spell = *ctx.state.spell(trigger.spell_local);
34 let ready = ctx
35 .buf
36 .cooldown(spell.idx())
37 .is_none_or(|cooldown| cooldown.is_ready(ctx.now.as_secs_f64()));
38
39 if !ready {
40 continue;
41 }
42
43 let primary_spent = ctx.state.runtime.resources.last_primary_spent;
44 let secondary_spent = ctx.state.runtime.resources.last_secondary_spent;
45
46 process_cast(ctx, spell.spell_id, 0);
47 ctx.state.runtime.resources.last_primary_spent = primary_spent;
48 ctx.state.runtime.resources.last_secondary_spent = secondary_spent;
49 }
50}
51
52pub(super) fn process_single_resource(
53 ctx: &mut CombatCtx<'_>,
54 cost: f64,
55 gain: ResourceGain,
56 is_secondary: bool,
57 now: SimTime,
58 source_spell_id: u32,
59) {
60 if cost > 0.0 {
61 let mut spent = cost;
62
63 if is_secondary {
64 if secondary_spends_all_points(ctx.state) {
69 spent = spend_all_secondary_resource(ctx.state, ctx.buf, cost);
70 } else {
71 spend_secondary_resource(ctx.state, ctx.buf, cost);
72 }
73
74 ctx.state.runtime.resources.last_secondary_spent = spent;
75 } else {
76 spend_resource(ctx.state, ctx.buf, cost);
77 ctx.state.runtime.resources.last_primary_spent = cost;
78 }
79
80 emit_resource(
81 ctx.state,
82 ctx.buf,
83 ctx.sink,
84 ResourceTelemetry {
85 kind: ResourceEventKind::Spend,
86 amount: spent,
87 secondary: is_secondary,
88 now,
89 source_spell_id,
90 },
91 );
92 }
93
94 if gain.amount > 0.0 {
95 gain_resource_with_procs(ctx, gain, is_secondary);
96 }
97}
98
99pub(super) fn process_resources(ctx: &mut CombatCtx<'_>, spell: &SpellData, now: SimTime) {
100 ctx.state.runtime.resources.last_primary_spent = 0.0;
101 ctx.state.runtime.resources.last_optional_primary_spent = 0.0;
102 let bypass = bypass_aura_active(
104 ctx.state,
105 ctx.buf,
106 spell.requirements.cost_bypass_aura_id,
107 ctx.source,
108 Some(ctx.target),
109 );
110 let bypass_primary_cost =
111 bypass || (spell.channel.behavior.is_channel && spell.channel.tick_cost > 0.0);
112 let unmodified_primary_cost =
114 crate::systems::base_primary_spell_cost(ctx.state, ctx.buf, spell);
115 let base_primary_cost = effective_spell_cost(
116 &ctx.view(),
117 spell.spell_id,
118 unmodified_primary_cost,
119 crate::state::ResourcePool::Primary,
120 );
121 let primary_cost = if bypass_primary_cost {
122 0.0
123 } else {
124 let optional_primary_cost = effective_optional_spell_cost(
125 &ctx.view(),
126 spell.spell_id,
127 crate::systems::base_optional_primary_spell_cost(ctx.state, ctx.buf, spell),
128 crate::state::ResourcePool::Primary,
129 );
130 let maximum_cost = effective_spell_cost(
131 &ctx.view(),
132 spell.spell_id,
133 unmodified_primary_cost + optional_primary_cost,
134 crate::state::ResourcePool::Primary,
135 );
136 let current = ctx
137 .state
138 .config
139 .base_stats
140 .resource_type
141 .and_then(|resource_type| ctx.buf.resource(resource_type))
142 .map_or(0.0, |resource| resource.current);
143
144 optional_cost_spend(base_primary_cost, maximum_cost, current)
145 };
146
147 ctx.state.runtime.resources.last_optional_primary_spent =
148 (primary_cost - base_primary_cost).max(0.0);
149 process_single_resource(
150 ctx,
151 primary_cost,
152 ResourceGain::modified(
153 spell.cost.resource_gain,
154 ResourceGainSource::Spell(spell.spell_id),
155 ),
156 false,
157 now,
158 spell.spell_id,
159 );
160 let secondary_cost = if bypass {
161 0.0
162 } else {
163 effective_spell_cost(
164 &ctx.view(),
165 spell.spell_id,
166 spell.cost.secondary_resource_cost,
167 crate::state::ResourcePool::Secondary,
168 )
169 };
170
171 process_single_resource(
172 ctx,
173 secondary_cost,
174 ResourceGain::modified(
175 spell.cost.secondary_resource_gain,
176 ResourceGainSource::Spell(spell.spell_id),
177 ),
178 true,
179 now,
180 spell.spell_id,
181 );
182
183 if !bypass
184 && (spell.cost.health_cost > 0.0
185 || spell.cost.health_cost_pct > 0.0
186 || spell.cost.health_max_cost_pct > 0.0
187 || spell.cost.health_optional_cost > 0.0
188 || spell.cost.health_optional_cost_pct.percent > 0.0)
189 {
190 let base_cost = crate::systems::spell_health_cost(
191 ctx.state,
192 ctx.source,
193 spell.cost.health_cost,
194 spell.cost.health_cost_pct,
195 );
196 let maximum_health = ctx
197 .state
198 .friendly_actor_max_health(ctx.source)
199 .unwrap_or(0.0);
200 let optional_cost = spell.cost.health_optional_cost.max(0.0)
201 + spell
202 .cost
203 .health_optional_cost_pct
204 .resolve(0.0, maximum_health);
205 let maximum_cost = base_cost
206 + optional_cost
207 + maximum_health * spell.cost.health_max_cost_pct.max(0.0) / HUNDRED;
208 let current_health = ctx.state.friendly_actor_health(ctx.source).unwrap_or(0.0);
209 let realized_cost = optional_cost_spend(base_cost, maximum_cost, current_health);
210
211 spend_health_cost(ctx.state, ctx.source, realized_cost, 0.0);
212 }
213}
214
215pub(super) fn optional_cost_spend(base_cost: f64, maximum_cost: f64, current_resource: f64) -> f64 {
216 base_cost
217 + (maximum_cost - base_cost)
218 .max(0.0)
219 .min((current_resource - base_cost).max(0.0))
220}