wows-compute 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,669 @@
1
+ import { d as gameDay, f as inRange, l as outcomeOf, m as weekBoundary, p as parseBattleTime, u as statOutcome } from "./match-J2T9QiGg.mjs";
2
+ import { PR_BANDS, PR_BANDS as PR_BANDS$1, PR_CATEGORY_LABELS, WIN_RATE_BANDS, WIN_RATE_BANDS as WIN_RATE_BANDS$1, calculateBattlePR, calculatePR, prCategory, prCategory as prCategory$1, shipExpectedValues, winRateCategory, winRateCategory as winRateCategory$1 } from "wows-number-api-sdk";
3
+ //#region src/rating/index.ts
4
+ const ACCOUNT_SHIP_ID = "__account__";
5
+ function baselineOf(expected) {
6
+ return String(expected.time);
7
+ }
8
+ function resultValue(result) {
9
+ return result === null ? null : result.pr;
10
+ }
11
+ /** Unweighted mean of every ship that carries expected values. */
12
+ function globalAverages(expected) {
13
+ let damage = 0;
14
+ let frags = 0;
15
+ let winRate = 0;
16
+ let count = 0;
17
+ for (const entry of Object.values(expected.data)) {
18
+ if (Array.isArray(entry)) continue;
19
+ damage += entry.average_damage_dealt;
20
+ frags += entry.average_frags;
21
+ winRate += entry.win_rate;
22
+ count += 1;
23
+ }
24
+ if (count === 0) return null;
25
+ return {
26
+ damage: damage / count,
27
+ frags: frags / count,
28
+ winRate: winRate / count
29
+ };
30
+ }
31
+ function estimateBias(expected, averages, shipMix) {
32
+ if (shipMix === void 0 || shipMix.length === 0) return {
33
+ direction: "unknown",
34
+ reason: "insufficient-data"
35
+ };
36
+ let battles = 0;
37
+ let expectedDamage = 0;
38
+ let expectedFrags = 0;
39
+ let expectedWins = 0;
40
+ for (const entry of shipMix) {
41
+ if (!Number.isFinite(entry.battles) || entry.battles <= 0) continue;
42
+ const values = shipExpectedValues(expected, entry.shipId);
43
+ if (values === null) continue;
44
+ battles += entry.battles;
45
+ expectedDamage += values.average_damage_dealt * entry.battles;
46
+ expectedFrags += values.average_frags * entry.battles;
47
+ expectedWins += values.win_rate / 100 * entry.battles;
48
+ }
49
+ if (battles === 0) return {
50
+ direction: "unknown",
51
+ reason: "insufficient-data"
52
+ };
53
+ const delta = {
54
+ damage: expectedDamage / battles / averages.damage - 1,
55
+ frags: expectedFrags / battles / averages.frags - 1,
56
+ wins: expectedWins / battles / (averages.winRate / 100) - 1
57
+ };
58
+ const directions = [
59
+ delta.damage,
60
+ delta.frags,
61
+ delta.wins
62
+ ];
63
+ const positives = directions.filter((value) => value > 1e-9).length;
64
+ const negatives = directions.filter((value) => value < -1e-9).length;
65
+ let direction = "unknown";
66
+ if (positives === directions.length) direction = "overestimate";
67
+ else if (negatives === directions.length) direction = "underestimate";
68
+ else if (positives > 0 && negatives > 0) direction = "mixed";
69
+ return {
70
+ direction,
71
+ reason: "ship-mix",
72
+ delta
73
+ };
74
+ }
75
+ /** Personal Rating for one ship, using that ship's own expected values. */
76
+ function shipPr(stats, expected, sample = {
77
+ kind: "lifetime",
78
+ matches: stats.battles
79
+ }) {
80
+ const value = resultValue(calculatePR([stats], expected));
81
+ if (value === null) return null;
82
+ return {
83
+ value,
84
+ basis: "ship",
85
+ exact: true,
86
+ sample,
87
+ baselineVersion: baselineOf(expected)
88
+ };
89
+ }
90
+ /** Personal Rating for a single battle. */
91
+ function battlePr(battle, expected, sample = {
92
+ kind: "match",
93
+ matches: 1
94
+ }) {
95
+ const value = resultValue(calculateBattlePR(battle, expected));
96
+ if (value === null) return null;
97
+ return {
98
+ value,
99
+ basis: "ship",
100
+ exact: true,
101
+ sample,
102
+ baselineVersion: baselineOf(expected)
103
+ };
104
+ }
105
+ /**
106
+ * Fast account PR*: account totals against the unweighted average of all
107
+ * expected values.
108
+ *
109
+ * This is an estimate, not a different formula: the account is treated as one
110
+ * synthetic ship. `exact: false` is a product promise — UI must mark it `PR*`.
111
+ */
112
+ function accountPrEstimated(account, expected, options = {}) {
113
+ if (!Number.isFinite(account.battles) || account.battles <= 0) return null;
114
+ const averages = globalAverages(expected);
115
+ if (averages === null) return null;
116
+ const syntheticExpected = {
117
+ time: expected.time,
118
+ data: { [ACCOUNT_SHIP_ID]: {
119
+ average_damage_dealt: averages.damage,
120
+ average_frags: averages.frags,
121
+ win_rate: averages.winRate
122
+ } }
123
+ };
124
+ const synthetic = {
125
+ shipId: ACCOUNT_SHIP_ID,
126
+ battles: account.battles,
127
+ damage: account.damage,
128
+ frags: account.frags,
129
+ wins: account.wins
130
+ };
131
+ const value = resultValue(calculatePR([synthetic], syntheticExpected));
132
+ if (value === null) return null;
133
+ return {
134
+ value,
135
+ basis: "account",
136
+ exact: false,
137
+ sample: {
138
+ kind: "lifetime",
139
+ matches: account.battles
140
+ },
141
+ baselineVersion: baselineOf(expected),
142
+ bias: estimateBias(expected, averages, options.shipMix)
143
+ };
144
+ }
145
+ /**
146
+ * Exact weighted account PR: pools per-ship totals and per-ship baselines.
147
+ *
148
+ * Ships without expected values are skipped by `calculatePR`, not counted
149
+ * as zero.
150
+ */
151
+ function accountPrPooled(ships, expected, sample) {
152
+ const value = resultValue(calculatePR(ships, expected));
153
+ if (value === null) return null;
154
+ return {
155
+ value,
156
+ basis: "account",
157
+ exact: true,
158
+ sample,
159
+ baselineVersion: baselineOf(expected)
160
+ };
161
+ }
162
+ //#endregion
163
+ //#region src/aggregate/index.ts
164
+ function emptyCoverage() {
165
+ return {
166
+ total: 0,
167
+ resolved: 0,
168
+ hidden: 0,
169
+ bot: 0,
170
+ failed: 0
171
+ };
172
+ }
173
+ function includedRecord(record, options) {
174
+ if (options.matchGroups !== void 0 && !options.matchGroups.includes(record.matchGroup)) return false;
175
+ if (record.battleTime === null) return options.from === void 0 && options.to === void 0;
176
+ if (options.from !== void 0 && record.battleTime < options.from) return false;
177
+ if (options.to !== void 0 && record.battleTime > options.to) return false;
178
+ return true;
179
+ }
180
+ function sampleOf(matches, players, options) {
181
+ const sample = {
182
+ kind: "ingested",
183
+ matches,
184
+ players
185
+ };
186
+ if (options.from !== void 0) sample.from = new Date(options.from).toISOString();
187
+ if (options.to !== void 0) sample.to = new Date(options.to).toISOString();
188
+ return sample;
189
+ }
190
+ function addShip(map, shipId, damage, frags, win) {
191
+ const key = String(shipId);
192
+ const current = map.get(key);
193
+ if (current === void 0) map.set(key, {
194
+ shipId,
195
+ battles: 1,
196
+ damage,
197
+ frags,
198
+ wins: win ? 1 : 0
199
+ });
200
+ else {
201
+ current.battles += 1;
202
+ current.damage += damage;
203
+ current.frags += frags;
204
+ if (win) current.wins += 1;
205
+ }
206
+ }
207
+ function scan(records, options, teamId) {
208
+ const coverage = emptyCoverage();
209
+ const players = /* @__PURE__ */ new Map();
210
+ const ships = /* @__PURE__ */ new Map();
211
+ let matches = 0;
212
+ for (const record of records) {
213
+ if (!includedRecord(record, options)) continue;
214
+ const participants = teamId === void 0 ? record.participants : record.participants.filter((participant) => participant.teamId === teamId);
215
+ if (participants.length === 0) continue;
216
+ matches += 1;
217
+ for (const participant of participants) {
218
+ coverage.total += 1;
219
+ if (participant.isBot) {
220
+ coverage.bot += 1;
221
+ continue;
222
+ }
223
+ if (participant.accountId === null) {
224
+ coverage.hidden += 1;
225
+ continue;
226
+ }
227
+ if (participant.result === null) {
228
+ coverage.failed += 1;
229
+ continue;
230
+ }
231
+ const outcome = outcomeOf(participant.teamId, record.winnerTeamId);
232
+ const statistical = statOutcome(outcome);
233
+ if (statistical === null) {
234
+ coverage.failed += 1;
235
+ continue;
236
+ }
237
+ coverage.resolved += 1;
238
+ const accountId = participant.accountId;
239
+ const damage = participant.result.damage ?? 0;
240
+ const frags = participant.result.frags ?? 0;
241
+ const win = statistical === "win";
242
+ let player = players.get(accountId);
243
+ if (player === void 0) {
244
+ player = {
245
+ accountId,
246
+ name: participant.name,
247
+ battles: 0,
248
+ wins: 0,
249
+ losses: 0,
250
+ draws: 0,
251
+ damage: 0,
252
+ frags: 0,
253
+ matches: 0,
254
+ ships: /* @__PURE__ */ new Map()
255
+ };
256
+ players.set(accountId, player);
257
+ }
258
+ player.matches += 1;
259
+ player.battles += 1;
260
+ if (outcome === "win") player.wins += 1;
261
+ else if (outcome === "loss") player.losses += 1;
262
+ else if (outcome === "draw") player.draws += 1;
263
+ player.damage += damage;
264
+ player.frags += frags;
265
+ if (participant.shipId !== null) {
266
+ addShip(player.ships, participant.shipId, damage, frags, win);
267
+ const ship = ships.get(participant.shipId);
268
+ if (ship === void 0) ships.set(participant.shipId, {
269
+ shipId: participant.shipId,
270
+ battles: 1,
271
+ wins: win ? 1 : 0,
272
+ losses: outcome === "loss" ? 1 : 0,
273
+ draws: outcome === "draw" ? 1 : 0,
274
+ damage,
275
+ frags,
276
+ players: /* @__PURE__ */ new Set([accountId])
277
+ });
278
+ else {
279
+ ship.battles += 1;
280
+ if (outcome === "win") ship.wins += 1;
281
+ else if (outcome === "loss") ship.losses += 1;
282
+ else ship.draws += 1;
283
+ ship.damage += damage;
284
+ ship.frags += frags;
285
+ ship.players.add(accountId);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ return {
291
+ matches,
292
+ coverage,
293
+ players,
294
+ ships
295
+ };
296
+ }
297
+ function ratioOf(numerator, denominator) {
298
+ return denominator === 0 ? null : numerator / denominator;
299
+ }
300
+ function weightedMean(entries, weighting) {
301
+ let total = 0;
302
+ let weight = 0;
303
+ for (const entry of entries) {
304
+ const entryWeight = weighting === "equal" ? 1 : entry.weight;
305
+ if (entryWeight <= 0) continue;
306
+ total += entry.value * entryWeight;
307
+ weight += entryWeight;
308
+ }
309
+ return weight === 0 ? null : total / weight;
310
+ }
311
+ function playerPr(player, options, sample) {
312
+ if (options.expected === void 0 || player.ships.size === 0) return null;
313
+ return accountPrPooled([...player.ships.values()], options.expected, sample)?.value ?? null;
314
+ }
315
+ function playerCoverage(player) {
316
+ return {
317
+ total: player.battles,
318
+ resolved: player.battles,
319
+ hidden: 0,
320
+ bot: 0,
321
+ failed: 0
322
+ };
323
+ }
324
+ function playerValues(player, options, sample) {
325
+ const coverage = playerCoverage(player);
326
+ return {
327
+ accountId: player.accountId,
328
+ name: player.name,
329
+ battles: player.battles,
330
+ wins: player.wins,
331
+ losses: player.losses,
332
+ draws: player.draws,
333
+ damage: player.damage,
334
+ frags: player.frags,
335
+ winRate: {
336
+ value: ratioOf(player.wins * 100, player.battles),
337
+ sample,
338
+ coverage
339
+ },
340
+ avgDamage: {
341
+ value: ratioOf(player.damage, player.battles),
342
+ sample,
343
+ coverage
344
+ },
345
+ avgFrags: {
346
+ value: ratioOf(player.frags, player.battles),
347
+ sample,
348
+ coverage
349
+ },
350
+ pr: {
351
+ value: playerPr(player, options, sample),
352
+ sample,
353
+ coverage
354
+ }
355
+ };
356
+ }
357
+ /** Per-player aggregates over the selected records. Ordered by battles, then account id. */
358
+ function aggregatePlayers(records, options) {
359
+ return [...scan(records, options).players.values()].map((player) => playerValues(player, options, sampleOf(player.matches, 1, options))).sort((a, b) => b.battles - a.battles || a.accountId - b.accountId);
360
+ }
361
+ /** One team's aggregate; coverage only counts that team's participants. */
362
+ function aggregateTeam(records, teamId, options) {
363
+ const scanned = teamId === null ? scan(records, options) : scan(records, options, teamId);
364
+ const players = [...scanned.players.values()];
365
+ const sample = sampleOf(scanned.matches, players.length, options);
366
+ const coverage = scanned.coverage;
367
+ const winRate = weightedMean(players.filter((player) => player.battles > 0).map((player) => ({
368
+ value: player.wins / player.battles * 100,
369
+ weight: player.battles
370
+ })), options.weighting);
371
+ const avgDamage = weightedMean(players.filter((player) => player.battles > 0).map((player) => ({
372
+ value: player.damage / player.battles,
373
+ weight: player.battles
374
+ })), options.weighting);
375
+ const avgFrags = weightedMean(players.filter((player) => player.battles > 0).map((player) => ({
376
+ value: player.frags / player.battles,
377
+ weight: player.battles
378
+ })), options.weighting);
379
+ let pr = null;
380
+ if (options.expected !== void 0) {
381
+ if (options.prAggregation === "pooled") {
382
+ const ships = players.flatMap((player) => [...player.ships.values()]);
383
+ pr = ships.length === 0 ? null : accountPrPooled(ships, options.expected, sample)?.value ?? null;
384
+ } else pr = weightedMean(players.map((player) => ({
385
+ value: playerPr(player, options, sample),
386
+ weight: player.battles
387
+ })).filter((entry) => entry.value !== null), options.weighting);
388
+ }
389
+ return {
390
+ teamId,
391
+ players: players.length,
392
+ coverage,
393
+ values: {
394
+ winRate: {
395
+ value: winRate,
396
+ sample,
397
+ coverage
398
+ },
399
+ avgDamage: {
400
+ value: avgDamage,
401
+ sample,
402
+ coverage
403
+ },
404
+ avgFrags: {
405
+ value: avgFrags,
406
+ sample,
407
+ coverage
408
+ },
409
+ pr: {
410
+ value: pr,
411
+ sample,
412
+ coverage
413
+ }
414
+ }
415
+ };
416
+ }
417
+ /** Per-ship aggregates over the selected records. Ordered by battles, then ship id. */
418
+ function aggregateShips(records, options) {
419
+ const scanned = scan(records, options);
420
+ const coverage = scanned.coverage;
421
+ const rows = [];
422
+ for (const ship of scanned.ships.values()) {
423
+ const sample = sampleOf(scanned.matches, ship.players.size, options);
424
+ const stats = {
425
+ shipId: ship.shipId,
426
+ battles: ship.battles,
427
+ damage: ship.damage,
428
+ frags: ship.frags,
429
+ wins: ship.wins
430
+ };
431
+ const pr = options.expected === void 0 ? null : accountPrPooled([stats], options.expected, sample)?.value ?? null;
432
+ rows.push({
433
+ shipId: ship.shipId,
434
+ battles: ship.battles,
435
+ wins: ship.wins,
436
+ losses: ship.losses,
437
+ draws: ship.draws,
438
+ damage: ship.damage,
439
+ frags: ship.frags,
440
+ winRate: {
441
+ value: ratioOf(ship.wins * 100, ship.battles),
442
+ sample,
443
+ coverage
444
+ },
445
+ avgDamage: {
446
+ value: ratioOf(ship.damage, ship.battles),
447
+ sample,
448
+ coverage
449
+ },
450
+ avgFrags: {
451
+ value: ratioOf(ship.frags, ship.battles),
452
+ sample,
453
+ coverage
454
+ },
455
+ pr: {
456
+ value: pr,
457
+ sample,
458
+ coverage
459
+ }
460
+ });
461
+ }
462
+ return rows.sort((a, b) => b.battles - a.battles || a.shipId - b.shipId);
463
+ }
464
+ /** Self performance per game day, ordered oldest first. */
465
+ function dailySeries(records, options) {
466
+ const days = /* @__PURE__ */ new Map();
467
+ for (const record of records) {
468
+ if (record.battleTime === null) continue;
469
+ if (options.matchGroups !== void 0 && !options.matchGroups.includes(record.matchGroup)) continue;
470
+ if (options.from !== void 0 && record.battleTime < options.from) continue;
471
+ if (options.to !== void 0 && record.battleTime > options.to) continue;
472
+ const self = record.participants.find((participant) => participant.relation === "self");
473
+ if (self === void 0 || self.result === null) continue;
474
+ const outcome = outcomeOf(self.teamId, record.winnerTeamId);
475
+ if (statOutcome(outcome) === null) continue;
476
+ const day = gameDay(record.battleTime, options.timeZone, options.boundaryHour);
477
+ let totals = days.get(day);
478
+ if (totals === void 0) {
479
+ totals = {
480
+ day,
481
+ matches: 0,
482
+ wins: 0,
483
+ losses: 0,
484
+ draws: 0,
485
+ damage: 0,
486
+ frags: 0,
487
+ ships: /* @__PURE__ */ new Map()
488
+ };
489
+ days.set(day, totals);
490
+ }
491
+ totals.matches += 1;
492
+ if (outcome === "win") totals.wins += 1;
493
+ else if (outcome === "loss") totals.losses += 1;
494
+ else totals.draws += 1;
495
+ const damage = self.result.damage ?? 0;
496
+ const frags = self.result.frags ?? 0;
497
+ totals.damage += damage;
498
+ totals.frags += frags;
499
+ if (self.shipId !== null) addShip(totals.ships, self.shipId, damage, frags, outcome === "win");
500
+ }
501
+ const points = [];
502
+ for (const totals of days.values()) {
503
+ const battles = totals.matches;
504
+ const coverage = {
505
+ total: battles,
506
+ resolved: battles,
507
+ hidden: 0,
508
+ bot: 0,
509
+ failed: 0
510
+ };
511
+ const sample = {
512
+ kind: "ingested",
513
+ matches: battles
514
+ };
515
+ const pr = options.expected === void 0 || totals.ships.size === 0 ? null : accountPrPooled([...totals.ships.values()], options.expected, sample)?.value ?? null;
516
+ points.push({
517
+ day: totals.day,
518
+ matches: totals.matches,
519
+ battles,
520
+ wins: totals.wins,
521
+ losses: totals.losses,
522
+ draws: totals.draws,
523
+ winRate: battles === 0 ? null : totals.wins / battles * 100,
524
+ avgDamage: battles === 0 ? null : totals.damage / battles,
525
+ avgFrags: battles === 0 ? null : totals.frags / battles,
526
+ pr: {
527
+ value: pr,
528
+ sample,
529
+ coverage
530
+ }
531
+ });
532
+ }
533
+ return points.sort((a, b) => a.day.localeCompare(b.day));
534
+ }
535
+ //#endregion
536
+ //#region src/levels/index.ts
537
+ /** Level key plus thresholds. Labels and colors belong to the application. */
538
+ function levelOf(kind, value) {
539
+ if (!Number.isFinite(value)) return null;
540
+ const bands = kind === "pr" ? PR_BANDS$1 : WIN_RATE_BANDS$1;
541
+ const levelKey = kind === "pr" ? prCategory$1(value) : winRateCategory$1(value);
542
+ const index = bands.findIndex((band) => band.category === levelKey);
543
+ const band = bands[index];
544
+ if (band === void 0) return null;
545
+ const nextFloor = bands[index + 1]?.floor ?? null;
546
+ return {
547
+ levelKey,
548
+ floor: band.floor,
549
+ nextFloor,
550
+ deltaToFloor: band.floor === null ? null : value - band.floor,
551
+ deltaToNext: nextFloor === null ? null : nextFloor - value
552
+ };
553
+ }
554
+ //#endregion
555
+ //#region src/recent/index.ts
556
+ const DEFAULT_WINDOW = 20;
557
+ function subjectParticipant(record, subject) {
558
+ if (subject.kind === "self") return record.participants.find((participant) => participant.relation === "self") ?? null;
559
+ return record.participants.find((participant) => participant.accountId === subject.accountId) ?? null;
560
+ }
561
+ function inDateRange(record, from, to) {
562
+ if (from === void 0 && to === void 0) return true;
563
+ if (record.battleTime === null) return false;
564
+ if (from !== void 0 && record.battleTime < from) return false;
565
+ if (to !== void 0 && record.battleTime > to) return false;
566
+ return true;
567
+ }
568
+ function compareNewestFirst(a, b) {
569
+ const timeA = a.battleTime ?? Number.NEGATIVE_INFINITY;
570
+ const timeB = b.battleTime ?? Number.NEGATIVE_INFINITY;
571
+ if (timeA !== timeB) return timeB - timeA;
572
+ return a.arenaId.localeCompare(b.arenaId);
573
+ }
574
+ function emptyStats() {
575
+ return {
576
+ battles: 0,
577
+ wins: 0,
578
+ losses: 0,
579
+ draws: 0,
580
+ winRate: null,
581
+ damage: 0,
582
+ frags: 0,
583
+ avgDamage: null,
584
+ avgFrags: null,
585
+ pr: null
586
+ };
587
+ }
588
+ /**
589
+ * Statistics over ingested matches only.
590
+ *
591
+ * No gap filling, no lifetime totals, no historical replay scan: a match is in
592
+ * the window only when it was successfully parsed and stored by the caller.
593
+ */
594
+ function recentMatches(records, query, expected) {
595
+ const drawPolicy = query.drawPolicy ?? "loss";
596
+ const limit = query.last === void 0 ? DEFAULT_WINDOW : Math.max(0, Math.trunc(query.last));
597
+ let matches = records.filter((record) => {
598
+ if (query.matchGroups !== void 0 && !query.matchGroups.includes(record.matchGroup)) return false;
599
+ if (!inDateRange(record, query.from, query.to)) return false;
600
+ const participant = subjectParticipant(record, query.subject);
601
+ if (participant === null || participant.result === null) return false;
602
+ const outcome = outcomeOf(participant.teamId, record.winnerTeamId);
603
+ if (statOutcome(outcome) === null) return false;
604
+ if (drawPolicy === "exclude" && outcome === "draw") return false;
605
+ return true;
606
+ });
607
+ matches = matches.sort(compareNewestFirst).slice(0, limit);
608
+ const stats = emptyStats();
609
+ const ships = /* @__PURE__ */ new Map();
610
+ let damage = 0;
611
+ let frags = 0;
612
+ let wins = 0;
613
+ let losses = 0;
614
+ let draws = 0;
615
+ for (const record of matches) {
616
+ const participant = subjectParticipant(record, query.subject);
617
+ if (participant === null || participant.result === null) continue;
618
+ const outcome = outcomeOf(participant.teamId, record.winnerTeamId);
619
+ if (outcome === "win") wins += 1;
620
+ else if (outcome === "loss") losses += 1;
621
+ else if (outcome === "draw") draws += 1;
622
+ const resultDamage = participant.result.damage ?? 0;
623
+ const resultFrags = participant.result.frags ?? 0;
624
+ damage += resultDamage;
625
+ frags += resultFrags;
626
+ if (participant.shipId !== null) {
627
+ const key = String(participant.shipId);
628
+ const ship = ships.get(key);
629
+ if (ship === void 0) ships.set(key, {
630
+ shipId: participant.shipId,
631
+ battles: 1,
632
+ damage: resultDamage,
633
+ frags: resultFrags,
634
+ wins: outcome === "win" ? 1 : 0
635
+ });
636
+ else {
637
+ ship.battles += 1;
638
+ ship.damage += resultDamage;
639
+ ship.frags += resultFrags;
640
+ if (outcome === "win") ship.wins += 1;
641
+ }
642
+ }
643
+ }
644
+ const battles = matches.length;
645
+ stats.battles = battles;
646
+ stats.wins = wins;
647
+ stats.losses = losses;
648
+ stats.draws = draws;
649
+ stats.winRate = battles === 0 ? null : wins / battles * 100;
650
+ stats.damage = damage;
651
+ stats.frags = frags;
652
+ stats.avgDamage = battles === 0 ? null : damage / battles;
653
+ stats.avgFrags = battles === 0 ? null : frags / battles;
654
+ const sample = {
655
+ kind: "ingested",
656
+ matches: battles
657
+ };
658
+ if (query.from !== void 0) sample.from = new Date(query.from).toISOString();
659
+ if (query.to !== void 0) sample.to = new Date(query.to).toISOString();
660
+ if (query.subject.kind === "participant") sample.sharedWith = battles;
661
+ if (expected !== void 0 && ships.size > 0) stats.pr = accountPrPooled([...ships.values()], expected, sample);
662
+ return {
663
+ matches,
664
+ sample,
665
+ stats
666
+ };
667
+ }
668
+ //#endregion
669
+ export { PR_BANDS, PR_CATEGORY_LABELS, WIN_RATE_BANDS, accountPrEstimated, accountPrPooled, aggregatePlayers, aggregateShips, aggregateTeam, battlePr, dailySeries, gameDay, inRange, levelOf, parseBattleTime, prCategory, recentMatches, shipPr, weekBoundary, winRateCategory };