Skip to main content

wowlab_engine_application/
intent_builder.rs

1//! Typestate builder for `SimRequest`, exposing request-shape variants for benchmark-style callers.
2
3use wowlab_engine_ports::ChunkAssignment;
4
5/// Canonical request envelope for [`crate::simulate_intent::simulate_intent_request`].
6#[derive(Clone, Debug)]
7pub struct SimRequest<'a> {
8    pub sim_config: &'a str,
9    pub chunk: &'a ChunkAssignment,
10    /// Master seed; combined with `chunk.seed_offset` at runtime.
11    pub seed_base: u64,
12    pub overrides: IntentOverrides,
13}
14
15impl<'a> SimRequest<'a> {
16    /// Starts a request builder.
17    #[must_use]
18    pub fn builder(sim_config: &'a str, chunk: &'a ChunkAssignment) -> IntentBuilder<'a> {
19        IntentBuilder {
20            request: Self {
21                sim_config,
22                chunk,
23                seed_base: 0,
24                overrides: IntentOverrides::default(),
25            },
26        }
27    }
28}
29
30/// Toggles that alter how `bootstrap` resolves stats and buffs.
31#[derive(Clone, Debug, Default)]
32#[non_exhaustive]
33pub struct IntentOverrides {
34    pub default_stats: bool,
35    pub no_buffs: bool,
36    /// Register one item's effects without rolling its stats into `CombatStats`; `None` = no extra item.
37    pub extra_item_id: Option<u32>,
38    /// Skip fetching and compiling the rotation, building with an empty rotation engine instead; used when only rotation-independent state is needed (paperdoll).
39    pub skip_rotation: bool,
40    /// Resolve the rotation script under this id instead of `config.rotation_id`; `None` = use the config's id.
41    pub rotation_id_override: Option<String>,
42}
43
44impl IntentOverrides {
45    /// Default overrides that resolve the rotation under `rotation_id` instead of the config's id.
46    #[must_use]
47    pub fn with_rotation_id_override(rotation_id: impl Into<String>) -> Self {
48        Self {
49            rotation_id_override: Some(rotation_id.into()),
50            ..Self::default()
51        }
52    }
53}
54
55/// Fluent builder for [`SimRequest`].
56#[derive(Clone, Debug)]
57#[non_exhaustive]
58pub struct IntentBuilder<'a> {
59    request: SimRequest<'a>,
60}
61
62impl<'a> IntentBuilder<'a> {
63    /// Set the master seed.
64    #[must_use]
65    pub fn seed_base(mut self, seed_base: u64) -> Self {
66        self.request.seed_base = seed_base;
67
68        self
69    }
70
71    /// Skip raid buffs / racial / weapon-enchant procs.
72    #[must_use]
73    pub fn no_buffs(mut self) -> Self {
74        self.request.overrides.no_buffs = true;
75
76        self
77    }
78
79    /// Bypass gear-derived stats; use [`wowlab_engine_domain::stats::default_stats`].
80    #[must_use]
81    pub fn default_stats(mut self) -> Self {
82        self.request.overrides.default_stats = true;
83
84        self
85    }
86
87    /// Register one item's effects (slotted as `Trinket1`) without rolling in its stats.
88    #[must_use]
89    pub fn item(mut self, item_id: u32) -> Self {
90        self.request.overrides.extra_item_id = Some(item_id);
91
92        self
93    }
94
95    /// Consume the builder and return the request.
96    #[must_use]
97    pub fn build(self) -> SimRequest<'a> {
98        self.request
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use googletest::prelude::*;
105
106    use super::*;
107
108    fn make_chunk() -> ChunkAssignment {
109        ChunkAssignment::single("test", 10)
110    }
111
112    #[gtest]
113    fn defaults_have_no_overrides() -> Result<()> {
114        let chunk = make_chunk();
115        let req = SimRequest::builder("config", &chunk).build();
116
117        verify_true!(!req.overrides.default_stats)?;
118        verify_true!(!req.overrides.no_buffs)?;
119        verify_that!(req.overrides.extra_item_id, eq(None))?;
120        verify_that!(req.seed_base, eq(0))?;
121
122        Ok(())
123    }
124
125    #[gtest]
126    fn fluent_setters_compose() -> Result<()> {
127        let chunk = make_chunk();
128        let req = SimRequest::builder("config", &chunk)
129            .seed_base(42)
130            .no_buffs()
131            .default_stats()
132            .item(231_265)
133            .build();
134
135        verify_that!(req.seed_base, eq(42))?;
136        verify_true!(req.overrides.no_buffs)?;
137        verify_true!(req.overrides.default_stats)?;
138        verify_that!(req.overrides.extra_item_id, eq(Some(231_265)))?;
139
140        Ok(())
141    }
142}