wowlab_engine_combat/handler/orchestration/
raid_events.rs1use wowlab_engine_ports::{Event, PullLifecycleGeneration, SimContext};
2use wowlab_types::{
3 constants::MS_PER_SECOND,
4 sim::{ActorId, EnemyIdx, SimTime},
5};
6
7use super::super::CombatHandler;
8use crate::context::HookCtxRequest;
9
10impl CombatHandler {
11 pub(in crate::handler) fn handle_raid_event(&mut self, index: usize, ctx: &mut SimContext) {
12 let Some(config) = self.state.config.raid_events.get(index).copied() else {
13 return;
14 };
15 let now = ctx.state.current_time;
16 let duration = SimTime::from_secs_f64(config.schedule.duration.max(0.0));
17
18 match config.event {
19 wowlab_engine_ports::RaidEventKind::Movement => {
20 self.state
21 .schedule_movement_window(ActorId::Player, now, duration);
22 }
23 wowlab_engine_ports::RaidEventKind::Stun => {
24 let _ = self.state.apply_crowd_control(
25 ActorId::Player,
26 crate::state::CrowdControlKind::Stun,
27 crate::state::DiminishingGroup::None,
28 duration,
29 now,
30 );
31 }
32 wowlab_engine_ports::RaidEventKind::PlayerDamage { amount } => {
33 let _ = crate::systems::deal_incoming_damage(
34 &mut self.state,
35 &mut self.buf,
36 crate::systems::IncomingDamage {
37 source: ActorId::External,
38 target: ActorId::Player,
39 amount,
40 school: wowlab_types::combat::DamageSchool::Physical,
41 mechanic_mask: 0,
42 is_aoe: true,
43 is_periodic: false,
44 at: now,
45 },
46 );
47 }
48 wowlab_engine_ports::RaidEventKind::Invulnerable { target } => {
49 if let Ok(target) = u16::try_from(target) {
50 self.state.grant_enemy_invulnerability(
51 EnemyIdx(target),
52 now.saturating_add(duration),
53 );
54 }
55 }
56 wowlab_engine_ports::RaidEventKind::Adds { target } => {
57 self.handle_raid_add_event(target, duration, ctx);
58 }
59 wowlab_engine_ports::RaidEventKind::EnemyCasting {
60 target,
61 spell_id,
62 school_mask,
63 } => {
64 if let Ok(target) = u16::try_from(target) {
65 self.state.begin_actor_cast(
66 ActorId::Enemy(EnemyIdx(target)),
67 wowlab_types::sim::SpellIdx::from_raw(spell_id),
68 wowlab_engine_domain::dbc::SpellSchoolMask::from_dbc(school_mask),
69 now.saturating_add(duration),
70 true,
71 );
72 }
73 }
74 wowlab_engine_ports::RaidEventKind::Interrupt { lockout_ms } => {
75 let _ = crate::systems::interrupt_cast(
76 &mut self.state,
77 &mut self.buf,
78 ActorId::Player,
79 now,
80 lockout_ms,
81 );
82 }
83 _ => {}
84 }
85 }
86
87 pub(in crate::handler) fn handle_external_buff(&mut self, index: usize, ctx: &mut SimContext) {
88 let Some(config) = self.state.config.external_buffs.get(index).copied() else {
89 return;
90 };
91 let Some(local) = self.state.aura_local(config.spell_id) else {
92 return;
93 };
94 let duration_ms = wowlab_types::numeric::f64_to_u32_saturating_round(
95 config.schedule.duration.max(0.0) * MS_PER_SECOND,
96 );
97
98 self.with_hook_ctx(
99 &mut *ctx.telemetry,
100 HookCtxRequest::targetless(ctx.state.current_time).with_source(ActorId::External),
101 |hook| crate::systems::apply_aura_with_duration(hook, local, Some(duration_ms)),
102 );
103 }
104
105 pub(in crate::handler) fn handle_raid_add_despawn(
106 &mut self,
107 target: EnemyIdx,
108 activation_generation: u64,
109 encounter_generation: PullLifecycleGeneration,
110 ctx: &mut SimContext,
111 ) {
112 if !self
113 .state
114 .runtime
115 .encounter
116 .is_current_generation(encounter_generation)
117 || !self.state.enemy(target).is_some_and(|enemy| {
118 enemy.is_active() && enemy.activation_generation() == activation_generation
119 })
120 {
121 return;
122 }
123
124 let _ = self.on_despawn_transition(target, ctx);
125 }
126
127 pub(super) fn handle_raid_add_event(
128 &mut self,
129 raw_target: usize,
130 duration: SimTime,
131 ctx: &mut SimContext,
132 ) {
133 let Ok(target) = u16::try_from(raw_target) else {
134 return;
135 };
136
137 let target = EnemyIdx(target);
138
139 let previous_generation = self
140 .state
141 .enemy(target)
142 .map(wowlab_engine_domain::encounter::EnemyState::activation_generation);
143
144 self.handle_enemy_activation(
145 self.state.runtime.encounter.active_pull(),
146 self.state.runtime.encounter.pull_epoch(),
147 target,
148 ctx,
149 );
150
151 if duration <= SimTime::ZERO {
152 return;
153 }
154
155 let Some(activation_generation) = self
156 .state
157 .enemy(target)
158 .filter(|enemy| enemy.is_active())
159 .map(wowlab_engine_domain::encounter::EnemyState::activation_generation)
160 .filter(|generation| Some(*generation) != previous_generation)
161 else {
162 return;
163 };
164
165 self.state.schedule(Event::RaidAddDespawn {
166 t: ctx.state.current_time.saturating_add(duration),
167 target,
168 activation_generation,
169 encounter_generation: self.state.runtime.encounter.generation(),
170 });
171 }
172}