wowlab_engine_content/hooks/balance_druid/
handler.rs1
2use wowlab_engine_combat::{AuraOps as _, CombatHandler, apply_aura, extend_aura};
3use wowlab_engine_event_authority::EngineScheduledSpellAccess as _;
4use wowlab_engine_ports::{HandlerParams, SimContext, SpecAction};
5
6#[cfg(test)]
7use wowlab_types::sim::SpellIdx;
8
9use crate::generated::specs::balance_druid::{AURA_STELLAR_AMPLIFICATION, SPELL};
10
11use super::config::register_balance;
12
13#[derive(Debug)]
14pub(crate) struct BalanceHandler {
15 inner: CombatHandler,
16 stellar_amp_cap_ms: u32,
17}
18
19impl BalanceHandler {
20 pub(crate) fn new(
21 parts: crate::composition::ContentHandlerParts,
22 params: &HandlerParams<'_>,
23 ) -> Result<Self, wowlab_engine_ports::EngineError> {
24 let mut parts = parts;
25 let built = parts.draft_mut();
26 let cfg = register_balance(built, params);
27
28 let built = parts.finalize()?;
29
30 Ok(Self {
31 inner: CombatHandler::from_built(built),
32 stellar_amp_cap_ms: cfg.stellar_amp_cap_ms,
33 })
34 }
35
36 fn trigger_stellar_amplification(
38 &mut self,
39 target: wowlab_types::sim::EnemyIdx,
40 ctx: &mut SimContext,
41 ) {
42 if self.stellar_amp_cap_ms == 0 {
43 return;
44 }
45
46 let now = ctx.state.current_time;
47 let def = *self.inner.program().aura(AURA_STELLAR_AMPLIFICATION);
48 let cap_ms = self.stellar_amp_cap_ms;
49
50 self.inner
51 .with_hook_ctx(ctx.telemetry, wowlab_engine_combat::HookCtxRequest::for_target(now, target), |aura_ctx| {
52 let remaining_ms = aura_ctx
53 .aura_remaining_ms(AURA_STELLAR_AMPLIFICATION)
54 .unwrap_or(0);
55
56 if remaining_ms == 0 {
57 apply_aura(aura_ctx, AURA_STELLAR_AMPLIFICATION);
58 } else {
59 let ext = def
60 .base_duration_ms
61 .min(cap_ms.saturating_sub(remaining_ms));
62
63 if ext > 0 {
64 extend_aura(aura_ctx, AURA_STELLAR_AMPLIFICATION, ext);
65 }
66 }
67 });
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use googletest::prelude::*;
74
75 use super::*;
76 use wowlab_engine_combat::{HookCtx, apply_aura_with_duration};
77 use wowlab_engine_ports::{CombatStats, ConsumableFlags, SimState, SpecHandler};
78 use wowlab_engine_telemetry::TelemetrySink;
79
80 use wowlab_types::{
81 constants::MS_PER_SECOND,
82 game::RaceId,
83 sim::{ActorId, AuraIdx, AuraKey, EnemyIdx, SimTime},
84 };
85
86 const BASE_DURATION_MS: u32 = 5_000;
87 const SECOND_DURATION_MS: u32 = 2_000;
88 const EXTENSION_CAP_MS: u32 = 20_000;
89
90 fn build_handler() -> Result<BalanceHandler> {
91 let stats = CombatStats::default();
92 let encounter = crate::hooks::shared::test_support::two_target_completion_encounter()?;
93 let game_data = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
94 let rotation = crate::test_support::wait_rotation();
95 let params = crate::test_support::handler_params(
96 crate::test_support::HandlerParamsFixture {
97 game_data,
98 rotation: &rotation,
99 stats: &stats,
100 talent_selections: &[],
101 encounter: &encounter,
102 consumables: ConsumableFlags::default(),
103 race: RaceId::Human,
104 },
105 )
106 .or_fail()?;
107
108 crate::composition::compose_handler(
109 crate::generated::specs::balance_druid::build_combat_system_draft,
110 ¶ms,
111 |parts, _| {
112 let mut parts = parts;
113
114 {
115 let draft = parts.draft_mut();
116
117 if !draft.patch_aura(AURA_STELLAR_AMPLIFICATION, |aura| {
118 aura.base_duration_ms = BASE_DURATION_MS;
119 }) {
120 return Err(wowlab_engine_ports::EngineError::spec_construction(
121 "missing Stellar Amplification fixture aura",
122 ));
123 }
124 }
125 let built = parts.finalize()?;
126
127 Ok(BalanceHandler {
128 inner: CombatHandler::from_built(built),
129 stellar_amp_cap_ms: EXTENSION_CAP_MS,
130 })
131 },
132 )
133 .or_fail()
134 }
135
136 #[gtest]
137 fn starsurge_completion_extends_only_its_locked_target_after_retarget() -> Result<()> {
138 let mut handler = build_handler()?;
139 let first = EnemyIdx::PRIMARY;
140 let second = EnemyIdx(1);
141 let mut sim_state = SimState::new(0, SimTime::from_secs_f64(300.0), 1);
142 let mut sink = TelemetrySink::default();
143
144 handler.on_sim_start(&mut SimContext {
145 state: &sim_state,
146 telemetry: &mut sink,
147 });
148
149 let (state, buf) = handler.inner.program_and_instance_mut();
150 let def = *state.aura(AURA_STELLAR_AMPLIFICATION);
151 let mut rng = || 0.5;
152
153 apply_aura(
154 &mut HookCtx::new(wowlab_engine_combat::HookCtxServices { program: state, instance: buf, sink: &mut sink, rng: &mut rng }, wowlab_engine_combat::HookCtxRequest::for_target(SimTime::ZERO, first)),
155 AURA_STELLAR_AMPLIFICATION,
156 );
157 apply_aura_with_duration(
158 &mut HookCtx::new(wowlab_engine_combat::HookCtxServices { program: state, instance: buf, sink: &mut sink, rng: &mut rng }, wowlab_engine_combat::HookCtxRequest::for_target(SimTime::ZERO, second)),
159 AURA_STELLAR_AMPLIFICATION,
160 Some(SECOND_DURATION_MS),
161 );
162 let first_key = AuraKey::new(
163 AuraIdx(def.aura_id),
164 ActorId::Player,
165 ActorId::Enemy(first),
166 def.on,
167 );
168 let second_key = AuraKey::new(
169 AuraIdx(def.aura_id),
170 ActorId::Player,
171 ActorId::Enemy(second),
172 def.on,
173 );
174 let first_before = buf.buffer.aura(first_key).or_fail()?.expires_at;
175 let second_before = buf.buffer.aura(second_key).or_fail()?.expires_at;
176
177 verify_that!(first_before, not(eq(second_before)))?;
178
179 handler.inner.instance_mut().runtime.current_target = Some(second);
180 sim_state.current_time = SimTime::from_millis(1_000);
181 handler.on_cast_complete(
182 wowlab_engine_ports::Event::CastComplete {
183 t: sim_state.current_time,
184 spell: handler.inner.__test_scheduled_spell(SpellIdx::from_raw(SPELL::STARSURGE)),
185 empower_rank: 0,
186 source: ActorId::Player,
187 target: first,
188 },
189 &mut SimContext {
190 state: &sim_state,
191 telemetry: &mut sink,
192 },
193 );
194
195 let (_, buf) = handler.inner.program_and_instance_mut();
196
197 verify_that!(
198 buf.buffer.aura(first_key).or_fail()?.expires_at,
199 eq(first_before + f64::from(BASE_DURATION_MS) / MS_PER_SECOND)
200 )?;
201
202 verify_that!(buf.buffer.aura(second_key).or_fail()?.expires_at, eq(second_before))
203 }
204}
205
206crate::hooks::define_spec_handler! {
207impl BalanceHandler(self, inner) {
208 fn on_sim_start(&mut self, ctx: &mut SimContext) {
209 self.inner.on_sim_start(ctx);
210 }
211
212 fn on_cast_complete(&mut self, event: wowlab_engine_ports::Event, ctx: &mut SimContext) {
213 let wowlab_engine_ports::Event::CastComplete {
214 spell, target, ..
215 } = event
216 else {
217 panic!("on_cast_complete received a non-cast-complete event");
218 };
219 self.inner.on_cast_complete(event, ctx);
220 if spell.engine_coordinates().as_u32() == SPELL::STARSURGE {
221 self.trigger_stellar_amplification(target, ctx);
222 }
223 }
224
225 fn on_spell_impact(&mut self, impact_id: wowlab_engine_ports::PendingImpactBinding, ctx: &mut SimContext) {
226 self.inner.on_spell_impact(impact_id, ctx);
227 }
228
229}
230delegate {
231 fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction>;
232 fn on_aura_tick(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
233 fn on_aura_expire(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
234 fn on_auto_attack(&mut self, source: wowlab_types::sim::ActorId, target: wowlab_types::sim::EnemyIdx, ctx: &mut SimContext);
235 fn on_cooldown_ready(&mut self, spell: wowlab_engine_ports::ScheduledSpellBinding, ctx: &mut SimContext);
236 fn flush_scheduled(&mut self, push: &mut dyn FnMut(wowlab_engine_ports::Event));
237 fn total_damage(&self) -> f64;
238 fn cast_time_ms(&self, spell: wowlab_engine_ports::ScheduledSpellBinding, empower_rank: u8) -> u32;
239 fn introspect(&self) -> wowlab_types::game::SpecIntrospection;
240 fn paperdoll(&self) -> Option<wowlab_engine_ports::Paperdoll>;
241 fn attach_decision_trace(
242 &mut self,
243 sink: std::sync::Arc<dyn wowlab_engine_ports::DecisionTraceSink>,
244 );
245}
246}