Skip to main content

wowlab_engine_combat/state/
mod.rs

1// #t(file: rust_similar_structs) Distinct finalized local identity types are intentionally non-interchangeable despite matching storage.
2
3//! Combat state: typed data structures for the combat system.
4
5mod aura_state;
6mod bounded_vec;
7pub(crate) mod buff_effect;
8mod casting;
9pub(crate) mod config;
10mod control;
11pub(crate) mod damage;
12mod deferred_work;
13pub(crate) mod defs;
14pub(crate) mod ground_effect;
15mod health;
16mod orchestration;
17pub(crate) mod program;
18pub(crate) mod resource;
19pub(crate) mod runtime;
20
21pub(crate) use aura_state::AuraState;
22pub(crate) use bounded_vec::BoundedVec;
23pub(crate) use buff_effect::{
24    BuffEffect, BuffTotals, EffectConsumer, EffectFamily, MAX_AURA_BUFF_EFFECTS, try_push_effect,
25};
26pub(crate) use casting::{ActiveCast, MovementState, SchoolLockout};
27pub(crate) use config::{
28    BaseStats, EffectProgramFrame, ScratchBuffers, TargetWorkspace, with_scratch,
29};
30pub(crate) use control::{
31    ControlDeadline, CrowdControlApplication, CrowdControlDuration, CrowdControlKind,
32    DiminishingGroup, DiminishingState,
33};
34pub(crate) use damage::{
35    ActiveSpellGate, DamageEffectRef, DamageReplication, DamageSource, DamageTransfer,
36    DamageTransferTiming, HealthThresholdDirection, MAX_CDR_EFFECTS, RuntimeDamageDef,
37    WeaponApType, resolved_damage_base_points,
38};
39pub(crate) use deferred_work::{DeferredWork, DeferredWorkQueue, PendingAuraExpireHook};
40pub(crate) use defs::{
41    AccumulatingImpactProc, AuraApplicationEvent, AuraData, AuraTickBehavior, AutoAttackData,
42    CastHookFn, CritChargeReduction, DriverSpellModifier, DynamicTickAction,
43    EmpowerReleaseAdjustment, EmpowerReleaseHookFn, GuardianAbility, GuardianAbilityState,
44    GuardianEvent, GuardianHandle, GuardianInstance, GuardianSpec, GuardianSummonError,
45    ImpactActorFilter, ImpactChanceScale, ImpactEffectProc, ImpactEvent, ImpactProc, ImpactProcFn,
46    ImpactSource, LandedImpactEvent, LandedImpactProc, MAX_AURA_APPLICATION_EXCLUSIONS,
47    PassiveDriverEffect, PendingEffectProgram, PendingHookTimer, PendingSpellImpact,
48    PendingTimerAction, PeriodicData, PeriodicDriver, PeriodicKind, PetAbilityCondition,
49    PetActionBar, PetActionState, PetOwnerCoefficients, PetStatInheritance, PetStatSnapshot,
50    ProcDriver, ProcEventKind, ProcHitMask, ProcPhaseMask, ProcProvenance, ProcSourceKind,
51    ProcTriggerSide, ResolvedWeaponEnchantBinding, ResolvedWeaponImbueBinding, ResourceGainEvent,
52    ResourceGainProc, ResourceGainProcAttempts, ResourcePool, ResourceThresholdTrigger,
53    SpellAvailability, SpellBehavior, SpellChannel, SpellChannelBehavior, SpellChannelCompletion,
54    SpellChannelTiming, SpellCooldown, SpellCost, SpellDamagePolicy, SpellData, SpellEffectData,
55    SpellEffectRange, SpellEquipmentRequirements, SpellFormRequirements, SpellGroup, SpellOverride,
56    SpellRequirements, SpellTargeting, SwingEvent, SwingHand, SwingHookFn, SwingMissChanceHookFn,
57    SwingReplacementHookFn, TriggerProgramPredicateFn, WeaponImbueBinding,
58};
59pub(crate) use ground_effect::{GroundEffectDefinition, GroundEffectId, PendingGroundPulse};
60pub(crate) use health::{
61    AbsorbEffect, AbsorbKind, ActiveAbsorb, HealthPool, IncomingImmunityWindow,
62    OverkillAbsorbDecision, OverkillAbsorbEvent, OverkillAbsorbScriptContext,
63    OverkillAbsorbScriptFn,
64};
65pub(crate) use orchestration::{EncounterRuntime, EnemyCompletion};
66pub use program::CombatProgram;
67pub(crate) use program::{
68    BufferBindings, CastAuraRequirement, CastProfile, ProgramConfig, RotationSpellBinding,
69    config::AttackPosition,
70};
71pub(crate) use resource::{
72    RUNE_COUNT, RUNE_RECHARGE_SECONDS, ResourceGain, ResourceGainScaling, ResourceGainSource,
73    RuneState,
74};
75pub use runtime::CombatInstance;
76#[cfg(all(
77    feature = "rotation-optimizations",
78    any(test, feature = "test-support")
79))]
80pub(crate) use runtime::DamageProjectionSheetStats;
81pub(crate) use runtime::{
82    ActiveChannelIdentity, AuraActivation, AuraMutation, AuraRefresh, AuraSchedule, AuraSnapshot,
83    AuraTransferState, CombatRuntime, CombatRuntimeInit, ImpactEffectRngState, ProjectionDirty,
84};
85#[cfg(feature = "rotation-optimizations")]
86pub(crate) use runtime::{DamageBuffProjection, DamageProjectionKey};
87pub(crate) use wowlab_engine_rng::{RppmTracker, ThresholdTracker};
88pub(crate) use wowlab_types::combat::SpellGroupRule;
89
90/// Local spell index scoped to a single [`CombatProgram`].
91#[derive(Clone, Copy, Debug)]
92#[must_use]
93pub struct LocalSpellIdx {
94    index: u8,
95    brand: u32,
96}
97
98/// Local aura index scoped to a single [`CombatProgram`].
99#[derive(Clone, Copy, Debug)]
100#[must_use]
101pub struct LocalAuraIdx {
102    index: u8,
103    brand: u32,
104}
105
106macro_rules! impl_local_identity_traits {
107    ($identity:ty) => {
108        impl PartialEq for $identity {
109            fn eq(&self, other: &Self) -> bool {
110                self.index == other.index
111            }
112        }
113
114        impl Eq for $identity {}
115
116        impl std::hash::Hash for $identity {
117            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
118                std::hash::Hash::hash(&self.index, state);
119            }
120        }
121
122        impl PartialOrd for $identity {
123            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
124                Some(self.cmp(other))
125            }
126        }
127
128        impl Ord for $identity {
129            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
130                self.index.cmp(&other.index)
131            }
132        }
133    };
134}
135
136impl_local_identity_traits!(LocalSpellIdx);
137impl_local_identity_traits!(LocalAuraIdx);
138
139/// Codegen-owned semantic aura coordinate resolved once for each combat program.
140#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
141#[must_use]
142pub struct AuraBinding(pub(crate) u16);
143
144/// Codegen-owned semantic spell coordinate resolved once for each combat program.
145#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
146#[must_use]
147pub struct SpellBinding(pub(crate) u16);
148
149/// Codegen-owned proof selecting one exact generated specialization namespace.
150#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151#[must_use]
152pub struct SpecIdentityProof {
153    brand: u32,
154    spec_id: u32,
155}
156
157/// Generated aura coordinate that must be paired with its specialization proof.
158#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
159#[must_use]
160pub struct SpecAuraBinding {
161    brand: u32,
162    local: u8,
163}
164
165/// Generated spell coordinate that must be paired with its specialization proof.
166// #t(rust_similar_structs) spell and aura tokens are intentionally non-interchangeable.
167#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
168#[must_use]
169pub struct SpecSpellBinding {
170    brand: u32,
171    local: u8,
172}
173
174/// Codegen-owned semantic RPPM coordinate resolved once for each combat program.
175#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
176#[must_use]
177pub struct RppmBinding(pub(crate) u16);
178
179/// Codegen-owned semantic system-RPPM coordinate.
180#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
181#[must_use]
182pub struct SystemRppmBinding(pub(crate) u8);
183
184/// Codegen-owned semantic proc-category recovery coordinate.
185#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
186#[must_use]
187pub struct ProcCategoryBinding(pub(crate) u8);
188
189/// Codegen-owned semantic periodic-driver coordinate.
190#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
191#[must_use]
192pub struct PeriodicDriverBinding(pub(crate) u8);
193
194/// Codegen-owned semantic runtime counter coordinate.
195#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
196#[must_use]
197pub struct ProcCounterBinding(pub(crate) u8);
198
199/// Opaque program-owned coordinate for one persistent-pet resource plan.
200#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
201#[must_use]
202pub struct PetResourceBinding {
203    brand: u32,
204    auto_attack: u16,
205}
206
207impl PetResourceBinding {
208    pub(crate) const fn new(brand: u32, auto_attack: u16) -> Self {
209        Self { brand, auto_attack }
210    }
211
212    pub(crate) const fn belongs_to(self, brand: u32) -> bool {
213        self.brand == brand
214    }
215
216    pub(crate) const fn auto_attack(self) -> u16 {
217        self.auto_attack
218    }
219}
220
221#[derive(Clone, Copy, Debug)]
222pub(crate) struct ResolvedSystemRppmBinding {
223    pub(crate) tracker: LocalRppmIdx,
224    pub(crate) recovery_ms: u32,
225}
226
227/// Local RPPM tracker index scoped to a single [`CombatProgram`].
228#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
229#[must_use]
230pub struct LocalRppmIdx(pub(crate) u8);
231
232/// Local accumulated-threshold tracker index scoped to a single [`CombatProgram`].
233#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
234#[must_use]
235pub struct LocalThresholdIdx(pub(crate) u8);
236
237impl LocalSpellIdx {
238    pub(crate) const fn new(index: u8) -> Self {
239        Self { index, brand: 0 }
240    }
241
242    const fn branded(index: u8, brand: u32) -> Self {
243        Self { index, brand }
244    }
245
246    #[inline]
247    #[must_use]
248    pub const fn as_usize(&self) -> usize {
249        self.index as usize
250    }
251
252    #[must_use]
253    pub(crate) const fn raw(self) -> u8 {
254        self.index
255    }
256}
257
258impl LocalAuraIdx {
259    pub(crate) const fn new(index: u8) -> Self {
260        Self { index, brand: 0 }
261    }
262
263    const fn branded(index: u8, brand: u32) -> Self {
264        Self { index, brand }
265    }
266
267    #[inline]
268    #[must_use]
269    pub const fn as_usize(&self) -> usize {
270        self.index as usize
271    }
272
273    #[must_use]
274    pub(crate) const fn raw(self) -> u8 {
275        self.index
276    }
277}
278
279impl AuraBinding {
280    pub(crate) const fn new(index: u16) -> Self {
281        Self(index)
282    }
283
284    #[inline]
285    #[expect(
286        clippy::trivially_copy_pass_by_ref,
287        reason = "C-CONV requires borrowed as_ receivers"
288    )]
289    pub(crate) const fn as_usize(&self) -> usize {
290        self.0 as usize
291    }
292}
293
294impl SpellBinding {
295    pub(crate) const fn new(index: u16) -> Self {
296        Self(index)
297    }
298
299    #[inline]
300    #[expect(
301        clippy::trivially_copy_pass_by_ref,
302        reason = "C-CONV requires borrowed as_ receivers"
303    )]
304    pub(crate) const fn as_usize(&self) -> usize {
305        self.0 as usize
306    }
307}
308
309impl SpecIdentityProof {
310    pub(crate) const fn new(brand: u32, spec_id: u32) -> Self {
311        Self { brand, spec_id }
312    }
313
314    pub(crate) const fn brand(self) -> u32 {
315        self.brand
316    }
317
318    pub(crate) const fn spec_id(self) -> u32 {
319        self.spec_id
320    }
321
322    #[cfg(test)]
323    pub(crate) const fn owns_spell(self, binding: SpecSpellBinding) -> bool {
324        self.brand == binding.brand
325    }
326
327    #[cfg(test)]
328    pub(crate) const fn owns_aura(self, binding: SpecAuraBinding) -> bool {
329        self.brand == binding.brand
330    }
331}
332
333impl SpecAuraBinding {
334    pub(crate) const fn new(brand: u32, local: u8) -> Self {
335        Self { brand, local }
336    }
337
338    /// Resolve this coordinate only with the proof from the same generated specialization.
339    ///
340    /// # Panics
341    ///
342    /// Panics during constant evaluation for a foreign specialization proof.
343    pub const fn bind(self, proof: SpecIdentityProof) -> LocalAuraIdx {
344        assert!(
345            self.brand == proof.brand,
346            "foreign specialization aura binding"
347        );
348
349        LocalAuraIdx::branded(self.local, self.brand)
350    }
351}
352
353impl SpecSpellBinding {
354    pub(crate) const fn new(brand: u32, local: u8) -> Self {
355        Self { brand, local }
356    }
357
358    /// Resolve this coordinate only with the proof from the same generated specialization.
359    ///
360    /// # Panics
361    ///
362    /// Panics during constant evaluation for a foreign specialization proof.
363    pub const fn bind(self, proof: SpecIdentityProof) -> LocalSpellIdx {
364        assert!(
365            self.brand == proof.brand,
366            "foreign specialization spell binding"
367        );
368
369        LocalSpellIdx::branded(self.local, self.brand)
370    }
371}
372
373impl RppmBinding {
374    pub(crate) const fn new(index: u16) -> Self {
375        Self(index)
376    }
377
378    #[inline]
379    #[expect(
380        clippy::trivially_copy_pass_by_ref,
381        reason = "C-CONV requires borrowed as_ receivers"
382    )]
383    pub(crate) const fn as_usize(&self) -> usize {
384        self.0 as usize
385    }
386}
387
388impl PeriodicDriverBinding {
389    pub(crate) const fn new(index: u8) -> Self {
390        Self(index)
391    }
392
393    #[inline]
394    #[expect(
395        clippy::trivially_copy_pass_by_ref,
396        reason = "C-CONV requires borrowed as_ receivers"
397    )]
398    pub(crate) const fn as_usize(&self) -> usize {
399        self.0 as usize
400    }
401}
402
403impl ProcCounterBinding {
404    pub(crate) const fn new(index: u8) -> Self {
405        Self(index)
406    }
407
408    #[inline]
409    #[expect(
410        clippy::trivially_copy_pass_by_ref,
411        reason = "C-CONV requires borrowed as_ receivers"
412    )]
413    pub(crate) const fn as_usize(&self) -> usize {
414        self.0 as usize
415    }
416}
417
418impl SystemRppmBinding {
419    pub(crate) const fn new(index: u8) -> Self {
420        Self(index)
421    }
422
423    #[inline]
424    #[expect(
425        clippy::trivially_copy_pass_by_ref,
426        reason = "C-CONV requires borrowed as_ receivers"
427    )]
428    pub(crate) const fn as_usize(&self) -> usize {
429        self.0 as usize
430    }
431}
432
433impl ProcCategoryBinding {
434    pub(crate) const fn new(index: u8) -> Self {
435        Self(index)
436    }
437
438    #[inline]
439    #[expect(
440        clippy::trivially_copy_pass_by_ref,
441        reason = "C-CONV requires borrowed as_ receivers"
442    )]
443    pub(crate) const fn as_usize(&self) -> usize {
444        self.0 as usize
445    }
446}
447
448impl LocalRppmIdx {
449    pub(crate) const fn new(index: u8) -> Self {
450        Self(index)
451    }
452
453    #[inline]
454    #[must_use]
455    pub const fn as_usize(&self) -> usize {
456        self.0 as usize
457    }
458}
459
460impl LocalThresholdIdx {
461    pub(crate) const fn new(index: u8) -> Self {
462        Self(index)
463    }
464
465    #[inline]
466    #[must_use]
467    pub const fn as_usize(&self) -> usize {
468        self.0 as usize
469    }
470}
471
472#[cfg(test)]
473mod identity_tests {
474    use wowlab_engine_ports::CombatStats;
475
476    use crate::{CombatSystemBuilder, generated_identities::specs};
477
478    #[googletest::gtest]
479    fn foreign_binding_and_matching_foreign_proof_are_rejected_by_program() {
480        let own_proof = specs::arms_warrior::PROOF;
481        let foreign_proof = specs::fury_warrior::PROOF;
482        let own_spell = specs::arms_warrior::SPELL_MORTAL_STRIKE.bind(own_proof);
483        let foreign_spell = specs::fury_warrior::SPELL_BLOODTHIRST.bind(foreign_proof);
484        let own_aura = specs::arms_warrior::AURA_COLOSSUS_SMASH.bind(own_proof);
485        let foreign_aura = specs::fury_warrior::AURA_ENRAGE.bind(foreign_proof);
486        let built = CombatSystemBuilder::new(CombatStats::default())
487            .spec_identity(own_proof)
488            .encounter(crate::test_support::default_encounter())
489            .spell("mortal_strike", 1, Ok)
490            .aura("sudden_death", 2, Ok)
491            .into_draft()
492            .expect("valid branded draft")
493            .finalize(&crate::test_support::minimal_rotation())
494            .expect("valid branded program");
495
496        assert!(built.program.resolve_spell_identity(own_spell).is_some());
497        assert!(built.program.resolve_aura_identity(own_aura).is_some());
498        assert!(
499            built
500                .program
501                .resolve_spell_identity(foreign_spell)
502                .is_none()
503        );
504        assert!(built.program.resolve_aura_identity(foreign_aura).is_none());
505    }
506}