1use wowlab_engine_telemetry::TelemetrySink;
2use wowlab_types::{
3 data::WeaponStats,
4 game::GearSlot,
5 sim::{
6 ActorId, AuraKey, EnemyIdx, GroupId, PositionedActorRef, PullId, SimTime, SpatialTransform,
7 SpellIdx, WaveId,
8 },
9};
10
11use crate::{
12 combat_stats::CombatStats, decision_trace::DecisionTraceSink, sim_state::SimState,
13 spatial::SpatialQueryError,
14};
15
16#[derive(Clone, Debug, serde::Serialize)]
18#[non_exhaustive]
19pub struct Paperdoll {
20 pub stats: CombatStats,
21 pub introspection: wowlab_types::game::SpecIntrospection,
22 pub precombat_aura_ids: Vec<u32>,
23 pub talent_ranks: Vec<(u32, u8)>,
24 pub item_use_spells: Vec<(GearSlot, u32)>,
25 pub weapon_main: WeaponStats,
26 pub weapon_off: Option<WeaponStats>,
27}
28
29#[derive(Clone, Debug)]
31pub struct PaperdollLoadout {
33 pub precombat_aura_ids: Vec<u32>,
34 pub talent_ranks: Vec<(u32, u8)>,
35 pub item_use_spells: Vec<(GearSlot, u32)>,
36 pub weapon_main: WeaponStats,
37 pub weapon_off: Option<WeaponStats>,
38}
39
40impl Paperdoll {
41 #[must_use]
42 pub fn new(
43 stats: CombatStats,
44 introspection: wowlab_types::game::SpecIntrospection,
45 loadout: PaperdollLoadout,
46 ) -> Self {
47 Self {
48 stats,
49 introspection,
50 precombat_aura_ids: loadout.precombat_aura_ids,
51 talent_ranks: loadout.talent_ranks,
52 item_use_spells: loadout.item_use_spells,
53 weapon_main: loadout.weapon_main,
54 weapon_off: loadout.weapon_off,
55 }
56 }
57}
58
59#[derive(Clone, Debug)]
61pub enum SpecAction {
64 Cast {
65 spell_id: SpellIdx,
66 empower_rank: u8,
67 source: ActorId,
68 target: EnemyIdx,
69 },
70 Wait {
71 until_ms: SimTime,
72 },
73}
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78#[non_exhaustive]
79pub enum EnemyDespawnOutcome {
80 Despawned,
81 AlreadyInactive,
82 UnknownEnemy,
83}
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum EncounterDespawnKind {
89 Authored { counts_as_completion: bool },
90 ForcedOptional,
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum EncounterTerminationReason {
97 FixedDuration,
98 PrimaryRequiredEnemyDead,
99 RequiredGroupsComplete,
100 AllEnemiesInRequiredGroupsDead,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct AuraEventRef {
106 pub key: AuraKey,
107 pub target: Option<EnemyIdx>,
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub struct PullScope {
113 pub pull: PullId,
114 pub epoch: u32,
115}
116
117wowlab_engine_macros::define_error! {
118#[derive(Debug)]
120pub struct SpecRuntimeError {
121 #[source]
122 kind: SpecRuntimeErrorKind,
123}
124
125#[derive(Debug, thiserror::Error)]
126enum SpecRuntimeErrorKind {
127 #[error("enemy {enemy:?} is absent from canonical encounter state")]
128 MissingEnemy { enemy: EnemyIdx },
129 #[error("failed to remove dying enemy {enemy:?} from the spatial index")]
130 EnemyDeathSpatialRemoval {
131 enemy: EnemyIdx,
132 #[source]
133 source: SpatialQueryError,
134 },
135 #[error(
136 "encounter stalled in pull {pull:?}: incomplete required groups {incomplete_required_groups:?}, inactive/live enemies {inactive_live_enemies:?}"
137 )]
138 EncounterStalled {
139 pull: PullId,
140 incomplete_required_groups: Vec<GroupId>,
141 inactive_live_enemies: Vec<EnemyIdx>,
142 },
143 #[error("invalid encounter script event in pull {pull:?}: {message}")]
144 EncounterScript { pull: PullId, message: String },
145 #[error(
146 "spell {spell_id} requested empower rank {requested_rank}, but its structured stage data supports ranks 1 through {max_rank}"
147 )]
148 InvalidEmpowerRank {
149 spell_id: u32,
150 requested_rank: u8,
151 max_rank: u8,
152 },
153}
154}
155
156impl SpecRuntimeError {
157 #[must_use]
159 pub const fn missing_enemy(enemy: EnemyIdx) -> Self {
160 Self {
161 kind: SpecRuntimeErrorKind::MissingEnemy { enemy },
162 }
163 }
164
165 #[must_use]
167 pub const fn enemy_death_spatial_removal(enemy: EnemyIdx, source: SpatialQueryError) -> Self {
168 Self {
169 kind: SpecRuntimeErrorKind::EnemyDeathSpatialRemoval { enemy, source },
170 }
171 }
172
173 #[must_use]
175 pub fn encounter_stalled(
176 pull: PullId,
177 incomplete_required_groups: Vec<GroupId>,
178 inactive_live_enemies: Vec<EnemyIdx>,
179 ) -> Self {
180 Self {
181 kind: SpecRuntimeErrorKind::EncounterStalled {
182 pull,
183 incomplete_required_groups,
184 inactive_live_enemies,
185 },
186 }
187 }
188
189 #[must_use]
191 pub fn encounter_script(pull: PullId, message: impl Into<String>) -> Self {
192 Self {
193 kind: SpecRuntimeErrorKind::EncounterScript {
194 pull,
195 message: message.into(),
196 },
197 }
198 }
199
200 #[must_use]
202 pub const fn invalid_empower_rank(spell_id: u32, requested_rank: u8, max_rank: u8) -> Self {
203 Self {
204 kind: SpecRuntimeErrorKind::InvalidEmpowerRank {
205 spell_id,
206 requested_rank,
207 max_rank,
208 },
209 }
210 }
211
212 #[must_use]
214 pub const fn enemy_death_spatial_removal_details(
215 &self,
216 ) -> Option<(EnemyIdx, SpatialQueryError)> {
217 match self.kind {
218 SpecRuntimeErrorKind::EnemyDeathSpatialRemoval { enemy, source } => {
219 Some((enemy, source))
220 }
221 _ => None,
222 }
223 }
224
225 #[must_use]
227 pub fn encounter_stalled_details(&self) -> Option<(PullId, &[GroupId], &[EnemyIdx])> {
228 match &self.kind {
229 SpecRuntimeErrorKind::EncounterStalled {
230 pull,
231 incomplete_required_groups,
232 inactive_live_enemies,
233 } => Some((
234 *pull,
235 incomplete_required_groups.as_slice(),
236 inactive_live_enemies.as_slice(),
237 )),
238 _ => None,
239 }
240 }
241}
242
243#[derive(Clone, Copy, Debug, Eq, PartialEq)]
245pub struct PullLifecycleGeneration {
246 pull: PullId,
247 pull_epoch: u32,
248 lifecycle_epoch: u64,
249}
250
251impl PullLifecycleGeneration {
252 #[must_use]
253 pub const fn new(pull: PullId, pull_epoch: u32, lifecycle_epoch: u64) -> Self {
254 Self {
255 pull,
256 pull_epoch,
257 lifecycle_epoch,
258 }
259 }
260
261 #[must_use]
262 pub const fn pull(self) -> PullId {
263 self.pull
264 }
265
266 #[must_use]
267 pub const fn pull_epoch(self) -> u32 {
268 self.pull_epoch
269 }
270
271 #[must_use]
272 pub const fn lifecycle_epoch(self) -> u64 {
273 self.lifecycle_epoch
274 }
275}
276
277#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub enum DeathEventScope {
281 Unscoped,
282 ScriptedLifetime(PullLifecycleGeneration),
283}
284
285#[derive(Clone, Copy, Debug)]
287pub enum Event {
289 PlayerReady {
290 t: SimTime,
291 },
292 OffGcdReady {
293 t: SimTime,
294 },
295 CastStart {
296 t: SimTime,
297 spell_id: SpellIdx,
298 empower_rank: u8,
299 source: ActorId,
300 target: EnemyIdx,
301 },
302 CastComplete {
303 t: SimTime,
304 spell_id: SpellIdx,
305 empower_rank: u8,
306 source: ActorId,
307 target: EnemyIdx,
308 },
309 TriggeredSpell {
311 t: SimTime,
312 spell_id: SpellIdx,
313 source: ActorId,
314 target: ActorId,
315 },
316 SpellLaunch {
317 t: SimTime,
318 impact_id: u32,
319 source: ActorId,
320 target: EnemyIdx,
321 },
322 SpellImpact {
323 t: SimTime,
324 impact_id: u32,
325 source: ActorId,
326 target: EnemyIdx,
327 },
328 GuardianAction {
329 t: SimTime,
330 guardian_id: u32,
331 guardian_generation: u32,
332 ability_index: usize,
333 source: ActorId,
334 target: EnemyIdx,
335 },
336 GuardianExpire {
337 t: SimTime,
338 guardian_id: u32,
339 guardian_generation: u32,
340 },
341 PetAction {
342 t: SimTime,
343 auto_attack_index: usize,
344 source: ActorId,
345 target: EnemyIdx,
346 },
347 AuraTick {
348 t: SimTime,
349 key: AuraKey,
350 target: Option<EnemyIdx>,
351 },
352 ChannelTick {
353 t: SimTime,
354 spell_id: SpellIdx,
355 source: ActorId,
356 target: EnemyIdx,
357 generation: u64,
358 },
359 AuraExpire {
360 t: SimTime,
361 key: AuraKey,
362 target: Option<EnemyIdx>,
363 },
364 ProcHeartbeat {
365 t: SimTime,
366 },
367 Death {
368 t: SimTime,
369 target: EnemyIdx,
370 scope: DeathEventScope,
371 },
372 EncounterDespawn {
373 t: SimTime,
374 scope: PullScope,
375 target: EnemyIdx,
376 kind: EncounterDespawnKind,
377 },
378 GroupComplete {
379 t: SimTime,
380 scope: PullScope,
381 group: GroupId,
382 },
383 PullComplete {
384 t: SimTime,
385 scope: PullScope,
386 finalize: bool,
387 },
388 EncounterTerminate {
389 t: SimTime,
390 reason: EncounterTerminationReason,
391 },
392 WaveActivate {
393 t: SimTime,
394 scope: PullScope,
395 wave: WaveId,
396 },
397 EnemyActivate {
398 t: SimTime,
399 scope: PullScope,
400 target: EnemyIdx,
401 },
402 Retarget {
403 t: SimTime,
404 scope: PullScope,
405 },
406 EncounterMove {
407 t: SimTime,
408 scope: PullScope,
409 actor: PositionedActorRef,
410 transform: SpatialTransform,
411 },
412 ActorMovement {
413 t: SimTime,
414 actor: ActorId,
415 moving: bool,
416 generation: u64,
417 },
418 RaidEvent {
419 t: SimTime,
420 index: usize,
421 },
422 RaidAddDespawn {
423 t: SimTime,
424 target: EnemyIdx,
425 activation_generation: u64,
426 encounter_generation: PullLifecycleGeneration,
427 },
428 ExternalBuff {
429 t: SimTime,
430 index: usize,
431 },
432 HookTimer {
433 t: SimTime,
434 timer_id: u32,
435 source: ActorId,
436 target: Option<EnemyIdx>,
437 },
438 CooldownReady {
439 t: SimTime,
440 cooldown_key: SpellIdx,
441 },
442 AutoAttack {
443 t: SimTime,
444 source: ActorId,
445 target: EnemyIdx,
446 },
447 EnemyAutoAttack {
448 t: SimTime,
449 source: EnemyIdx,
450 target: ActorId,
451 },
452}
453
454const EVENT_PRIORITY_FINALIZATION: u8 = 0;
455const EVENT_PRIORITY_GROUP_COMPLETION: u8 = 1;
456const EVENT_PRIORITY_PULL_COMPLETION: u8 = 2;
457const EVENT_PRIORITY_TERMINATION: u8 = 3;
458const EVENT_PRIORITY_ACTIVATION: u8 = 4;
459const EVENT_PRIORITY_RETARGET: u8 = 5;
460const EVENT_PRIORITY_MOVEMENT: u8 = 6;
461const EVENT_PRIORITY_CAST: u8 = 7;
462const EVENT_PRIORITY_IMPACT: u8 = 8;
463const EVENT_PRIORITY_AURA_EXPIRE: u8 = 9;
464const EVENT_PRIORITY_READY: u8 = 10;
465
466impl Event {
467 #[inline]
469 #[must_use]
470 pub const fn priority(&self) -> u8 {
471 match self {
472 Event::Death { .. }
473 | Event::EncounterDespawn { .. }
474 | Event::RaidAddDespawn { .. }
475 | Event::GuardianExpire { .. } => EVENT_PRIORITY_FINALIZATION,
476 Event::GroupComplete { .. } => EVENT_PRIORITY_GROUP_COMPLETION,
477 Event::PullComplete { .. } => EVENT_PRIORITY_PULL_COMPLETION,
478 Event::EncounterTerminate { .. } => EVENT_PRIORITY_TERMINATION,
479 Event::WaveActivate { .. } | Event::EnemyActivate { .. } => EVENT_PRIORITY_ACTIVATION,
480 Event::Retarget { .. } => EVENT_PRIORITY_RETARGET,
481 Event::EncounterMove { .. } | Event::ActorMovement { .. } | Event::RaidEvent { .. } => {
482 EVENT_PRIORITY_MOVEMENT
483 }
484 Event::CastComplete { .. }
485 | Event::TriggeredSpell { .. }
486 | Event::SpellLaunch { .. } => EVENT_PRIORITY_CAST,
487 Event::SpellImpact { .. }
488 | Event::GuardianAction { .. }
489 | Event::PetAction { .. }
490 | Event::AuraTick { .. }
491 | Event::ChannelTick { .. }
492 | Event::ProcHeartbeat { .. }
493 | Event::HookTimer { .. }
494 | Event::AutoAttack { .. }
495 | Event::EnemyAutoAttack { .. }
496 | Event::ExternalBuff { .. } => EVENT_PRIORITY_IMPACT,
497 Event::AuraExpire { .. } => EVENT_PRIORITY_AURA_EXPIRE,
498 Event::PlayerReady { .. }
499 | Event::OffGcdReady { .. }
500 | Event::CastStart { .. }
501 | Event::CooldownReady { .. } => EVENT_PRIORITY_READY,
502 }
503 }
504
505 #[inline]
506 #[must_use]
508 pub fn timestamp(&self) -> SimTime {
509 match self {
510 Event::PlayerReady { t }
511 | Event::OffGcdReady { t }
512 | Event::CastStart { t, .. }
513 | Event::CastComplete { t, .. }
514 | Event::TriggeredSpell { t, .. }
515 | Event::SpellLaunch { t, .. }
516 | Event::SpellImpact { t, .. }
517 | Event::GuardianAction { t, .. }
518 | Event::GuardianExpire { t, .. }
519 | Event::PetAction { t, .. }
520 | Event::AuraTick { t, .. }
521 | Event::ChannelTick { t, .. }
522 | Event::AuraExpire { t, .. }
523 | Event::ProcHeartbeat { t }
524 | Event::Death { t, .. }
525 | Event::EncounterDespawn { t, .. }
526 | Event::GroupComplete { t, .. }
527 | Event::PullComplete { t, .. }
528 | Event::EncounterTerminate { t, .. }
529 | Event::WaveActivate { t, .. }
530 | Event::EnemyActivate { t, .. }
531 | Event::Retarget { t, .. }
532 | Event::EncounterMove { t, .. }
533 | Event::ActorMovement { t, .. }
534 | Event::RaidEvent { t, .. }
535 | Event::RaidAddDespawn { t, .. }
536 | Event::ExternalBuff { t, .. }
537 | Event::HookTimer { t, .. }
538 | Event::CooldownReady { t, .. }
539 | Event::AutoAttack { t, .. }
540 | Event::EnemyAutoAttack { t, .. } => *t,
541 }
542 }
543 }
545
546#[wowlab_engine_macros::spec_handler_delegation]
548pub trait SpecHandler: Send {
549 fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction>;
550
551 fn on_cast_complete(&mut self, event: Event, ctx: &mut SimContext);
552
553 fn on_triggered_spell(
554 &mut self,
555 _spell_id: SpellIdx,
556 _source: ActorId,
557 _target: ActorId,
558 _ctx: &mut SimContext,
559 ) {
560 }
561
562 fn on_spell_impact(&mut self, _impact_id: u32, _ctx: &mut SimContext) {}
563
564 fn on_spell_launch(&mut self, _impact_id: u32, _ctx: &mut SimContext) {}
565
566 fn on_guardian_action(
567 &mut self,
568 _guardian_id: u32,
569 _guardian_generation: u32,
570 _ability_index: usize,
571 _ctx: &mut SimContext,
572 ) {
573 }
574
575 fn on_guardian_expire(
576 &mut self,
577 _guardian_id: u32,
578 _guardian_generation: u32,
579 _ctx: &mut SimContext,
580 ) {
581 }
582
583 fn on_pet_action(&mut self, _auto_attack_index: usize, _ctx: &mut SimContext) {}
584
585 fn on_aura_tick(&mut self, _event: AuraEventRef, _ctx: &mut SimContext) {}
586
587 fn on_channel_tick(
588 &mut self,
589 _spell_id: SpellIdx,
590 _source: ActorId,
591 _target: EnemyIdx,
592 _generation: u64,
593 _ctx: &mut SimContext,
594 ) {
595 }
596
597 fn on_aura_expire(&mut self, _event: AuraEventRef, _ctx: &mut SimContext) {}
598
599 fn on_proc_heartbeat(&mut self, _ctx: &mut SimContext) {}
600
601 fn on_death(&mut self, _target: EnemyIdx, _scope: DeathEventScope, _ctx: &mut SimContext) {}
602
603 fn on_encounter_despawn(
604 &mut self,
605 _scope: PullScope,
606 _target: EnemyIdx,
607 _kind: EncounterDespawnKind,
608 _ctx: &mut SimContext,
609 ) {
610 }
611
612 fn on_group_complete(&mut self, _scope: PullScope, _group: GroupId, _ctx: &mut SimContext) {}
613
614 fn on_pull_complete(&mut self, _scope: PullScope, _finalize: bool, _ctx: &mut SimContext) {}
615
616 fn on_encounter_terminate(
617 &mut self,
618 _reason: EncounterTerminationReason,
619 _ctx: &mut SimContext,
620 ) {
621 }
622
623 fn on_wave_activate(&mut self, _scope: PullScope, _wave: WaveId, _ctx: &mut SimContext) {}
624
625 fn on_enemy_activate(&mut self, _scope: PullScope, _target: EnemyIdx, _ctx: &mut SimContext) {}
626
627 fn on_retarget(&mut self, _scope: PullScope, _ctx: &mut SimContext) {}
628
629 fn on_encounter_move(
630 &mut self,
631 _scope: PullScope,
632 _actor: PositionedActorRef,
633 _transform: SpatialTransform,
634 _ctx: &mut SimContext,
635 ) {
636 }
637
638 fn on_actor_movement(
639 &mut self,
640 _actor: ActorId,
641 _moving: bool,
642 _generation: u64,
643 _ctx: &mut SimContext,
644 ) {
645 }
646
647 fn on_raid_event(&mut self, _index: usize, _ctx: &mut SimContext) {}
648
649 fn on_raid_add_despawn(
650 &mut self,
651 _target: EnemyIdx,
652 _activation_generation: u64,
653 _encounter_generation: PullLifecycleGeneration,
654 _ctx: &mut SimContext,
655 ) {
656 }
657
658 fn on_external_buff(&mut self, _index: usize, _ctx: &mut SimContext) {}
659
660 fn on_event_queue_empty(&mut self, _ctx: &mut SimContext) {}
662
663 fn encounter_termination_time(&self) -> Option<SimTime> {
665 None
666 }
667
668 fn on_despawn(&mut self, _target: EnemyIdx, _ctx: &mut SimContext) -> EnemyDespawnOutcome {
670 EnemyDespawnOutcome::UnknownEnemy
671 }
672
673 fn on_hook_timer(
675 &mut self,
676 _timer_id: u32,
677 _source: ActorId,
678 _target: Option<EnemyIdx>,
679 _ctx: &mut SimContext,
680 ) {
681 }
682
683 fn on_auto_attack(&mut self, _source: ActorId, _target: EnemyIdx, _ctx: &mut SimContext) {}
684
685 fn on_enemy_auto_attack(&mut self, _source: EnemyIdx, _target: ActorId, _ctx: &mut SimContext) {
686 }
687
688 fn on_cooldown_ready(&mut self, _cooldown_key: SpellIdx, _ctx: &mut SimContext) {}
689
690 fn on_sim_start(&mut self, ctx: &mut SimContext);
691
692 fn reset(&mut self) {}
694
695 fn flush_scheduled(&mut self, _push: &mut dyn FnMut(Event)) {}
696
697 fn take_runtime_error(&mut self) -> Option<SpecRuntimeError> {
699 None
700 }
701
702 fn total_damage(&self) -> f64;
703
704 fn cast_time_ms(&self, spell_id: SpellIdx, empower_rank: u8) -> u32;
706
707 fn paperdoll(&self) -> Option<Paperdoll> {
708 None
709 }
710
711 fn introspect(&self) -> wowlab_types::game::SpecIntrospection {
712 wowlab_types::game::SpecIntrospection::default()
713 }
714
715 fn attach_decision_trace(&mut self, _sink: std::sync::Arc<dyn DecisionTraceSink>) {}
716}
717
718#[derive(Debug)]
720pub struct SimContext<'a> {
721 pub state: &'a SimState,
722 pub telemetry: &'a mut TelemetrySink,
723}
724
725#[cfg(test)]
726mod tests {
727 use googletest::prelude::*;
728 use rstest::rstest;
729
730 use super::*;
731
732 fn spell(id: u32) -> SpellIdx {
733 SpellIdx::from_raw(id)
734 }
735
736 fn key(id: u32) -> AuraKey {
737 AuraKey::new(
738 wowlab_types::sim::AuraIdx(id),
739 ActorId::Player,
740 ActorId::Enemy(EnemyIdx::PRIMARY),
741 wowlab_types::sim::AuraOn::Target,
742 )
743 }
744
745 fn transform() -> SpatialTransform {
746 SpatialTransform {
747 layer: wowlab_types::sim::SpatialLayerId(0),
748 position: wowlab_types::sim::Position2::new(1.0, 2.0),
749 heading: 0.5,
750 }
751 }
752
753 #[gtest]
754 #[rstest]
755 #[case::player_ready(Event::PlayerReady { t: SimTime::from_millis(1) }, 1)]
756 #[case::off_gcd_ready(Event::OffGcdReady { t: SimTime::from_millis(2) }, 2)]
757 #[case::cast_start(Event::CastStart { t: SimTime::from_millis(3), spell_id: spell(10), empower_rank: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 3)]
758 #[case::cast_complete(Event::CastComplete { t: SimTime::from_millis(4), spell_id: spell(10), empower_rank: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 4)]
759 #[case::spell_launch(Event::SpellLaunch { t: SimTime::from_millis(14), impact_id: 6, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 14)]
760 #[case::spell_impact(Event::SpellImpact { t: SimTime::from_millis(5), impact_id: 7, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 5)]
761 #[case::guardian_action(Event::GuardianAction { t: SimTime::from_millis(10), guardian_id: 1, guardian_generation: 1, ability_index: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 10)]
762 #[case::guardian_expire(Event::GuardianExpire { t: SimTime::from_millis(11), guardian_id: 1, guardian_generation: 1 }, 11)]
763 #[case::aura_tick(Event::AuraTick { t: SimTime::from_millis(6), key: key(20), target: Some(EnemyIdx::PRIMARY) }, 6)]
764 #[case::channel_tick(Event::ChannelTick { t: SimTime::from_millis(23), spell_id: spell(20), source: ActorId::Player, target: EnemyIdx::PRIMARY, generation: 1 }, 23)]
765 #[case::aura_expire(Event::AuraExpire { t: SimTime::from_millis(7), key: key(20), target: Some(EnemyIdx::PRIMARY) }, 7)]
766 #[case::death(Event::Death { t: SimTime::from_millis(13), target: EnemyIdx::PRIMARY, scope: DeathEventScope::Unscoped }, 13)]
767 #[case::encounter_despawn(Event::EncounterDespawn { t: SimTime::from_millis(15), scope: PullScope { pull: PullId(0), epoch: 1 }, target: EnemyIdx::PRIMARY, kind: EncounterDespawnKind::ForcedOptional }, 15)]
768 #[case::group_complete(Event::GroupComplete { t: SimTime::from_millis(16), scope: PullScope { pull: PullId(0), epoch: 1 }, group: GroupId(0) }, 16)]
769 #[case::pull_complete(Event::PullComplete { t: SimTime::from_millis(17), scope: PullScope { pull: PullId(0), epoch: 1 }, finalize: false }, 17)]
770 #[case::encounter_terminate(Event::EncounterTerminate { t: SimTime::from_millis(18), reason: EncounterTerminationReason::RequiredGroupsComplete }, 18)]
771 #[case::wave_activate(Event::WaveActivate { t: SimTime::from_millis(19), scope: PullScope { pull: PullId(0), epoch: 1 }, wave: WaveId(0) }, 19)]
772 #[case::enemy_activate(Event::EnemyActivate { t: SimTime::from_millis(20), scope: PullScope { pull: PullId(0), epoch: 1 }, target: EnemyIdx::PRIMARY }, 20)]
773 #[case::retarget(Event::Retarget { t: SimTime::from_millis(21), scope: PullScope { pull: PullId(0), epoch: 1 } }, 21)]
774 #[case::encounter_move(Event::EncounterMove { t: SimTime::from_millis(22), scope: PullScope { pull: PullId(0), epoch: 1 }, actor: PositionedActorRef::Player, transform: transform() }, 22)]
775 #[case::hook_timer(Event::HookTimer {
776 t: SimTime::from_millis(12),
777 timer_id: 3,
778 source: ActorId::Player,
779 target: Some(EnemyIdx::PRIMARY),
780 }, 12)]
781 #[case::cooldown_ready(Event::CooldownReady { t: SimTime::from_millis(8), cooldown_key: spell(30) }, 8)]
782 #[case::auto_attack(Event::AutoAttack { t: SimTime::from_millis(9), source: ActorId::Player, target: EnemyIdx::PRIMARY }, 9)]
783 fn event_timestamp_reads_every_variant(
784 #[case] event: Event,
785 #[case] expected_ms: u32,
786 ) -> Result<()> {
787 verify_that!(event.timestamp(), eq(SimTime::from_millis(expected_ms)))
788 }
789
790 #[gtest]
791 #[rstest]
792 #[case::player_ready(Event::PlayerReady { t: SimTime::ZERO }, 10)]
793 #[case::off_gcd_ready(Event::OffGcdReady { t: SimTime::ZERO }, 10)]
794 #[case::cast_start(Event::CastStart { t: SimTime::ZERO, spell_id: spell(10), empower_rank: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 10)]
795 #[case::cast_complete(Event::CastComplete { t: SimTime::ZERO, spell_id: spell(10), empower_rank: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 7)]
796 #[case::spell_launch(Event::SpellLaunch { t: SimTime::ZERO, impact_id: 6, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 7)]
797 #[case::spell_impact(Event::SpellImpact { t: SimTime::ZERO, impact_id: 7, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 8)]
798 #[case::guardian_action(Event::GuardianAction { t: SimTime::ZERO, guardian_id: 1, guardian_generation: 1, ability_index: 0, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 8)]
799 #[case::guardian_expire(Event::GuardianExpire { t: SimTime::ZERO, guardian_id: 1, guardian_generation: 1 }, 0)]
800 #[case::aura_tick(Event::AuraTick { t: SimTime::ZERO, key: key(20), target: Some(EnemyIdx::PRIMARY) }, 8)]
801 #[case::channel_tick(Event::ChannelTick { t: SimTime::ZERO, spell_id: spell(20), source: ActorId::Player, target: EnemyIdx::PRIMARY, generation: 1 }, 8)]
802 #[case::aura_expire(Event::AuraExpire { t: SimTime::ZERO, key: key(20), target: Some(EnemyIdx::PRIMARY) }, 9)]
803 #[case::death(Event::Death { t: SimTime::ZERO, target: EnemyIdx::PRIMARY, scope: DeathEventScope::Unscoped }, 0)]
804 #[case::encounter_despawn(Event::EncounterDespawn { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, target: EnemyIdx::PRIMARY, kind: EncounterDespawnKind::ForcedOptional }, 0)]
805 #[case::group_complete(Event::GroupComplete { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, group: GroupId(0) }, 1)]
806 #[case::pull_complete(Event::PullComplete { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, finalize: false }, 2)]
807 #[case::encounter_terminate(Event::EncounterTerminate { t: SimTime::ZERO, reason: EncounterTerminationReason::RequiredGroupsComplete }, 3)]
808 #[case::wave_activate(Event::WaveActivate { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, wave: WaveId(0) }, 4)]
809 #[case::enemy_activate(Event::EnemyActivate { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, target: EnemyIdx::PRIMARY }, 4)]
810 #[case::retarget(Event::Retarget { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 } }, 5)]
811 #[case::encounter_move(Event::EncounterMove { t: SimTime::ZERO, scope: PullScope { pull: PullId(0), epoch: 1 }, actor: PositionedActorRef::Player, transform: transform() }, 6)]
812 #[case::hook_timer(Event::HookTimer {
813 t: SimTime::ZERO,
814 timer_id: 3,
815 source: ActorId::Player,
816 target: Some(EnemyIdx::PRIMARY),
817 }, 8)]
818 #[case::cooldown_ready(Event::CooldownReady { t: SimTime::ZERO, cooldown_key: spell(30) }, 10)]
819 #[case::auto_attack(Event::AutoAttack { t: SimTime::ZERO, source: ActorId::Player, target: EnemyIdx::PRIMARY }, 8)]
820 fn event_priority_matches_same_timestamp_contract(
821 #[case] event: Event,
822 #[case] expected_priority: u8,
823 ) -> Result<()> {
824 verify_that!(event.priority(), eq(expected_priority))
825 }
826
827 #[gtest]
828 fn event_reference_value_types_preserve_fields() -> Result<()> {
829 let aura = AuraEventRef {
830 key: key(44),
831 target: Some(EnemyIdx(3)),
832 };
833 let scope = PullScope {
834 pull: PullId(7),
835 epoch: 11,
836 };
837
838 verify_that!(
839 aura,
840 eq(AuraEventRef {
841 key: key(44),
842 target: Some(EnemyIdx(3)),
843 })
844 )?;
845
846 verify_that!(
847 scope,
848 eq(PullScope {
849 pull: PullId(7),
850 epoch: 11,
851 })
852 )
853 }
854}