@ekmanss/5eplay 20260716.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 +21 -0
- package/README.md +184 -0
- package/dist/async_queue.d.ts +6 -0
- package/dist/async_queue.js +44 -0
- package/dist/capture.d.ts +11 -0
- package/dist/capture.js +199 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +42 -0
- package/dist/http.d.ts +23 -0
- package/dist/http.js +75 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/input.d.ts +3 -0
- package/dist/input.js +45 -0
- package/dist/live_matches.d.ts +2 -0
- package/dist/live_matches.js +209 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.js +153 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +508 -0
- package/dist/mqtt.d.ts +29 -0
- package/dist/mqtt.js +285 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.js +169 -0
- package/dist/transform.d.ts +21 -0
- package/dist/transform.js +466 -0
- package/dist/types.d.ts +504 -0
- package/dist/types.js +2 -0
- package/dist/value.d.ts +13 -0
- package/dist/value.js +76 -0
- package/dist/write_markdown.d.ts +11 -0
- package/dist/write_markdown.js +34 -0
- package/docs/data-contracts.md +113 -0
- package/docs/recipes.md +129 -0
- package/package.json +55 -0
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { integer, record, records, text } from './value.js';
|
|
2
|
+
function plain(value) {
|
|
3
|
+
if (value === null || value === undefined || value === '')
|
|
4
|
+
return '—';
|
|
5
|
+
return String(value).replace(/[\r\n]+/g, ' ').trim() || '—';
|
|
6
|
+
}
|
|
7
|
+
function cell(value) {
|
|
8
|
+
return plain(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
9
|
+
}
|
|
10
|
+
function html(value) {
|
|
11
|
+
return plain(value)
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>')
|
|
15
|
+
.replace(/"/g, '"');
|
|
16
|
+
}
|
|
17
|
+
function table(headers, rows) {
|
|
18
|
+
return [
|
|
19
|
+
`| ${headers.map(cell).join(' | ')} |`,
|
|
20
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
21
|
+
...rows.map((row) => `| ${row.map(cell).join(' | ')} |`),
|
|
22
|
+
].join('\n');
|
|
23
|
+
}
|
|
24
|
+
function percent(value) {
|
|
25
|
+
return value === null ? '—' : `${value}%`;
|
|
26
|
+
}
|
|
27
|
+
function number(value) {
|
|
28
|
+
return value === null ? '—' : String(value);
|
|
29
|
+
}
|
|
30
|
+
function yesNo(value) {
|
|
31
|
+
return value === null ? '—' : value ? '是' : '否';
|
|
32
|
+
}
|
|
33
|
+
function timestamp(value) {
|
|
34
|
+
if (value === null)
|
|
35
|
+
return '—';
|
|
36
|
+
const date = new Date(value * 1_000);
|
|
37
|
+
return Number.isNaN(date.valueOf()) ? String(value) : date.toISOString();
|
|
38
|
+
}
|
|
39
|
+
function compactTimestamp(value) {
|
|
40
|
+
if (value === null)
|
|
41
|
+
return '—';
|
|
42
|
+
const date = new Date(value * 1_000);
|
|
43
|
+
return Number.isNaN(date.valueOf())
|
|
44
|
+
? String(value)
|
|
45
|
+
: `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
46
|
+
}
|
|
47
|
+
function status(value) {
|
|
48
|
+
if (value === 'live')
|
|
49
|
+
return '进行中';
|
|
50
|
+
if (value === 'completed')
|
|
51
|
+
return '已完成';
|
|
52
|
+
if (value === 'upcoming')
|
|
53
|
+
return '未开始';
|
|
54
|
+
return '未知';
|
|
55
|
+
}
|
|
56
|
+
function teamName(match, teamId) {
|
|
57
|
+
if (!teamId)
|
|
58
|
+
return '—';
|
|
59
|
+
return match.teams.find((team) => team.id === teamId)?.name ?? teamId;
|
|
60
|
+
}
|
|
61
|
+
function teamScore(match, teamId) {
|
|
62
|
+
return match.match.seriesScore.find((entry) => entry.teamId === teamId)?.score ?? null;
|
|
63
|
+
}
|
|
64
|
+
function halfScore(map, index, half) {
|
|
65
|
+
const value = map.teams[index]?.[half];
|
|
66
|
+
if (!value)
|
|
67
|
+
return '—';
|
|
68
|
+
if (value.side === null && value.score === null)
|
|
69
|
+
return '—';
|
|
70
|
+
return `${number(value.score)}(${value.side ?? '阵营未知'})`;
|
|
71
|
+
}
|
|
72
|
+
function roundSequence(map, index, half) {
|
|
73
|
+
const rounds = map.teams[index]?.[half].roundResults ?? [];
|
|
74
|
+
return rounds.length ? rounds.map((result) => result === 0 ? '负' : '胜').join(' · ') : '—';
|
|
75
|
+
}
|
|
76
|
+
function playerRows(match, groups, includeEquipment) {
|
|
77
|
+
return groups.flatMap((group) => group.players.map((player) => {
|
|
78
|
+
const metrics = player.metrics;
|
|
79
|
+
const base = [
|
|
80
|
+
teamName(match, group.teamId), player.name,
|
|
81
|
+
`${number(metrics.kills)}-${number(metrics.deaths)}-${number(metrics.assists)}`,
|
|
82
|
+
number(metrics.rating), number(metrics.kdRatio), number(metrics.kdDifference),
|
|
83
|
+
percent(metrics.kastRate), number(metrics.adr), percent(metrics.roundSwingRate),
|
|
84
|
+
number(metrics.killsPerRound), number(metrics.deathsPerRound),
|
|
85
|
+
percent(metrics.headshotRate), number(metrics.firstKills), number(metrics.firstDeaths),
|
|
86
|
+
number(metrics.flashAssists), number(metrics.tradedDeaths), number(metrics.clutchWins),
|
|
87
|
+
];
|
|
88
|
+
if (includeEquipment) {
|
|
89
|
+
base.push(number(player.equipment.health), number(player.equipment.money), player.equipment.weapon, yesNo(player.equipment.armor), yesNo(player.equipment.helmet), yesNo(player.equipment.defuseKit), yesNo(player.equipment.alive));
|
|
90
|
+
}
|
|
91
|
+
return base;
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function playerTable(match, groups, includeEquipment = false) {
|
|
95
|
+
const headers = [
|
|
96
|
+
'战队', '选手', 'K-D-A', 'Rating', 'K/D', 'KD差', 'KAST', 'ADR', 'Swing',
|
|
97
|
+
'KPR', 'DPR', '爆头率', '首杀', '首死', '闪光助攻', '被补枪', '残局胜利',
|
|
98
|
+
];
|
|
99
|
+
if (includeEquipment) {
|
|
100
|
+
headers.push('生命', '金钱', '武器', '护甲', '头盔', '拆弹器', '存活');
|
|
101
|
+
}
|
|
102
|
+
const rows = playerRows(match, groups, includeEquipment);
|
|
103
|
+
return rows.length ? table(headers, rows) : '_暂无选手数据_';
|
|
104
|
+
}
|
|
105
|
+
function splitPlayerDetails(match, stats) {
|
|
106
|
+
const ct = stats.map((group) => ({ teamId: group.teamId, players: group.ct }));
|
|
107
|
+
const terrorists = stats.map((group) => ({ teamId: group.teamId, players: group.t }));
|
|
108
|
+
if (![...ct, ...terrorists].some((group) => group.players.length))
|
|
109
|
+
return [];
|
|
110
|
+
return [
|
|
111
|
+
'<details>',
|
|
112
|
+
'<summary>CT / T 分侧数据</summary>',
|
|
113
|
+
'',
|
|
114
|
+
'#### CT 数据',
|
|
115
|
+
'',
|
|
116
|
+
playerTable(match, ct),
|
|
117
|
+
'',
|
|
118
|
+
'#### T 数据',
|
|
119
|
+
'',
|
|
120
|
+
playerTable(match, terrorists),
|
|
121
|
+
'',
|
|
122
|
+
'</details>',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function duelMatrix(match, map) {
|
|
126
|
+
const left = map.playerStats[0]?.overall ?? [];
|
|
127
|
+
const right = map.playerStats[1]?.overall ?? [];
|
|
128
|
+
if (!left.length || !right.length)
|
|
129
|
+
return '_暂无选手对比数据_';
|
|
130
|
+
const kills = new Map(map.playerDuels.map((duel) => [
|
|
131
|
+
`${duel.playerId}:${duel.opponentPlayerId}`,
|
|
132
|
+
duel.kills,
|
|
133
|
+
]));
|
|
134
|
+
const rows = left.map((player) => [
|
|
135
|
+
player.name,
|
|
136
|
+
...right.map((opponent) => {
|
|
137
|
+
const forward = kills.get(`${player.id}:${opponent.id}`) ?? 0;
|
|
138
|
+
const reverse = kills.get(`${opponent.id}:${player.id}`) ?? 0;
|
|
139
|
+
return `${forward}:${reverse}`;
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
const leftName = teamName(match, map.playerStats[0]?.teamId ?? null);
|
|
143
|
+
const rightName = teamName(match, map.playerStats[1]?.teamId ?? null);
|
|
144
|
+
return [
|
|
145
|
+
`_${leftName}(行)击杀数 : ${rightName}(列)击杀数_`,
|
|
146
|
+
'',
|
|
147
|
+
table(['选手', ...right.map((player) => player.name)], rows),
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
function logPlayer(player) {
|
|
151
|
+
return `${plain(player.name)}${player.side ? ` [${player.side}]` : ''}`;
|
|
152
|
+
}
|
|
153
|
+
function logDescription(event) {
|
|
154
|
+
if (event.kill) {
|
|
155
|
+
const kill = event.kill;
|
|
156
|
+
const flags = [
|
|
157
|
+
kill.headshot ? '爆头' : null,
|
|
158
|
+
kill.wallbang ? '穿墙' : null,
|
|
159
|
+
kill.throughSmoke ? '穿烟' : null,
|
|
160
|
+
kill.noScope ? '盲狙' : null,
|
|
161
|
+
kill.killerBlind ? '击杀者致盲' : null,
|
|
162
|
+
].filter((value) => value !== null);
|
|
163
|
+
const extras = [
|
|
164
|
+
kill.assister ? `助攻 ${logPlayer(kill.assister)}` : null,
|
|
165
|
+
kill.flasher ? `闪光 ${logPlayer(kill.flasher)}` : null,
|
|
166
|
+
flags.length ? flags.join('、') : null,
|
|
167
|
+
].filter((value) => value !== null);
|
|
168
|
+
return `${logPlayer(kill.killer)} → ${logPlayer(kill.victim)}(${plain(kill.weapon)}${extras.length ? `;${extras.join(';')}` : ''})`;
|
|
169
|
+
}
|
|
170
|
+
if (event.roundStart) {
|
|
171
|
+
return `第 ${number(event.roundStart.round)} 回合开始`;
|
|
172
|
+
}
|
|
173
|
+
if (event.roundEnd) {
|
|
174
|
+
const end = event.roundEnd;
|
|
175
|
+
return `回合结束:CT ${number(end.ctScore)} : ${number(end.tScore)} T;胜者 ${plain(end.winnerSide)};原因 ${plain(end.reason)}(${number(end.reasonCode)})`;
|
|
176
|
+
}
|
|
177
|
+
if (event.bombPlanted) {
|
|
178
|
+
const bomb = event.bombPlanted;
|
|
179
|
+
return `${logPlayer(bomb.player)} 在 ${plain(bomb.site)} 区下包;存活 CT ${number(bomb.ctPlayers)} / T ${number(bomb.tPlayers)}`;
|
|
180
|
+
}
|
|
181
|
+
if (event.bombDefused)
|
|
182
|
+
return `${logPlayer(event.bombDefused)} 完成拆包`;
|
|
183
|
+
if (event.suicide)
|
|
184
|
+
return `${logPlayer(event.suicide.player)} 使用 ${plain(event.suicide.weapon)} 自杀`;
|
|
185
|
+
if (event.playerJoined)
|
|
186
|
+
return `${logPlayer(event.playerJoined)} 加入比赛`;
|
|
187
|
+
if (event.playerLeft)
|
|
188
|
+
return `${logPlayer(event.playerLeft)} 离开比赛`;
|
|
189
|
+
if (event.kind === 'match-started')
|
|
190
|
+
return '比赛开始';
|
|
191
|
+
if (event.kind === 'restart')
|
|
192
|
+
return `比赛重启:${JSON.stringify(event.restart)}`;
|
|
193
|
+
return `未知事件(类型 ${number(event.type)})`;
|
|
194
|
+
}
|
|
195
|
+
function eventLog(map) {
|
|
196
|
+
if (!map.eventLog.events.length)
|
|
197
|
+
return ['_暂无比赛日志_'];
|
|
198
|
+
let round = null;
|
|
199
|
+
const rows = map.eventLog.events.map((event, index) => {
|
|
200
|
+
if (event.roundStart?.round !== null && event.roundStart?.round !== undefined) {
|
|
201
|
+
round = event.roundStart.round;
|
|
202
|
+
}
|
|
203
|
+
return [index + 1, round, event.kind, logDescription(event)];
|
|
204
|
+
});
|
|
205
|
+
return [
|
|
206
|
+
'<details>',
|
|
207
|
+
`<summary>比赛日志回顾(${map.eventLog.events.length} 条;${map.eventLog.complete ? '完整' : '持续更新中'})</summary>`,
|
|
208
|
+
'',
|
|
209
|
+
table(['#', '回合', '类型', '内容'], rows),
|
|
210
|
+
'',
|
|
211
|
+
'</details>',
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
function highlightsAndMilestones(match, map) {
|
|
215
|
+
if (!map.highlights.length && !map.milestones.length)
|
|
216
|
+
return [];
|
|
217
|
+
const players = new Map(map.playerStats.flatMap((team) => team.overall.map((player) => [
|
|
218
|
+
player.id,
|
|
219
|
+
player.name,
|
|
220
|
+
])));
|
|
221
|
+
const highlightRows = map.highlights.map((value) => {
|
|
222
|
+
const highlight = record(value);
|
|
223
|
+
const metrics = records(highlight.data);
|
|
224
|
+
const player1Id = text(highlight.t1_player_id);
|
|
225
|
+
const player2Id = text(highlight.t2_player_id);
|
|
226
|
+
const player1Data = metrics.map((metric) => `${text(metric.title) ?? '指标'} ${text(metric.t1_data) ?? '—'}`).join(';');
|
|
227
|
+
const player2Data = metrics.map((metric) => `${text(metric.title) ?? '指标'} ${text(metric.t2_data) ?? '—'}`).join(';');
|
|
228
|
+
return [
|
|
229
|
+
text(highlight.title),
|
|
230
|
+
player1Id ? players.get(player1Id) ?? player1Id : null,
|
|
231
|
+
player1Data || null,
|
|
232
|
+
player2Id ? players.get(player2Id) ?? player2Id : null,
|
|
233
|
+
player2Data || null,
|
|
234
|
+
];
|
|
235
|
+
});
|
|
236
|
+
const milestoneRows = map.milestones.map((value) => {
|
|
237
|
+
const milestone = record(value);
|
|
238
|
+
return [
|
|
239
|
+
text(milestone.player_name), text(milestone.honor_text), text(milestone.detail),
|
|
240
|
+
text(milestone.achieve_time),
|
|
241
|
+
];
|
|
242
|
+
});
|
|
243
|
+
const lines = ['### 高光与里程碑', ''];
|
|
244
|
+
if (highlightRows.length) {
|
|
245
|
+
lines.push('#### 高光表现', '', table([
|
|
246
|
+
'项目',
|
|
247
|
+
`${match.teams[0]?.name ?? '队伍1'}选手`,
|
|
248
|
+
'关键数据',
|
|
249
|
+
`${match.teams[1]?.name ?? '队伍2'}选手`,
|
|
250
|
+
'关键数据',
|
|
251
|
+
], highlightRows), '');
|
|
252
|
+
}
|
|
253
|
+
if (milestoneRows.length) {
|
|
254
|
+
lines.push('#### 里程碑', '', table(['选手', '荣誉', '说明', '达成日期'], milestoneRows), '');
|
|
255
|
+
}
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
function jsonDetails(summary, value) {
|
|
259
|
+
return [
|
|
260
|
+
'<details>',
|
|
261
|
+
`<summary>${html(summary)}</summary>`,
|
|
262
|
+
'',
|
|
263
|
+
'```json',
|
|
264
|
+
JSON.stringify(value, null, 2),
|
|
265
|
+
'```',
|
|
266
|
+
'',
|
|
267
|
+
'</details>',
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
function mapSection(match, map) {
|
|
271
|
+
if (map.status === 'upcoming') {
|
|
272
|
+
return [
|
|
273
|
+
`## 地图 ${map.number} · ${plain(map.name)} · ${status(map.status)}`,
|
|
274
|
+
'',
|
|
275
|
+
'> 本地图尚未开始,暂无比分和比赛数据。',
|
|
276
|
+
'',
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
const first = match.teams[0];
|
|
280
|
+
const second = match.teams[1];
|
|
281
|
+
const score = map.teams.map((team) => number(team.score)).join(' : ');
|
|
282
|
+
const rows = [
|
|
283
|
+
['状态', status(map.status)],
|
|
284
|
+
['比分', `${first?.name ?? '队伍1'} ${score} ${second?.name ?? '队伍2'}`],
|
|
285
|
+
['选择方', map.pickAction === 'left' ? '决胜图' : teamName(match, map.pickedByTeamId)],
|
|
286
|
+
['胜者', teamName(match, map.resultTeamId)],
|
|
287
|
+
['当前回合', map.currentRound],
|
|
288
|
+
['回合阶段', map.roundStage],
|
|
289
|
+
['回合计时(秒)', map.gameTimeSeconds],
|
|
290
|
+
['炸弹已安放', map.bombPlanted ? '是' : '否'],
|
|
291
|
+
['开始时间', timestamp(map.startedAtUnixSeconds)],
|
|
292
|
+
['结束时间', timestamp(map.endedAtUnixSeconds)],
|
|
293
|
+
];
|
|
294
|
+
const halfScores = map.teams.map((team, index) => [
|
|
295
|
+
teamName(match, team.teamId), team.score, team.currentSide,
|
|
296
|
+
halfScore(map, index, 'firstHalf'), halfScore(map, index, 'secondHalf'),
|
|
297
|
+
halfScore(map, index, 'overtime'),
|
|
298
|
+
]);
|
|
299
|
+
const roundSequences = map.teams.map((team, index) => [
|
|
300
|
+
teamName(match, team.teamId),
|
|
301
|
+
roundSequence(map, index, 'firstHalf'), roundSequence(map, index, 'secondHalf'),
|
|
302
|
+
roundSequence(map, index, 'overtime'),
|
|
303
|
+
]);
|
|
304
|
+
const lines = [
|
|
305
|
+
`## 地图 ${map.number} · ${plain(map.name)} · ${status(map.status)}`,
|
|
306
|
+
'',
|
|
307
|
+
table(['项目', '内容'], rows),
|
|
308
|
+
'',
|
|
309
|
+
'### 半场与回合',
|
|
310
|
+
'',
|
|
311
|
+
'#### 比分拆分',
|
|
312
|
+
'',
|
|
313
|
+
table(['战队', '总比分', '当前阵营', '上半场比分(阵营)', '下半场比分(阵营)', '加时比分(阵营)'], halfScores),
|
|
314
|
+
'',
|
|
315
|
+
'#### 逐回合胜负',
|
|
316
|
+
'',
|
|
317
|
+
'> “胜”表示该战队赢下该回合,“负”表示该战队输掉该回合;顺序从左到右。',
|
|
318
|
+
'',
|
|
319
|
+
table(['战队', '上半场', '下半场', '加时'], roundSequences),
|
|
320
|
+
'',
|
|
321
|
+
];
|
|
322
|
+
if (map.status === 'live') {
|
|
323
|
+
lines.push('### 实时计分板', '', playerTable(match, map.playerStats.map((group) => ({
|
|
324
|
+
teamId: group.teamId, players: group.overall,
|
|
325
|
+
})), true), '');
|
|
326
|
+
}
|
|
327
|
+
else if (map.playerStats.some((group) => group.overall.length)) {
|
|
328
|
+
lines.push('### 数据总览', '', playerTable(match, map.playerStats.map((group) => ({
|
|
329
|
+
teamId: group.teamId, players: group.overall,
|
|
330
|
+
}))), '', ...splitPlayerDetails(match, map.playerStats), '');
|
|
331
|
+
}
|
|
332
|
+
if (map.playerDuels.length) {
|
|
333
|
+
lines.push('### 选手对比', '', duelMatrix(match, map), '');
|
|
334
|
+
}
|
|
335
|
+
lines.push(...highlightsAndMilestones(match, map));
|
|
336
|
+
lines.push('### 比赛日志', '', ...eventLog(map), '');
|
|
337
|
+
return lines;
|
|
338
|
+
}
|
|
339
|
+
function recentMatchesSection(match) {
|
|
340
|
+
const analysis = match.analysis;
|
|
341
|
+
if (!analysis)
|
|
342
|
+
return [];
|
|
343
|
+
const lines = ['### 近期比赛', ''];
|
|
344
|
+
for (const item of analysis.recentMatches) {
|
|
345
|
+
const rows = item.matches.flatMap((group) => records(record(group).matches)).map((game) => {
|
|
346
|
+
const home = record(game.home_info);
|
|
347
|
+
const opponent = record(game.opponent_info);
|
|
348
|
+
const teamIsHome = text(home.id) === item.teamId || text(opponent.id) !== item.teamId;
|
|
349
|
+
const teamScore = integer(game[teamIsHome ? 'home_score' : 'opponent_score']);
|
|
350
|
+
const opponentScore = integer(game[teamIsHome ? 'opponent_score' : 'home_score']);
|
|
351
|
+
const opponentName = text((teamIsHome ? opponent : home).disp_name);
|
|
352
|
+
const outcome = teamScore === null || opponentScore === null ? '—'
|
|
353
|
+
: teamScore > opponentScore ? '胜' : teamScore < opponentScore ? '负' : '平';
|
|
354
|
+
return [
|
|
355
|
+
compactTimestamp(integer(game.ts)),
|
|
356
|
+
opponentName,
|
|
357
|
+
`${number(teamScore)} : ${number(opponentScore)}`,
|
|
358
|
+
outcome,
|
|
359
|
+
];
|
|
360
|
+
});
|
|
361
|
+
lines.push(`#### ${plain(teamName(match, item.teamId))}`, '', rows.length
|
|
362
|
+
? table(['比赛时间', '对手', '比分(本队 : 对手)', '结果'], rows)
|
|
363
|
+
: '_暂无近期比赛数据_', '');
|
|
364
|
+
}
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
const POWER_COLUMNS = [
|
|
368
|
+
['fire_power_value', '火力'],
|
|
369
|
+
['entrying_value', '突破'],
|
|
370
|
+
['opening_value', '首杀'],
|
|
371
|
+
['utility_value', '道具'],
|
|
372
|
+
['sniping_value', '狙击'],
|
|
373
|
+
['clutching_value', '残局'],
|
|
374
|
+
['trading_value', '补枪'],
|
|
375
|
+
];
|
|
376
|
+
function playerPowerSection(match) {
|
|
377
|
+
const analysis = match.analysis;
|
|
378
|
+
if (!analysis)
|
|
379
|
+
return [];
|
|
380
|
+
const rows = analysis.playerPower.flatMap((team) => team.players.map((value) => {
|
|
381
|
+
const player = record(value);
|
|
382
|
+
const identity = record(player.player_item);
|
|
383
|
+
const scores = new Map(records(player.player_power_data_items).flatMap((item) => {
|
|
384
|
+
const key = text(item.label_key);
|
|
385
|
+
return key ? [[key, text(item.score)]] : [];
|
|
386
|
+
}));
|
|
387
|
+
return [
|
|
388
|
+
teamName(match, team.teamId),
|
|
389
|
+
text(identity.player_name),
|
|
390
|
+
text(identity.hltv_rating),
|
|
391
|
+
...POWER_COLUMNS.map(([key]) => scores.get(key) ?? null),
|
|
392
|
+
];
|
|
393
|
+
}));
|
|
394
|
+
return [
|
|
395
|
+
'### 选手能力', '',
|
|
396
|
+
'> 能力项为源站 0–100 评分;数值越高,代表该项近期表现越突出。', '',
|
|
397
|
+
rows.length
|
|
398
|
+
? table(['战队', '选手', 'Rating', ...POWER_COLUMNS.map(([, label]) => label)], rows)
|
|
399
|
+
: '_暂无选手能力数据_',
|
|
400
|
+
'',
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function analysisSection(match) {
|
|
404
|
+
const analysis = match.analysis;
|
|
405
|
+
if (!analysis)
|
|
406
|
+
return ['## 赛前分析', '', '_本次采集没有赛前分析数据_', ''];
|
|
407
|
+
const teamRows = analysis.teams.map((team) => [
|
|
408
|
+
teamName(match, team.teamId), percent(team.winRate), number(team.rating),
|
|
409
|
+
number(team.kdRatio), percent(team.firstHalfPistolWinRate),
|
|
410
|
+
percent(team.secondHalfPistolWinRate),
|
|
411
|
+
]);
|
|
412
|
+
const playerRows = analysis.teams.flatMap((team) => team.players.map((player) => [
|
|
413
|
+
teamName(match, team.teamId), player.name, player.country, number(player.rating),
|
|
414
|
+
number(player.kdRatio), percent(player.kastRate), number(player.adr),
|
|
415
|
+
number(player.killsPerRound), number(player.impact), number(player.multiKillRating),
|
|
416
|
+
percent(player.roundSwingRate),
|
|
417
|
+
]));
|
|
418
|
+
const mapRows = analysis.maps.flatMap((map) => map.teams.map((team) => [
|
|
419
|
+
map.name, map.localizedName, map.bpType, teamName(match, team.teamId),
|
|
420
|
+
team.matches, team.wins, percent(team.winRate), team.picks, percent(team.pickRate),
|
|
421
|
+
team.bans, percent(team.banRate),
|
|
422
|
+
]));
|
|
423
|
+
const headToHead = analysis.headToHead.matches.map((value) => {
|
|
424
|
+
const historicalMatch = record(record(value).match);
|
|
425
|
+
return [
|
|
426
|
+
compactTimestamp(integer(historicalMatch.ts)),
|
|
427
|
+
integer(historicalMatch.t1_score),
|
|
428
|
+
integer(historicalMatch.t2_score),
|
|
429
|
+
];
|
|
430
|
+
});
|
|
431
|
+
const lines = [
|
|
432
|
+
'## 赛前分析', '',
|
|
433
|
+
'### 战队对比', '',
|
|
434
|
+
table(['战队', '胜率', 'Rating', 'K/D', '上半场手枪胜率', '下半场手枪胜率'], teamRows), '',
|
|
435
|
+
'### 选手分析', '',
|
|
436
|
+
playerRows.length ? table(['战队', '选手', '国家', 'Rating', 'K/D', 'KAST', 'ADR', 'KPR', 'Impact', '多杀Rating', 'Swing'], playerRows) : '_暂无选手分析_', '',
|
|
437
|
+
'### 地图分析', '',
|
|
438
|
+
mapRows.length ? table(['地图', '本地名', 'BP类型', '战队', '场次', '胜场', '胜率', '选择', '选择率', '禁用', '禁用率'], mapRows) : '_暂无地图分析_', '',
|
|
439
|
+
'### 历史交手', '',
|
|
440
|
+
headToHead.length
|
|
441
|
+
? table(['比赛时间', match.teams[0]?.name ?? '队伍1', match.teams[1]?.name ?? '队伍2'], headToHead)
|
|
442
|
+
: '_暂无历史交手数据_',
|
|
443
|
+
'',
|
|
444
|
+
...recentMatchesSection(match),
|
|
445
|
+
...playerPowerSection(match),
|
|
446
|
+
];
|
|
447
|
+
return lines;
|
|
448
|
+
}
|
|
449
|
+
function diagnosticsSection(result) {
|
|
450
|
+
const diagnostics = result.diagnostics;
|
|
451
|
+
return [
|
|
452
|
+
'## 采集诊断', '',
|
|
453
|
+
table(['项目', '内容'], [
|
|
454
|
+
['Schema', diagnostics.schemaVersion],
|
|
455
|
+
['开始时间', diagnostics.startedAt],
|
|
456
|
+
['完成时间', diagnostics.completedAt],
|
|
457
|
+
['耗时', `${diagnostics.durationMs} ms`],
|
|
458
|
+
['请求数', diagnostics.requests.length],
|
|
459
|
+
['警告数', diagnostics.warnings.length],
|
|
460
|
+
]), '',
|
|
461
|
+
'<details>',
|
|
462
|
+
'<summary>HTTP 请求明细</summary>', '',
|
|
463
|
+
table(['类型', 'HTTP', '耗时(ms)', '字节', '地图', '页签'], diagnostics.requests.map((request) => [
|
|
464
|
+
request.kind, request.status, request.durationMs, request.bytes,
|
|
465
|
+
request.mapNumber, request.tab,
|
|
466
|
+
])), '',
|
|
467
|
+
'</details>', '',
|
|
468
|
+
...jsonDetails(`采集警告(${diagnostics.warnings.length})`, diagnostics.warnings), '',
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
export function renderFiveEPlayMatchMarkdown(result, options = {}) {
|
|
472
|
+
const match = result.data;
|
|
473
|
+
const first = match.teams[0];
|
|
474
|
+
const second = match.teams[1];
|
|
475
|
+
const title = `${first?.name ?? '队伍1'} ${number(first ? teamScore(match, first.id) : null)} : ${number(second ? teamScore(match, second.id) : null)} ${second?.name ?? '队伍2'}`;
|
|
476
|
+
const overviewRows = [
|
|
477
|
+
['比赛状态', status(match.match.status)],
|
|
478
|
+
['比赛ID', match.match.id],
|
|
479
|
+
['比赛版本', match.match.version],
|
|
480
|
+
['赛制', match.match.bestOf === null ? null : `BO${match.match.bestOf}`],
|
|
481
|
+
['采集时间', match.capturedAt],
|
|
482
|
+
['源页面', match.source.url],
|
|
483
|
+
];
|
|
484
|
+
const teamRows = match.teams.map((team) => [
|
|
485
|
+
team.name, team.id, team.country, team.seriesScore, team.rank, team.valveRank,
|
|
486
|
+
]);
|
|
487
|
+
const vetoRows = match.veto.map((entry) => [
|
|
488
|
+
entry.order,
|
|
489
|
+
entry.action === 'ban' ? '禁用' : entry.action === 'pick' ? '选择' : entry.action === 'left' ? '决胜图' : '未知',
|
|
490
|
+
teamName(match, entry.teamId), entry.map,
|
|
491
|
+
]);
|
|
492
|
+
const lines = [
|
|
493
|
+
`# ${plain(title)}`, '',
|
|
494
|
+
'## 比赛概览', '',
|
|
495
|
+
table(['项目', '内容'], overviewRows), '',
|
|
496
|
+
'### 战队', '',
|
|
497
|
+
table(['战队', 'ID', '国家', '大比分', '5E排名', 'Valve排名'], teamRows), '',
|
|
498
|
+
'### 地图 BP', '',
|
|
499
|
+
vetoRows.length ? table(['顺序', '操作', '战队', '地图'], vetoRows) : '_暂无 BP 数据_', '',
|
|
500
|
+
];
|
|
501
|
+
for (const map of match.maps) {
|
|
502
|
+
lines.push(...mapSection(match, map));
|
|
503
|
+
}
|
|
504
|
+
lines.push(...analysisSection(match));
|
|
505
|
+
if (options.includeDiagnostics !== false)
|
|
506
|
+
lines.push(...diagnosticsSection(result));
|
|
507
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
508
|
+
}
|
package/dist/mqtt.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FiveEPlayWebSocketFactory } from './types.js';
|
|
2
|
+
interface Credentials {
|
|
3
|
+
clientId: string;
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DecodedMqttPacket {
|
|
8
|
+
type: number;
|
|
9
|
+
flags: number;
|
|
10
|
+
payload: Uint8Array;
|
|
11
|
+
topic?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function encodeConnectPacket(credentials: Credentials, keepAliveSeconds?: number): Uint8Array;
|
|
14
|
+
export declare function encodeSubscribePacket(topic: string, packetId?: number): Uint8Array;
|
|
15
|
+
export declare function decodeMqttPackets(bytes: Uint8Array): DecodedMqttPacket[];
|
|
16
|
+
export interface MqttTopicConnectionOptions {
|
|
17
|
+
topic: string;
|
|
18
|
+
fetch: typeof globalThis.fetch;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
webSocketFactory?: FiveEPlayWebSocketFactory;
|
|
21
|
+
onPayload(payload: unknown): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class MqttTopicConnection {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options: MqttTopicConnectionOptions);
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|