Skip to main content

wowlab_engine_application/iteration_trace/
mod.rs

1//! Projection of representative chunk telemetry into an iteration trace.
2
3use serde::Serialize;
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8use wowlab_engine_ports::Decision;
9use wowlab_types::{
10    constants::{HUNDRED, MS_PER_SECOND, PROTO_DPS_SCALE},
11    numeric::{u64_to_f64, usize_to_u32_saturating},
12    proto::ChunkTelemetry,
13};
14
15/// Single deterministic-seed iteration trace.
16#[derive(Clone, Debug, Serialize)]
17#[cfg_attr(feature = "wasm", derive(Tsify))]
18#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
19#[serde(rename_all = "camelCase")]
20pub struct IterationTrace {
21    pub duration_ms: u32,
22    pub seed: u64,
23    pub estimated_dps: f64,
24    pub total_casts: u64,
25    pub gcd_utilization_pct: f64,
26    pub decisions: Vec<Decision>,
27    pub resource_samples: Vec<ResourceSample>,
28    pub dps_samples: Vec<DpsSample>,
29    pub aura_events: Vec<AuraEvent>,
30    pub action_stats: Vec<ActionStat>,
31}
32
33/// Per-spell cast and damage stats.
34#[derive(Clone, Debug, Serialize)]
35#[cfg_attr(feature = "wasm", derive(Tsify))]
36#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
37#[serde(rename_all = "camelCase")]
38pub struct ActionStat {
39    pub spell_id: u32,
40    pub target_id: u32,
41    pub source_kind: i32,
42    pub source_id: u32,
43    pub pull_id: Option<u32>,
44    pub group_id: Option<u32>,
45    pub casts: u64,
46    pub direct_hits: u64,
47    pub crits: u64,
48    pub total_damage: f64,
49}
50
51/// One 100ms-bucketed resource sample.
52#[derive(Clone, Debug, Serialize)]
53#[cfg_attr(feature = "wasm", derive(Tsify))]
54#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
55#[serde(rename_all = "camelCase")]
56pub struct ResourceSample {
57    pub time_ms: u32,
58    pub resource_name: String,
59    pub current: f64,
60    pub max: f64,
61    pub gain: f64,
62    pub loss: f64,
63}
64
65/// One per-bucket DPS sample for the preview damage-over-time chart.
66#[derive(Clone, Debug, Serialize)]
67#[cfg_attr(feature = "wasm", derive(Tsify))]
68#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
69#[serde(rename_all = "camelCase")]
70pub struct DpsSample {
71    pub time_ms: u32,
72    pub dps: f64,
73}
74
75/// One aura interval; `end_ms == None` for open-ended intervals.
76#[derive(Clone, Debug, Serialize)]
77#[cfg_attr(feature = "wasm", derive(Tsify))]
78#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
79#[serde(rename_all = "camelCase")]
80pub struct AuraEvent {
81    pub start_ms: u32,
82    pub end_ms: Option<u32>,
83    pub aura_slug: String,
84    pub source: String,
85    pub target_id: u32,
86    pub source_kind: i32,
87    pub source_id: u32,
88    pub affected_kind: i32,
89    pub affected_id: u32,
90    pub pull_id: Option<u32>,
91    pub group_id: Option<u32>,
92}
93
94/// Projects one decoded chunk and its decisions into the host-independent trace view.
95#[must_use]
96pub fn project_iteration_trace(
97    seed: u64,
98    telemetry: ChunkTelemetry,
99    decisions: Vec<Decision>,
100) -> IterationTrace {
101    let duration_ms = u32::try_from(telemetry.total_fight_time_ms).unwrap_or(u32::MAX);
102    let estimated_dps = telemetry.mean_dps();
103    let total_casts = telemetry.actions.iter().map(|action| action.casts).sum();
104    let gcd_utilization_pct = if duration_ms == 0 {
105        0.0
106    } else {
107        telemetry.execution.as_ref().map_or(0.0, |execution| {
108            u64_to_f64(execution.gcd_locked_time_ms) / f64::from(duration_ms) * HUNDRED
109        })
110    };
111
112    let bucket_ms = telemetry.bucket_ms.max(1);
113    let bucket_secs = f64::from(bucket_ms) / MS_PER_SECOND;
114    let dps_samples = telemetry
115        .dps_bucket_sums_x10
116        .into_iter()
117        .enumerate()
118        .map(|(index, sum_x10)| DpsSample {
119            time_ms: usize_to_u32_saturating(index).saturating_mul(bucket_ms),
120            dps: u64_to_f64(sum_x10) / PROTO_DPS_SCALE / bucket_secs,
121        })
122        .collect();
123
124    let (resource_samples, aura_events) = telemetry.representative.map_or_else(
125        || (Vec::new(), Vec::new()),
126        |snapshot| {
127            let resource_samples = snapshot
128                .resource_samples
129                .into_iter()
130                .map(|sample| ResourceSample {
131                    time_ms: sample.time_ms,
132                    resource_name: sample.resource_name,
133                    current: f64::from(sample.current),
134                    max: f64::from(sample.max),
135                    gain: f64::from(sample.gain),
136                    loss: f64::from(sample.loss),
137                })
138                .collect();
139            let aura_events = snapshot
140                .aura_intervals
141                .into_iter()
142                .map(|interval| AuraEvent {
143                    start_ms: interval.start_ms,
144                    end_ms: (!interval.open_ended).then_some(interval.end_ms),
145                    aura_slug: interval.aura_slug,
146                    source: interval.source,
147                    target_id: interval.target_id,
148                    source_kind: interval.source_kind,
149                    source_id: interval.source_id,
150                    affected_kind: interval.affected_kind,
151                    affected_id: interval.affected_id,
152                    pull_id: interval.pull_id,
153                    group_id: interval.group_id,
154                })
155                .collect();
156
157            (resource_samples, aura_events)
158        },
159    );
160
161    let action_stats = telemetry
162        .actions
163        .into_iter()
164        .map(|action| ActionStat {
165            spell_id: action.spell_id,
166            target_id: action.target_id,
167            source_kind: action.source_kind,
168            source_id: action.source_id,
169            pull_id: action.pull_id,
170            group_id: action.group_id,
171            casts: action.casts,
172            direct_hits: action.direct_hits,
173            crits: action.crits,
174            total_damage: action.total_damage(),
175        })
176        .collect();
177
178    IterationTrace {
179        duration_ms,
180        seed,
181        estimated_dps,
182        total_casts,
183        gcd_utilization_pct,
184        decisions,
185        resource_samples,
186        dps_samples,
187        aura_events,
188        action_stats,
189    }
190}
191
192#[cfg(test)]
193mod tests;