Skip to main content

wowlab_engine_combat/context/
combat_ctx.rs

1//! Read-only combat views and the mutable combat-operation context.
2
3#[cfg(test)]
4use std::cell::Cell;
5use std::cell::OnceCell;
6
7use wowlab_engine_domain::rotation::DenseBuffer;
8use wowlab_engine_telemetry::TelemetrySink;
9use wowlab_types::sim::{ActorId, EnemyIdx, SimTime};
10
11use super::{HookCtx, HookCtxRequest, HookCtxServices};
12use crate::{
13    state::{BuffTotals, CombatState},
14    systems,
15};
16
17/// Read-only combat state and dense-buffer projection.
18#[derive(Debug)]
19#[must_use]
20pub struct CombatView<'a> {
21    pub state: &'a CombatState,
22    pub buf: &'a DenseBuffer,
23    buff_totals: OnceCell<BuffTotals>,
24    #[cfg(test)]
25    buff_traversals: Cell<u32>,
26}
27
28impl<'a> CombatView<'a> {
29    pub fn new(state: &'a CombatState, buf: &'a DenseBuffer) -> Self {
30        Self {
31            state,
32            buf,
33            buff_totals: OnceCell::new(),
34            #[cfg(test)]
35            buff_traversals: Cell::new(0),
36        }
37    }
38
39    pub const fn for_actor(&self, actor: ActorId) -> ActorView<'a> {
40        ActorView {
41            state: self.state,
42            buf: self.buf,
43            actor,
44        }
45    }
46
47    pub const fn for_hook(&self, now: SimTime, target: Option<EnemyIdx>) -> HookView<'a> {
48        HookView {
49            state: self.state,
50            buf: self.buf,
51            target,
52            now,
53        }
54    }
55
56    pub(crate) fn buff_totals(&self) -> &BuffTotals {
57        self.buff_totals.get_or_init(|| {
58            #[cfg(test)]
59            self.buff_traversals
60                .set(self.buff_traversals.get().saturating_add(1));
61
62            systems::accumulate_buffs(self.state, self.buf, None)
63        })
64    }
65
66    #[cfg(test)]
67    pub(crate) fn buff_traversals(&self) -> u32 {
68        self.buff_traversals.get()
69    }
70}
71
72/// Read-only combat projection scoped to one actor.
73#[derive(Clone, Copy, Debug)]
74#[must_use]
75pub struct ActorView<'a> {
76    pub state: &'a CombatState,
77    pub buf: &'a DenseBuffer,
78    pub actor: ActorId,
79}
80
81impl<'a> ActorView<'a> {
82    pub const fn new(state: &'a CombatState, buf: &'a DenseBuffer, actor: ActorId) -> Self {
83        Self { state, buf, actor }
84    }
85
86    pub fn combat(self) -> CombatView<'a> {
87        CombatView::new(self.state, self.buf)
88    }
89}
90
91/// Read-only combat projection scoped to a hook target and simulation time.
92#[derive(Clone, Copy, Debug)]
93#[must_use]
94pub struct HookView<'a> {
95    pub state: &'a CombatState,
96    pub buf: &'a DenseBuffer,
97    pub target: Option<EnemyIdx>,
98    pub now: SimTime,
99}
100
101impl<'a> HookView<'a> {
102    pub fn combat(self) -> CombatView<'a> {
103        CombatView::new(self.state, self.buf)
104    }
105
106    pub fn for_actor(self, actor: ActorId) -> ActorView<'a> {
107        self.combat().for_actor(actor)
108    }
109
110    #[must_use]
111    pub fn last_secondary_spent(self) -> f64 {
112        self.state.runtime.resources.last_secondary_spent
113    }
114}
115
116/// Short-lived borrow bundle for combat operations.
117#[must_use]
118pub struct CombatCtx<'a> {
119    pub state: &'a mut CombatState,
120    pub buf: &'a mut DenseBuffer,
121    pub sink: &'a mut TelemetrySink,
122    pub now: SimTime,
123    pub rng: &'a mut dyn FnMut() -> f64,
124    pub source: ActorId,
125    pub target: EnemyIdx,
126}
127
128/// Coordinates for constructing a combat context from an owning runtime.
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130#[must_use]
131pub struct CombatCtxRequest {
132    pub now: SimTime,
133    pub source: ActorId,
134    pub target: EnemyIdx,
135}
136
137impl CombatCtxRequest {
138    pub const fn new(now: SimTime, source: ActorId, target: EnemyIdx) -> Self {
139        Self {
140            now,
141            source,
142            target,
143        }
144    }
145}
146
147impl CombatCtx<'_> {
148    /// Runs `operation` with a combat context only when a concrete target exists.
149    ///
150    /// The callback keeps the targetless branch explicit and never fabricates an [`EnemyIdx`].
151    pub fn for_optional_target<'ctx, R>(
152        hook: &'ctx mut HookCtx<'_>,
153        target: Option<EnemyIdx>,
154        operation: impl FnOnce(&mut CombatCtx<'ctx>) -> R,
155    ) -> Option<R> {
156        let target = target?;
157        let mut ctx = CombatCtx {
158            state: &mut *hook.state,
159            buf: &mut *hook.buf,
160            sink: &mut *hook.sink,
161            now: hook.now,
162            rng: &mut *hook.rng,
163            source: hook.source,
164            target,
165        };
166
167        Some(operation(&mut ctx))
168    }
169
170    pub fn view(&self) -> CombatView<'_> {
171        CombatView::new(self.state, self.buf)
172    }
173
174    pub(crate) fn hook_ctx(&mut self) -> HookCtx<'_> {
175        self.hook_ctx_with_source(self.source)
176    }
177
178    pub(crate) fn hook_ctx_with_source(&mut self, source: ActorId) -> HookCtx<'_> {
179        HookCtx::new(
180            HookCtxServices {
181                state: self.state,
182                buf: self.buf,
183                sink: self.sink,
184                rng: self.rng,
185            },
186            HookCtxRequest::for_target(self.now, self.target).with_source(source),
187        )
188    }
189}
190
191impl std::fmt::Debug for CombatCtx<'_> {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.debug_struct("CombatCtx")
194            .field("now", &self.now)
195            .field("source", &self.source)
196            .field("target", &self.target)
197            .finish_non_exhaustive()
198    }
199}