Skip to main content

wowlab_engine_combat/systems/
spell_gates.rs

1use wowlab_engine_domain::{
2    dbc::{
3        ResolvedGameDataSemanticExt as _, SpellAttributeKind, SpellEffectKind, spell_attribute_is,
4        spell_effect_is,
5    },
6    pool::{SpellGate, SpellGateUpdate},
7    rotation::DenseBuffer,
8};
9use wowlab_types::sim::SpellIdx;
10
11use super::buffs::for_each_active_aura_effect;
12use crate::state::{ActiveSpellGate, BuffEffect, CombatState, LocalSpellIdx};
13
14#[derive(Clone, Copy)]
15struct ActiveDbcGate {
16    source_spell_id: u32,
17    effect_index: u8,
18    gate: ActiveSpellGate,
19}
20
21/// Set one independent availability gate and project the composed result.
22pub fn set_spell_gate(
23    state: &mut CombatState,
24    buf: &mut DenseBuffer,
25    spell: LocalSpellIdx,
26    gate: SpellGate,
27    available: bool,
28) {
29    let spell_idx = state.spell(spell).idx();
30    let Some(base_enabled) = buf.spell(spell_idx).map(|slot| slot.is_enabled != 0) else {
31        return;
32    };
33    let enabled = state.runtime.pools.spell_gates.set(
34        spell,
35        gate,
36        SpellGateUpdate {
37            base_enabled,
38            available,
39        },
40    );
41
42    if let Some(slot) = buf.spell_mut(spell_idx) {
43        if (slot.is_enabled != 0) != enabled {
44            tracing::trace!(
45                spell_id = spell_idx.0,
46                ?gate,
47                available,
48                enabled,
49                "SPELL_GATE_TRANSITION"
50            );
51        }
52
53        slot.is_enabled = i32::from(enabled);
54    }
55}
56
57/// Rebuild active DBC availability restrictions and aura-state exceptions.
58pub(crate) fn synchronize_active_spell_gates(state: &mut CombatState, buf: &mut DenseBuffer) {
59    let mut active = Vec::new();
60
61    for_each_active_aura_effect(state, buf, |_stacks, effect| {
62        if let BuffEffect::SpellGateFromEffect {
63            source_spell_id,
64            effect_index,
65            gate,
66        } = effect
67        {
68            active.push(ActiveDbcGate {
69                source_spell_id: *source_spell_id,
70                effect_index: *effect_index,
71                gate: *gate,
72            });
73        }
74    });
75
76    for raw_local in 0..state.defs.spells.len() {
77        let Some(local) = u8::try_from(raw_local).ok().map(LocalSpellIdx) else {
78            continue;
79        };
80        let Some(spell) = state.defs.spells.get(raw_local) else {
81            continue;
82        };
83        let spell_id = spell.spell_id;
84        let affected = |rule: &ActiveDbcGate| {
85            state.config.game_data.effect_affects_spell(
86                SpellIdx::from_raw(rule.source_spell_id),
87                rule.effect_index,
88                SpellIdx::from_raw(spell_id),
89            )
90        };
91        let spell_idx = SpellIdx::from_raw(spell_id);
92        let casting_exception = state
93            .config
94            .game_data
95            .spell_attributes(spell_idx)
96            .is_some_and(|attributes| {
97                spell_attribute_is(attributes, SpellAttributeKind::RangedAbility)
98                    || spell_attribute_is(attributes, SpellAttributeKind::IgnoreCastingDisabled)
99            })
100            || (1..=state.config.game_data.max_effect_index(spell_idx)).any(|effect_index| {
101                spell_effect_is(
102                    state.config.game_data.effect_type(spell_idx, effect_index),
103                    SpellEffectKind::Attack,
104                )
105            });
106        let restricted = !casting_exception
107            && active
108                .iter()
109                .any(|rule| rule.gate == ActiveSpellGate::DisableAbilities && !affected(rule));
110        let ignores_aura_state = active
111            .iter()
112            .any(|rule| rule.gate == ActiveSpellGate::IgnoreAuraState && affected(rule));
113
114        state
115            .runtime
116            .pools
117            .spell_gates
118            .set_aura_state_exception(local, ignores_aura_state);
119        set_spell_gate(
120            state,
121            buf,
122            local,
123            SpellGate::ActiveAuraRestriction,
124            !restricted,
125        );
126    }
127}
128
129pub(crate) fn ignores_aura_state(state: &CombatState, spell_id: u32) -> bool {
130    state
131        .spell_local(spell_id)
132        .is_some_and(|local| state.runtime.pools.spell_gates.ignores_aura_state(&local))
133}
134
135pub(crate) fn reset_spell_gates(state: &mut CombatState, buf: &mut DenseBuffer) {
136    for (spell, enabled) in state.runtime.pools.spell_gates.reset() {
137        let spell_idx = state.spell(spell).idx();
138
139        if let Some(slot) = buf.spell_mut(spell_idx) {
140            slot.is_enabled = i32::from(enabled);
141        }
142    }
143}
144
145#[cfg(test)]
146#[path = "spell_gates/tests.rs"]
147mod tests;