1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
5#[serde(rename_all = "camelCase")]
6#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
7#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
8pub struct ChunkResult {
9 pub iterations: u32,
10 pub mean_dps: f64,
11 pub std_dps: f64,
12 pub min_dps: f64,
13 pub max_dps: f64,
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
15 pub spells: Vec<ChunkSpellAggregate>,
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
17 pub auras: Vec<ChunkAuraAggregate>,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
19 pub resources: Vec<ChunkResourceAggregate>,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub cooldowns: Vec<ChunkCooldownAggregate>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub timeline: Option<ChunkTimeline>,
24 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub metric_descriptors: Vec<ChunkMetricDescriptor>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub metric_values: Vec<ChunkMetricValue>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub dps_percentiles: Option<DpsPercentiles>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub damage_profile: Option<ChunkDamageProfile>,
32}
33
34#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ChunkMetricDescriptor {
37 pub id: u32,
38 pub key: String,
39 pub name: String,
40 pub unit: String,
41 pub metric_type: String,
42 pub group: String,
43 pub plugin: String,
44}
45
46#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct ChunkMetricValue {
49 pub id: u32,
50 pub value: f64,
51}
52
53#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct DpsPercentiles {
56 pub p5: f64,
57 pub p25: f64,
58 pub p50: f64,
59 pub p75: f64,
60 pub p95: f64,
61}
62
63#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ChunkSpellAggregate {
66 pub spell_id: u32,
67 pub casts: u32,
68 pub hits: u32,
69 pub crits: u32,
70 pub total_damage: f32,
71}
72
73#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct ChunkAuraAggregate {
76 pub aura_id: u32,
77 pub uptime_pct_x100: u32,
79}
80
81#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ChunkResourceAggregate {
84 pub resource_type: u32,
85 pub total_gained: f32,
86 pub total_spent: f32,
87 pub wasted: f32,
88 pub time_at_cap_pct_x100: u32,
90}
91
92#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ChunkCooldownAggregate {
95 pub spell_id: u32,
96 pub uses: u32,
97 pub possible_uses: u32,
98}
99
100#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ChunkTimeline {
104 pub duration_ms: u32,
105 pub bucket_ms: u32,
106 pub dps_per_bucket: Vec<u32>,
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub markers: Vec<ChunkTimelineMarker>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub aura_windows: Vec<ChunkAuraWindow>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub cooldown_windows: Vec<ChunkCooldownWindow>,
113}
114
115#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct ChunkTimelineMarker {
118 pub time_ms: u32,
119 #[serde(default)]
120 pub sequence: u64,
121 pub kind: crate::proto::MarkerKind,
122 pub spell_or_aura_id: u32,
123 pub target: u32,
124 pub amount: u32,
125 pub is_crit: bool,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub source: Option<super::ActorId>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub pull: Option<super::PullId>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub group: Option<super::GroupId>,
132}
133
134#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct ChunkAuraWindow {
137 pub aura_id: u32,
138 pub target: u32,
139 pub start_ms: u32,
140 pub end_ms: u32,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub source: Option<super::ActorId>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub affected: Option<super::ActorId>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub pull: Option<super::PullId>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub group: Option<super::GroupId>,
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct ChunkCooldownWindow {
155 pub spell_id: u32,
156 pub start_ms: u32,
157 pub duration_ms: u32,
158}
159
160#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
162#[serde(rename_all = "camelCase")]
163pub struct ChunkDamageProfile {
164 pub direct_damage: f64,
165 pub periodic_damage: f64,
166 pub pet_damage: f64,
167}
168
169#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
171#[serde(rename_all = "camelCase")]
172#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
173#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
174pub struct SimulationResult {
175 pub mean_dps: f64,
176 pub min_dps: f64,
177 pub max_dps: f64,
178 pub total_iterations: u32,
179 pub chunks_completed: u32,
180}
181
182impl SimulationResult {
183 #[must_use]
184 pub fn new() -> Self {
185 Self {
186 mean_dps: 0.0,
187 min_dps: f64::MAX,
188 max_dps: f64::MIN,
189 total_iterations: 0,
190 chunks_completed: 0,
191 }
192 }
193
194 pub fn merge_chunk(&mut self, chunk: &ChunkResult) {
195 self.min_dps = self.min_dps.min(chunk.min_dps);
196 self.max_dps = self.max_dps.max(chunk.max_dps);
197
198 let total_before = f64::from(self.total_iterations);
199 let chunk_count = f64::from(chunk.iterations);
200 let total_after = total_before + chunk_count;
201
202 if total_after > 0.0 {
203 self.mean_dps =
204 (self.mean_dps * total_before + chunk.mean_dps * chunk_count) / total_after;
205 }
206
207 self.total_iterations += chunk.iterations;
208 self.chunks_completed += 1;
209 }
210}
211
212impl Default for SimulationResult {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use googletest::prelude::*;
221
222 use super::*;
223
224 const TOL: f64 = 1e-10;
225
226 #[gtest]
227 fn test_merge_chunks() -> Result<()> {
228 let mut result = SimulationResult::new();
229
230 result.merge_chunk(&ChunkResult {
231 iterations: 1000,
232 mean_dps: 50000.0,
233 std_dps: 1000.0,
234 min_dps: 45000.0,
235 max_dps: 55000.0,
236 spells: vec![],
237 auras: vec![],
238 resources: vec![],
239 cooldowns: vec![],
240 timeline: None,
241 metric_descriptors: vec![],
242 metric_values: vec![],
243 dps_percentiles: None,
244 damage_profile: None,
245 });
246
247 wowlab_test_support::verify_all!(
248 result.total_iterations => eq(1_000),
249 result.mean_dps => near(50_000.0, TOL),
250 result.min_dps => near(45_000.0, TOL),
251 result.max_dps => near(55_000.0, TOL),
252 result.chunks_completed => eq(1),
253 )?;
254
255 result.merge_chunk(&ChunkResult {
256 iterations: 1000,
257 mean_dps: 52000.0,
258 std_dps: 1200.0,
259 min_dps: 46000.0,
260 max_dps: 58000.0,
261 spells: vec![],
262 auras: vec![],
263 resources: vec![],
264 cooldowns: vec![],
265 timeline: None,
266 metric_descriptors: vec![],
267 metric_values: vec![],
268 dps_percentiles: None,
269 damage_profile: None,
270 });
271
272 wowlab_test_support::verify_all!(
273 result.total_iterations => eq(2_000),
274 result.mean_dps => near(51_000.0, TOL),
275 result.min_dps => near(45_000.0, TOL),
276 result.max_dps => near(58_000.0, TOL),
277 result.chunks_completed => eq(2),
278 )
279 }
280}