Skip to main content

wowlab_engine_combat/state/defs/
guardians.rs

1//! Temporary-guardian definitions and runtime state.
2
3use wowlab_types::sim::{ActorId, EnemyIdx, PetIdx};
4
5use crate::{
6    context::HookCtx,
7    state::{PetOwnerCoefficients, PetStatInheritance},
8};
9
10pub type GuardianActionFn = fn(&mut HookCtx<'_>, GuardianEvent);
11
12pub(crate) const MAX_GUARDIAN_ABILITIES: usize = 8;
13
14fn empty_guardian_action(_ctx: &mut HookCtx<'_>, _event: GuardianEvent) {}
15
16/// Stable handle for one temporary guardian instance during an iteration.
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18#[must_use]
19pub struct GuardianHandle(pub(crate) u32, pub(crate) u32);
20
21impl GuardianHandle {
22    pub const fn new(slot: u32, generation: u32) -> Self {
23        Self(slot, generation)
24    }
25
26    #[must_use]
27    pub const fn as_u32(&self) -> u32 {
28        self.0
29    }
30
31    #[must_use]
32    pub const fn generation(&self) -> u32 {
33        self.1
34    }
35
36    pub(crate) fn actor(self) -> Option<ActorId> {
37        PetIdx::from_guardian_slot(self.0 as usize).map(ActorId::Pet)
38    }
39}
40
41/// Error returned when every finalized temporary-guardian actor slot is occupied.
42#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
43#[error("temporary guardian capacity of {capacity} actors is exhausted")]
44pub struct GuardianSummonError {
45    capacity: usize,
46}
47
48impl GuardianSummonError {
49    pub(crate) const fn saturated() -> Self {
50        Self {
51            capacity: PetIdx::MAX - 1,
52        }
53    }
54
55    #[must_use]
56    pub const fn capacity(&self) -> usize {
57        self.capacity
58    }
59}
60
61/// Context supplied to a guardian action callback.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63#[must_use]
64pub struct GuardianEvent {
65    pub handle: GuardianHandle,
66    pub tag: u32,
67    pub ability_index: usize,
68    pub action_index: u16,
69    pub spawned_at: wowlab_types::sim::SimTime,
70    pub expires_at: wowlab_types::sim::SimTime,
71}
72
73/// One independently scheduled ability owned by a temporary guardian.
74#[derive(Clone, Copy, Debug)]
75#[must_use]
76#[expect(
77    clippy::struct_excessive_bools,
78    reason = "guardian cadence, lifetime, and scheduler ownership are independent encounter capabilities"
79)]
80pub struct GuardianAbility {
81    pub(crate) first_action_delay_ms: u32,
82    pub(crate) action_interval_ms: u32,
83    pub(crate) hasted_first_action: bool,
84    pub(crate) hasted_action_interval: bool,
85    pub(crate) max_actions: u16,
86    pub(crate) ends_guardian: bool,
87    pub(crate) self_scheduled: bool,
88    pub(crate) action: GuardianActionFn,
89}
90
91impl GuardianAbility {
92    const EMPTY: Self = Self {
93        first_action_delay_ms: 0,
94        action_interval_ms: 0,
95        hasted_first_action: false,
96        hasted_action_interval: false,
97        max_actions: 0,
98        ends_guardian: false,
99        self_scheduled: false,
100        action: empty_guardian_action,
101    };
102
103    pub const fn new(action_interval_ms: u32, action: GuardianActionFn) -> Self {
104        Self {
105            first_action_delay_ms: 0,
106            action_interval_ms,
107            hasted_first_action: false,
108            hasted_action_interval: false,
109            max_actions: 0,
110            ends_guardian: false,
111            self_scheduled: true,
112            action,
113        }
114    }
115
116    /// An owner-driven ability that never self-schedules.
117    /// Dispatch it with [`crate::GuardianOps::command_guardians`].
118    pub const fn on_demand(action: GuardianActionFn) -> Self {
119        let mut ability = Self::new(0, action);
120
121        ability.self_scheduled = false;
122
123        ability
124    }
125
126    pub const fn first_action_delay(mut self, delay_ms: u32) -> Self {
127        self.first_action_delay_ms = delay_ms;
128
129        self
130    }
131
132    pub const fn hasted_actions(mut self) -> Self {
133        self.hasted_first_action = true;
134        self.hasted_action_interval = true;
135
136        self
137    }
138
139    /// Scale repeating action intervals with live haste while preserving a fixed initial delay.
140    pub const fn hasted_action_interval(mut self) -> Self {
141        self.hasted_action_interval = true;
142
143        self
144    }
145
146    /// Stop this ability after exactly `count` actions; zero means lifetime-only.
147    pub const fn max_actions(mut self, count: u16) -> Self {
148        self.max_actions = count;
149
150        self
151    }
152
153    /// Despawn the owning guardian when this bounded ability completes.
154    pub const fn ends_guardian(mut self) -> Self {
155        self.ends_guardian = true;
156
157        self
158    }
159}
160
161/// Definition used when a cast hook summons a temporary guardian.
162#[derive(Clone, Copy, Debug)]
163#[must_use]
164pub struct GuardianSpec {
165    pub(crate) tag: u32,
166    pub(crate) npc_id: Option<u32>,
167    pub(crate) stat_inheritance: PetStatInheritance,
168    pub(crate) duration_ms: u32,
169    abilities: [GuardianAbility; MAX_GUARDIAN_ABILITIES],
170    ability_count: u8,
171    pub(crate) on_demise: Option<GuardianActionFn>,
172    pub(crate) owner_coefficients: PetOwnerCoefficients,
173}
174
175impl GuardianSpec {
176    pub const fn new(tag: u32, duration_ms: u32) -> Self {
177        Self {
178            tag,
179            npc_id: None,
180            stat_inheritance: PetStatInheritance::IDENTITY,
181            duration_ms,
182            abilities: [GuardianAbility::EMPTY; MAX_GUARDIAN_ABILITIES],
183            ability_count: 0,
184            on_demise: None,
185            owner_coefficients: PetOwnerCoefficients::new(),
186        }
187    }
188
189    pub const fn new_inheriting_owner(tag: u32, duration_ms: u32) -> Self {
190        Self::new(tag, duration_ms).inherit_owner_stats()
191    }
192
193    /// Assign the creature identity used by NPC-filtered companion modifiers.
194    pub const fn npc_id(mut self, npc_id: u32) -> Self {
195        self.npc_id = Some(npc_id);
196
197        self
198    }
199
200    /// Assign build-time-resolved creature stat inheritance multipliers.
201    pub const fn stat_inheritance(mut self, inheritance: PetStatInheritance) -> Self {
202        self.stat_inheritance = inheritance;
203
204        self
205    }
206
207    /// Adds one ability to the guardian's fixed action list.
208    ///
209    /// # Panics
210    ///
211    /// Panics when the fixed guardian ability capacity is exceeded.
212    pub const fn ability(mut self, ability: GuardianAbility) -> Self {
213        assert!(
214            (self.ability_count as usize) < MAX_GUARDIAN_ABILITIES,
215            "guardian ability count exceeds the fixed runtime bound"
216        );
217        // #t(rust_unchecked_indexing) the fixed bound assertion above proves this ability slot exists.
218        self.abilities[self.ability_count as usize] = ability;
219        self.ability_count += 1;
220
221        self
222    }
223
224    pub const fn on_demise(mut self, hook: GuardianActionFn) -> Self {
225        self.on_demise = Some(hook);
226
227        self
228    }
229
230    pub const fn owner_coefficients(mut self, coefficients: PetOwnerCoefficients) -> Self {
231        self.owner_coefficients = coefficients;
232
233        self
234    }
235
236    pub const fn inherit_owner_stats(self) -> Self {
237        self.owner_coefficients(PetOwnerCoefficients::owner_identity())
238    }
239
240    pub(crate) fn abilities(&self) -> &[GuardianAbility] {
241        self.abilities
242            .get(..usize::from(self.ability_count))
243            .expect("guardian ability count is bounded at construction")
244    }
245
246    pub(crate) const fn ability_count(&self) -> usize {
247        self.ability_count as usize
248    }
249}
250
251#[derive(Clone, Copy, Debug)]
252pub(crate) struct GuardianAbilityState {
253    pub(crate) next_action_at: Option<wowlab_types::sim::SimTime>,
254    pub(crate) actions_executed: u16,
255}
256
257#[derive(Clone, Debug)]
258pub(crate) struct GuardianInstance {
259    pub(crate) handle: GuardianHandle,
260    pub(crate) spec: GuardianSpec,
261    pub(crate) spawned_at: wowlab_types::sim::SimTime,
262    pub(crate) expires_at: wowlab_types::sim::SimTime,
263    pub(crate) abilities: [GuardianAbilityState; MAX_GUARDIAN_ABILITIES],
264    pub(crate) source: ActorId,
265    pub(crate) target: EnemyIdx,
266    pub(crate) stat_snapshot: super::PetStatSnapshot,
267}
268
269impl GuardianInstance {
270    pub(crate) fn ability_states(&self) -> &[GuardianAbilityState] {
271        self.abilities
272            .get(..self.spec.ability_count())
273            .expect("guardian ability state count matches its bounded specification")
274    }
275}