Skip to main content

wowlab_engine_combat/state/
config.rs

1use wowlab_engine_ports::CombatStats;
2use wowlab_types::sim::ActorId;
3
4use super::{
5    AccumulatingImpactProc, CastHookFn, DamageTransfer, ImpactEffectProc, ImpactProc,
6    LandedImpactProc, ResourceGainProc, SpellEffectRange,
7};
8
9#[derive(Clone, Copy, Debug)]
10pub(crate) struct EffectProgramFrame {
11    pub(crate) range: SpellEffectRange,
12    pub(crate) profile_spell_id: u32,
13    pub(crate) profile_action: Option<crate::state::program::LocalDamageActionIdx>,
14    pub(crate) flags: crate::DamageFlags,
15    pub(crate) next: usize,
16    pub(crate) end: usize,
17    pub(crate) reflected: bool,
18}
19
20impl Default for EffectProgramFrame {
21    fn default() -> Self {
22        Self {
23            range: SpellEffectRange::default(),
24            profile_spell_id: 0,
25            profile_action: None,
26            flags: crate::DamageFlags::empty(),
27            next: 0,
28            end: 0,
29            reflected: false,
30        }
31    }
32}
33
34#[derive(Debug)]
35pub(crate) struct TargetWorkspace {
36    candidates: Vec<wowlab_types::sim::EnemyIdx>,
37    candidate_count: usize,
38    targets: Vec<wowlab_types::sim::EnemyIdx>,
39    target_count: usize,
40}
41
42impl TargetWorkspace {
43    pub(crate) fn new(target_capacity: usize) -> Self {
44        Self {
45            candidates: vec![wowlab_types::sim::EnemyIdx::PRIMARY; target_capacity],
46            candidate_count: 0,
47            targets: vec![wowlab_types::sim::EnemyIdx::PRIMARY; target_capacity],
48            target_count: 0,
49        }
50    }
51
52    pub(crate) fn clear(&mut self) {
53        self.candidate_count = 0;
54        self.target_count = 0;
55    }
56
57    pub(crate) fn candidate_storage(&mut self) -> &mut [wowlab_types::sim::EnemyIdx] {
58        &mut self.candidates
59    }
60
61    pub(crate) fn set_candidate_count(&mut self, count: usize) {
62        assert!(
63            count <= self.candidates.len(),
64            "candidate count {count} exceeds finalized target capacity {}",
65            self.candidates.len()
66        );
67        self.candidate_count = count;
68    }
69
70    pub(crate) fn candidates(&self) -> &[wowlab_types::sim::EnemyIdx] {
71        // BOUNDS: every writer asserts the finalized candidate-storage bound before setting the initialized count.
72        &self.candidates[..self.candidate_count]
73    }
74
75    pub(crate) const fn candidate_count(&self) -> usize {
76        self.candidate_count
77    }
78
79    pub(crate) fn candidate_at(&self, index: usize) -> Option<wowlab_types::sim::EnemyIdx> {
80        self.candidates().get(index).copied()
81    }
82
83    pub(crate) fn retain_candidates(
84        &mut self,
85        mut keep: impl FnMut(wowlab_types::sim::EnemyIdx) -> bool,
86    ) {
87        let mut retained = 0;
88
89        for index in 0..self.candidate_count {
90            // BOUNDS: the candidate count never exceeds the fixed finalized storage length.
91            let candidate = self.candidates[index];
92
93            if keep(candidate) {
94                // BOUNDS: retained cannot exceed the already-validated source index.
95                self.candidates[retained] = candidate;
96                retained += 1;
97            }
98        }
99
100        self.candidate_count = retained;
101    }
102
103    pub(crate) fn retain_candidates_excluding_targets(
104        &mut self,
105        mut keep: impl FnMut(wowlab_types::sim::EnemyIdx) -> bool,
106    ) {
107        let mut retained = 0;
108
109        for index in 0..self.candidate_count {
110            // BOUNDS: the candidate count never exceeds the fixed finalized storage length.
111            let candidate = self.candidates[index];
112
113            if keep(candidate) && !self.targets().contains(&candidate) {
114                // BOUNDS: retained cannot exceed the already-validated source index.
115                self.candidates[retained] = candidate;
116                retained += 1;
117            }
118        }
119
120        self.candidate_count = retained;
121    }
122
123    pub(crate) fn push_target(&mut self, target: wowlab_types::sim::EnemyIdx) {
124        assert!(
125            self.target_count < self.targets.len(),
126            "target count {} exhausts finalized capacity {}",
127            self.target_count,
128            self.targets.len()
129        );
130        // BOUNDS: the preceding finalized-capacity assertion validates this target slot.
131        self.targets[self.target_count] = target;
132        self.target_count += 1;
133    }
134
135    pub(crate) fn contains_target(&self, target: wowlab_types::sim::EnemyIdx) -> bool {
136        self.targets().contains(&target)
137    }
138
139    pub(crate) fn targets(&self) -> &[wowlab_types::sim::EnemyIdx] {
140        // BOUNDS: target_count advances only after the finalized-capacity assertion in push_target.
141        &self.targets[..self.target_count]
142    }
143
144    pub(crate) fn truncate_targets(&mut self, count: usize) {
145        self.target_count = self.target_count.min(count);
146    }
147}
148
149/// Base stats that remain constant within a single simulation iteration.
150#[derive(Clone, Debug)]
151pub struct BaseStats {
152    pub stats: CombatStats,
153    pub primary_attribute: Option<wowlab_types::game::Attribute>,
154    pub base_regen: f64,
155    pub resource_max: f64,
156    pub resource_start: f64,
157    pub resource_name: String,
158    pub has_pet: bool,
159    pub secondary_resource_name: Option<String>,
160    pub secondary_resource_max: f64,
161    pub resource_type: Option<wowlab_types::combat::ResourceType>,
162    pub secondary_resource_type: Option<wowlab_types::combat::ResourceType>,
163    pub swing_resource_gain_mult: f64,
164}
165
166/// Pre-allocated scratch buffers reused by the cast pipeline via the `mem::take` + restore pattern.
167#[derive(Debug, Default)]
168pub(crate) struct ScratchBuffers {
169    pub(crate) absorb_indices: crate::state::BoundedVec<usize>,
170    pub(crate) active_damage_transfers: Vec<(ActorId, DamageTransfer)>,
171    pub hooks: Vec<CastHookFn>,
172    pub impacts: Vec<ImpactProc>,
173    pub landed_impacts: Vec<LandedImpactProc>,
174    pub(crate) landed_impact_events: Vec<crate::state::ImpactEvent>,
175    pub accumulating_impacts: Vec<AccumulatingImpactProc>,
176    pub impact_effects: Vec<ImpactEffectProc>,
177    pub resource_gains: Vec<ResourceGainProc>,
178    pub(crate) rotation_masks: Vec<(wowlab_engine_domain::rotation::SpellSlotHandle, i32)>,
179    pub(crate) locked_action_masks: Vec<(wowlab_engine_domain::rotation::SpellSlotHandle, i32)>,
180    pub(crate) rotation_candidates: Vec<crate::handler::can_cast::CastCandidate>,
181    pub(crate) rotation_trace_reasons: Option<Vec<(u32, crate::handler::can_cast::CastReject)>>,
182    pub(crate) effect_program_frames: Vec<EffectProgramFrame>,
183    pub(crate) effect_program_frame_count: usize,
184    pub(crate) target_workspaces: Vec<TargetWorkspace>,
185    pub(crate) health_threshold_triggers: Vec<wowlab_engine_ports::ScheduledSpellBinding>,
186}
187
188impl ScratchBuffers {
189    pub(crate) fn size_absorb_scratch(&mut self, capacity: usize) {
190        self.absorb_indices = crate::state::BoundedVec::new(capacity);
191    }
192
193    pub(crate) fn size_damage_transfer_scratch(&mut self, capacity: usize) {
194        self.active_damage_transfers = Vec::with_capacity(capacity);
195    }
196
197    pub(crate) fn size_rotation_masks(&mut self, availability_count: usize) {
198        self.rotation_masks = Vec::with_capacity(availability_count);
199        self.locked_action_masks = Vec::with_capacity(availability_count);
200        self.rotation_candidates = Vec::with_capacity(availability_count);
201    }
202
203    pub(crate) fn size_execution_scratch(
204        &mut self,
205        effect_count: usize,
206        target_program_count: usize,
207        target_capacity: usize,
208    ) {
209        self.effect_program_frames =
210            vec![EffectProgramFrame::default(); effect_count.saturating_add(1)];
211        self.effect_program_frame_count = 0;
212        let target_resolution_depth = effect_count
213            .saturating_add(target_program_count)
214            .saturating_add(1);
215
216        self.target_workspaces = (0..target_resolution_depth)
217            .map(|_| TargetWorkspace::new(target_capacity))
218            .collect();
219    }
220
221    pub(crate) fn push_effect_program_frame(&mut self, frame: EffectProgramFrame) {
222        assert!(
223            self.effect_program_frame_count < self.effect_program_frames.len(),
224            "effect execution depth {} exhausts finalized frame bound {}",
225            self.effect_program_frame_count,
226            self.effect_program_frames.len()
227        );
228        // BOUNDS: the preceding finalized-depth assertion validates this frame slot.
229        self.effect_program_frames[self.effect_program_frame_count] = frame;
230        self.effect_program_frame_count += 1;
231    }
232
233    pub(crate) fn pop_effect_program_frame_after(
234        &mut self,
235        base: usize,
236    ) -> Option<EffectProgramFrame> {
237        if self.effect_program_frame_count <= base {
238            return None;
239        }
240
241        self.effect_program_frame_count -= 1;
242
243        // BOUNDS: the non-empty check and decrement select the initialized top frame.
244
245        Some(self.effect_program_frames[self.effect_program_frame_count])
246    }
247}
248
249pub(crate) fn with_scratch<T>(
250    source: &[T],
251    mut scratch: Vec<T>,
252    use_entries: impl FnOnce(&mut Vec<T>),
253) -> Vec<T>
254where
255    T: Copy,
256{
257    scratch.clear();
258    debug_assert!(
259        scratch.capacity() >= source.len(),
260        "finalized scratch capacity must cover its immutable source plan"
261    );
262    scratch.extend(source.iter().copied());
263    use_entries(&mut scratch);
264
265    scratch
266}