@cs2dak/contract 1.0.0 → 1.1.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 +3 -3
- package/src/analysis.ts +11 -5
- package/src/bracket.ts +68 -0
- package/src/cohort.ts +2 -1
- package/src/contract.test.ts +75 -10
- package/src/demo-package.ts +2 -2
- package/src/dependency-boundaries.test.ts +27 -0
- package/src/duel.ts +105 -0
- package/src/event-package.test.ts +48 -0
- package/src/event-package.ts +163 -0
- package/src/evidence.ts +22 -0
- package/src/index.ts +9 -0
- package/src/map-intelligence.ts +202 -0
- package/src/map-role.ts +317 -0
- package/src/player.ts +15 -4
- package/src/radar-field.ts +79 -0
- package/src/scoring.ts +9 -1
- package/src/trails.ts +56 -0
- package/src/upstream.ts +27 -7
- package/src/veto.ts +44 -0
- package/src/workspace.ts +184 -10
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { seriesVetoSchema } from "./veto.js";
|
|
3
|
+
|
|
4
|
+
export const eventBracketNodeSchema = z.object({
|
|
5
|
+
id: z.string().min(1),
|
|
6
|
+
label: z.string().min(1),
|
|
7
|
+
round: z.number().int().positive(),
|
|
8
|
+
lane: z.enum(["single", "winner", "loser", "grand"]).default("single"),
|
|
9
|
+
nextWinNodeId: z.string().nullable().optional(),
|
|
10
|
+
nextLossNodeId: z.string().nullable().optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const eventStageSchema = z.object({
|
|
14
|
+
key: z.string().min(1),
|
|
15
|
+
name: z.string().min(1),
|
|
16
|
+
type: z.enum(["round_robin", "swiss", "single_elim", "double_elim", "gsl_group"]),
|
|
17
|
+
teamCount: z.number().int().positive(),
|
|
18
|
+
advanceCount: z.number().int().nonnegative().default(0),
|
|
19
|
+
matchFormat: z.enum(["bo1", "bo3", "bo5"]).optional(),
|
|
20
|
+
finalFormat: z.enum(["bo3", "bo5"]).optional(),
|
|
21
|
+
bracketNodes: z.array(eventBracketNodeSchema).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const eventTeamSchema = z.object({
|
|
25
|
+
key: z.string().min(1),
|
|
26
|
+
name: z.string().min(1),
|
|
27
|
+
players: z.array(z.object({
|
|
28
|
+
name: z.string().min(1),
|
|
29
|
+
steamId64: z.string().nullable().optional(),
|
|
30
|
+
})).default([]),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const eventMapSchema = z.object({
|
|
34
|
+
order: z.number().int().positive(),
|
|
35
|
+
mapName: z.string().min(1),
|
|
36
|
+
pickedBy: z.enum(["teamA", "teamB"]).nullable().optional(),
|
|
37
|
+
teamAStartSide: z.enum(["t", "ct"]).nullable().optional(),
|
|
38
|
+
scoreA: z.number().int().nonnegative().nullable().optional(),
|
|
39
|
+
scoreB: z.number().int().nonnegative().nullable().optional(),
|
|
40
|
+
demoHint: z.object({
|
|
41
|
+
fileName: z.string().nullable().optional(),
|
|
42
|
+
sha256: z.string().nullable().optional(),
|
|
43
|
+
}).nullable().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const rawDemoHintSchema = z.object({
|
|
47
|
+
downloadUrl: z.string().url().nullable().optional(),
|
|
48
|
+
fileName: z.string().nullable().optional(),
|
|
49
|
+
}).nullable().optional();
|
|
50
|
+
|
|
51
|
+
export const eventSeriesSchema = z.object({
|
|
52
|
+
key: z.string().min(1),
|
|
53
|
+
stage: z.string().nullable().optional(),
|
|
54
|
+
round: z.number().int().nonnegative().nullable().optional(),
|
|
55
|
+
entryRound: z.string().nullable().optional(),
|
|
56
|
+
bracketNodeId: z.string().nullable().optional(),
|
|
57
|
+
status: z.enum(["scheduled", "in_progress", "finished", "cancelled"]).default("scheduled"),
|
|
58
|
+
teamARecordBefore: z.string().nullable().optional(),
|
|
59
|
+
teamBRecordBefore: z.string().nullable().optional(),
|
|
60
|
+
format: z.enum(["bo1", "bo3", "bo5"]),
|
|
61
|
+
matchUrl: z.string().url().nullable().optional(), // 该系列来源页(HLTV match 页),展示层可链回
|
|
62
|
+
rawDemoHint: rawDemoHintSchema, // 可选:打包侧已知的原始 demo 包直链(如 HLTV r2-demos rar)
|
|
63
|
+
teamAKey: z.string().min(1),
|
|
64
|
+
teamBKey: z.string().min(1),
|
|
65
|
+
scoreA: z.number().int().nonnegative().nullable().optional(),
|
|
66
|
+
scoreB: z.number().int().nonnegative().nullable().optional(),
|
|
67
|
+
scheduledAt: z.string().datetime().nullable().optional(),
|
|
68
|
+
completedAt: z.string().datetime().nullable().optional(),
|
|
69
|
+
veto: seriesVetoSchema.nullable().optional(),
|
|
70
|
+
maps: z.array(eventMapSchema).default([]),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const eventPackageSchema = z.object({
|
|
74
|
+
version: z.literal("cs2-demo-analysis-kit/event-package-1.0"),
|
|
75
|
+
source: z.enum(["rivalhub", "manual", "r2"]),
|
|
76
|
+
exportedAt: z.string().datetime(),
|
|
77
|
+
event: z.object({
|
|
78
|
+
slug: z.string().min(1),
|
|
79
|
+
name: z.string().min(1),
|
|
80
|
+
kind: z.string().min(1),
|
|
81
|
+
sourceUrl: z.string().url().optional(), // 赛事来源页(如 HLTV results 页),展示层可链回
|
|
82
|
+
group: z.string().optional(), // 共享归组键:同一赛事按 stage 拆多包时共用,Gallery 折叠
|
|
83
|
+
stages: z.array(eventStageSchema).default([]),
|
|
84
|
+
}),
|
|
85
|
+
teams: z.array(eventTeamSchema),
|
|
86
|
+
series: z.array(eventSeriesSchema),
|
|
87
|
+
}).superRefine((value, context) => {
|
|
88
|
+
const teamKeys = new Set(value.teams.map((team) => team.key));
|
|
89
|
+
const stageKeys = new Set(value.event.stages.map((stage) => stage.key));
|
|
90
|
+
const seriesKeys = new Set(value.series.map((series) => series.key));
|
|
91
|
+
if (teamKeys.size !== value.teams.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["teams"], message: "队伍 key 必须唯一" });
|
|
92
|
+
if (stageKeys.size !== value.event.stages.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["event", "stages"], message: "阶段 key 必须唯一" });
|
|
93
|
+
if (seriesKeys.size !== value.series.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["series"], message: "系列赛 key 必须唯一" });
|
|
94
|
+
for (const [stageIndex, stage] of value.event.stages.entries()) {
|
|
95
|
+
const nodes = stage.bracketNodes ?? [];
|
|
96
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
97
|
+
if (nodeIds.size !== nodes.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["event", "stages", stageIndex, "bracketNodes"], message: "bracket 节点 id 必须唯一" });
|
|
98
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
99
|
+
for (const target of [node.nextWinNodeId, node.nextLossNodeId]) {
|
|
100
|
+
if (target && !nodeIds.has(target)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["event", "stages", stageIndex, "bracketNodes", nodeIndex], message: "bracket 晋级关系引用了不存在的节点" });
|
|
101
|
+
const targetNode = target ? nodes.find((candidate) => candidate.id === target) : null;
|
|
102
|
+
if (targetNode && targetNode.round <= node.round) context.addIssue({ code: z.ZodIssueCode.custom, path: ["event", "stages", stageIndex, "bracketNodes", nodeIndex], message: "bracket 晋级关系必须指向后续轮次" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const visiting = new Set<string>();
|
|
106
|
+
const visited = new Set<string>();
|
|
107
|
+
const visit = (id: string): boolean => {
|
|
108
|
+
if (visiting.has(id)) return true;
|
|
109
|
+
if (visited.has(id)) return false;
|
|
110
|
+
visiting.add(id);
|
|
111
|
+
const node = nodes.find((candidate) => candidate.id === id);
|
|
112
|
+
const cyclic = [node?.nextWinNodeId, node?.nextLossNodeId].some((target) => target ? visit(target) : false);
|
|
113
|
+
visiting.delete(id);
|
|
114
|
+
visited.add(id);
|
|
115
|
+
return cyclic;
|
|
116
|
+
};
|
|
117
|
+
if (nodes.some((node) => visit(node.id))) context.addIssue({ code: z.ZodIssueCode.custom, path: ["event", "stages", stageIndex, "bracketNodes"], message: "bracket 晋级关系不能成环" });
|
|
118
|
+
}
|
|
119
|
+
for (const [index, series] of value.series.entries()) {
|
|
120
|
+
if (!teamKeys.has(series.teamAKey) || !teamKeys.has(series.teamBKey)) {
|
|
121
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["series", index], message: "series 引用了不存在的队伍" });
|
|
122
|
+
}
|
|
123
|
+
if (series.stage && !stageKeys.has(series.stage)) {
|
|
124
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["series", index, "stage"], message: "series 引用了不存在的阶段" });
|
|
125
|
+
}
|
|
126
|
+
if (series.bracketNodeId) {
|
|
127
|
+
const stage = value.event.stages.find((candidate) => candidate.key === series.stage);
|
|
128
|
+
if (!stage?.bracketNodes?.some((node) => node.id === series.bracketNodeId)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["series", index, "bracketNodeId"], message: "series 引用了不存在的 bracket 节点" });
|
|
129
|
+
}
|
|
130
|
+
if (series.teamAKey === series.teamBKey) {
|
|
131
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["series", index], message: "系列赛双方不能是同一队" });
|
|
132
|
+
}
|
|
133
|
+
if (new Set(series.maps.map((map) => map.order)).size !== series.maps.length) {
|
|
134
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["series", index, "maps"], message: "系列赛地图顺序必须唯一" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export type EventPackage = z.infer<typeof eventPackageSchema>;
|
|
140
|
+
export type EventStage = z.infer<typeof eventStageSchema>;
|
|
141
|
+
export type EventBracketNode = z.infer<typeof eventBracketNodeSchema>;
|
|
142
|
+
export type EventTeam = z.infer<typeof eventTeamSchema>;
|
|
143
|
+
export type EventSeries = z.infer<typeof eventSeriesSchema>;
|
|
144
|
+
export type RawDemoHint = z.infer<typeof rawDemoHintSchema>;
|
|
145
|
+
|
|
146
|
+
export const eventsManifestSchema = z.object({
|
|
147
|
+
version: z.literal("cs2-demo-analysis-kit/events-manifest-1.0"),
|
|
148
|
+
generatedAt: z.string().datetime(),
|
|
149
|
+
events: z.array(z.object({
|
|
150
|
+
slug: z.string().min(1),
|
|
151
|
+
name: z.string().min(1),
|
|
152
|
+
size: z.number().int().nonnegative(),
|
|
153
|
+
sha256: z.string().regex(/^[a-fA-F0-9]{64}$/),
|
|
154
|
+
urls: z.array(z.string().url()).min(1),
|
|
155
|
+
packageVersion: z.string().min(1),
|
|
156
|
+
// 展示层可选字段(向后兼容;旧 manifest 不带也合法):
|
|
157
|
+
description: z.string().optional(), // 卡片副标题文案
|
|
158
|
+
builtin: z.boolean().optional(), // 首发/内置:进入页面提示一键下载
|
|
159
|
+
group: z.string().optional(), // UI 归组键(同 group 折叠成一个赛事、展开见各阶段,如 iem-cologne-major-2026)
|
|
160
|
+
})),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export type EventsManifest = z.infer<typeof eventsManifestSchema>;
|
package/src/evidence.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 可定位的分析证据。
|
|
5
|
+
*
|
|
6
|
+
* 这是跨 core/cohort/presentation 的纯数据合同;它只说明要看哪一段比赛、
|
|
7
|
+
* 以及这一段为何与结果有关,不携带 Studio 导航或持久化语义。
|
|
8
|
+
*/
|
|
9
|
+
export const evidenceRoleSchema = z.enum(["example", "supporting", "counterexample"]);
|
|
10
|
+
|
|
11
|
+
export const evidenceRefSchema = z.object({
|
|
12
|
+
matchId: z.string().min(1),
|
|
13
|
+
roundNumber: z.number().int().positive(),
|
|
14
|
+
tick: z.number().int().positive().optional(),
|
|
15
|
+
eventKey: z.string().min(1).optional(),
|
|
16
|
+
areaKey: z.string().min(1).optional(),
|
|
17
|
+
reason: z.string().min(1),
|
|
18
|
+
role: evidenceRoleSchema.optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type EvidenceRole = z.infer<typeof evidenceRoleSchema>;
|
|
22
|
+
export type EvidenceRef = z.infer<typeof evidenceRefSchema>;
|
package/src/index.ts
CHANGED
|
@@ -9,3 +9,12 @@ export * from "./player.js";
|
|
|
9
9
|
export * from "./team.js";
|
|
10
10
|
export * from "./series.js";
|
|
11
11
|
export * from "./workspace.js";
|
|
12
|
+
export * from "./trails.js";
|
|
13
|
+
export * from "./duel.js";
|
|
14
|
+
export * from "./veto.js";
|
|
15
|
+
export * from "./event-package.js";
|
|
16
|
+
export * from "./bracket.js";
|
|
17
|
+
export * from "./radar-field.js";
|
|
18
|
+
export * from "./evidence.js";
|
|
19
|
+
export * from "./map-intelligence.js";
|
|
20
|
+
export * from "./map-role.js";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { playerIndexSchema, sideSchema, steamId64Schema, teamKeySchema } from "./upstream.js";
|
|
3
|
+
|
|
4
|
+
/** Bump when the compact map-intelligence producer changes its factual meaning. */
|
|
5
|
+
export const MAP_INTELLIGENCE_FACT_VERSION = 6;
|
|
6
|
+
export const OPENING_RESPONSIBILITY_WINDOW_VERSION = 1;
|
|
7
|
+
export const CT_ROTATION_FACT_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
const availabilitySchema = z.enum(["available", "degraded", "missing"]);
|
|
10
|
+
|
|
11
|
+
export const mapIntelligenceAvailabilitySchema = z.object({
|
|
12
|
+
replay: availabilitySchema,
|
|
13
|
+
nav: availabilitySchema,
|
|
14
|
+
callouts: availabilitySchema,
|
|
15
|
+
shots: availabilitySchema,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const tacticalRegionSchema = z.enum(["a", "b", "mid"]);
|
|
19
|
+
export const ctRotationAvailabilitySchema = mapIntelligenceAvailabilitySchema.extend({
|
|
20
|
+
combatTimeline: availabilitySchema,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const positionGroupDwellSchema = z.object({
|
|
24
|
+
positionGroupId: z.string().min(1),
|
|
25
|
+
seconds: z.number().nonnegative(),
|
|
26
|
+
share: z.number().min(0).max(1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const responsibilityWindowSchema = z.object({
|
|
30
|
+
version: z.literal(OPENING_RESPONSIBILITY_WINDOW_VERSION),
|
|
31
|
+
startTick: z.number().int().nonnegative(),
|
|
32
|
+
endTick: z.number().int().nonnegative(),
|
|
33
|
+
configuredSeconds: z.number().positive(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const openingPathPointSchema = z.object({
|
|
37
|
+
tick: z.number().int().nonnegative(),
|
|
38
|
+
callout: z.string().nullable(),
|
|
39
|
+
positionGroupId: z.string().nullable(),
|
|
40
|
+
x: z.number(), y: z.number(), z: z.number(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** A compact, locatable period where a player was separated from their team component. */
|
|
44
|
+
export const isolationSegmentSchema = z.object({
|
|
45
|
+
startTick: z.number().int().nonnegative(),
|
|
46
|
+
endTick: z.number().int().nonnegative(),
|
|
47
|
+
seconds: z.number().nonnegative(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** A conservative isolated-to-stable-group transition inside one round. */
|
|
51
|
+
export const delayedConvergenceSchema = z.object({
|
|
52
|
+
tick: z.number().int().nonnegative(),
|
|
53
|
+
priorIsolationSeconds: z.number().nonnegative(),
|
|
54
|
+
joinedComponentSize: z.number().int().min(3),
|
|
55
|
+
persistenceSeconds: z.number().positive(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const playerPositionRoundFactSchema = z.object({
|
|
59
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
60
|
+
matchId: z.string().min(1),
|
|
61
|
+
mapName: z.string().min(1),
|
|
62
|
+
roundNumber: z.number().int().positive(),
|
|
63
|
+
teamKey: teamKeySchema,
|
|
64
|
+
side: sideSchema,
|
|
65
|
+
playerIndex: playerIndexSchema,
|
|
66
|
+
steamId64: steamId64Schema,
|
|
67
|
+
economyType: z.enum(["pistol", "eco", "semi", "force", "full"]).nullable(),
|
|
68
|
+
openingWindow: responsibilityWindowSchema.nullable(),
|
|
69
|
+
openingEligibleSeconds: z.number().nonnegative().nullable(),
|
|
70
|
+
openingPositionGroupDwell: z.array(positionGroupDwellSchema),
|
|
71
|
+
openingMeanComponentSize: z.number().positive().nullable(),
|
|
72
|
+
openingIsolationSeconds: z.number().nonnegative().nullable(),
|
|
73
|
+
openingUtilityUseCount: z.number().int().nonnegative(),
|
|
74
|
+
openingPath: z.array(openingPathPointSchema).max(8),
|
|
75
|
+
/** Full-round movement/action coverage, from freeze end until death or round end. */
|
|
76
|
+
eligibleSeconds: z.number().nonnegative().nullable(),
|
|
77
|
+
positionGroupDwell: z.array(positionGroupDwellSchema),
|
|
78
|
+
unresolvedCalloutSeconds: z.number().nonnegative().nullable(),
|
|
79
|
+
calloutCoverage: z.number().min(0).max(1).nullable(),
|
|
80
|
+
meanNearestTeammateDistance: z.number().nonnegative().nullable(),
|
|
81
|
+
meanTeamCentroidDistance: z.number().nonnegative().nullable(),
|
|
82
|
+
meanComponentSize: z.number().positive().nullable(),
|
|
83
|
+
isolationSegments: z.array(isolationSegmentSchema),
|
|
84
|
+
rejoinTicks: z.array(z.number().int().nonnegative()),
|
|
85
|
+
delayedConvergences: z.array(delayedConvergenceSchema),
|
|
86
|
+
movementSync: z.number().min(-1).max(1).nullable(),
|
|
87
|
+
utilityUseCount: z.number().int().nonnegative(),
|
|
88
|
+
freezeAwpOwnership: z.boolean().nullable(),
|
|
89
|
+
activeAwpSeconds: z.number().nonnegative().nullable(),
|
|
90
|
+
awpShots: z.number().int().nonnegative().nullable(),
|
|
91
|
+
awpKills: z.number().int().nonnegative().nullable(),
|
|
92
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const teamShapeWindowSchema = z.object({
|
|
96
|
+
startTick: z.number().int().nonnegative(),
|
|
97
|
+
endTick: z.number().int().nonnegative(),
|
|
98
|
+
coverageSeconds: z.number().nonnegative(),
|
|
99
|
+
/** Descending component sizes, e.g. [4, 1] for a 4+1. */
|
|
100
|
+
componentSizes: z.array(z.number().int().positive()).min(1),
|
|
101
|
+
partition: z.string().min(1),
|
|
102
|
+
componentPlayerIndices: z.array(z.array(playerIndexSchema).min(1)).min(1),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const teamShapeRoundFactSchema = z.object({
|
|
106
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
107
|
+
matchId: z.string().min(1),
|
|
108
|
+
mapName: z.string().min(1),
|
|
109
|
+
roundNumber: z.number().int().positive(),
|
|
110
|
+
teamKey: teamKeySchema,
|
|
111
|
+
side: sideSchema,
|
|
112
|
+
openingWindow: responsibilityWindowSchema.nullable(),
|
|
113
|
+
openingWindows: z.array(teamShapeWindowSchema),
|
|
114
|
+
/** Full-round component continuity windows. */
|
|
115
|
+
coverageSeconds: z.number().nonnegative().nullable(),
|
|
116
|
+
windows: z.array(teamShapeWindowSchema),
|
|
117
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const teamAwpRoundFactSchema = z.object({
|
|
121
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
122
|
+
matchId: z.string().min(1),
|
|
123
|
+
mapName: z.string().min(1),
|
|
124
|
+
roundNumber: z.number().int().positive(),
|
|
125
|
+
teamKey: teamKeySchema,
|
|
126
|
+
side: sideSchema,
|
|
127
|
+
economyType: z.enum(["pistol", "eco", "semi", "force", "full"]),
|
|
128
|
+
opponentEconomyType: z.enum(["pistol", "eco", "semi", "force", "full"]),
|
|
129
|
+
scorePhase: z.enum(["first_half", "second_half", "overtime"]),
|
|
130
|
+
won: z.boolean(),
|
|
131
|
+
roundStartAwpPlayerIndices: z.array(playerIndexSchema),
|
|
132
|
+
doubleAwpActiveSeconds: z.number().nonnegative().nullable(),
|
|
133
|
+
awpActiveSeconds: z.number().nonnegative().nullable(),
|
|
134
|
+
awpShots: z.number().int().nonnegative().nullable(),
|
|
135
|
+
awpKills: z.number().int().nonnegative().nullable(),
|
|
136
|
+
awpDamage: z.number().nonnegative().nullable(),
|
|
137
|
+
openingKills: z.number().int().nonnegative(),
|
|
138
|
+
openingDeaths: z.number().int().nonnegative(),
|
|
139
|
+
availability: mapIntelligenceAvailabilitySchema,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/** One CT player-round observation. It records response facts, never a role label. */
|
|
143
|
+
export const ctRotationRoundFactSchema = z.object({
|
|
144
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
145
|
+
factVersion: z.literal(CT_ROTATION_FACT_VERSION),
|
|
146
|
+
matchId: z.string().min(1),
|
|
147
|
+
mapName: z.string().min(1),
|
|
148
|
+
roundNumber: z.number().int().positive(),
|
|
149
|
+
teamKey: teamKeySchema,
|
|
150
|
+
side: z.literal("ct"),
|
|
151
|
+
playerIndex: playerIndexSchema,
|
|
152
|
+
steamId64: steamId64Schema,
|
|
153
|
+
initialPositionGroupId: z.string().min(1).nullable(),
|
|
154
|
+
initialRegion: tacticalRegionSchema.nullable(),
|
|
155
|
+
initialPositionGroupShare: z.number().min(0).max(1).nullable(),
|
|
156
|
+
initialResponsibilityResolved: z.boolean(),
|
|
157
|
+
initialWindowEligibleSeconds: z.number().nonnegative().nullable(),
|
|
158
|
+
firstOwnAreaContactTick: z.number().int().nonnegative().nullable(),
|
|
159
|
+
firstOtherAreaContactTick: z.number().int().nonnegative().nullable(),
|
|
160
|
+
firstTeamContactTick: z.number().int().nonnegative().nullable(),
|
|
161
|
+
leftInitialPositionGroupTick: z.number().int().nonnegative().nullable(),
|
|
162
|
+
/** Signed: negative means the stable departure preceded the first observed other-area contact. */
|
|
163
|
+
leaveDelayAfterFirstOtherAreaContactSeconds: z.number().nullable(),
|
|
164
|
+
firstStableDestinationPositionGroupId: z.string().min(1).nullable(),
|
|
165
|
+
firstStableDestinationRegion: tacticalRegionSchema.nullable(),
|
|
166
|
+
crossedResponsibilityArea: z.boolean().nullable(),
|
|
167
|
+
returnedToInitialPositionGroup: z.boolean().nullable(),
|
|
168
|
+
transitToStableDestinationSeconds: z.number().nonnegative().nullable(),
|
|
169
|
+
/** Competition rank by observed departure tick; simultaneous departures share a rank. */
|
|
170
|
+
crossAreaDepartureOrder: z.number().int().positive().nullable(),
|
|
171
|
+
firstCrossAreaDeparture: z.boolean().nullable(),
|
|
172
|
+
priorCrossAreaDeparturesAlive: z.number().int().nonnegative().nullable(),
|
|
173
|
+
initialAreaStillCovered: z.boolean().nullable(),
|
|
174
|
+
deathTick: z.number().int().nonnegative().nullable(),
|
|
175
|
+
censoredByDeath: z.boolean().nullable(),
|
|
176
|
+
roundEndTick: z.number().int().nonnegative(),
|
|
177
|
+
availability: ctRotationAvailabilitySchema,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/** Compact per-match replay-derived map facts. It intentionally contains no role conclusion. */
|
|
181
|
+
export const matchMapIntelligenceFactsSchema = z.object({
|
|
182
|
+
analysisVersion: z.literal(MAP_INTELLIGENCE_FACT_VERSION),
|
|
183
|
+
matchId: z.string().min(1),
|
|
184
|
+
mapName: z.string().min(1),
|
|
185
|
+
playerPositionRounds: z.array(playerPositionRoundFactSchema),
|
|
186
|
+
teamShapeRounds: z.array(teamShapeRoundFactSchema),
|
|
187
|
+
teamAwpRounds: z.array(teamAwpRoundFactSchema),
|
|
188
|
+
ctRotationRounds: z.array(ctRotationRoundFactSchema),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export type MapIntelligenceAvailability = z.infer<typeof mapIntelligenceAvailabilitySchema>;
|
|
192
|
+
export type PositionGroupDwell = z.infer<typeof positionGroupDwellSchema>;
|
|
193
|
+
export type ResponsibilityWindow = z.infer<typeof responsibilityWindowSchema>;
|
|
194
|
+
export type IsolationSegment = z.infer<typeof isolationSegmentSchema>;
|
|
195
|
+
export type DelayedConvergence = z.infer<typeof delayedConvergenceSchema>;
|
|
196
|
+
export type PlayerPositionRoundFact = z.infer<typeof playerPositionRoundFactSchema>;
|
|
197
|
+
export type TeamShapeWindow = z.infer<typeof teamShapeWindowSchema>;
|
|
198
|
+
export type TeamShapeRoundFact = z.infer<typeof teamShapeRoundFactSchema>;
|
|
199
|
+
export type TeamAwpRoundFact = z.infer<typeof teamAwpRoundFactSchema>;
|
|
200
|
+
export type CtRotationAvailability = z.infer<typeof ctRotationAvailabilitySchema>;
|
|
201
|
+
export type CtRotationRoundFact = z.infer<typeof ctRotationRoundFactSchema>;
|
|
202
|
+
export type MatchMapIntelligenceFacts = z.infer<typeof matchMapIntelligenceFactsSchema>;
|