@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.
@@ -0,0 +1,466 @@
1
+ import { mergeLogEvents, transformLogRecord } from './log.js';
2
+ import { flag, integer, jsonObjects, numeric, record, records, side, strings, text, } from './value.js';
3
+ function cleanName(value) {
4
+ return text(value) ?? '';
5
+ }
6
+ function teamFromSource(source, series, sideKey) {
7
+ const rank = record(source.v_rank);
8
+ return {
9
+ id: text(source.id) ?? sideKey,
10
+ name: cleanName(source.disp_name),
11
+ logoUrl: text(source.logo),
12
+ country: text(source.country),
13
+ rank: integer(source.rank),
14
+ valveRank: integer(rank.rank),
15
+ seriesScore: integer(series[`${sideKey}_score`]),
16
+ quickScore: integer(series[`${sideKey}_quick_score`]),
17
+ odds: numeric(series[`${sideKey}_odds`]),
18
+ oddsPercent: numeric(series[`${sideKey}_odds_percent`]),
19
+ };
20
+ }
21
+ function vetoAction(value) {
22
+ const normalized = text(value);
23
+ if (normalized === 'ban' || normalized === 'pick' || normalized === 'left')
24
+ return normalized;
25
+ return 'unknown';
26
+ }
27
+ function vetoEntries(value, teams) {
28
+ return records(value).map((entry, index) => {
29
+ const teamSide = text(entry.team_side);
30
+ return {
31
+ order: index + 1,
32
+ action: vetoAction(entry.bp_type),
33
+ teamId: teamSide === 't1' ? teams[0]?.id ?? null
34
+ : teamSide === 't2' ? teams[1]?.id ?? null : null,
35
+ map: cleanName(entry.map_name),
36
+ iconUrl: text(entry.map_icon),
37
+ backgroundUrl: text(entry.map_logo),
38
+ };
39
+ });
40
+ }
41
+ function mapStatus(value) {
42
+ if (value === 2 || value === '2')
43
+ return 'completed';
44
+ if (value === 1 || value === '1')
45
+ return 'live';
46
+ if (value === -1 || value === '-1' || value === 0 || value === '0')
47
+ return 'upcoming';
48
+ return 'unknown';
49
+ }
50
+ function half(source, prefix) {
51
+ const rawResults = source[`${prefix}_data`];
52
+ return {
53
+ side: side(source[`${prefix}_role`]),
54
+ score: integer(source[`${prefix}_score`]),
55
+ roundResults: Array.isArray(rawResults)
56
+ ? rawResults.flatMap((item) => {
57
+ const result = integer(item);
58
+ return result === null ? [] : [result];
59
+ })
60
+ : [],
61
+ };
62
+ }
63
+ function mapTeamState(value, fallbackTeamId) {
64
+ const source = record(value);
65
+ return {
66
+ teamId: text(source.id) ?? fallbackTeamId,
67
+ currentSide: side(source.role),
68
+ score: integer(source.all_score),
69
+ quickScore: integer(source.quick_score),
70
+ firstHalf: half(source, 'fh'),
71
+ secondHalf: half(source, 'sh'),
72
+ overtime: half(source, 'ot'),
73
+ flags: strings(source.flags),
74
+ };
75
+ }
76
+ function countMap(value) {
77
+ return Object.fromEntries(Object.entries(record(value)).flatMap(([key, raw]) => {
78
+ const parsed = integer(raw);
79
+ return parsed === null ? [] : [[key, parsed]];
80
+ }));
81
+ }
82
+ function playerStats(value) {
83
+ const source = record(value);
84
+ const hp = integer(source.hp);
85
+ return {
86
+ id: text(source.id) ?? '',
87
+ name: cleanName(source.name),
88
+ countryLogoUrl: text(source.country_logo),
89
+ portraitUrl: text(source.portrait),
90
+ halfPortraitUrl: text(source.half_portrait),
91
+ equipment: {
92
+ health: hp,
93
+ money: integer(source.money),
94
+ armor: flag(source.kevlar),
95
+ helmet: flag(source.helmet),
96
+ defuseKit: flag(source.has_defusekit),
97
+ alive: hp === null ? null : hp > 0,
98
+ weapon: text(source.weapon),
99
+ weaponLogoUrl: text(source.weapon_logo),
100
+ },
101
+ metrics: {
102
+ kills: integer(source.kill),
103
+ deaths: integer(source.death),
104
+ assists: integer(source.assist),
105
+ kdRatio: numeric(source.kd_rate),
106
+ kdDifference: integer(source.kd_diff),
107
+ rating: numeric(source.rating),
108
+ kastRate: numeric(source.kast),
109
+ adr: numeric(source.adr),
110
+ killsPerRound: numeric(source.kpr),
111
+ deathsPerRound: numeric(source.dpr),
112
+ impact: numeric(source.impact),
113
+ multiKillRating: numeric(source.mk_rating),
114
+ roundSwingRate: numeric(source.swing),
115
+ headshots: integer(source.headshot),
116
+ headshotRate: numeric(source.head_shot_rate),
117
+ firstKills: integer(source.first_blood_num),
118
+ firstDeaths: integer(source.first_death_num),
119
+ firstKillRate: numeric(source.first_blood_rate),
120
+ flashAssists: integer(source.flash_assist),
121
+ tradedDeaths: integer(source.traded_death),
122
+ clutchWins: integer(source.cl_win_num),
123
+ roundMvp: integer(source.round_mvp),
124
+ multiKills: {
125
+ two: integer(source.k2), three: integer(source.k3),
126
+ four: integer(source.k4), five: integer(source.k5),
127
+ },
128
+ clutches: {
129
+ oneVsOne: integer(source.v1), oneVsTwo: integer(source.v2),
130
+ oneVsThree: integer(source.v3), oneVsFour: integer(source.v4),
131
+ oneVsFive: integer(source.v5),
132
+ },
133
+ },
134
+ versusKills: countMap(source.counter_kill_map),
135
+ firstKillsByOpponent: countMap(source.first_kill_map),
136
+ };
137
+ }
138
+ function players(value) {
139
+ return records(value).map(playerStats).filter((item) => item.id || item.name);
140
+ }
141
+ function mapPlayerStats(source, teams) {
142
+ return teams.map((team, index) => {
143
+ const key = index === 0 ? 't1' : 't2';
144
+ return {
145
+ teamId: team.id,
146
+ overall: players(source[`${key}_pr_stats`]),
147
+ ct: players(source[`${key}_pr_stats_ct`]),
148
+ t: players(source[`${key}_pr_stats_t`]),
149
+ };
150
+ });
151
+ }
152
+ function mapDuels(stats) {
153
+ return stats.flatMap((team) => team.overall.flatMap((player) => Object.entries(player.versusKills).map(([opponentPlayerId, kills]) => ({
154
+ playerId: player.id,
155
+ opponentPlayerId,
156
+ kills,
157
+ }))));
158
+ }
159
+ function pickTeamId(value, teams) {
160
+ const normalized = text(value);
161
+ if (normalized?.startsWith('t1_'))
162
+ return teams[0]?.id ?? null;
163
+ if (normalized?.startsWith('t2_'))
164
+ return teams[1]?.id ?? null;
165
+ return null;
166
+ }
167
+ function mapFromSource(value, identity, teams, logs) {
168
+ const source = record(value);
169
+ const number = integer(source.bout_num) ?? 0;
170
+ const stats = mapPlayerStats(source, teams);
171
+ const capture = logs.get(number);
172
+ const transformedLogs = capture?.rows.map(transformLogRecord) ?? [];
173
+ const action = text(source.bp_act);
174
+ return {
175
+ id: `${identity.id}_${number}`,
176
+ number,
177
+ label: text(source.disp_name) ?? `Map ${number}`,
178
+ name: cleanName(source.map_name),
179
+ status: mapStatus(source.status),
180
+ display: source.display !== '2' && source.display !== 2,
181
+ pickedByTeamId: pickTeamId(source.bp_act, teams),
182
+ pickAction: action === 'left' ? 'left' : action?.endsWith('_pick') ? 'pick' : 'unknown',
183
+ resultTeamId: source.result === 't1' ? teams[0]?.id ?? null
184
+ : source.result === 't2' ? teams[1]?.id ?? null : null,
185
+ iconUrl: text(source.map_icon),
186
+ backgroundUrl: text(source.map_bgm),
187
+ startedAtUnixSeconds: integer(source.start_time),
188
+ endedAtUnixSeconds: integer(source.end_time),
189
+ currentRound: integer(source.curr_round_num),
190
+ roundStage: text(source.curr_bout_stage),
191
+ gameTimeSeconds: integer(source.game_time),
192
+ roundStartedAtUnixSeconds: integer(source.round_start_time),
193
+ bombPlanted: flag(source.bomb_planted) === true,
194
+ bombPlantedAtUnixSeconds: integer(source.bomb_planted_time),
195
+ teams: [
196
+ mapTeamState(source.t1_stats, teams[0]?.id ?? null),
197
+ mapTeamState(source.t2_stats, teams[1]?.id ?? null),
198
+ ],
199
+ playerStats: stats,
200
+ playerDuels: mapDuels(stats),
201
+ highlights: jsonObjects(source.pr_stats),
202
+ milestones: jsonObjects(source.milestones),
203
+ eventLog: {
204
+ order: 'chronological',
205
+ complete: capture?.complete ?? false,
206
+ fromVersion: capture?.fromVersion ?? null,
207
+ toVersion: capture?.toVersion ?? null,
208
+ events: mergeLogEvents([], transformedLogs),
209
+ },
210
+ };
211
+ }
212
+ function upcomingMap(entry, number, identity, teams) {
213
+ return {
214
+ bout_num: String(number),
215
+ disp_name: `第${number}局`,
216
+ map_name: entry.map,
217
+ map_icon: entry.iconUrl ?? '',
218
+ map_bgm: entry.backgroundUrl ?? '',
219
+ bp_act: entry.action === 'left' ? 'left' : entry.teamId ? 'pick' : '',
220
+ status: '-1',
221
+ display: '1',
222
+ t1_stats: { id: teams[0]?.id ?? null },
223
+ t2_stats: { id: teams[1]?.id ?? null },
224
+ match_id: identity.id,
225
+ };
226
+ }
227
+ function allMaps(sourceBouts, veto, identity, teams, logs) {
228
+ const bouts = records(sourceBouts);
229
+ const seriesMaps = veto.filter((entry) => entry.action === 'pick' || entry.action === 'left');
230
+ for (const [index, entry] of seriesMaps.entries()) {
231
+ if (!bouts.some((bout) => text(bout.map_name) === entry.map)) {
232
+ const synthesized = upcomingMap(entry, index + 1, identity, teams);
233
+ if (entry.action === 'pick') {
234
+ synthesized.bp_act = entry.teamId === teams[0]?.id ? 't1_pick' : 't2_pick';
235
+ }
236
+ bouts.push(synthesized);
237
+ }
238
+ }
239
+ return bouts
240
+ .map((bout) => mapFromSource(bout, identity, teams, logs))
241
+ .filter((map) => map.number > 0 && map.name)
242
+ .sort((left, right) => left.number - right.number);
243
+ }
244
+ function matchStatus(global, maps) {
245
+ if (maps.some((map) => map.status === 'live') || global.status === '1' || global.status === 1)
246
+ return 'live';
247
+ if (global.status === '-1' || global.status === -1)
248
+ return 'upcoming';
249
+ if (global.status === '2' || global.status === 2)
250
+ return 'completed';
251
+ const meaningful = maps.filter((map) => map.status !== 'unknown');
252
+ if (meaningful.length && meaningful.every((map) => map.status === 'completed'))
253
+ return 'completed';
254
+ if (meaningful.some((map) => map.status === 'upcoming'))
255
+ return 'upcoming';
256
+ return 'unknown';
257
+ }
258
+ function analysisPlayer(value) {
259
+ const source = record(value);
260
+ return {
261
+ id: text(source.id) ?? '',
262
+ name: cleanName(source.name),
263
+ country: text(source.country_name),
264
+ countryLogoUrl: text(source.country_logo),
265
+ logoUrl: text(source.logo),
266
+ halfPortraitUrl: text(source.half_logo),
267
+ rating: numeric(source.Rating),
268
+ kdRatio: numeric(source.kd),
269
+ kastRate: numeric(source.kast),
270
+ adr: numeric(source.adr),
271
+ killsPerRound: numeric(source.kpr),
272
+ impact: numeric(source.impact),
273
+ multiKillRating: numeric(source.mk_rating),
274
+ roundSwingRate: numeric(source.swing),
275
+ };
276
+ }
277
+ function analysisMap(value, teams) {
278
+ const source = record(value);
279
+ return {
280
+ id: text(source.id),
281
+ name: cleanName(source.name),
282
+ localizedName: text(source.name_zh),
283
+ iconUrl: text(source.icon),
284
+ backgroundUrl: text(source.bgm),
285
+ bpType: text(source.bp_type),
286
+ teams: teams.map((team, index) => {
287
+ const key = index === 0 ? 't1' : 't2';
288
+ return {
289
+ teamId: team.id,
290
+ matches: integer(source[`${key}_count`]),
291
+ wins: integer(source[`${key}_win_num`]),
292
+ winRate: numeric(source[`${key}_rate`]),
293
+ picks: integer(source[`${key}_pick_count`]),
294
+ pickRate: numeric(source[`${key}_pick_rate`]),
295
+ bans: integer(source[`${key}_ban_count`]),
296
+ banRate: numeric(source[`${key}_ban_rate`]),
297
+ };
298
+ }),
299
+ };
300
+ }
301
+ function prematchAnalysis(value, teams) {
302
+ if (value === null)
303
+ return null;
304
+ const data = record(value);
305
+ const result = record(data.result);
306
+ const comparison = record(result.comparison);
307
+ if (!Object.keys(result).length)
308
+ return null;
309
+ const power = record(result.power_comparison);
310
+ const teamAnalysis = teams.map((team, index) => {
311
+ const key = index === 0 ? 't1' : 't2';
312
+ const metrics = record(comparison[`${key}_stats`]);
313
+ return {
314
+ teamId: team.id,
315
+ winRate: numeric(metrics.win_rate),
316
+ rating: numeric(metrics.rating),
317
+ kdRatio: numeric(metrics.kd),
318
+ firstHalfPistolWinRate: numeric(metrics.f_rate),
319
+ secondHalfPistolWinRate: numeric(metrics.s_rate),
320
+ players: records(comparison[`${key}_player_stats`]).map(analysisPlayer),
321
+ };
322
+ });
323
+ return {
324
+ hidden: flag(power.is_hide) === true,
325
+ teams: teamAnalysis,
326
+ maps: records(comparison.team_map_stats).map((map) => analysisMap(map, teams)),
327
+ playerPower: teams.map((team, index) => ({
328
+ teamId: team.id,
329
+ players: jsonObjects(power[index === 0 ? 't1_player_stats' : 't2_player_stats']),
330
+ })),
331
+ recentMatches: teams.map((team, index) => ({
332
+ teamId: team.id,
333
+ matches: jsonObjects(record(result[index === 0 ? 't1_rec_matches' : 't2_rec_matches']).matches),
334
+ })),
335
+ headToHead: {
336
+ teamWinRates: teams.map((team, index) => ({
337
+ teamId: team.id,
338
+ winRate: numeric(record(result.rec_vs_matches)[index === 0 ? 't1_win_rate' : 't2_win_rate']),
339
+ })),
340
+ matches: jsonObjects(record(result.rec_vs_matches).matches),
341
+ },
342
+ };
343
+ }
344
+ function communityCard(value) {
345
+ const source = record(value);
346
+ const score = record(source.score);
347
+ return {
348
+ tab: text(source.tab) ?? '',
349
+ contentType: text(source.card_content_tab),
350
+ id: text(source.id) ?? '',
351
+ name: cleanName(source.name),
352
+ logoUrl: text(source.logo),
353
+ teamLogoUrl: text(source.team_logo),
354
+ countryLogoUrl: text(source.country_logo),
355
+ detail: text(source.detail),
356
+ positions: strings(source.positions),
357
+ content: strings(source.content),
358
+ score: {
359
+ average: numeric(score.avg_score),
360
+ userCount: integer(score.user_cnt) ?? 0,
361
+ text: text(score.score_text),
362
+ starCounts: Array.isArray(score.star_num_user_cnt)
363
+ ? score.star_num_user_cnt.flatMap((item) => integer(item) ?? []) : [],
364
+ starPercentages: Array.isArray(score.star_num_user_pct)
365
+ ? score.star_num_user_pct.map(numeric) : [],
366
+ },
367
+ starLabels: strings(source.star_text),
368
+ };
369
+ }
370
+ function communityRatings(capture) {
371
+ if (!capture)
372
+ return null;
373
+ return {
374
+ tabs: capture.tabs.map((value) => {
375
+ const tab = record(value);
376
+ const tabName = text(tab.tab) ?? '';
377
+ const id = text(tab.id) ?? '';
378
+ return {
379
+ tab: tabName,
380
+ id,
381
+ name: cleanName(tab.name),
382
+ logoUrl: text(tab.logo),
383
+ selected: flag(tab.is_selected) === true,
384
+ cards: (capture.cardsByTab.get(`${tabName}:${id}`) ?? []).map(communityCard),
385
+ };
386
+ }),
387
+ };
388
+ }
389
+ export function buildFiveEPlayMatch(input) {
390
+ const detail = record(input.detailData);
391
+ const sourceMatch = record(detail.match);
392
+ const global = record(sourceMatch.global_state);
393
+ const matchInfo = record(sourceMatch.mc_info);
394
+ const tournamentInfo = record(sourceMatch.tt_info);
395
+ const teams = [
396
+ teamFromSource(record(matchInfo.t1_info), global, 't1'),
397
+ teamFromSource(record(matchInfo.t2_info), global, 't2'),
398
+ ];
399
+ const veto = vetoEntries(global.bp_map_item, teams);
400
+ const maps = allMaps(sourceMatch.bouts_state, veto, input.identity, teams, input.logs);
401
+ const current = maps.find((map) => map.status === 'live') ?? null;
402
+ return {
403
+ schemaVersion: '1.0.0',
404
+ capturedAt: input.capturedAt,
405
+ sport: 'cs2',
406
+ source: { provider: '5eplay', url: input.identity.url },
407
+ stateVersion: text(detail.state_ver),
408
+ match: {
409
+ id: input.identity.id,
410
+ numericId: input.identity.numericId,
411
+ status: matchStatus(global, maps),
412
+ version: text(matchInfo.match_version),
413
+ bestOf: integer(matchInfo.format),
414
+ scheduledAtUnixSeconds: integer(matchInfo.plan_ts),
415
+ stage: text(matchInfo.tt_stage),
416
+ stageDescription: text(matchInfo.tt_stage_desc),
417
+ seriesScore: teams.map((team) => ({ teamId: team.id, score: team.seriesScore })),
418
+ },
419
+ tournament: {
420
+ id: text(tournamentInfo.id),
421
+ name: cleanName(tournamentInfo.disp_name),
422
+ logoUrl: text(tournamentInfo.logo),
423
+ status: text(tournamentInfo.status),
424
+ grade: text(tournamentInfo.grade),
425
+ gradeLabel: text(tournamentInfo.grade_label),
426
+ location: text(tournamentInfo.addr) ?? text(tournamentInfo.city_name),
427
+ prize: text(tournamentInfo.bonus),
428
+ startsAt: text(tournamentInfo.start_time),
429
+ endsAt: text(tournamentInfo.end_time),
430
+ color: text(tournamentInfo.color),
431
+ },
432
+ teams,
433
+ veto,
434
+ maps,
435
+ current,
436
+ analysis: prematchAnalysis(input.analysisData, teams),
437
+ communityRatings: communityRatings(input.community),
438
+ };
439
+ }
440
+ export function mergeDetailData(base, update) {
441
+ const baseData = record(base);
442
+ const updateData = record(update);
443
+ const baseMatch = record(baseData.match);
444
+ const updateMatch = record(updateData.match);
445
+ const mergedBouts = records(baseMatch.bouts_state);
446
+ for (const bout of records(updateMatch.bouts_state)) {
447
+ const number = integer(bout.bout_num);
448
+ const index = mergedBouts.findIndex((candidate) => integer(candidate.bout_num) === number);
449
+ if (index >= 0)
450
+ mergedBouts[index] = { ...mergedBouts[index], ...bout };
451
+ else
452
+ mergedBouts.push(bout);
453
+ }
454
+ return {
455
+ ...baseData,
456
+ ...updateData,
457
+ match: {
458
+ ...baseMatch,
459
+ ...updateMatch,
460
+ global_state: { ...record(baseMatch.global_state), ...record(updateMatch.global_state) },
461
+ mc_info: { ...record(baseMatch.mc_info), ...record(updateMatch.mc_info) },
462
+ tt_info: { ...record(baseMatch.tt_info), ...record(updateMatch.tt_info) },
463
+ bouts_state: mergedBouts,
464
+ },
465
+ };
466
+ }