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