@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/src/trails.ts ADDED
@@ -0,0 +1,119 @@
1
+ import {
2
+ decodeDelta,
3
+ openingTrailsModelSchema,
4
+ type DemoPackage,
5
+ type OpeningTrailRound,
6
+ type OpeningTrailsModel
7
+ } from "@cs2dak/contract";
8
+ import { normalizeDemoPackage } from "@cs2dak/core";
9
+ import { nameForSteamId } from "./workspace-utils.js";
10
+
11
+ type TrailEconomy = OpeningTrailRound["economyType"];
12
+
13
+ export interface OpeningTrailsOptions {
14
+ /** 开局窗口秒数,默认 30。 */
15
+ windowSeconds?: number;
16
+ /** 纳入的经济类型,默认 ["full"](长枪局)。 */
17
+ economyTypes?: TrailEconomy[];
18
+ }
19
+
20
+ /**
21
+ * 提取单选手在指定经济类型回合开局窗口内的走位轨迹与道具投掷。
22
+ * 轨迹来自 replay.json(8Hz),道具来自 grenades.json,回合边界与经济来自 rounds.json。
23
+ * 输出 world 坐标;radar 投影由渲染端用 @cs2dak/maps 完成。
24
+ */
25
+ export function buildOpeningTrails(
26
+ input: unknown,
27
+ matchId: string,
28
+ steamId64: string,
29
+ opts: OpeningTrailsOptions = {}
30
+ ): OpeningTrailsModel {
31
+ const pkg = normalizeDemoPackage(input) as DemoPackage;
32
+ const windowSeconds = opts.windowSeconds ?? 30;
33
+ const economyTypes = opts.economyTypes ?? ["full"];
34
+ const playerName = nameForSteamId(pkg, steamId64) ?? steamId64;
35
+
36
+ const base = {
37
+ version: "cs2-demo-analysis-kit/opening-trails-0.2" as const,
38
+ matchId,
39
+ mapName: pkg.match.mapName,
40
+ steamId64,
41
+ playerName,
42
+ windowSeconds
43
+ };
44
+
45
+ const replay = pkg.replay;
46
+ if (!replay) {
47
+ return openingTrailsModelSchema.parse({ ...base, available: false, rounds: [] });
48
+ }
49
+
50
+ const tickrate = replay.meta.tickrate;
51
+ const roundRowByNumber = new Map(pkg.rounds.map((row) => [row.roundNumber, row]));
52
+
53
+ const rounds = replay.rounds.flatMap((replayRound) => {
54
+ const roundRow = roundRowByNumber.get(replayRound.roundNumber);
55
+ const player = replayRound.players.find((p) => {
56
+ const playerRow = pkg.players[p.playerIndex];
57
+ return playerRow?.steamId64 === steamId64;
58
+ });
59
+ if (!roundRow || !player) return [];
60
+
61
+ const playerRow = pkg.players[player.playerIndex];
62
+ if (!playerRow) return [];
63
+ const economyType = playerRow.teamKey === "teamA" ? roundRow.teamAEconomy : roundRow.teamBEconomy;
64
+ if (!economyTypes.includes(economyType)) return [];
65
+
66
+ const windowStart = roundRow.freezeEndTick;
67
+ const windowEnd = windowStart + windowSeconds * tickrate;
68
+
69
+ const points: { t: number; x: number; y: number }[] = [];
70
+ const xs = decodeDelta(player.x);
71
+ const ys = decodeDelta(player.y);
72
+ const coordScale = replay.meta.coordScale;
73
+ for (let index = 0; index < replayRound.frameCount; index += 1) {
74
+ const tick = replayRound.startTick + index * replayRound.tickStep;
75
+ if (tick < windowStart || tick > windowEnd) continue;
76
+ const alive = ((player.flags[index] ?? 0) & 1) !== 0;
77
+ if (!alive) break; // 阵亡即终止轨迹
78
+ points.push({
79
+ t: (tick - windowStart) / tickrate,
80
+ x: (xs[index] ?? 0) * coordScale,
81
+ y: (ys[index] ?? 0) * coordScale,
82
+ });
83
+ }
84
+ if (points.length === 0) return [];
85
+
86
+ const grenades = pkg.grenades
87
+ .filter(
88
+ (row) =>
89
+ row.roundNumber === replayRound.roundNumber &&
90
+ (pkg.players[row.throwerIndex]?.steamId64 ?? null) === steamId64 &&
91
+ row.throwTick >= windowStart &&
92
+ row.throwTick <= windowEnd
93
+ )
94
+ .map((row) => ({
95
+ t: (row.throwTick - windowStart) / tickrate,
96
+ x: row.throwPosition.x,
97
+ y: row.throwPosition.y,
98
+ grenade: row.grenade,
99
+ effectT: Math.max(0, (row.effectTick - windowStart) / tickrate),
100
+ destroyT: row.destroyTick == null ? null : Math.max(0, (row.destroyTick - windowStart) / tickrate),
101
+ effectX: row.effectPosition.x,
102
+ effectY: row.effectPosition.y
103
+ }))
104
+ .sort((a, b) => a.t - b.t);
105
+
106
+ return [
107
+ {
108
+ matchId,
109
+ roundNumber: replayRound.roundNumber,
110
+ side: playerRow.teamKey === "teamA" ? roundRow.teamASide : roundRow.teamBSide,
111
+ economyType,
112
+ points,
113
+ grenades
114
+ }
115
+ ];
116
+ });
117
+
118
+ return openingTrailsModelSchema.parse({ ...base, available: true, rounds });
119
+ }
package/src/weapons.ts CHANGED
@@ -41,7 +41,7 @@ const WEAPON_DISPLAY_NAMES: Record<string, string> = {
41
41
  cz75a: "CZ75-Auto",
42
42
  cz75: "CZ75-Auto",
43
43
  elite: "Dual Berettas",
44
- // SMGs
44
+ // 冲锋枪
45
45
  mp9: "MP9",
46
46
  mp7: "MP7",
47
47
  mp5sd: "MP5-SD",
@@ -1,21 +1,11 @@
1
1
  import type { DemoPackage } from "@cs2dak/contract";
2
+ export { groupBy } from "@cs2dak/core";
2
3
 
3
- export function round(value: number, digits: number): number {
4
+ export function round(value: number, digits = 3): number {
4
5
  const factor = 10 ** digits;
5
6
  return Math.round(value * factor) / factor;
6
7
  }
7
8
 
8
- export function groupBy<T, K>(rows: T[], keyFor: (row: T) => K): Map<K, T[]> {
9
- const grouped = new Map<K, T[]>();
10
- for (const row of rows) {
11
- const key = keyFor(row);
12
- const current = grouped.get(key);
13
- if (current) current.push(row);
14
- else grouped.set(key, [row]);
15
- }
16
- return grouped;
17
- }
18
-
19
9
  export function normalizeWeapon(weapon: string): string {
20
10
  return weapon.trim().toLowerCase().replace(/^weapon_/, "");
21
11
  }