Skip to main content

wowlab_engine_combat/context/
combat_ctx.rs

1//! Read-only combat views and the mutable combat-operation context.
2
3use wowlab_engine_telemetry::TelemetrySink;
4use wowlab_types::sim::{ActorId, EnemyIdx, SimTime};
5
6use super::{HookCtx, HookCtxRequest, HookCtxServices};
7use crate::{
8    state::{BuffTotals, CombatInstance, CombatProgram},
9    systems,
10};
11
12/// Read-only combat state and dense-buffer projection.
13#[derive(Debug)]
14#[must_use]
15pub struct CombatView<'a> {
16    pub(crate) program: &'a CombatProgram,
17    pub(crate) instance: &'a CombatInstance,
18}
19
20impl<'a> CombatView<'a> {
21    pub fn new(program: &'a CombatProgram, instance: &'a CombatInstance) -> Self {
22        Self { program, instance }
23    }
24
25    pub const fn for_actor(&self, actor: ActorId) -> ActorView<'a> {
26        ActorView {
27            program: self.program,
28            instance: self.instance,
29            actor,
30        }
31    }
32
33    pub const fn for_hook(&self, now: SimTime, target: Option<EnemyIdx>) -> HookView<'a> {
34        HookView {
35            program: self.program,
36            instance: self.instance,
37            target,
38            now,
39        }
40    }
41
42    /// Current primary-resource fraction, or `1.0` when the spec has no bounded pool.
43    #[must_use]
44    pub fn primary_resource_fraction(&self) -> f64 {
45        self.program
46            .config
47            .base_stats
48            .resource_type
49            .and_then(|resource_type| self.program.buffer_resource(self.instance, resource_type))
50            .map_or(1.0, |resource| {
51                if resource.max > 0.0 {
52                    resource.current / resource.max
53                } else {
54                    1.0
55                }
56            })
57    }
58
59    /// Remaining duration of one player aura at an explicit combat time.
60    #[must_use]
61    pub fn player_aura_remaining_ms_at(
62        &self,
63        local: crate::LocalAuraIdx,
64        now: SimTime,
65    ) -> Option<u32> {
66        systems::aura_remaining_ms(
67            self.program,
68            self.instance,
69            local,
70            ActorId::Player,
71            None,
72            now,
73        )
74    }
75
76    /// Whether the player currently owns this aura on a hostile target.
77    #[must_use]
78    pub fn is_player_aura_active_on_target(
79        &self,
80        local: crate::LocalAuraIdx,
81        target: EnemyIdx,
82    ) -> bool {
83        systems::is_aura_active(self, local, ActorId::Player, Some(target))
84    }
85
86    pub(crate) fn buff_totals(&self) -> BuffTotals {
87        systems::buff_totals(self.program, self.instance, None)
88    }
89}
90
91/// Read-only combat projection scoped to one actor.
92#[derive(Clone, Copy, Debug)]
93#[must_use]
94pub struct ActorView<'a> {
95    pub(crate) program: &'a CombatProgram,
96    pub(crate) instance: &'a CombatInstance,
97    pub(crate) actor: ActorId,
98}
99
100impl<'a> ActorView<'a> {
101    pub const fn new(
102        program: &'a CombatProgram,
103        instance: &'a CombatInstance,
104        actor: ActorId,
105    ) -> Self {
106        Self {
107            program,
108            instance,
109            actor,
110        }
111    }
112
113    pub fn combat(self) -> CombatView<'a> {
114        CombatView::new(self.program, self.instance)
115    }
116}
117
118/// Read-only combat projection scoped to a hook target and simulation time.
119#[derive(Clone, Copy, Debug)]
120#[must_use]
121pub struct HookView<'a> {
122    pub(crate) program: &'a CombatProgram,
123    pub(crate) instance: &'a CombatInstance,
124    pub target: Option<EnemyIdx>,
125    pub now: SimTime,
126}
127
128/// Focused immutable program metadata exposed to runtime content hooks.
129#[derive(Clone, Copy, Debug)]
130pub struct HookProgramView<'a> {
131    pub(super) program: &'a CombatProgram,
132}
133
134impl<'a> HookProgramView<'a> {
135    pub(crate) const fn new(program: &'a CombatProgram) -> Self {
136        Self { program }
137    }
138
139    #[must_use]
140    pub fn spec_config<T>(self) -> Option<&'a T>
141    where
142        T: wowlab_engine_ports::SpecConfig,
143    {
144        self.program.spec_config::<T>()
145    }
146
147    #[must_use]
148    pub fn base_stats(self) -> &'a crate::state::BaseStats {
149        self.program.base_stats()
150    }
151
152    #[must_use]
153    pub fn encounter(self) -> &'a wowlab_engine_ports::ResolvedEncounter {
154        self.program.encounter()
155    }
156
157    /// Read one finalized spell definition through its typed local coordinate.
158    pub fn spell(self, local: crate::state::LocalSpellIdx) -> &'a crate::state::SpellData {
159        self.program.spell(local)
160    }
161
162    /// Read one finalized aura definition through its typed local coordinate.
163    pub fn aura(self, local: crate::state::LocalAuraIdx) -> &'a crate::state::AuraData {
164        self.program.aura(local)
165    }
166
167    /// Resolve a sealed semantic aura binding through the program's frozen dense table.
168    #[must_use]
169    pub fn bound_aura(self, binding: crate::AuraBinding) -> Option<crate::LocalAuraIdx> {
170        self.program
171            .aura_bindings
172            .get(binding.as_usize())
173            .copied()
174            .flatten()
175    }
176
177    /// Resolve a sealed semantic spell binding through the program's frozen dense table.
178    #[must_use]
179    pub fn bound_spell(self, binding: crate::SpellBinding) -> Option<crate::LocalSpellIdx> {
180        self.program
181            .spell_bindings
182            .get(binding.as_usize())
183            .copied()
184            .flatten()
185    }
186
187    /// Resolve a sealed semantic RPPM binding through the program's frozen dense table.
188    #[must_use]
189    pub fn bound_rppm(self, binding: crate::RppmBinding) -> Option<crate::LocalRppmIdx> {
190        self.program
191            .rppm_bindings
192            .get(binding.as_usize())
193            .copied()
194            .flatten()
195    }
196
197    /// Test-only finalized aura catalog introspection.
198    #[cfg(feature = "test-support")]
199    pub fn auras(self) -> &'a [crate::state::AuraData] {
200        self.program.auras()
201    }
202
203    /// Test-only finalized precombat aura coordinates.
204    #[cfg(feature = "test-support")]
205    pub fn precombat_auras(self) -> &'a [crate::state::LocalAuraIdx] {
206        self.program.precombat_auras()
207    }
208}
209
210impl<'a> HookView<'a> {
211    #[must_use]
212    pub const fn program(self) -> HookProgramView<'a> {
213        HookProgramView::new(self.program)
214    }
215
216    #[must_use]
217    pub const fn instance(self) -> &'a CombatInstance {
218        self.instance
219    }
220
221    pub fn combat(self) -> CombatView<'a> {
222        CombatView::new(self.program, self.instance)
223    }
224
225    pub fn for_actor(self, actor: ActorId) -> ActorView<'a> {
226        self.combat().for_actor(actor)
227    }
228
229    #[must_use]
230    pub fn last_secondary_spent(self) -> f64 {
231        self.instance.runtime.resources.last_secondary_spent
232    }
233
234    /// Exact target-health fraction for this hook's locked target.
235    #[must_use]
236    pub fn target_health_fraction(self) -> f64 {
237        self.target
238            .and_then(|target| {
239                self.instance
240                    .enemy_health_fraction(self.program, target, self.now)
241            })
242            .unwrap_or(1.0)
243    }
244}
245
246/// Short-lived borrow bundle for combat operations.
247#[must_use]
248// #t(rust_similar_structs) explicit test-support visibility mirrors the same fields without changing the production surface.
249pub struct CombatCtx<'a> {
250    #[cfg(feature = "test-support")]
251    #[doc(hidden)]
252    pub program: &'a CombatProgram,
253    #[cfg(not(feature = "test-support"))]
254    pub(crate) program: &'a CombatProgram,
255    #[cfg(feature = "test-support")]
256    #[doc(hidden)]
257    pub instance: &'a mut CombatInstance,
258    #[cfg(not(feature = "test-support"))]
259    pub(crate) instance: &'a mut CombatInstance,
260    #[cfg(feature = "test-support")]
261    #[doc(hidden)]
262    pub sink: &'a mut TelemetrySink,
263    #[cfg(not(feature = "test-support"))]
264    pub(crate) sink: &'a mut TelemetrySink,
265    #[cfg(feature = "test-support")]
266    #[doc(hidden)]
267    pub now: SimTime,
268    #[cfg(not(feature = "test-support"))]
269    pub(crate) now: SimTime,
270    #[cfg(feature = "test-support")]
271    #[doc(hidden)]
272    pub rng: &'a mut dyn FnMut() -> f64,
273    #[cfg(not(feature = "test-support"))]
274    pub(crate) rng: &'a mut dyn FnMut() -> f64,
275    #[cfg(feature = "test-support")]
276    #[doc(hidden)]
277    pub source: ActorId,
278    #[cfg(not(feature = "test-support"))]
279    pub(crate) source: ActorId,
280    #[cfg(feature = "test-support")]
281    #[doc(hidden)]
282    pub target: EnemyIdx,
283    #[cfg(not(feature = "test-support"))]
284    pub(crate) target: EnemyIdx,
285}
286
287/// Coordinates for constructing a combat context from an owning runtime.
288#[derive(Clone, Copy, Debug, Eq, PartialEq)]
289#[must_use]
290pub struct CombatCtxRequest {
291    pub now: SimTime,
292    pub source: ActorId,
293    pub target: EnemyIdx,
294}
295
296impl CombatCtxRequest {
297    pub const fn new(now: SimTime, source: ActorId, target: EnemyIdx) -> Self {
298        Self {
299            now,
300            source,
301            target,
302        }
303    }
304}
305
306impl CombatCtx<'_> {
307    /// Runs `operation` with a combat context only when a concrete target exists.
308    ///
309    /// The callback keeps the targetless branch explicit and never fabricates an [`EnemyIdx`].
310    pub fn for_optional_target<'ctx, R>(
311        hook: &'ctx mut HookCtx<'_>,
312        target: Option<EnemyIdx>,
313        operation: impl FnOnce(&mut CombatCtx<'ctx>) -> R,
314    ) -> Option<R> {
315        let target = target?;
316        let mut ctx = CombatCtx {
317            program: hook.program,
318            instance: &mut *hook.instance,
319            sink: &mut *hook.sink,
320            now: hook.now,
321            rng: &mut *hook.rng,
322            source: hook.source,
323            target,
324        };
325
326        Some(operation(&mut ctx))
327    }
328
329    pub fn view(&self) -> CombatView<'_> {
330        CombatView::new(self.program, self.instance)
331    }
332
333    pub(crate) fn hook_ctx(&mut self) -> HookCtx<'_> {
334        self.hook_ctx_with_source(self.source)
335    }
336
337    pub(crate) fn hook_ctx_with_source(&mut self, source: ActorId) -> HookCtx<'_> {
338        HookCtx::new(
339            HookCtxServices {
340                program: self.program,
341                instance: self.instance,
342                sink: self.sink,
343                rng: self.rng,
344            },
345            HookCtxRequest::for_target(self.now, self.target).with_source(source),
346        )
347    }
348}
349
350impl std::fmt::Debug for CombatCtx<'_> {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        f.debug_struct("CombatCtx")
353            .field("now", &self.now)
354            .field("source", &self.source)
355            .field("target", &self.target)
356            .finish_non_exhaustive()
357    }
358}