@cs2dak/presentation 1.0.0 → 2.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/LICENSE +7 -0
- package/package.json +8 -7
- package/src/double-awp.test.ts +29 -0
- package/src/double-awp.ts +50 -0
- package/src/duel.test.ts +190 -0
- package/src/duel.ts +359 -0
- package/src/findings.test.ts +62 -0
- package/src/findings.ts +174 -0
- package/src/index.test.ts +41 -8
- package/src/index.ts +90 -6
- package/src/insights.test.ts +522 -0
- package/src/insights.ts +1151 -0
- package/src/leaderboard.test.ts +48 -74
- package/src/map-roles.test.ts +131 -0
- package/src/map-roles.ts +219 -0
- package/src/player-map-pool.ts +28 -0
- package/src/player.test.ts +139 -110
- package/src/player.ts +66 -28
- package/src/radar-field.ts +121 -0
- package/src/replay-clock.test.ts +33 -0
- package/src/replay-clock.ts +61 -0
- package/src/season-metrics.ts +5 -0
- package/src/season-validation.test.ts +28 -8
- package/src/series.test.ts +7 -23
- package/src/series.ts +0 -19
- package/src/tactical-labels.ts +80 -0
- package/src/team.test.ts +19 -67
- package/src/team.ts +307 -127
- package/src/test-fixtures.ts +191 -0
- package/src/tournament-compat.ts +591 -0
- package/src/trails.test.ts +63 -0
- package/src/trails.ts +119 -0
- package/src/weapons.ts +1 -1
- package/src/workspace-utils.ts +2 -12
- package/src/workspace.ts +395 -83
- package/src/labels.ts +0 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { DuelFinderRow } from "@cs2dak/contract";
|
|
3
|
+
import type { TacticalCluster } from "@cs2dak/cohort";
|
|
4
|
+
import {
|
|
5
|
+
buildMistakeFindings,
|
|
6
|
+
findingFromDuel,
|
|
7
|
+
findingFromTacticalCluster,
|
|
8
|
+
findingFromUtilityDamage,
|
|
9
|
+
} from "./findings.js";
|
|
10
|
+
|
|
11
|
+
const evidence = { matchId: "m1", roundNumber: 12, tick: 128, reason: "长枪局首死", role: "example" as const };
|
|
12
|
+
|
|
13
|
+
describe("AnalysisFinding adapters", () => {
|
|
14
|
+
it("only turns evidence-backed mistake rules into system findings", () => {
|
|
15
|
+
const findings = buildMistakeFindings({
|
|
16
|
+
lowBuyFirstDeaths: { count: 0, attempts: 3, evidence: [] },
|
|
17
|
+
fullBuyFirstDeaths: { count: 3, attempts: 9, evidence: [{ ...evidence, detail: "长枪局首死" }] },
|
|
18
|
+
antiEcoFirstDeaths: { count: 1, attempts: 2, evidence: [] },
|
|
19
|
+
deathTiming: { early: 0, mid: 0, late: 0, total: 0 },
|
|
20
|
+
clutchLosses: { count: 0, evidence: [] },
|
|
21
|
+
}, { id: "self", label: "FalleN" });
|
|
22
|
+
|
|
23
|
+
expect(findings).toHaveLength(1);
|
|
24
|
+
expect(findings[0]).toMatchObject({
|
|
25
|
+
key: "mistake:full-buy-first-death:self",
|
|
26
|
+
origin: "system",
|
|
27
|
+
sample: { numerator: 3, denominator: 9 },
|
|
28
|
+
evidence: [evidence],
|
|
29
|
+
});
|
|
30
|
+
expect(findings[0]?.limitations[0]).toContain("不");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("preserves concrete duel and utility evidence with a reason", () => {
|
|
34
|
+
const duel = {
|
|
35
|
+
id: "m1:k1", matchId: "m1", mapName: "de_inferno", roundNumber: 12, tick: 128,
|
|
36
|
+
killerSteamId64: "a", victimSteamId64: "b", killerName: "FalleN", victimName: "ropz", weapon: "ak47",
|
|
37
|
+
classification: "contested_duel", hpBucket: "full_hp", thirdParty: false, fullHealth: true,
|
|
38
|
+
victimHealthBefore: 100, killerHealthBefore: 100, ttkMs: 320, oneShotKill: false,
|
|
39
|
+
evidenceTicks: { engagementStartTick: 64, engagementEndTick: 128, killerFirstShotTick: 100, victimResponseTick: 110, killTick: 128 },
|
|
40
|
+
killerPosition: null, victimPosition: null, roundTimeLabel: "1:20", evidence,
|
|
41
|
+
} satisfies DuelFinderRow;
|
|
42
|
+
const utility = { ...evidence, kind: "he" as const, playerName: "FalleN", victimCount: 2, damage: 86 };
|
|
43
|
+
|
|
44
|
+
expect(findingFromDuel(duel).evidence).toEqual([evidence]);
|
|
45
|
+
expect(findingFromUtilityDamage(utility).evidence).toEqual([utility]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps tactical clusters within their observed opening-pattern boundary", () => {
|
|
49
|
+
const cluster = {
|
|
50
|
+
id: "cluster-1", mapName: "de_nuke", side: "t", economyEntry: "gun", teamIdentity: "furia", teamName: "FURIA",
|
|
51
|
+
opponentNames: ["Spirit"], opponentIdentities: ["spirit"], openingIntent: { regionCounts: { a: 2, mid: 1, b: 2, unknown: 0 }, spread: "split" },
|
|
52
|
+
positionGroupCounts: {}, primaryCategory: "均衡控图", openingSignature: "test",
|
|
53
|
+
entryEvidence: { coveredRounds: 3, totalRounds: 4, coveragePercent: 75, routes: [] }, roundCount: 4,
|
|
54
|
+
winRatePercent: 50, plantRatePercent: 50,
|
|
55
|
+
rounds: [{ matchId: "m1", roundNumber: 5, teamKey: "teamA", won: true, economy: "full", planted: true }],
|
|
56
|
+
} satisfies TacticalCluster;
|
|
57
|
+
const finding = findingFromTacticalCluster(cluster);
|
|
58
|
+
|
|
59
|
+
expect(finding?.evidence[0]).toMatchObject({ matchId: "m1", roundNumber: 5, role: "example" });
|
|
60
|
+
expect(finding?.limitations[0]).toContain("不识别完整中期战术");
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/findings.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { DuelFinderRow, EvidenceRef } from "@cs2dak/contract";
|
|
2
|
+
import type { TacticalCluster } from "@cs2dak/cohort";
|
|
3
|
+
import { duelClassificationLabel } from "./duel.js";
|
|
4
|
+
import type {
|
|
5
|
+
EnemyFlashIncident,
|
|
6
|
+
FirstDeathStat,
|
|
7
|
+
MistakeReview,
|
|
8
|
+
UtilityDamageEvidence,
|
|
9
|
+
} from "./insights.js";
|
|
10
|
+
|
|
11
|
+
export type FindingOrigin = "system" | "user";
|
|
12
|
+
|
|
13
|
+
/** 产品中立的、可交给证据查看器或行动面的系统结论。 */
|
|
14
|
+
export interface AnalysisFinding {
|
|
15
|
+
/** capability 内稳定的 key,不承担跨产品持久化 identity。 */
|
|
16
|
+
key: string;
|
|
17
|
+
capability: "mistake-review" | "duel" | "utility" | "tactical";
|
|
18
|
+
producerVersion: string;
|
|
19
|
+
origin: FindingOrigin;
|
|
20
|
+
title: string;
|
|
21
|
+
statement: string;
|
|
22
|
+
subject: { id?: string; label: string };
|
|
23
|
+
relationship?: string;
|
|
24
|
+
sample: {
|
|
25
|
+
label: string;
|
|
26
|
+
numerator?: number;
|
|
27
|
+
denominator?: number;
|
|
28
|
+
coverage?: string;
|
|
29
|
+
};
|
|
30
|
+
baseline: string | null;
|
|
31
|
+
evidence: EvidenceRef[];
|
|
32
|
+
basis: string[];
|
|
33
|
+
limitations: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FindingSubject {
|
|
37
|
+
id?: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PRODUCER_VERSION = "cs2-demo-analysis-kit/finding-0.1";
|
|
42
|
+
|
|
43
|
+
function systemFinding(
|
|
44
|
+
finding: Omit<AnalysisFinding, "origin" | "producerVersion">
|
|
45
|
+
): AnalysisFinding {
|
|
46
|
+
return { ...finding, origin: "system", producerVersion: PRODUCER_VERSION };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mistakeFinding(
|
|
50
|
+
key: string,
|
|
51
|
+
title: string,
|
|
52
|
+
statementLabel: string,
|
|
53
|
+
stat: FirstDeathStat,
|
|
54
|
+
subject: FindingSubject,
|
|
55
|
+
): AnalysisFinding | null {
|
|
56
|
+
if (stat.count === 0 || stat.evidence.length === 0) return null;
|
|
57
|
+
return systemFinding({
|
|
58
|
+
key: `mistake:${key}:${subject.id ?? subject.label}`,
|
|
59
|
+
capability: "mistake-review",
|
|
60
|
+
title,
|
|
61
|
+
statement: `${subject.label} 在 ${stat.attempts} 个${statementLabel}中有 ${stat.count} 次首死。`,
|
|
62
|
+
subject,
|
|
63
|
+
sample: { label: statementLabel, numerator: stat.count, denominator: stat.attempts },
|
|
64
|
+
baseline: null,
|
|
65
|
+
evidence: stat.evidence,
|
|
66
|
+
basis: ["按回合首个死亡事件统计。"],
|
|
67
|
+
limitations: ["该规则只定位首死,不能据此判断首死成因或自动给出训练处方。"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 只转换已有稳定规则和证据的 Mistake Review;没有证据的统计不形成系统 Finding。 */
|
|
72
|
+
export function buildMistakeFindings(
|
|
73
|
+
mistakes: MistakeReview,
|
|
74
|
+
subject: FindingSubject,
|
|
75
|
+
): AnalysisFinding[] {
|
|
76
|
+
const firstDeath = [
|
|
77
|
+
mistakeFinding("full-buy-first-death", "优先复盘:长枪局首死", "长枪局", mistakes.fullBuyFirstDeaths, subject),
|
|
78
|
+
mistakeFinding("anti-eco-first-death", "优先复盘:反 ECO 首死", "对手 ECO/半起局", mistakes.antiEcoFirstDeaths, subject),
|
|
79
|
+
mistakeFinding("low-buy-first-death", "优先复盘:劣势经济首死", "劣势经济局", mistakes.lowBuyFirstDeaths, subject),
|
|
80
|
+
].filter((finding): finding is AnalysisFinding => finding != null);
|
|
81
|
+
|
|
82
|
+
if (mistakes.clutchLosses.count > 0 && mistakes.clutchLosses.evidence.length > 0) {
|
|
83
|
+
firstDeath.push(systemFinding({
|
|
84
|
+
key: `mistake:clutch-loss:${subject.id ?? subject.label}`,
|
|
85
|
+
capability: "mistake-review",
|
|
86
|
+
title: "优先复盘:残局失利",
|
|
87
|
+
statement: `${subject.label} 有 ${mistakes.clutchLosses.count} 次已记录的残局失利。`,
|
|
88
|
+
subject,
|
|
89
|
+
sample: { label: "残局失利", numerator: mistakes.clutchLosses.count },
|
|
90
|
+
baseline: null,
|
|
91
|
+
evidence: mistakes.clutchLosses.evidence,
|
|
92
|
+
basis: ["按导出的残局事实统计。"],
|
|
93
|
+
limitations: ["该规则不判断残局决策、对手配置或失利成因。"],
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
return firstDeath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 单条对枪事实可作为有明确回合证据的 Finding,不把分类扩展为因果结论。 */
|
|
100
|
+
export function findingFromDuel(row: DuelFinderRow): AnalysisFinding {
|
|
101
|
+
const label = duelClassificationLabel(row.classification);
|
|
102
|
+
return systemFinding({
|
|
103
|
+
key: `duel:${row.id}`,
|
|
104
|
+
capability: "duel",
|
|
105
|
+
title: label,
|
|
106
|
+
statement: `${row.killerName} 在 ${row.mapName} R${row.roundNumber} 使用 ${row.weapon} 击杀 ${row.victimName}。`,
|
|
107
|
+
subject: { id: row.killerSteamId64, label: row.killerName },
|
|
108
|
+
relationship: `${row.killerName} 对 ${row.victimName}`,
|
|
109
|
+
sample: { label: "单次对枪", numerator: 1, denominator: 1 },
|
|
110
|
+
baseline: null,
|
|
111
|
+
evidence: [row.evidence],
|
|
112
|
+
basis: ["使用已有对枪分类与击杀事实。"],
|
|
113
|
+
limitations: ["单条对枪仅用于复核具体交战,不代表长期机制画像或因果判断。"],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function findingFromUtilityDamage(row: UtilityDamageEvidence): AnalysisFinding {
|
|
118
|
+
const label = row.kind === "he" ? "HE 手雷" : "火焰";
|
|
119
|
+
return systemFinding({
|
|
120
|
+
key: `utility:${row.kind}:${row.matchId}:${row.roundNumber}:${row.tick ?? 0}:${row.playerId ?? row.playerName}`,
|
|
121
|
+
capability: "utility",
|
|
122
|
+
title: `${label}伤害证据`,
|
|
123
|
+
statement: `${row.playerName} 在 R${row.roundNumber} 用${label}对 ${row.victimCount} 人造成 ${row.damage} 点伤害。`,
|
|
124
|
+
subject: { id: row.playerId, label: row.playerName },
|
|
125
|
+
sample: { label: "单次道具伤害事件", numerator: row.damage },
|
|
126
|
+
baseline: null,
|
|
127
|
+
evidence: [row],
|
|
128
|
+
basis: ["使用导出的伤害事件聚合。"],
|
|
129
|
+
limitations: ["伤害事件不证明该道具导致了回合胜负,也不构成最佳点位或战术建议。"],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function findingFromUtilityFlash(row: EnemyFlashIncident & { playerId?: string; playerName: string }): AnalysisFinding {
|
|
134
|
+
return systemFinding({
|
|
135
|
+
key: `utility:flash:${row.matchId}:${row.roundNumber}:${row.tick ?? 0}:${row.playerId ?? row.playerName}`,
|
|
136
|
+
capability: "utility",
|
|
137
|
+
title: "闪光贡献证据",
|
|
138
|
+
statement: `${row.playerName} 在 R${row.roundNumber} 致盲 ${row.victimCount} 名敌方,共 ${row.enemySeconds.toFixed(1)} 秒。`,
|
|
139
|
+
subject: { id: row.playerId, label: row.playerName },
|
|
140
|
+
sample: { label: "单颗闪光事件", numerator: row.enemySeconds },
|
|
141
|
+
baseline: null,
|
|
142
|
+
evidence: [row],
|
|
143
|
+
basis: ["按同一闪光事件归并敌方致盲秒数。"],
|
|
144
|
+
limitations: ["致盲时长不证明这颗闪光导致了击杀或回合胜负。"],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Tactical Cluster 只陈述开局/进点模式与代表回合,不推断完整战术意图或最佳反制。 */
|
|
149
|
+
export function findingFromTacticalCluster(cluster: TacticalCluster): AnalysisFinding | null {
|
|
150
|
+
const representative = cluster.rounds[0];
|
|
151
|
+
if (!representative) return null;
|
|
152
|
+
const sample = `${cluster.mapName} ${cluster.side.toUpperCase()} ${cluster.economyEntry}`;
|
|
153
|
+
return systemFinding({
|
|
154
|
+
key: `tactical:${cluster.id}`,
|
|
155
|
+
capability: "tactical",
|
|
156
|
+
title: "开局模式证据",
|
|
157
|
+
statement: `${cluster.teamName} 在 ${sample} 样本中出现 ${cluster.roundCount} 回合相同开局结构。`,
|
|
158
|
+
subject: { id: cluster.teamIdentity, label: cluster.teamName },
|
|
159
|
+
sample: {
|
|
160
|
+
label: sample,
|
|
161
|
+
numerator: cluster.roundCount,
|
|
162
|
+
coverage: `${cluster.entryEvidence.coveredRounds}/${cluster.entryEvidence.totalRounds} 回合有真实进点入口证据`,
|
|
163
|
+
},
|
|
164
|
+
baseline: null,
|
|
165
|
+
evidence: [{
|
|
166
|
+
matchId: representative.matchId,
|
|
167
|
+
roundNumber: representative.roundNumber,
|
|
168
|
+
reason: "该回合是此开局结构的代表样本",
|
|
169
|
+
role: "example",
|
|
170
|
+
}],
|
|
171
|
+
basis: ["按默认位开局结构与真实进点事实聚类。"],
|
|
172
|
+
limitations: ["该聚类不识别完整中期战术、佯攻、转点意图或最佳反制。"],
|
|
173
|
+
});
|
|
174
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -4,16 +4,49 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { analyzeDemoPackage, loadDemoPackageFromZip } from "@cs2dak/core";
|
|
5
5
|
import { buildDemoViewModel, buildMatchWorkspaceModel } from "./index";
|
|
6
6
|
|
|
7
|
+
const fixturePath = fileURLToPath(new URL("../../../fixtures/input/sample-2026-05-17_de_ancient_Team_Spirit_13-10_Team_Falcons.zip", import.meta.url));
|
|
8
|
+
const workspaceFixture = (async () => {
|
|
9
|
+
const pkg = await loadDemoPackageFromZip(await readFile(fixturePath));
|
|
10
|
+
return {
|
|
11
|
+
pkg,
|
|
12
|
+
view: buildDemoViewModel(analyzeDemoPackage(pkg)),
|
|
13
|
+
workspace: buildMatchWorkspaceModel(pkg)
|
|
14
|
+
};
|
|
15
|
+
})();
|
|
16
|
+
|
|
7
17
|
describe("@cs2dak/presentation", () => {
|
|
8
18
|
it("builds view and workspace models from canonical core analysis", async () => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(view.scoreline).toBe("3:13");
|
|
15
|
-
expect(workspace.title).toBe("她还爱我对不 队 vs 車一进一宝贝队");
|
|
16
|
-
expect(workspace.rounds).toHaveLength(16);
|
|
19
|
+
const { view, workspace } = await workspaceFixture;
|
|
20
|
+
|
|
21
|
+
expect(view.scoreline).toBe("13:10");
|
|
22
|
+
expect(workspace.title).toBe("Team Spirit vs Team Falcons");
|
|
23
|
+
expect(workspace.rounds).toHaveLength(23);
|
|
17
24
|
expect(workspace.replay.available).toBe(true);
|
|
18
25
|
});
|
|
26
|
+
|
|
27
|
+
it("uses round-persistent loadout facts for replay weapons and utility", async () => {
|
|
28
|
+
const { pkg, workspace } = await workspaceFixture;
|
|
29
|
+
const economy = pkg.playerEconomies.find((row) => row.roundNumber === 2 && row.primaryWeapon && row.grenadeCount > 0);
|
|
30
|
+
expect(economy).toBeTruthy();
|
|
31
|
+
|
|
32
|
+
const sourcePlayer = pkg.players[economy!.playerIndex];
|
|
33
|
+
const replayRound = workspace.replay.rounds.find((row) => row.roundNumber === economy!.roundNumber);
|
|
34
|
+
const replayPlayer = replayRound?.players.find((player) => player.steamId64 === sourcePlayer?.steamId64);
|
|
35
|
+
|
|
36
|
+
expect(replayPlayer?.loadout.primaryWeapon).toBe(economy!.primaryWeapon);
|
|
37
|
+
expect(replayPlayer?.loadout.secondaryWeapon).toBe(economy!.secondaryWeapon);
|
|
38
|
+
expect(replayPlayer?.loadout.grenadeCount).toBe(economy!.grenadeCount);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("uses next round start tick as replay target end when available", async () => {
|
|
42
|
+
const { pkg, workspace } = await workspaceFixture;
|
|
43
|
+
const firstReplayRound = workspace.replay.rounds[0]!;
|
|
44
|
+
const firstPackageRound = pkg.rounds.find((round) => round.roundNumber === firstReplayRound.roundNumber)!;
|
|
45
|
+
const nextPackageRound = pkg.rounds
|
|
46
|
+
.filter((round) => round.startTick > firstPackageRound.startTick)
|
|
47
|
+
.sort((a, b) => a.startTick - b.startTick)[0]!;
|
|
48
|
+
|
|
49
|
+
expect(firstReplayRound.targetEndTick).toBe(nextPackageRound.startTick);
|
|
50
|
+
expect(firstReplayRound.freezeEndTick).toBe(firstPackageRound.freezeEndTick);
|
|
51
|
+
});
|
|
19
52
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,94 @@
|
|
|
1
1
|
export { buildDemoViewModel, buildMatchWorkspaceModel } from "./workspace.js";
|
|
2
|
+
export { buildOpeningTrails, type OpeningTrailsOptions } from "./trails.js";
|
|
2
3
|
export { buildSeasonLeaderboardModel } from "./leaderboard.js";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
4
|
+
export { buildAllPlayerSeasonProfiles } from "./player.js";
|
|
5
|
+
export { buildPlayerMapRoleProfiles, buildTeamMapRoleMatrices } from "./map-roles.js";
|
|
6
|
+
export { buildDoubleAwpAnalyses, type DoubleAwpAnalysisOptions } from "./double-awp.js";
|
|
7
|
+
export { buildPlayerMapPool, type PlayerMapPerformanceInput } from "./player-map-pool.js";
|
|
8
|
+
export {
|
|
9
|
+
buildTeamComparison,
|
|
10
|
+
buildTeamComparisonFromFacts,
|
|
11
|
+
buildTeamOverviewFromFacts,
|
|
12
|
+
extractTeamComparisonFacts,
|
|
13
|
+
type TeamComparisonFacts,
|
|
14
|
+
type TeamComparisonModel,
|
|
15
|
+
type TeamOverviewModel
|
|
16
|
+
} from "./team.js";
|
|
17
|
+
export { buildSeriesSummary } from "./series.js";
|
|
18
|
+
export {
|
|
19
|
+
buildDuelInsights,
|
|
20
|
+
buildDuelInsightsFromFacts,
|
|
21
|
+
extractDuelInsightsFacts,
|
|
22
|
+
duelClassificationLabel,
|
|
23
|
+
weaponCategory,
|
|
24
|
+
CATEGORY_METRICS,
|
|
25
|
+
mechanicsMetricsForWeapon
|
|
26
|
+
} from "./duel.js";
|
|
27
|
+
export type { DuelInsightsFacts, MechanicsMetricKey, WeaponCategory } from "./duel.js";
|
|
28
|
+
export { SEASON_STAT_VIEWS, formatPercent } from "./season-metrics.js";
|
|
29
|
+
export {
|
|
30
|
+
buildPlayerSeasonInsights,
|
|
31
|
+
buildUtilityValueSummary,
|
|
32
|
+
mergeUtilityValueSummaries,
|
|
33
|
+
buildPlayerMechanicsProfile,
|
|
34
|
+
buildPlayerMechanicsProfileFromRows,
|
|
35
|
+
buildPlayerWeaponStats,
|
|
36
|
+
buildPlayerFlashSummaries,
|
|
37
|
+
buildMatchBuyQuality,
|
|
38
|
+
buildMatchReportMarkdown,
|
|
39
|
+
type SeasonInsightsDemo,
|
|
40
|
+
type PlayerSeasonInsights,
|
|
41
|
+
type PlayerMechanicsProfile,
|
|
42
|
+
type PlayerMechanicsWeaponProfile,
|
|
43
|
+
type PlayerWeaponStat,
|
|
44
|
+
type PlayerFlashSummary,
|
|
45
|
+
type UtilityValueSummary,
|
|
46
|
+
type UtilityValueRow,
|
|
47
|
+
type UtilityDamageEvidence,
|
|
48
|
+
type UtilityDamageKind,
|
|
49
|
+
type PlayerTrendPoint,
|
|
50
|
+
type FlashValueSummary,
|
|
51
|
+
type TeamFlashIncident,
|
|
52
|
+
type EnemyFlashIncident,
|
|
53
|
+
type MistakeReview,
|
|
54
|
+
type MistakeEvidence,
|
|
55
|
+
type MatchBuyQuality,
|
|
56
|
+
} from "./insights.js";
|
|
57
|
+
export {
|
|
58
|
+
buildTournamentInsights,
|
|
59
|
+
buildTournamentInsightsFromFacts,
|
|
60
|
+
extractTournamentFacts,
|
|
61
|
+
type TournamentEcoUpsetStat,
|
|
62
|
+
type TournamentEconomyMatrixCell,
|
|
63
|
+
type TournamentFacts,
|
|
64
|
+
type TournamentInsights,
|
|
65
|
+
type TournamentManAdvantageStat,
|
|
66
|
+
type TournamentMapStat,
|
|
67
|
+
type TournamentTeamEconomySummary,
|
|
68
|
+
type TournamentTeamManAdvantageStat,
|
|
69
|
+
type TournamentTeamManAdvantageState,
|
|
70
|
+
type TournamentTeamPistolStat,
|
|
71
|
+
type TournamentWeaponStat,
|
|
72
|
+
} from "./tournament-compat.js";
|
|
7
73
|
export { displayWeaponName } from "./weapons.js";
|
|
8
74
|
export { economyLabelCn, ECONOMY_LABEL_SHORT } from "./economy.js";
|
|
9
|
-
export {
|
|
10
|
-
|
|
75
|
+
export {
|
|
76
|
+
buildMistakeFindings,
|
|
77
|
+
findingFromDuel,
|
|
78
|
+
findingFromUtilityDamage,
|
|
79
|
+
findingFromUtilityFlash,
|
|
80
|
+
findingFromTacticalCluster,
|
|
81
|
+
type AnalysisFinding,
|
|
82
|
+
type FindingOrigin,
|
|
83
|
+
type FindingSubject,
|
|
84
|
+
} from "./findings.js";
|
|
85
|
+
export * from "./tactical-labels.js";
|
|
86
|
+
export * from "./replay-clock.js";
|
|
87
|
+
export {
|
|
88
|
+
RADAR_FIELD_MODES,
|
|
89
|
+
radarModeFrame,
|
|
90
|
+
radarFieldRoundCount,
|
|
91
|
+
type RadarFieldMode,
|
|
92
|
+
type RadarModeOption,
|
|
93
|
+
type RadarModeFrame
|
|
94
|
+
} from "./radar-field.js";
|