@cs2dak/core 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 +4 -3
- package/src/duel-window.ts +199 -0
- package/src/duels.test.ts +370 -0
- package/src/duels.ts +538 -0
- package/src/fixture-invariants.test.ts +40 -0
- package/src/index.test.ts +46 -53
- package/src/index.ts +25 -8
- package/src/loader.ts +31 -17
- package/src/map-intelligence/awp.test.ts +57 -0
- package/src/map-intelligence/awp.ts +45 -0
- package/src/map-intelligence/ct-rotation.test.ts +149 -0
- package/src/map-intelligence/ct-rotation.ts +324 -0
- package/src/map-intelligence/index.ts +74 -0
- package/src/map-intelligence/map-intelligence.test.ts +62 -0
- package/src/map-intelligence/opening-window.ts +19 -0
- package/src/map-intelligence/player-position.test.ts +28 -0
- package/src/map-intelligence/player-position.ts +250 -0
- package/src/map-intelligence/spatial.test.ts +25 -0
- package/src/map-intelligence/spatial.ts +112 -0
- package/src/map-intelligence/team-awp-round.ts +60 -0
- package/src/map-intelligence/team-shape.test.ts +43 -0
- package/src/map-intelligence/team-shape.ts +58 -0
- package/src/mechanics.test.ts +375 -0
- package/src/mechanics.ts +628 -0
- package/src/normalize.ts +27 -1
- package/src/qa.test.ts +115 -0
- package/src/qa.ts +51 -15
- package/src/radar-field.test.ts +79 -0
- package/src/radar-field.ts +395 -0
- package/src/resolve.test.ts +100 -0
- package/src/resolve.ts +69 -0
- package/src/scoreboard.ts +68 -38
- package/src/signals.ts +149 -112
- package/src/spatial/annotate.test.ts +133 -0
- package/src/spatial/annotate.ts +163 -0
- package/src/spatial/index.ts +16 -0
- package/src/spatial/mapcontrol.test.ts +131 -0
- package/src/spatial/mapcontrol.ts +277 -0
- package/src/spatial/phase.test.ts +171 -0
- package/src/spatial/phase.ts +179 -0
- package/src/spatial/trade-closure.test.ts +38 -0
- package/src/spatial/types.ts +56 -0
- package/src/spatial/utility-geometry.test.ts +88 -0
- package/src/spatial/utility-geometry.ts +167 -0
- package/src/spatial/utility.integration.test.ts +38 -0
- package/src/spatial/utility.test.ts +120 -0
- package/src/spatial/utility.ts +399 -0
- package/src/tactics/formations.ts +151 -0
- package/src/tactics/index.ts +16 -0
- package/src/tactics/replay-round-context.ts +96 -0
- package/src/tactics/round-facts.ts +406 -0
- package/src/tactics/segments.ts +68 -0
- package/src/tactics/tactics.test.ts +112 -0
- package/src/tactics/types.ts +73 -0
- package/src/timeline.ts +73 -52
- package/src/utility-facts.test.ts +55 -0
- package/src/utility-facts.ts +137 -0
- package/src/utils.ts +27 -25
- package/src/weapon-highlights.ts +4 -4
- package/src/economy.test.ts +0 -37
- package/src/economy.ts +0 -57
package/src/scoreboard.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computeRR,
|
|
3
3
|
computePrism,
|
|
4
|
-
|
|
4
|
+
hltv2BaselineWeightsV1,
|
|
5
5
|
prismWeightsV1,
|
|
6
6
|
rrToPercentile,
|
|
7
7
|
type RRResult,
|
|
@@ -10,17 +10,19 @@ import {
|
|
|
10
10
|
type PrismComputeInput
|
|
11
11
|
} from "@rivalhub/rival-rating";
|
|
12
12
|
import type {
|
|
13
|
-
AccountSignalsV2,
|
|
14
13
|
DemoPackage,
|
|
15
14
|
PlayerIndicatorRow,
|
|
16
15
|
PlayerRoundFact,
|
|
17
16
|
PlayerScoreboardRow,
|
|
18
17
|
RRIndicators
|
|
19
18
|
} from "@cs2dak/contract";
|
|
20
|
-
import type {
|
|
19
|
+
import type { RRSignals } from "@rivalhub/rival-rating";
|
|
20
|
+
import type { AccountRatingResult } from "./signals.js";
|
|
21
21
|
import { normalizeDemoPackage } from "./normalize.js";
|
|
22
|
-
import { round, firstKillMap, clutchSplit,
|
|
22
|
+
import { activeDamages, round, firstKillMap, clutchSplit, killWeaponName } from "./utils.js";
|
|
23
|
+
import { createResolverFromPackage } from "./resolve.js";
|
|
23
24
|
import { fieldAvailability, fieldConfidence } from "./qa.js";
|
|
25
|
+
import { buildPlayerRoundUtilityFacts } from "./utility-facts.js";
|
|
24
26
|
|
|
25
27
|
export function deriveRRIndicators(input: unknown): RRIndicators[] {
|
|
26
28
|
const pkg = normalizeDemoPackage(input);
|
|
@@ -28,22 +30,32 @@ export function deriveRRIndicators(input: unknown): RRIndicators[] {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export function buildPlayerRoundFacts(pkg: DemoPackage): PlayerRoundFact[] {
|
|
33
|
+
const resolver = createResolverFromPackage(pkg);
|
|
31
34
|
const firstKillByRound = firstKillMap(pkg);
|
|
35
|
+
const damageRows = activeDamages(pkg);
|
|
36
|
+
const utilityFacts = new Map(buildPlayerRoundUtilityFacts(pkg).map((fact) => [`${fact.roundNumber}:${fact.steamId64}`, fact]));
|
|
32
37
|
|
|
33
38
|
return pkg.rounds.flatMap((roundRow) =>
|
|
34
|
-
pkg.players.map((player) => {
|
|
35
|
-
const kills = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.
|
|
36
|
-
const deaths = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.
|
|
37
|
-
const assists = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
39
|
+
pkg.players.map((player, playerIdx) => {
|
|
40
|
+
const kills = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.killerIndex === playerIdx);
|
|
41
|
+
const deaths = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.victimIndex === playerIdx);
|
|
42
|
+
const assists = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.assisterIndex === playerIdx);
|
|
43
|
+
// Keep the existing PlayerRoundFact assist/KAST semantics (the
|
|
44
|
+
// flashAssists field itself is supplied by the shared utility helper).
|
|
45
|
+
const flashAssistRows = pkg.kills.filter((kill) => kill.roundNumber === roundRow.roundNumber && kill.flashAssisterIndex === playerIdx);
|
|
46
|
+
const utility = utilityFacts.get(`${roundRow.roundNumber}:${player.steamId64}`)!;
|
|
47
|
+
const playerDamageRows = damageRows.filter((row) =>
|
|
48
|
+
row.roundNumber === roundRow.roundNumber &&
|
|
49
|
+
row.attackerIndex === playerIdx &&
|
|
50
|
+
resolver.byIndexOrNull(row.victimIndex)?.teamKey !== player.teamKey
|
|
51
|
+
);
|
|
52
|
+
const economy = pkg.playerEconomies.find((row) => row.roundNumber === roundRow.roundNumber && row.playerIndex === playerIdx);
|
|
41
53
|
const side = player.teamKey === "teamA" ? roundRow.teamASide : roundRow.teamBSide;
|
|
42
54
|
const firstKill = firstKillByRound.get(roundRow.roundNumber);
|
|
43
55
|
const kastTags = new Set<PlayerRoundFact["kastTags"][number]>();
|
|
44
56
|
|
|
45
57
|
if (kills.length > 0) kastTags.add("kill");
|
|
46
|
-
if (assists.length > 0 ||
|
|
58
|
+
if (assists.length > 0 || flashAssistRows.length > 0) kastTags.add("assist");
|
|
47
59
|
if (deaths.length === 0) kastTags.add("survive");
|
|
48
60
|
if (deaths.some((death) => death.tradeDeath)) kastTags.add("trade");
|
|
49
61
|
|
|
@@ -56,13 +68,13 @@ export function buildPlayerRoundFacts(pkg: DemoPackage): PlayerRoundFact[] {
|
|
|
56
68
|
survived: deaths.length === 0,
|
|
57
69
|
kills: kills.length,
|
|
58
70
|
deaths: deaths.length,
|
|
59
|
-
assists: assists.length +
|
|
60
|
-
damage:
|
|
61
|
-
utilityDamage:
|
|
62
|
-
flashAssists: flashAssists
|
|
71
|
+
assists: assists.length + flashAssistRows.length,
|
|
72
|
+
damage: playerDamageRows.reduce((sum, row) => sum + row.healthDamage, 0),
|
|
73
|
+
utilityDamage: utility.utilityDamage,
|
|
74
|
+
flashAssists: utility.flashAssists,
|
|
63
75
|
tradeKills: kills.filter((kill) => kill.tradeKill).length,
|
|
64
76
|
tradedDeaths: deaths.filter((death) => death.tradeDeath).length,
|
|
65
|
-
openingDuel: firstKill?.
|
|
77
|
+
openingDuel: firstKill?.killerIndex === playerIdx ? "won" : firstKill?.victimIndex === playerIdx ? "lost" : "none",
|
|
66
78
|
kastTags: [...kastTags],
|
|
67
79
|
equipmentValue: economy?.equipmentValue ?? null,
|
|
68
80
|
economyType: economy?.type ?? null
|
|
@@ -72,14 +84,30 @@ export function buildPlayerRoundFacts(pkg: DemoPackage): PlayerRoundFact[] {
|
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
export function buildPlayerIndicators(pkg: DemoPackage, facts: PlayerRoundFact[]): PlayerIndicatorRow[] {
|
|
75
|
-
const
|
|
76
|
-
|
|
87
|
+
const statsMap = new Map(pkg.playerStats.map((row) => [row.playerIndex, row]));
|
|
88
|
+
const utilityTotals = new Map<string, {
|
|
89
|
+
utilityDamage: number;
|
|
90
|
+
flashAssists: number;
|
|
91
|
+
enemyBlindSeconds: number;
|
|
92
|
+
teamBlindSeconds: number;
|
|
93
|
+
}>();
|
|
94
|
+
for (const fact of buildPlayerRoundUtilityFacts(pkg)) {
|
|
95
|
+
const total = utilityTotals.get(fact.steamId64) ?? { utilityDamage: 0, flashAssists: 0, enemyBlindSeconds: 0, teamBlindSeconds: 0 };
|
|
96
|
+
total.utilityDamage += fact.utilityDamage;
|
|
97
|
+
total.flashAssists += fact.flashAssists;
|
|
98
|
+
total.enemyBlindSeconds += fact.enemyBlindSeconds;
|
|
99
|
+
total.teamBlindSeconds += fact.teamBlindSeconds;
|
|
100
|
+
utilityTotals.set(fact.steamId64, total);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const indicators = pkg.players.map((player, playerIdx) => {
|
|
104
|
+
const stats = statsMap.get(playerIdx);
|
|
77
105
|
const playerFacts = facts.filter((fact) => fact.steamId64 === player.steamId64);
|
|
78
|
-
const playerKills = pkg.kills.filter((kill) => kill.
|
|
79
|
-
const playerDeaths = pkg.kills.filter((kill) => kill.
|
|
80
|
-
const playerEconomies = pkg.playerEconomies.filter((row) => row.
|
|
81
|
-
const playerClutches = pkg.clutches.filter((row) => row.
|
|
82
|
-
const
|
|
106
|
+
const playerKills = pkg.kills.filter((kill) => kill.killerIndex === playerIdx);
|
|
107
|
+
const playerDeaths = pkg.kills.filter((kill) => kill.victimIndex === playerIdx);
|
|
108
|
+
const playerEconomies = pkg.playerEconomies.filter((row) => row.playerIndex === playerIdx);
|
|
109
|
+
const playerClutches = pkg.clutches.filter((row) => row.clutcherIndex === playerIdx);
|
|
110
|
+
const utility = utilityTotals.get(player.steamId64) ?? { utilityDamage: 0, flashAssists: 0, enemyBlindSeconds: 0, teamBlindSeconds: 0 };
|
|
83
111
|
const totalRounds = Math.max(playerFacts.length, 1);
|
|
84
112
|
const killsByRound = new Map<number, number>();
|
|
85
113
|
for (const kill of playerKills) {
|
|
@@ -91,15 +119,11 @@ export function buildPlayerIndicators(pkg: DemoPackage, facts: PlayerRoundFact[]
|
|
|
91
119
|
const openingDuels = firstKillCount + firstDeathCount;
|
|
92
120
|
const awpKills = playerKills.filter((kill) => killWeaponName(kill) === "awp").length;
|
|
93
121
|
const sniperKills = playerKills.filter((kill) => ["awp", "ssg08", "scout"].includes(killWeaponName(kill))).length;
|
|
94
|
-
const utilityDamage = stats?.utilityDamage ??
|
|
95
|
-
const flashAssistCount = stats?.flashAssistCount ??
|
|
96
|
-
const enemyFlashDurationSeconds = stats?.enemyFlashDurationSeconds ??
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const teamFlashDurationSeconds = stats?.teamFlashDurationSeconds ?? playerBlinds
|
|
100
|
-
.filter((blind) => blind.flashedTeamKey === player.teamKey && blind.flashedSteamId64 !== player.steamId64)
|
|
101
|
-
.reduce((sum, blind) => sum + blind.durationSeconds, 0);
|
|
102
|
-
const grenadeCount = pkg.grenades.filter((grenade) => grenade.throwerSteamId64 === player.steamId64).length;
|
|
122
|
+
const utilityDamage = stats?.utilityDamage ?? utility.utilityDamage;
|
|
123
|
+
const flashAssistCount = stats?.flashAssistCount ?? utility.flashAssists;
|
|
124
|
+
const enemyFlashDurationSeconds = stats?.enemyFlashDurationSeconds ?? utility.enemyBlindSeconds;
|
|
125
|
+
const teamFlashDurationSeconds = stats?.teamFlashDurationSeconds ?? utility.teamBlindSeconds;
|
|
126
|
+
const grenadeCount = pkg.grenades.filter((grenade) => grenade.throwerIndex === playerIdx).length;
|
|
103
127
|
const deaths = stats?.deaths ?? playerDeaths.length;
|
|
104
128
|
const kills = stats?.kills ?? playerKills.length;
|
|
105
129
|
const assists = stats?.assists ?? playerFacts.reduce((sum, fact) => sum + fact.assists, 0);
|
|
@@ -185,7 +209,7 @@ export function buildPlayerIndicators(pkg: DemoPackage, facts: PlayerRoundFact[]
|
|
|
185
209
|
} satisfies RRIndicators;
|
|
186
210
|
});
|
|
187
211
|
|
|
188
|
-
const rrWeights =
|
|
212
|
+
const rrWeights = hltv2BaselineWeightsV1 as unknown as RRWeights;
|
|
189
213
|
const prismWeights = prismWeightsV1 as unknown as PrismWeights;
|
|
190
214
|
const rrResults = indicators.map((indicator) => computeRR(indicator, rrWeights));
|
|
191
215
|
const rrScores = rrResults.map((result) => result.rr);
|
|
@@ -213,22 +237,27 @@ export function buildPlayerIndicators(pkg: DemoPackage, facts: PlayerRoundFact[]
|
|
|
213
237
|
export function buildScoreboard(
|
|
214
238
|
pkg: DemoPackage,
|
|
215
239
|
rows: PlayerIndicatorRow[],
|
|
216
|
-
accountRatings: Array<{ signals:
|
|
240
|
+
accountRatings: Array<{ signals: RRSignals; rr: AccountRatingResult }>
|
|
217
241
|
): PlayerScoreboardRow[] {
|
|
242
|
+
const resolver = createResolverFromPackage(pkg);
|
|
218
243
|
const accountBySteamId = new Map(accountRatings.map((row) => [row.signals.steamId64, row]));
|
|
219
|
-
const
|
|
244
|
+
const statsMap = new Map(pkg.playerStats.map((row) => [row.playerIndex, row]));
|
|
220
245
|
const availability = fieldAvailability(pkg);
|
|
221
246
|
const confidence = fieldConfidence(availability);
|
|
222
247
|
return rows.map((row) => {
|
|
223
248
|
const account = accountBySteamId.get(row.steamId64);
|
|
224
249
|
const accountRr = account?.rr;
|
|
225
250
|
const combatSignals = account?.signals.combat;
|
|
226
|
-
const
|
|
227
|
-
const
|
|
251
|
+
const playerIdx = resolver.indexOfSteamId(row.steamId64);
|
|
252
|
+
const stats = playerIdx != null ? statsMap.get(playerIdx) : undefined;
|
|
253
|
+
const playerKills = playerIdx != null
|
|
254
|
+
? pkg.kills.filter((kill) => kill.killerIndex === playerIdx)
|
|
255
|
+
: [];
|
|
228
256
|
return {
|
|
229
257
|
steamId64: row.steamId64,
|
|
230
258
|
name: row.name,
|
|
231
259
|
teamKey: row.teamKey,
|
|
260
|
+
indicators: row.indicators,
|
|
232
261
|
kills: row.indicators.kills,
|
|
233
262
|
deaths: row.indicators.deaths,
|
|
234
263
|
assists: row.indicators.assists,
|
|
@@ -258,6 +287,7 @@ export function buildScoreboard(
|
|
|
258
287
|
accountBreakdown: {
|
|
259
288
|
combat: round(accountRr?.accounts.combat ?? 0, 4),
|
|
260
289
|
trade: round(accountRr?.accounts.trade ?? 0, 4),
|
|
290
|
+
mapControl: round(accountRr?.accounts.mapControl ?? 0, 4),
|
|
261
291
|
clutch: round(accountRr?.accounts.clutch ?? 0, 4),
|
|
262
292
|
objective: round(accountRr?.accounts.objective ?? 0, 4),
|
|
263
293
|
utility: round(accountRr?.accounts.utility ?? 0, 4)
|
package/src/signals.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
computeFrozenProBaselineRR,
|
|
3
|
+
computeRRSixAccounts,
|
|
4
|
+
rrSixAccountProBaselineV0,
|
|
5
|
+
rrSixAccountWeightsV1
|
|
5
6
|
} from "@rivalhub/rival-rating";
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
7
|
+
import type { DemoPackage, RRSignals, RRSixAccountWeights } from "@cs2dak/contract";
|
|
8
|
+
import type { CohortAccountResult, ProBaselineConfig, RRSixAccountResult } from "@rivalhub/rival-rating";
|
|
8
9
|
import { normalizeDemoPackage } from "./normalize.js";
|
|
10
|
+
import { loadSpatialAssets } from "./spatial/annotate.js";
|
|
11
|
+
import { buildOfficialMapControl } from "./spatial/mapcontrol.js";
|
|
12
|
+
import { createResolverFromPackage, type PlayerResolver } from "./resolve.js";
|
|
13
|
+
import { buildPlayerRoundUtilityFacts } from "./utility-facts.js";
|
|
9
14
|
import {
|
|
10
15
|
type BuyDeltaBuckets,
|
|
11
16
|
type ManStateBuckets,
|
|
@@ -15,7 +20,7 @@ import {
|
|
|
15
20
|
getOrInit,
|
|
16
21
|
zeroBuyDelta,
|
|
17
22
|
zeroManState,
|
|
18
|
-
|
|
23
|
+
clutchSplit,
|
|
19
24
|
sumDamageForPlayer,
|
|
20
25
|
openingKillsForPlayer,
|
|
21
26
|
openingDeathsForPlayer,
|
|
@@ -24,25 +29,31 @@ import {
|
|
|
24
29
|
firstKillMap
|
|
25
30
|
} from "./utils.js";
|
|
26
31
|
|
|
27
|
-
export
|
|
32
|
+
export type AccountRatingResult = CohortAccountResult & Pick<RRSixAccountResult, "combatContextFactor" | "weightsVersion" | "model">;
|
|
33
|
+
|
|
34
|
+
export function deriveRRSignals(input: unknown): RRSignals[] {
|
|
28
35
|
const pkg = normalizeDemoPackage(input);
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// 而非零桶——零桶语义是"源在、但该选手无相关样本"。区分二者是 v2 可信展示的前提。
|
|
36
|
+
const resolver = createResolverFromPackage(pkg);
|
|
37
|
+
const statsMap = new Map(pkg.playerStats.map((row) => [row.playerIndex, row]));
|
|
38
|
+
const killsByBuyDelta = buildKillsByBuyDelta(pkg, resolver);
|
|
39
|
+
const killsByManState = buildKillsByManState(pkg, resolver);
|
|
40
|
+
const tradedOpeningDeaths = buildTradedOpeningDeaths(pkg, resolver);
|
|
41
|
+
const objective = buildObjectiveSignals(pkg, resolver);
|
|
42
|
+
const utility = buildUtilitySignals(pkg, statsMap);
|
|
43
|
+
|
|
38
44
|
const buyDeltaAvailable = pkg.playerEconomies.length > 0;
|
|
39
45
|
const manStateAvailable = pkg.rounds.length > 0;
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const spatialAssets = loadSpatialAssets(pkg.match?.mapName ?? pkg.manifest?.mapName ?? "");
|
|
48
|
+
const officialMapControl = buildOfficialMapControl(pkg, spatialAssets);
|
|
49
|
+
// replay-based spatial re-impl (step 3)
|
|
50
|
+
const spatialObservable = spatialAssets.routes != null && (pkg.replay?.rounds?.length ?? 0) > 0;
|
|
51
|
+
|
|
52
|
+
return pkg.players.map((player, playerIdx) => {
|
|
53
|
+
const stats = statsMap.get(playerIdx);
|
|
54
|
+
const playerKills = pkg.kills.filter((kill) => kill.killerIndex === playerIdx);
|
|
55
|
+
const playerDeaths = pkg.kills.filter((kill) => kill.victimIndex === playerIdx);
|
|
56
|
+
const playerClutches = pkg.clutches.filter((row) => row.clutcherIndex === playerIdx);
|
|
46
57
|
const rounds = Math.max(stats?.rounds ?? pkg.rounds.length, 0);
|
|
47
58
|
|
|
48
59
|
return {
|
|
@@ -53,9 +64,9 @@ export function deriveAccountSignalsV2(input: unknown): AccountSignalsV2[] {
|
|
|
53
64
|
kills: stats?.kills ?? playerKills.length,
|
|
54
65
|
deaths: stats?.deaths ?? playerDeaths.length,
|
|
55
66
|
assists: stats?.assists ?? 0,
|
|
56
|
-
effectiveDamage: stats?.damageHealth ?? sumDamageForPlayer(pkg,
|
|
57
|
-
openingKills: stats?.firstKillCount ?? openingKillsForPlayer(pkg,
|
|
58
|
-
openingDeaths: stats?.firstDeathCount ?? openingDeathsForPlayer(pkg,
|
|
67
|
+
effectiveDamage: stats?.damageHealth ?? sumDamageForPlayer(pkg, playerIdx),
|
|
68
|
+
openingKills: stats?.firstKillCount ?? openingKillsForPlayer(pkg, playerIdx),
|
|
69
|
+
openingDeaths: stats?.firstDeathCount ?? openingDeathsForPlayer(pkg, playerIdx),
|
|
59
70
|
multiKills: {
|
|
60
71
|
two: stats?.twoKillCount ?? multiKillRounds(playerKills, 2),
|
|
61
72
|
three: stats?.threeKillCount ?? multiKillRounds(playerKills, 3),
|
|
@@ -71,55 +82,78 @@ export function deriveAccountSignalsV2(input: unknown): AccountSignalsV2[] {
|
|
|
71
82
|
tradeKills: stats?.tradeKillCount ?? playerKills.filter((kill) => kill.tradeKill).length,
|
|
72
83
|
tradedDeaths: stats?.tradeDeathCount ?? playerDeaths.filter((kill) => kill.tradeDeath).length,
|
|
73
84
|
deaths: stats?.deaths ?? playerDeaths.length,
|
|
74
|
-
tradedOpeningDeaths: tradedOpeningDeaths.get(player.steamId64) ?? 0
|
|
85
|
+
tradedOpeningDeaths: tradedOpeningDeaths.get(player.steamId64) ?? 0,
|
|
86
|
+
strategicIsolationDeaths: spatialObservable
|
|
87
|
+
? (officialMapControl.get(player.steamId64)?.strategicIsolationDeaths ?? 0)
|
|
88
|
+
: null
|
|
89
|
+
},
|
|
90
|
+
mapControl: {
|
|
91
|
+
uniqueStrategicControlSeconds: null,
|
|
92
|
+
contestedFrontierControlSeconds: null,
|
|
93
|
+
routeDenialSeconds: null,
|
|
94
|
+
teammateAdvanceUnits: null,
|
|
95
|
+
firstControlEvents: null
|
|
75
96
|
},
|
|
76
97
|
clutch: {
|
|
77
|
-
vsOne:
|
|
78
|
-
vsTwo:
|
|
79
|
-
vsThree:
|
|
80
|
-
vsFour:
|
|
81
|
-
vsFive:
|
|
98
|
+
vsOne: clutchSplit(stats?.vsOneCount, stats?.vsOneWonCount, playerClutches, 1),
|
|
99
|
+
vsTwo: clutchSplit(stats?.vsTwoCount, stats?.vsTwoWonCount, playerClutches, 2),
|
|
100
|
+
vsThree: clutchSplit(stats?.vsThreeCount, stats?.vsThreeWonCount, playerClutches, 3),
|
|
101
|
+
vsFour: clutchSplit(stats?.vsFourCount, stats?.vsFourWonCount, playerClutches, 4),
|
|
102
|
+
vsFive: clutchSplit(stats?.vsFiveCount, stats?.vsFiveWonCount, playerClutches, 5)
|
|
82
103
|
},
|
|
83
104
|
objective: objective.get(player.steamId64) ?? { plants: 0, defuses: 0, plantsConverted: 0 },
|
|
84
|
-
utility:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
utility: {
|
|
106
|
+
...(utility.get(player.steamId64) ?? {
|
|
107
|
+
flashAssists: stats?.flashAssistCount ?? 0,
|
|
108
|
+
effectiveEnemyFlashSeconds: stats?.enemyFlashDurationSeconds ?? 0,
|
|
109
|
+
teamFlashSuppressionSeconds: stats?.teamFlashDurationSeconds ?? 0,
|
|
110
|
+
smokeProtectedCrossings: null,
|
|
111
|
+
smokeSightlineDenialSeconds: null,
|
|
112
|
+
smokeIsolationSeconds: null,
|
|
113
|
+
incendiaryPathDelayUnits: null,
|
|
114
|
+
incendiaryDisplacementEvents: null,
|
|
115
|
+
utilityDamage: stats?.utilityDamage ?? 0
|
|
116
|
+
})
|
|
89
117
|
}
|
|
90
|
-
} satisfies
|
|
118
|
+
} satisfies RRSignals;
|
|
91
119
|
});
|
|
92
120
|
}
|
|
93
121
|
|
|
94
|
-
export function computeAccountRatingsV2(input: unknown): Array<{ signals:
|
|
95
|
-
const weights =
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
export function computeAccountRatingsV2(input: unknown): Array<{ signals: RRSignals; rr: AccountRatingResult }> {
|
|
123
|
+
const weights = rrSixAccountWeightsV1 as unknown as RRSixAccountWeights;
|
|
124
|
+
const baseline = rrSixAccountProBaselineV0 as unknown as ProBaselineConfig;
|
|
125
|
+
const signals = deriveRRSignals(input);
|
|
126
|
+
const rawBySteamId = new Map(signals.map((row) => [row.steamId64, computeRRSixAccounts(row, weights)]));
|
|
127
|
+
const balanced = signals.map((signal) => computeFrozenProBaselineRR(signal, weights, baseline));
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
return balanced.map((rr, index) => {
|
|
130
|
+
const signal = signals[index]!;
|
|
131
|
+
const raw = rawBySteamId.get(signal.steamId64);
|
|
132
|
+
return {
|
|
133
|
+
signals: signal,
|
|
134
|
+
rr: {
|
|
135
|
+
...rr,
|
|
136
|
+
combatContextFactor: raw?.combatContextFactor ?? 1,
|
|
137
|
+
weightsVersion: raw?.weightsVersion ?? weights.version,
|
|
138
|
+
model: raw?.model ?? "rr-six-accounts"
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
});
|
|
110
142
|
}
|
|
111
143
|
|
|
112
|
-
function buildKillsByBuyDelta(pkg: DemoPackage): Map<string, BuyDeltaBuckets> {
|
|
144
|
+
function buildKillsByBuyDelta(pkg: DemoPackage, resolver: PlayerResolver): Map<string, BuyDeltaBuckets> {
|
|
113
145
|
const out = new Map<string, BuyDeltaBuckets>();
|
|
114
|
-
const economyByPlayerRound = new Map(pkg.playerEconomies.map((row) => [`${row.roundNumber}:${row.
|
|
146
|
+
const economyByPlayerRound = new Map(pkg.playerEconomies.map((row) => [`${row.roundNumber}:${row.playerIndex}`, row]));
|
|
115
147
|
|
|
116
148
|
for (const kill of pkg.kills) {
|
|
117
|
-
if (
|
|
118
|
-
const killerEconomy = economyByPlayerRound.get(`${kill.roundNumber}:${kill.
|
|
119
|
-
const victimEconomy = economyByPlayerRound.get(`${kill.roundNumber}:${kill.
|
|
149
|
+
if (kill.killerIndex === null) continue;
|
|
150
|
+
const killerEconomy = economyByPlayerRound.get(`${kill.roundNumber}:${kill.killerIndex}`);
|
|
151
|
+
const victimEconomy = economyByPlayerRound.get(`${kill.roundNumber}:${kill.victimIndex}`);
|
|
120
152
|
if (!killerEconomy || !victimEconomy) continue;
|
|
121
153
|
|
|
122
|
-
const
|
|
154
|
+
const killerSteamId = resolver.steamIdOf(kill.killerIndex);
|
|
155
|
+
if (!killerSteamId) continue;
|
|
156
|
+
const buckets = getOrInit(out, killerSteamId, zeroBuyDelta);
|
|
123
157
|
const delta = killerEconomy.equipmentValue - victimEconomy.equipmentValue;
|
|
124
158
|
if (delta <= -BUY_DELTA_EVEN_THRESHOLD) {
|
|
125
159
|
buckets.disadvantage += 1;
|
|
@@ -133,63 +167,68 @@ function buildKillsByBuyDelta(pkg: DemoPackage): Map<string, BuyDeltaBuckets> {
|
|
|
133
167
|
return out;
|
|
134
168
|
}
|
|
135
169
|
|
|
136
|
-
function buildKillsByManState(pkg: DemoPackage): Map<string, ManStateBuckets> {
|
|
170
|
+
function buildKillsByManState(pkg: DemoPackage, resolver: PlayerResolver): Map<string, ManStateBuckets> {
|
|
137
171
|
const out = new Map<string, ManStateBuckets>();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
teamB: pkg.players.filter((player) => player.teamKey === "teamB").map((player) => player.steamId64)
|
|
141
|
-
};
|
|
172
|
+
const teamAIndices = new Set(pkg.players.flatMap((p, i) => p.teamKey === "teamA" ? [i] : []));
|
|
173
|
+
const teamBIndices = new Set(pkg.players.flatMap((p, i) => p.teamKey === "teamB" ? [i] : []));
|
|
142
174
|
|
|
143
175
|
for (const roundRow of pkg.rounds) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
teamB: new Set(playersByTeam.teamB)
|
|
147
|
-
};
|
|
176
|
+
const aliveA = new Set(teamAIndices);
|
|
177
|
+
const aliveB = new Set(teamBIndices);
|
|
148
178
|
const roundKills = pkg.kills
|
|
149
179
|
.filter((kill) => kill.roundNumber === roundRow.roundNumber)
|
|
150
180
|
.sort((a, b) => a.tick - b.tick);
|
|
151
181
|
|
|
152
182
|
for (const kill of roundKills) {
|
|
153
|
-
if (kill.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
if (kill.killerIndex === null) continue;
|
|
184
|
+
const killerPlayer = resolver.byIndexOrNull(kill.killerIndex);
|
|
185
|
+
const victimPlayer = resolver.byIndexOrNull(kill.victimIndex);
|
|
186
|
+
if (!killerPlayer || !victimPlayer || killerPlayer.teamKey === victimPlayer.teamKey) continue;
|
|
187
|
+
|
|
188
|
+
const killerSteamId = killerPlayer.steamId64;
|
|
189
|
+
const buckets = getOrInit(out, killerSteamId, zeroManState);
|
|
190
|
+
const killerAlive = killerPlayer.teamKey === "teamA" ? aliveA.size : aliveB.size;
|
|
191
|
+
const victimAlive = victimPlayer.teamKey === "teamA" ? aliveA.size : aliveB.size;
|
|
192
|
+
const diff = killerAlive - victimAlive;
|
|
193
|
+
if (diff < 0) {
|
|
194
|
+
buckets.manDown += 1;
|
|
195
|
+
} else if (diff > 0) {
|
|
196
|
+
buckets.manUp += 1;
|
|
197
|
+
} else {
|
|
198
|
+
buckets.even += 1;
|
|
165
199
|
}
|
|
166
|
-
|
|
200
|
+
|
|
201
|
+
if (victimPlayer.teamKey === "teamA") aliveA.delete(kill.victimIndex);
|
|
202
|
+
else aliveB.delete(kill.victimIndex);
|
|
167
203
|
}
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
return out;
|
|
171
207
|
}
|
|
172
208
|
|
|
173
|
-
function buildTradedOpeningDeaths(pkg: DemoPackage): Map<string, number> {
|
|
209
|
+
function buildTradedOpeningDeaths(pkg: DemoPackage, resolver: PlayerResolver): Map<string, number> {
|
|
174
210
|
const out = new Map<string, number>();
|
|
175
211
|
for (const kill of firstKillMap(pkg).values()) {
|
|
176
212
|
if (kill.tradeDeath) {
|
|
177
|
-
|
|
213
|
+
const victimSteamId = resolver.steamIdOf(kill.victimIndex);
|
|
214
|
+
if (victimSteamId) out.set(victimSteamId, (out.get(victimSteamId) ?? 0) + 1);
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
return out;
|
|
181
218
|
}
|
|
182
219
|
|
|
183
|
-
function buildObjectiveSignals(pkg: DemoPackage): Map<string, ObjectiveBuckets> {
|
|
220
|
+
function buildObjectiveSignals(pkg: DemoPackage, resolver: PlayerResolver): Map<string, ObjectiveBuckets> {
|
|
184
221
|
const out = new Map<string, ObjectiveBuckets>();
|
|
185
222
|
const roundWinner = new Map(pkg.rounds.map((round) => [round.roundNumber, round.winnerTeamKey]));
|
|
186
223
|
|
|
187
224
|
for (const bomb of pkg.bombs) {
|
|
188
|
-
if (
|
|
189
|
-
const
|
|
225
|
+
if (bomb.actorIndex === null) continue;
|
|
226
|
+
const actorPlayer = resolver.byIndexOrNull(bomb.actorIndex);
|
|
227
|
+
if (!actorPlayer) continue;
|
|
228
|
+
const buckets = getOrInit(out, actorPlayer.steamId64, () => ({ plants: 0, defuses: 0, plantsConverted: 0 }));
|
|
190
229
|
if (bomb.type === "planted") {
|
|
191
230
|
buckets.plants += 1;
|
|
192
|
-
if (
|
|
231
|
+
if (roundWinner.get(bomb.roundNumber) === actorPlayer.teamKey) {
|
|
193
232
|
buckets.plantsConverted = (buckets.plantsConverted ?? 0) + 1;
|
|
194
233
|
}
|
|
195
234
|
} else if (bomb.type === "defused") {
|
|
@@ -200,33 +239,31 @@ function buildObjectiveSignals(pkg: DemoPackage): Map<string, ObjectiveBuckets>
|
|
|
200
239
|
return out;
|
|
201
240
|
}
|
|
202
241
|
|
|
203
|
-
function buildUtilitySignals(pkg: DemoPackage): Map<string, UtilityBuckets> {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
utilityDamage: stats?.utilityDamage ?? 0
|
|
214
|
-
});
|
|
242
|
+
function buildUtilitySignals(pkg: DemoPackage, statsMap: Map<number, DemoPackage["playerStats"][number]>): Map<string, UtilityBuckets> {
|
|
243
|
+
const utilityFacts = buildPlayerRoundUtilityFacts(pkg);
|
|
244
|
+
const totals = new Map<string, { flashAssists: number; enemyBlindSeconds: number; teamBlindSeconds: number; utilityDamage: number }>();
|
|
245
|
+
for (const fact of utilityFacts) {
|
|
246
|
+
const total = totals.get(fact.steamId64) ?? { flashAssists: 0, enemyBlindSeconds: 0, teamBlindSeconds: 0, utilityDamage: 0 };
|
|
247
|
+
total.flashAssists += fact.flashAssists;
|
|
248
|
+
total.enemyBlindSeconds += fact.enemyBlindSeconds;
|
|
249
|
+
total.teamBlindSeconds += fact.teamBlindSeconds;
|
|
250
|
+
total.utilityDamage += fact.utilityDamage;
|
|
251
|
+
totals.set(fact.steamId64, total);
|
|
215
252
|
}
|
|
216
253
|
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
return new Map(pkg.players.map((player, playerIdx) => {
|
|
255
|
+
const stats = statsMap.get(playerIdx);
|
|
256
|
+
const fact = totals.get(player.steamId64) ?? { flashAssists: 0, enemyBlindSeconds: 0, teamBlindSeconds: 0, utilityDamage: 0 };
|
|
257
|
+
return [player.steamId64, {
|
|
258
|
+
flashAssists: stats?.flashAssistCount ?? fact.flashAssists,
|
|
259
|
+
effectiveEnemyFlashSeconds: stats?.enemyFlashDurationSeconds ?? round(fact.enemyBlindSeconds, 3),
|
|
260
|
+
teamFlashSuppressionSeconds: stats?.teamFlashDurationSeconds ?? round(fact.teamBlindSeconds, 3),
|
|
261
|
+
smokeProtectedCrossings: null,
|
|
262
|
+
smokeSightlineDenialSeconds: null,
|
|
263
|
+
smokeIsolationSeconds: null,
|
|
264
|
+
incendiaryPathDelayUnits: null,
|
|
265
|
+
incendiaryDisplacementEvents: null,
|
|
266
|
+
utilityDamage: stats?.utilityDamage ?? fact.utilityDamage
|
|
267
|
+
} satisfies UtilityBuckets];
|
|
268
|
+
}));
|
|
232
269
|
}
|