Skip to main content

wowlab_engine_domain/rotation/lower/ops/
spell.rs

1//! `SpellSlot` ops, including the [`spell_usable`] cross-slot composite.
2
3use super::super::super::{backend::RotationBackend, context::SpellUsableOffsets};
4use crate::rotation::buffer::{AuraSlot, SpellSlot};
5
6pub(crate) struct SpellUsableArgs {
7    pub(crate) spell_offset: usize,
8    pub(crate) resource_current_offset: Option<usize>,
9    pub(crate) secondary_resource_current_offset: Option<usize>,
10    pub(crate) cooldown_base: Option<usize>,
11    pub(crate) gating_aura_offset: Option<usize>,
12    pub(crate) gating_aura_min_stacks: u8,
13    pub(crate) cost_bypass_aura_offset: Option<usize>,
14    pub(crate) cooldown_bypass_aura_offset: Option<usize>,
15    pub(crate) override_spell_offset: Option<usize>,
16    pub(crate) override_cooldown_base: Option<usize>,
17    pub(crate) override_aura_offset: Option<usize>,
18}
19
20impl SpellUsableArgs {
21    pub(crate) fn from_offsets(spell_offset: usize, e: &SpellUsableOffsets) -> Self {
22        Self {
23            spell_offset,
24            resource_current_offset: e.resource_current,
25            secondary_resource_current_offset: e.secondary_resource_current,
26            cooldown_base: e.cooldown_base,
27            gating_aura_offset: e.gating_aura_offset,
28            gating_aura_min_stacks: e.gating_aura_min_stacks,
29            cost_bypass_aura_offset: e.cost_bypass_aura_offset,
30            cooldown_bypass_aura_offset: e.cooldown_bypass_aura_offset,
31            override_spell_offset: e.override_spell_offset,
32            override_cooldown_base: e.override_cooldown_base,
33            override_aura_offset: e.override_aura_offset,
34        }
35    }
36
37    pub(crate) fn bare(spell_offset: usize) -> Self {
38        Self {
39            spell_offset,
40            resource_current_offset: None,
41            secondary_resource_current_offset: None,
42            cooldown_base: None,
43            gating_aura_offset: None,
44            gating_aura_min_stacks: 0,
45            cost_bypass_aura_offset: None,
46            cooldown_bypass_aura_offset: None,
47            override_spell_offset: None,
48            override_cooldown_base: None,
49            override_aura_offset: None,
50        }
51    }
52}
53
54const IS_ENABLED_OFFSET: usize = SpellSlot::OFF_IS_ENABLED - SpellSlot::OFF_IS_USABLE;
55const COST_OFFSET: usize = SpellSlot::OFF_COST - SpellSlot::OFF_IS_USABLE;
56const SECONDARY_COST_OFFSET: usize = SpellSlot::OFF_SECONDARY_COST - SpellSlot::OFF_IS_USABLE;
57
58const AURA_STACKS_OFFSET: usize = AuraSlot::OFF_STACKS - AuraSlot::OFF_EXPIRES_AT;
59
60pub(crate) fn spell_enabled<B>(b: &mut B, spell_offset: usize) -> B::Bool
61where
62    B: RotationBackend,
63{
64    #[cfg(feature = "rotation-optimizations")]
65    return b.load_bool(spell_offset + IS_ENABLED_OFFSET);
66
67    #[cfg(not(feature = "rotation-optimizations"))]
68    let enabled = b.load_i32(spell_offset + IS_ENABLED_OFFSET);
69    #[cfg(not(feature = "rotation-optimizations"))]
70    let zero = b.const_i32(0);
71
72    #[cfg(not(feature = "rotation-optimizations"))]
73    b.cmp_ne_i(enabled, zero)
74}
75
76fn affordable_or_free<B>(
77    b: &mut B,
78    cost_off: usize,
79    cur_off: usize,
80    bypass_off: Option<usize>,
81) -> B::Bool
82where
83    B: RotationBackend,
84{
85    let zero_f = b.const_f64(0.0);
86    let cost = b.load_f64(cost_off);
87    let cur = b.load_f64(cur_off);
88    let affordable = b.cmp_gte_f(cur, cost);
89    let no_cost = b.cmp_eq_f(cost, zero_f);
90    let mut cost_ok = b.or_b(no_cost, affordable);
91
92    if let Some(bypass_off) = bypass_off {
93        let bexp = b.load_f64(bypass_off);
94        let now = b.now();
95        let bypass_active = b.cmp_gt_f(bexp, now);
96
97        cost_ok = b.or_b(cost_ok, bypass_active);
98    }
99
100    cost_ok
101}
102
103struct SpellUsableAt {
104    spell_offset: usize,
105    resource_current_offset: Option<usize>,
106    secondary_resource_current_offset: Option<usize>,
107    cooldown_base: Option<usize>,
108    gating_aura_offset: Option<usize>,
109    gating_aura_min_stacks: u8,
110    cost_bypass_aura_offset: Option<usize>,
111    cooldown_bypass_aura_offset: Option<usize>,
112}
113
114fn spell_usable_at<B>(b: &mut B, args: &SpellUsableAt) -> B::Bool
115where
116    B: RotationBackend,
117{
118    let mut ok = spell_enabled(b, args.spell_offset);
119
120    if let Some(res_off) = args.resource_current_offset {
121        let cost_ok = affordable_or_free(
122            b,
123            args.spell_offset + COST_OFFSET,
124            res_off,
125            args.cost_bypass_aura_offset,
126        );
127
128        ok = b.and_b(ok, cost_ok);
129    }
130
131    // No cost-bypass aura applies to secondary costs.
132
133    if let Some(sec_res_off) = args.secondary_resource_current_offset {
134        let sec_ok = affordable_or_free(
135            b,
136            args.spell_offset + SECONDARY_COST_OFFSET,
137            sec_res_off,
138            None,
139        );
140
141        ok = b.and_b(ok, sec_ok);
142    }
143
144    if let Some(aura_off) = args.gating_aura_offset {
145        let expires_at = b.load_f64(aura_off);
146        let now = b.now();
147        let aura_active = b.cmp_gt_f(expires_at, now);
148        let stacks = b.load_i32(aura_off + AURA_STACKS_OFFSET);
149        let min_stacks = b.const_i32(i32::from(args.gating_aura_min_stacks));
150        let stacks_ok = b.cmp_gte_i(stacks, min_stacks);
151        let gate_ok = b.and_b(aura_active, stacks_ok);
152
153        ok = b.and_b(ok, gate_ok);
154    }
155
156    if let Some(cd_base) = args.cooldown_base {
157        let cooldown_ok =
158            super::cooldown::cooldown_ready(b, cd_base, args.cooldown_bypass_aura_offset);
159
160        ok = b.and_b(ok, cooldown_ok);
161    }
162
163    ok
164}
165
166pub(crate) fn spell_usable<B>(b: &mut B, args: &SpellUsableArgs) -> B::Bool
167where
168    B: RotationBackend,
169{
170    let base_ok = spell_usable_at(
171        b,
172        &SpellUsableAt {
173            spell_offset: args.spell_offset,
174            resource_current_offset: args.resource_current_offset,
175            secondary_resource_current_offset: args.secondary_resource_current_offset,
176            cooldown_base: args.cooldown_base,
177            gating_aura_offset: args.gating_aura_offset,
178            gating_aura_min_stacks: args.gating_aura_min_stacks,
179            cost_bypass_aura_offset: args.cost_bypass_aura_offset,
180            cooldown_bypass_aura_offset: args.cooldown_bypass_aura_offset,
181        },
182    );
183    let (Some(replacement_offset), Some(aura_offset)) =
184        (args.override_spell_offset, args.override_aura_offset)
185    else {
186        return base_ok;
187    };
188    let replacement_ok = spell_usable_at(
189        b,
190        &SpellUsableAt {
191            spell_offset: replacement_offset,
192            resource_current_offset: args.resource_current_offset,
193            secondary_resource_current_offset: args.secondary_resource_current_offset,
194            cooldown_base: args.override_cooldown_base,
195            gating_aura_offset: None,
196            gating_aura_min_stacks: 0,
197            cost_bypass_aura_offset: None,
198            cooldown_bypass_aura_offset: None,
199        },
200    );
201    let expires_at = b.load_f64(aura_offset);
202    let now = b.now();
203    let override_active = b.cmp_gt_f(expires_at, now);
204    let override_inactive = b.not_b(override_active);
205    let use_replacement = b.and_b(override_active, replacement_ok);
206    let use_base = b.and_b(override_inactive, base_ok);
207    let either = b.or_b(use_replacement, use_base);
208
209    // An aura-332 override changes what the requested button casts, never whether the character
210    // has it, so the requested slot's own `is_enabled` gates the replacement path too. Without
211    // this the availability selector and selected-action validator both read the replacement's
212    // flag and a content `disable_spell` on the base is silently undone while the override is up.
213    let zero_i = b.const_i32(0);
214    let base_enabled_raw = b.load_i32(args.spell_offset + IS_ENABLED_OFFSET);
215    let base_enabled = b.cmp_ne_i(base_enabled_raw, zero_i);
216
217    b.and_b(base_enabled, either)
218}