Skip to main content

wowlab_engine_combat/pipeline/
resources.rs

1use 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        // BOUNDS: `index` is bounded by the captured trigger vector length.
13        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            // A combo-point cost is a usability floor; the cast consumes every available point.
65            // The pool maximum is the only cap — the talents that raise it (Deeper/Devious/Secret/
66            // Sanguine Stratagem) are the same ones SimC folds into `consume_cp_max`, so the spell's
67            // own DBC power-entry maximum would pin every finisher to the untalented five.
68            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    // docref:start cast-pipeline-process-resources
103    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    // docref:end cast-pipeline-process-resources
113    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}