Skip to main content

wowlab_engine_combat/pipeline/
resources.rs

1use 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        // BOUNDS: `index` is bounded by the captured trigger vector length.
13        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            // A combo-point cost is a usability floor; the cast consumes every available point.
75            // The pool maximum is the only cap. The spell's own DBC power-entry maximum would pin
76            // every finisher to the untalented five despite talents that raise the pool maximum.
77            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    // docref:start cast-pipeline-process-resources
117    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    // docref:end cast-pipeline-process-resources
125    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}