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
60fn affordable_or_free<B>(
61    b: &mut B,
62    cost_off: usize,
63    cur_off: usize,
64    bypass_off: Option<usize>,
65) -> B::Bool
66where
67    B: RotationBackend,
68{
69    let zero_f = b.const_f64(0.0);
70    let cost = b.load_f64(cost_off);
71    let cur = b.load_f64(cur_off);
72    let affordable = b.cmp_gte_f(cur, cost);
73    let no_cost = b.cmp_eq_f(cost, zero_f);
74    let mut cost_ok = b.or_b(no_cost, affordable);
75
76    if let Some(bypass_off) = bypass_off {
77        let bexp = b.load_f64(bypass_off);
78        let now = b.now();
79        let bypass_active = b.cmp_gt_f(bexp, now);
80
81        cost_ok = b.or_b(cost_ok, bypass_active);
82    }
83
84    cost_ok
85}
86
87struct SpellUsableAt {
88    spell_offset: usize,
89    resource_current_offset: Option<usize>,
90    secondary_resource_current_offset: Option<usize>,
91    cooldown_base: Option<usize>,
92    gating_aura_offset: Option<usize>,
93    gating_aura_min_stacks: u8,
94    cost_bypass_aura_offset: Option<usize>,
95    cooldown_bypass_aura_offset: Option<usize>,
96}
97
98fn spell_usable_at<B>(b: &mut B, args: &SpellUsableAt) -> B::Bool
99where
100    B: RotationBackend,
101{
102    let zero_i = b.const_i32(0);
103
104    let is_enabled = b.load_i32(args.spell_offset + IS_ENABLED_OFFSET);
105    let mut ok = b.cmp_ne_i(is_enabled, zero_i);
106
107    if let Some(res_off) = args.resource_current_offset {
108        let cost_ok = affordable_or_free(
109            b,
110            args.spell_offset + COST_OFFSET,
111            res_off,
112            args.cost_bypass_aura_offset,
113        );
114
115        ok = b.and_b(ok, cost_ok);
116    }
117
118    // No cost-bypass aura applies to secondary costs.
119
120    if let Some(sec_res_off) = args.secondary_resource_current_offset {
121        let sec_ok = affordable_or_free(
122            b,
123            args.spell_offset + SECONDARY_COST_OFFSET,
124            sec_res_off,
125            None,
126        );
127
128        ok = b.and_b(ok, sec_ok);
129    }
130
131    if let Some(aura_off) = args.gating_aura_offset {
132        let expires_at = b.load_f64(aura_off);
133        let now = b.now();
134        let aura_active = b.cmp_gt_f(expires_at, now);
135        let stacks = b.load_i32(aura_off + AURA_STACKS_OFFSET);
136        let min_stacks = b.const_i32(i32::from(args.gating_aura_min_stacks));
137        let stacks_ok = b.cmp_gte_i(stacks, min_stacks);
138        let gate_ok = b.and_b(aura_active, stacks_ok);
139
140        ok = b.and_b(ok, gate_ok);
141    }
142
143    if let Some(cd_base) = args.cooldown_base {
144        let cooldown_ok =
145            super::cooldown::cooldown_ready(b, cd_base, args.cooldown_bypass_aura_offset);
146
147        ok = b.and_b(ok, cooldown_ok);
148    }
149
150    ok
151}
152
153pub(crate) fn spell_usable<B>(b: &mut B, args: &SpellUsableArgs) -> B::Bool
154where
155    B: RotationBackend,
156{
157    let base_ok = spell_usable_at(
158        b,
159        &SpellUsableAt {
160            spell_offset: args.spell_offset,
161            resource_current_offset: args.resource_current_offset,
162            secondary_resource_current_offset: args.secondary_resource_current_offset,
163            cooldown_base: args.cooldown_base,
164            gating_aura_offset: args.gating_aura_offset,
165            gating_aura_min_stacks: args.gating_aura_min_stacks,
166            cost_bypass_aura_offset: args.cost_bypass_aura_offset,
167            cooldown_bypass_aura_offset: args.cooldown_bypass_aura_offset,
168        },
169    );
170    let (Some(replacement_offset), Some(aura_offset)) =
171        (args.override_spell_offset, args.override_aura_offset)
172    else {
173        return base_ok;
174    };
175    let replacement_ok = spell_usable_at(
176        b,
177        &SpellUsableAt {
178            spell_offset: replacement_offset,
179            resource_current_offset: args.resource_current_offset,
180            secondary_resource_current_offset: args.secondary_resource_current_offset,
181            cooldown_base: args.override_cooldown_base,
182            gating_aura_offset: None,
183            gating_aura_min_stacks: 0,
184            cost_bypass_aura_offset: None,
185            cooldown_bypass_aura_offset: None,
186        },
187    );
188    let expires_at = b.load_f64(aura_offset);
189    let now = b.now();
190    let override_active = b.cmp_gt_f(expires_at, now);
191    let override_inactive = b.not_b(override_active);
192    let use_replacement = b.and_b(override_active, replacement_ok);
193    let use_base = b.and_b(override_inactive, base_ok);
194    let either = b.or_b(use_replacement, use_base);
195
196    // An aura-332 override changes what the requested button casts, never whether the character
197    // has it, so the requested slot's own `is_enabled` gates the replacement path too. Without
198    // this the selector and `cast_rejection` both read the replacement's flag and a content
199    // `disable_spell` on the base is silently undone for as long as the override aura is up.
200    let zero_i = b.const_i32(0);
201    let base_enabled_raw = b.load_i32(args.spell_offset + IS_ENABLED_OFFSET);
202    let base_enabled = b.cmp_ne_i(base_enabled_raw, zero_i);
203
204    b.and_b(base_enabled, either)
205}