Skip to main content

wowlab_engine_combat/state/program/
definitions.rs

1#[cfg(all(test, feature = "bench-metrics"))]
2use wowlab_engine_domain::rotation::SpellSlot;
3use wowlab_engine_domain::{
4    encounter::EnemyActorDefinition,
5    rotation::{
6        AuraSlot, CooldownSlot, CooldownSlotHandle, ResourceSlot, ResourceSlotHandle, SwingSlot,
7        TalentSlot, UnitSlot, WeaponImbueSlot,
8    },
9};
10#[cfg(test)]
11use wowlab_types::sim::SpellIdx;
12use wowlab_types::{combat::ResourceType, sim::AuraKey};
13
14use super::{CombatProgram, ConcreteAuraBinding};
15use crate::state::{AuraData, CombatInstance, LocalAuraIdx, LocalSpellIdx, SpellData};
16
17impl CombatProgram {
18    #[must_use]
19    pub fn base_stats(&self) -> &crate::state::BaseStats {
20        &self.config.base_stats
21    }
22
23    #[must_use]
24    pub fn game_data(&self) -> &wowlab_engine_gamedata::ResolvedGameData {
25        &self.config.game_data
26    }
27
28    #[must_use]
29    pub fn encounter(&self) -> &wowlab_engine_ports::ResolvedEncounter {
30        &self.config.encounter
31    }
32
33    #[must_use]
34    pub fn spec_config<T>(&self) -> Option<&T>
35    where
36        T: wowlab_engine_ports::SpecConfig,
37    {
38        self.config
39            .spec_config
40            .as_ref()
41            .as_any()
42            .downcast_ref::<T>()
43    }
44
45    // #t(fn: rust_unchecked_indexing) spell_by_id is finalized from this same ordered spell slice and local-index constructors are private.
46    #[cfg(feature = "test-support")]
47    #[doc(hidden)]
48    #[must_use]
49    pub fn diagnostic_spell_data(&self, spell_id: u32) -> Option<(LocalSpellIdx, &SpellData)> {
50        self.lookup_spell_data(spell_id)
51    }
52
53    // #t(fn: rust_unchecked_indexing) LocalSpellIdx constructors are private and finalization validates the complete local-index domain.
54    /// Returns the spell definition for a program-owned local identity.
55    ///
56    /// # Panics
57    ///
58    /// Panics when `local` belongs to a different specialization.
59    pub fn spell(&self, local: LocalSpellIdx) -> &SpellData {
60        let local = self
61            .resolve_spell_identity(local)
62            .expect("foreign specialization spell binding");
63
64        &self.spells[local.as_usize()]
65    }
66
67    pub fn spells(&self) -> &[SpellData] {
68        &self.spells
69    }
70
71    // #t(fn: rust_unchecked_indexing) LocalAuraIdx constructors are private and finalization validates the complete local-index domain.
72    /// Returns the aura definition for a program-owned local identity.
73    ///
74    /// # Panics
75    ///
76    /// Panics when `local` belongs to a different specialization.
77    pub fn aura(&self, local: LocalAuraIdx) -> &AuraData {
78        let local = self
79            .resolve_aura_identity(local)
80            .expect("foreign specialization aura binding");
81
82        &self.auras[local.as_usize()]
83    }
84
85    pub fn auras(&self) -> &[AuraData] {
86        &self.auras
87    }
88
89    pub fn precombat_auras(&self) -> &[LocalAuraIdx] {
90        &self.precombat_auras
91    }
92
93    pub fn talent_aura_stacks(&self) -> &[(LocalAuraIdx, u8)] {
94        &self.talent_aura_stacks
95    }
96
97    #[must_use]
98    pub fn talent_rank(&self, talent: crate::DamageActionId) -> u8 {
99        self.damage
100            .generated_action(talent)
101            .and_then(|local| self.talent_ranks.get(local.index()).copied())
102            .unwrap_or(0)
103    }
104
105    /// Keyed test oracle for fixtures that do not have generated identities.
106    #[cfg(any(test, feature = "test-support"))]
107    #[must_use]
108    pub fn diagnostic_talent_rank(&self, spell_id: u32) -> Option<u8> {
109        wowlab_types::bench_metrics::record_keyed_lookup();
110        let ordinal = self.damage.diagnostic_action_ordinal(spell_id)?;
111
112        self.talent_ranks.get(ordinal).copied()
113    }
114
115    #[must_use]
116    pub fn impact_effect_proc_count(&self) -> usize {
117        self.impact_effect_procs.len()
118    }
119
120    #[must_use]
121    pub fn impact_proc_count(&self) -> usize {
122        self.impact_procs.len()
123    }
124
125    #[must_use]
126    pub fn impact_proc(&self, index: usize) -> Option<&crate::ImpactProc> {
127        self.impact_procs.get(index)
128    }
129
130    #[must_use]
131    pub fn player_cast_hook_count(&self) -> usize {
132        self.player_cast_hooks.len()
133    }
134
135    #[must_use]
136    pub fn impact_effect_proc(&self, index: usize) -> Option<&crate::state::ImpactEffectProc> {
137        self.impact_effect_procs.get(index)
138    }
139
140    #[must_use]
141    pub fn spell_effects(&self) -> &[crate::SpellEffectData] {
142        &self.spell_effects
143    }
144
145    #[must_use]
146    pub fn enemy_definition(
147        &self,
148        enemy: wowlab_types::sim::EnemyIdx,
149    ) -> Option<&EnemyActorDefinition> {
150        self.enemies
151            .get(enemy.as_usize())
152            .filter(|definition| definition.id() == enemy)
153    }
154
155    #[must_use]
156    pub fn enemy_definitions(&self) -> impl ExactSizeIterator<Item = &EnemyActorDefinition> {
157        self.enemies.iter()
158    }
159
160    #[cfg_attr(not(test), expect(dead_code, reason = "test-support diagnostic"))]
161    pub(crate) fn spell_data(&self, spell_id: u32) -> Option<(LocalSpellIdx, &SpellData)> {
162        self.lookup_spell_data(spell_id)
163    }
164
165    pub(crate) fn scheduled_spell(
166        &self,
167        local: LocalSpellIdx,
168    ) -> wowlab_engine_ports::ScheduledSpellBinding {
169        self.scheduled_spells
170            .bind(u16::from(local.raw()))
171            .expect("finalized local spell has a scheduled binding")
172    }
173
174    pub(crate) fn validate_scheduled_spell(
175        &self,
176        binding: wowlab_engine_ports::ScheduledSpellBinding,
177    ) -> Option<LocalSpellIdx> {
178        match self.scheduled_spell_execution(binding)? {
179            super::ScheduledSpellExecution::Registered(local) => Some(local),
180            super::ScheduledSpellExecution::TriggerOnly(_)
181            | super::ScheduledSpellExecution::TriggerOnlyEmpty => None,
182        }
183    }
184
185    pub(crate) fn scheduled_spell_execution(
186        &self,
187        binding: wowlab_engine_ports::ScheduledSpellBinding,
188    ) -> Option<super::ScheduledSpellExecution> {
189        let local = usize::from(self.scheduled_spells.validate(binding)?);
190
191        self.scheduled_spell_executions.get(local).copied()
192    }
193
194    /// Deliberately keyed channel-slot reference path for exact-identity differential tests.
195    #[cfg(all(test, feature = "bench-metrics"))]
196    pub(crate) fn reference_buffer_spell<'a>(
197        &self,
198        instance: &'a CombatInstance,
199        spell: SpellIdx,
200    ) -> Option<&'a SpellSlot> {
201        let local = self.spell_local(spell.as_u32())?;
202
203        Some(instance.buffer.spell_at(self.buffer_bindings.spell(local)))
204    }
205
206    #[cfg(test)]
207    pub(crate) fn active_channel_identity(
208        &self,
209        spell: SpellIdx,
210    ) -> Option<crate::state::ActiveChannelIdentity> {
211        let &local = self.spell_by_id.get(&spell.as_u32())?;
212
213        Some(crate::state::ActiveChannelIdentity {
214            local,
215            spell,
216            handle: self.buffer_bindings.spell(local),
217            tick_cost_aura: self.rotation.cast_profile(local).channel_tick_cost_aura,
218            tick_damage_aura: self.rotation.cast_profile(local).channel_tick_damage_aura,
219        })
220    }
221
222    #[expect(
223        clippy::unnecessary_wraps,
224        reason = "cooldown access stays optional at subsystem boundaries that handle absent cooldown state"
225    )]
226    pub(crate) fn buffer_cooldown<'a>(
227        &self,
228        instance: &'a CombatInstance,
229        local: LocalSpellIdx,
230    ) -> Option<&'a CooldownSlot> {
231        Some(
232            instance
233                .buffer
234                .cooldown_at(self.buffer_bindings.cooldown(local)),
235        )
236    }
237
238    #[expect(
239        clippy::unnecessary_wraps,
240        reason = "cooldown access stays optional at subsystem boundaries that handle absent cooldown state"
241    )]
242    pub(crate) fn buffer_cooldown_mut<'a>(
243        &self,
244        instance: &'a mut CombatInstance,
245        local: LocalSpellIdx,
246    ) -> Option<&'a mut CooldownSlot> {
247        Some(
248            instance
249                .buffer
250                .cooldown_mut_at(self.buffer_bindings.cooldown(local)),
251        )
252    }
253
254    #[expect(
255        clippy::unnecessary_wraps,
256        reason = "cooldown binding lookup shares the optional subsystem-boundary contract"
257    )]
258    pub(crate) fn cooldown_binding(&self, local: LocalSpellIdx) -> Option<CooldownSlotHandle> {
259        Some(self.buffer_bindings.cooldown(local))
260    }
261
262    pub(crate) fn aura_binding(
263        &self,
264        local: LocalAuraIdx,
265        key: AuraKey,
266    ) -> Option<ConcreteAuraBinding> {
267        self.buffer_bindings.aura(local, key)
268    }
269
270    pub(crate) fn aura_pool_index(&self, local: LocalAuraIdx, key: AuraKey) -> Option<usize> {
271        Some(self.aura_binding(local, key)?.index().as_usize())
272    }
273
274    pub(crate) fn aura_event(
275        &self,
276        local: LocalAuraIdx,
277        key: AuraKey,
278        target: Option<wowlab_types::sim::EnemyIdx>,
279    ) -> Option<wowlab_engine_ports::AuraEventBinding> {
280        let concrete = u32::try_from(self.aura_binding(local, key)?.index().as_usize()).ok()?;
281
282        Some(self.runtime_events.aura(local.raw(), concrete, key, target))
283    }
284
285    pub(crate) fn validate_aura_event(
286        &self,
287        event: wowlab_engine_ports::AuraEventBinding,
288    ) -> Option<(LocalAuraIdx, AuraKey, Option<wowlab_types::sim::EnemyIdx>)> {
289        let (local, concrete, key, target) = self.runtime_events.validate_aura(event)?;
290        let local = LocalAuraIdx::new(local);
291
292        (self.aura_binding(local, key)?.index().as_usize() == concrete as usize)
293            .then_some((local, key, target))
294    }
295
296    pub(crate) fn buffer_aura<'a>(
297        &self,
298        instance: &'a CombatInstance,
299        local: LocalAuraIdx,
300        key: AuraKey,
301    ) -> Option<&'a AuraSlot> {
302        self.aura_binding(local, key)
303            .map(|binding| instance.buffer.aura_at(binding.handle))
304    }
305
306    pub(crate) fn buffer_resource<'a>(
307        &self,
308        instance: &'a CombatInstance,
309        resource: ResourceType,
310    ) -> Option<&'a ResourceSlot> {
311        self.buffer_bindings
312            .resource(resource)
313            .map(|handle| instance.buffer.resource_at(handle))
314    }
315
316    pub(crate) fn buffer_resource_mut<'a>(
317        &self,
318        instance: &'a mut CombatInstance,
319        resource: ResourceType,
320    ) -> Option<&'a mut ResourceSlot> {
321        self.buffer_bindings
322            .resource(resource)
323            .map(|handle| instance.buffer.resource_mut_at(handle))
324    }
325
326    pub(crate) fn resource_binding(&self, resource: ResourceType) -> Option<ResourceSlotHandle> {
327        self.buffer_bindings.resource(resource)
328    }
329
330    pub(crate) fn buffer_talent_mut<'a>(
331        &self,
332        instance: &'a mut CombatInstance,
333        key: &str,
334    ) -> Option<&'a mut TalentSlot> {
335        self.buffer_bindings
336            .talent(key)
337            .map(|handle| instance.buffer.talent_mut_at(handle))
338    }
339
340    pub(crate) fn buffer_swing<'a>(
341        &self,
342        instance: &'a CombatInstance,
343        index: usize,
344    ) -> Option<&'a SwingSlot> {
345        self.buffer_bindings
346            .swing(index)
347            .map(|handle| instance.buffer.swing_at(handle))
348    }
349
350    pub(crate) fn buffer_swing_mut<'a>(
351        &self,
352        instance: &'a mut CombatInstance,
353        index: usize,
354    ) -> Option<&'a mut SwingSlot> {
355        self.buffer_bindings
356            .swing(index)
357            .map(|handle| instance.buffer.swing_mut_at(handle))
358    }
359
360    pub(crate) fn target_unit_mut<'a>(&self, instance: &'a mut CombatInstance) -> &'a mut UnitSlot {
361        instance
362            .buffer
363            .unit_mut_at(self.buffer_bindings.target_unit())
364    }
365
366    #[cfg(test)]
367    pub(crate) fn target_unit<'a>(&self, instance: &'a CombatInstance) -> &'a UnitSlot {
368        instance.buffer.unit_at(self.buffer_bindings.target_unit())
369    }
370
371    pub(crate) fn buffer_weapon_imbue_mut<'a>(
372        &self,
373        instance: &'a mut CombatInstance,
374        key: &str,
375    ) -> Option<&'a mut WeaponImbueSlot> {
376        self.buffer_bindings
377            .weapon_imbue(key)
378            .map(|handle| instance.buffer.weapon_imbue_mut_at(handle))
379    }
380
381    fn lookup_spell_data(&self, spell_id: u32) -> Option<(LocalSpellIdx, &SpellData)> {
382        let local = self.spell_local(spell_id)?;
383
384        // BOUNDS: `spell_local` returns a coordinate minted from this spell slice.
385
386        Some((local, &self.spells[local.as_usize()]))
387    }
388}