wowlab_engine_application/iteration_trace/
mod.rs1use 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#[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#[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#[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#[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#[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#[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;