Skip to main content

wowlab_engine_combat/builder/combat_builder/
entities.rs

1use wowlab_engine_domain::rotation::to_simc_key;
2
3use super::CombatSystemBuilder;
4use crate::builder::{
5    AuraDefinitionDraft, AutoAttackDefinitionDraft, BuilderError, SpellDefinitionDraft,
6};
7
8impl CombatSystemBuilder {
9    /// Declares the generated content descriptor's finite spell/effect identity universe.
10    ///
11    /// The identities become eligible for damage-plan lowering without becoming castable actions.
12    pub fn effect_identities(mut self, identities: &[crate::DamageActionId]) -> Self {
13        for identity in identities.iter().copied() {
14            if !self.generated_damage_identities.contains(&identity) {
15                self.generated_damage_identities.push(identity);
16                self.effect_spell_ids.push(identity.spell_id());
17            }
18        }
19
20        self
21    }
22
23    /// Declares a damage-only identity referenced by an opaque hook or callback.
24    ///
25    /// Unlike [`Self::spell`], this assigns hook-owned child damage local indices without making it castable.
26    #[cfg(any(test, feature = "test-support"))]
27    #[doc(hidden)]
28    pub fn damage_identity(mut self, _name: &str, id: u32) -> Self {
29        if id != 0 {
30            self.damage_spell_ids.push(id);
31            self.generated_damage_identities
32                .push(crate::DamageActionId::new(0, id));
33        }
34
35        self
36    }
37
38    /// Diagnostic-only bulk declaration for raw game-data fixtures.
39    #[cfg(any(test, feature = "test-support"))]
40    #[doc(hidden)]
41    pub fn diagnostic_effect_identities(mut self, ids: &[u32]) -> Self {
42        wowlab_types::bench_metrics::record_keyed_lookup();
43
44        for &id in ids.iter().filter(|id| **id != 0) {
45            self.effect_spell_ids.push(id);
46            self.generated_damage_identities
47                .push(crate::DamageActionId::new(0, id));
48        }
49
50        self
51    }
52
53    /// Registers a spell definition.
54    ///
55    /// # Panics
56    ///
57    /// Panics if the validated aura registry exceeds the local-index capacity.
58    pub fn spell(
59        mut self,
60        name: &str,
61        id: u32,
62        f: impl FnOnce(SpellDefinitionDraft) -> Result<SpellDefinitionDraft, BuilderError>,
63    ) -> Self {
64        if self.pending_error.is_some() {
65            return self;
66        }
67
68        let mut builder = match f(SpellDefinitionDraft::new(name, id)) {
69            Ok(b) => b,
70            Err(e) => {
71                self.pending_error = Some(e);
72
73                return self;
74            }
75        };
76
77        let aura_ids = std::mem::take(&mut builder.applies_aura_ids);
78
79        for aura_id in aura_ids {
80            if let Some(pos) = self.aura_defs.iter().position(|a| a.aura_id == aura_id) {
81                builder = builder.applies_aura(crate::LocalAuraIdx::new(
82                    u8::try_from(pos).expect("aura count fits in u8"),
83                ));
84            } else {
85                self.pending_error = Some(crate::builder::BuilderErrorKind::GameData(
86                    // #t(rust_alloc_in_loop) allocation occurs only on the construction-failure path
87                    wowlab_engine_ports::EngineError::spec_construction(format!(
88                        "applies_aura_id: aura {aura_id} is not registered before spell {name} ({id})"
89                    )),
90                )
91                .into());
92
93                return self;
94            }
95        }
96
97        self.spell_ids.insert(name.to_string(), id);
98
99        if builder.cooldown_secs.is_some() {
100            let snake = to_simc_key(name);
101
102            self.hints.cooldown_spells.push(snake.clone());
103
104            if builder.max_charges > 0 {
105                self.hints.charged_spells.push(snake);
106            }
107        }
108
109        let hook = builder.hook;
110        let empower_release_hook = builder.empower_release_hook;
111        let tick_hook = builder.tick_hook;
112
113        self.spells.push(builder.finalize(name.to_string(), id));
114        self.cast_hooks.push(hook);
115        self.empower_release_hooks.push(empower_release_hook);
116        self.tick_hooks.push(tick_hook);
117
118        self
119    }
120
121    pub fn aura(
122        mut self,
123        name: &str,
124        id: u32,
125        f: impl FnOnce(AuraDefinitionDraft) -> Result<AuraDefinitionDraft, BuilderError>,
126    ) -> Self {
127        if self.pending_error.is_some() {
128            return self;
129        }
130
131        let builder = match f(AuraDefinitionDraft::new(name, id)) {
132            Ok(b) => b,
133            Err(e) => {
134                self.pending_error = Some(e);
135
136                return self;
137            }
138        };
139
140        if builder.on_tick.is_some() && builder.periodic.is_none() {
141            self.pending_error = Some(
142                crate::builder::BuilderErrorKind::GameData(
143                    wowlab_engine_ports::EngineError::spec_construction(format!(
144                        "aura {name} ({id}): tick_hook requires a registered periodic"
145                    )),
146                )
147                .into(),
148            );
149
150            return self;
151        }
152
153        self.aura_ids.insert(name.to_string(), id);
154
155        if builder.periodic.is_some() {
156            self.hints.dot_auras.push(to_simc_key(name));
157        }
158
159        self.aura_defs.push(builder.finalize());
160
161        self
162    }
163
164    pub fn auto_attack(
165        mut self,
166        f: impl FnOnce(AutoAttackDefinitionDraft) -> AutoAttackDefinitionDraft,
167    ) -> Self {
168        self.auto_attacks.push(f(AutoAttackDefinitionDraft::new()));
169
170        self
171    }
172}