Skip to main content

wowlab_engine_combat/state/
orchestration.rs

1//! Deterministic group, wave, pull, and termination state for one iteration.
2
3use wowlab_engine_ports::PullLifecycleGeneration;
4use wowlab_types::sim::{EncounterDefinition, EnemyIdx, GroupId, PullId, SimTime, WaveId};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub(crate) enum EnemyCompletion {
8    Pending,
9    Dead,
10    CompletionDespawn,
11    NonCompletionDespawn,
12    ForcedOptionalDespawn,
13}
14
15#[derive(Debug)]
16pub(crate) struct EncounterRuntime {
17    active_pull: PullId,
18    pull_epoch: u32,
19    lifecycle_epoch: u64,
20    pull_started_at: SimTime,
21    waves_active: Vec<bool>,
22    groups_complete: Vec<bool>,
23    groups_queued: Vec<bool>,
24    pulls_complete: Vec<bool>,
25    pull_completion_queued: bool,
26    enemy_completion: Vec<EnemyCompletion>,
27    terminated_at: Option<SimTime>,
28    started: bool,
29    pending_target_precombat: Option<PullLifecycleGeneration>,
30}
31
32impl EncounterRuntime {
33    pub(crate) fn new(definition: &EncounterDefinition) -> Self {
34        let mut runtime = Self {
35            active_pull: PullId(0),
36            pull_epoch: 1,
37            lifecycle_epoch: 1,
38            pull_started_at: SimTime::ZERO,
39            waves_active: vec![false; definition.waves.len()],
40            groups_complete: vec![false; definition.groups.len()],
41            groups_queued: vec![false; definition.groups.len()],
42            pulls_complete: vec![false; definition.pulls.len()],
43            pull_completion_queued: false,
44            enemy_completion: vec![EnemyCompletion::Pending; definition.enemies.len()],
45            terminated_at: None,
46            started: false,
47            pending_target_precombat: None,
48        };
49
50        runtime.mark_initial_waves(definition);
51
52        runtime
53    }
54
55    pub(crate) fn reset(&mut self, definition: &EncounterDefinition) {
56        debug_assert_eq!(
57            self.waves_active.len(),
58            definition.waves.len(),
59            "wave runtime length is finalized"
60        );
61        debug_assert_eq!(
62            self.groups_complete.len(),
63            definition.groups.len(),
64            "group completion runtime length is finalized"
65        );
66        debug_assert_eq!(
67            self.groups_queued.len(),
68            definition.groups.len(),
69            "group queue runtime length is finalized"
70        );
71        debug_assert_eq!(
72            self.pulls_complete.len(),
73            definition.pulls.len(),
74            "pull runtime length is finalized"
75        );
76        debug_assert_eq!(
77            self.enemy_completion.len(),
78            definition.enemies.len(),
79            "enemy completion runtime length is finalized"
80        );
81        self.active_pull = PullId(0);
82        self.pull_epoch = 1;
83        self.lifecycle_epoch = self.lifecycle_epoch.saturating_add(1);
84        self.pull_started_at = SimTime::ZERO;
85        self.waves_active.fill(false);
86        self.groups_complete.fill(false);
87        self.groups_queued.fill(false);
88        self.pulls_complete.fill(false);
89        self.pull_completion_queued = false;
90        self.enemy_completion.fill(EnemyCompletion::Pending);
91        self.terminated_at = None;
92        self.started = false;
93        self.pending_target_precombat = None;
94        self.mark_initial_waves(definition);
95    }
96
97    pub(crate) const fn active_pull(&self) -> PullId {
98        self.active_pull
99    }
100
101    pub(crate) const fn pull_epoch(&self) -> u32 {
102        self.pull_epoch
103    }
104
105    pub(crate) const fn generation(&self) -> PullLifecycleGeneration {
106        PullLifecycleGeneration::new(self.active_pull, self.pull_epoch, self.lifecycle_epoch)
107    }
108
109    pub(crate) const fn pull_started_at(&self) -> SimTime {
110        self.pull_started_at
111    }
112
113    pub(crate) fn is_current(&self, pull: PullId, epoch: u32) -> bool {
114        self.active_pull.0 == pull.0
115            && self.pull_epoch == epoch
116            && self.terminated_at.is_none()
117            && !self
118                .pulls_complete
119                .get(pull.as_usize())
120                .copied()
121                .unwrap_or(true)
122    }
123
124    pub(crate) fn is_current_generation(&self, generation: PullLifecycleGeneration) -> bool {
125        generation.lifecycle_epoch() == self.lifecycle_epoch
126            && self.is_current(generation.pull(), generation.pull_epoch())
127    }
128
129    pub(crate) fn start_once(&mut self) -> bool {
130        if self.started {
131            return false;
132        }
133
134        self.started = true;
135
136        true
137    }
138
139    pub(crate) fn wave_is_active(&self, wave: WaveId) -> bool {
140        self.waves_active
141            .get(wave.as_usize())
142            .copied()
143            .unwrap_or(false)
144    }
145
146    pub(crate) fn activate_wave(&mut self, wave: WaveId) -> bool {
147        let Some(active) = self.waves_active.get_mut(wave.as_usize()) else {
148            return false;
149        };
150
151        if *active {
152            return false;
153        }
154
155        *active = true;
156
157        true
158    }
159
160    pub(crate) fn group_is_complete(&self, group: GroupId) -> bool {
161        self.groups_complete
162            .get(group.as_usize())
163            .copied()
164            .unwrap_or(false)
165    }
166
167    pub(crate) fn queue_group_completion(&mut self, group: GroupId) -> bool {
168        if self.group_is_complete(group) {
169            return false;
170        }
171
172        let Some(queued) = self.groups_queued.get_mut(group.as_usize()) else {
173            return false;
174        };
175
176        if *queued {
177            return false;
178        }
179
180        *queued = true;
181
182        true
183    }
184
185    pub(crate) fn complete_group(&mut self, group: GroupId) -> bool {
186        let Some(completed) = self.groups_complete.get_mut(group.as_usize()) else {
187            return false;
188        };
189
190        if *completed {
191            return false;
192        }
193
194        *completed = true;
195
196        true
197    }
198
199    pub(crate) fn queue_pull_completion(&mut self) -> bool {
200        if self.pull_completion_queued {
201            return false;
202        }
203
204        self.pull_completion_queued = true;
205
206        true
207    }
208
209    pub(crate) fn complete_pull(&mut self, pull: PullId) -> bool {
210        let Some(completed) = self.pulls_complete.get_mut(pull.as_usize()) else {
211            return false;
212        };
213
214        if *completed {
215            return false;
216        }
217
218        *completed = true;
219
220        true
221    }
222
223    pub(crate) fn begin_next_pull(&mut self, pull: PullId, at: SimTime) {
224        self.active_pull = pull;
225        self.pull_epoch = self.pull_epoch.saturating_add(1);
226        self.pull_started_at = at;
227        self.pull_completion_queued = false;
228    }
229
230    pub(crate) fn queue_target_precombat(&mut self) {
231        self.pending_target_precombat = Some(self.generation());
232    }
233
234    pub(crate) fn take_current_target_precombat(&mut self) -> bool {
235        if !self
236            .pending_target_precombat
237            .is_some_and(|generation| self.is_current_generation(generation))
238        {
239            return false;
240        }
241
242        self.pending_target_precombat.take();
243
244        true
245    }
246
247    pub(crate) fn record_enemy_completion(
248        &mut self,
249        enemy: EnemyIdx,
250        completion: EnemyCompletion,
251    ) -> bool {
252        let Some(current) = self.enemy_completion.get_mut(enemy.as_usize()) else {
253            return false;
254        };
255
256        if *current != EnemyCompletion::Pending {
257            return false;
258        }
259
260        *current = completion;
261
262        true
263    }
264
265    pub(crate) fn enemy_completion(&self, enemy: EnemyIdx) -> Option<EnemyCompletion> {
266        self.enemy_completion.get(enemy.as_usize()).copied()
267    }
268
269    pub(crate) fn terminate(&mut self, at: SimTime) {
270        if self.terminated_at.is_none() {
271            self.terminated_at = Some(at);
272        }
273    }
274
275    pub(crate) const fn terminated_at(&self) -> Option<SimTime> {
276        self.terminated_at
277    }
278
279    fn mark_initial_waves(&mut self, definition: &EncounterDefinition) {
280        let Some(first_pull) = definition.pulls.first() else {
281            return;
282        };
283
284        for wave_id in &first_pull.wave_ids {
285            let Some(wave) = definition.waves.get(wave_id.as_usize()) else {
286                continue;
287            };
288
289            if wave.minimum_activation_s.abs() <= f64::EPSILON
290                && wave.depends_on_groups.is_empty()
291                && let Some(active) = self.waves_active.get_mut(wave.id.as_usize())
292            {
293                *active = true;
294            }
295        }
296    }
297}