@rine-kit/pubg-domain 0.0.0-stage → 0.1.1

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/dist/config/default-team.json +27 -0
  4. package/dist/src/config/chicken-index.d.ts +9 -0
  5. package/dist/src/config/chicken-index.js +37 -0
  6. package/dist/src/config/chicken-index.js.map +1 -0
  7. package/dist/src/config/team.d.ts +22 -0
  8. package/dist/src/config/team.js +69 -0
  9. package/dist/src/config/team.js.map +1 -0
  10. package/dist/src/context/context-store.d.ts +40 -0
  11. package/dist/src/context/context-store.js +119 -0
  12. package/dist/src/context/context-store.js.map +1 -0
  13. package/dist/src/context/team-context.d.ts +13 -0
  14. package/dist/src/context/team-context.js +16 -0
  15. package/dist/src/context/team-context.js.map +1 -0
  16. package/dist/src/core.d.ts +23 -0
  17. package/dist/src/core.js +24 -0
  18. package/dist/src/core.js.map +1 -0
  19. package/dist/src/data/model.d.ts +123 -0
  20. package/dist/src/data/model.js +100 -0
  21. package/dist/src/data/model.js.map +1 -0
  22. package/dist/src/data/pubg-api-client.d.ts +59 -0
  23. package/dist/src/data/pubg-api-client.js +441 -0
  24. package/dist/src/data/pubg-api-client.js.map +1 -0
  25. package/dist/src/engine/query-engine.d.ts +20 -0
  26. package/dist/src/engine/query-engine.js +765 -0
  27. package/dist/src/engine/query-engine.js.map +1 -0
  28. package/dist/src/index.d.ts +27 -0
  29. package/dist/src/index.js +28 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/migration/importer.d.ts +35 -0
  32. package/dist/src/migration/importer.js +229 -0
  33. package/dist/src/migration/importer.js.map +1 -0
  34. package/dist/src/review/combat-scope.d.ts +11 -0
  35. package/dist/src/review/combat-scope.js +101 -0
  36. package/dist/src/review/combat-scope.js.map +1 -0
  37. package/dist/src/review/event-combination-engine.d.ts +17 -0
  38. package/dist/src/review/event-combination-engine.js +534 -0
  39. package/dist/src/review/event-combination-engine.js.map +1 -0
  40. package/dist/src/review/fight-detector.d.ts +4 -0
  41. package/dist/src/review/fight-detector.js +138 -0
  42. package/dist/src/review/fight-detector.js.map +1 -0
  43. package/dist/src/review/fight-integrity-validator.d.ts +13 -0
  44. package/dist/src/review/fight-integrity-validator.js +134 -0
  45. package/dist/src/review/fight-integrity-validator.js.map +1 -0
  46. package/dist/src/review/fun-candidate-generator.d.ts +9 -0
  47. package/dist/src/review/fun-candidate-generator.js +267 -0
  48. package/dist/src/review/fun-candidate-generator.js.map +1 -0
  49. package/dist/src/review/fun-event-generator.d.ts +8 -0
  50. package/dist/src/review/fun-event-generator.js +740 -0
  51. package/dist/src/review/fun-event-generator.js.map +1 -0
  52. package/dist/src/review/fun-intelligence.d.ts +13 -0
  53. package/dist/src/review/fun-intelligence.js +20 -0
  54. package/dist/src/review/fun-intelligence.js.map +1 -0
  55. package/dist/src/review/fun-ranker.d.ts +13 -0
  56. package/dist/src/review/fun-ranker.js +68 -0
  57. package/dist/src/review/fun-ranker.js.map +1 -0
  58. package/dist/src/review/heavy-weapon-intelligence.d.ts +5 -0
  59. package/dist/src/review/heavy-weapon-intelligence.js +215 -0
  60. package/dist/src/review/heavy-weapon-intelligence.js.map +1 -0
  61. package/dist/src/review/key-operation-detector.d.ts +3 -0
  62. package/dist/src/review/key-operation-detector.js +126 -0
  63. package/dist/src/review/key-operation-detector.js.map +1 -0
  64. package/dist/src/review/match-selector.d.ts +18 -0
  65. package/dist/src/review/match-selector.js +202 -0
  66. package/dist/src/review/match-selector.js.map +1 -0
  67. package/dist/src/review/review-analyzer.d.ts +2 -0
  68. package/dist/src/review/review-analyzer.js +652 -0
  69. package/dist/src/review/review-analyzer.js.map +1 -0
  70. package/dist/src/review/review-facts.d.ts +17 -0
  71. package/dist/src/review/review-facts.js +560 -0
  72. package/dist/src/review/review-facts.js.map +1 -0
  73. package/dist/src/review/review-share.d.ts +48 -0
  74. package/dist/src/review/review-share.js +37 -0
  75. package/dist/src/review/review-share.js.map +1 -0
  76. package/dist/src/review/selection-store.d.ts +25 -0
  77. package/dist/src/review/selection-store.js +80 -0
  78. package/dist/src/review/selection-store.js.map +1 -0
  79. package/dist/src/review/session-review.d.ts +72 -0
  80. package/dist/src/review/session-review.js +492 -0
  81. package/dist/src/review/session-review.js.map +1 -0
  82. package/dist/src/review/special-events.d.ts +3 -0
  83. package/dist/src/review/special-events.js +258 -0
  84. package/dist/src/review/special-events.js.map +1 -0
  85. package/dist/src/review/subgraph.d.ts +26 -0
  86. package/dist/src/review/subgraph.js +146 -0
  87. package/dist/src/review/subgraph.js.map +1 -0
  88. package/dist/src/review/supplemental-intelligence.d.ts +15 -0
  89. package/dist/src/review/supplemental-intelligence.js +431 -0
  90. package/dist/src/review/supplemental-intelligence.js.map +1 -0
  91. package/dist/src/review/team-damage.d.ts +8 -0
  92. package/dist/src/review/team-damage.js +180 -0
  93. package/dist/src/review/team-damage.js.map +1 -0
  94. package/dist/src/review/telemetry-events.d.ts +72 -0
  95. package/dist/src/review/telemetry-events.js +573 -0
  96. package/dist/src/review/telemetry-events.js.map +1 -0
  97. package/dist/src/review/telemetry.d.ts +86 -0
  98. package/dist/src/review/telemetry.js +266 -0
  99. package/dist/src/review/telemetry.js.map +1 -0
  100. package/dist/src/review/types.d.ts +501 -0
  101. package/dist/src/review/types.js +2 -0
  102. package/dist/src/review/types.js.map +1 -0
  103. package/dist/src/review/vehicle-intelligence.d.ts +3 -0
  104. package/dist/src/review/vehicle-intelligence.js +148 -0
  105. package/dist/src/review/vehicle-intelligence.js.map +1 -0
  106. package/dist/src/schema/query.d.ts +876 -0
  107. package/dist/src/schema/query.js +185 -0
  108. package/dist/src/schema/query.js.map +1 -0
  109. package/dist/src/schema/status.d.ts +66 -0
  110. package/dist/src/schema/status.js +49 -0
  111. package/dist/src/schema/status.js.map +1 -0
  112. package/dist/src/service/pubg-domain-service.d.ts +192 -0
  113. package/dist/src/service/pubg-domain-service.js +1598 -0
  114. package/dist/src/service/pubg-domain-service.js.map +1 -0
  115. package/dist/src/storage/repository-port.d.ts +84 -0
  116. package/dist/src/storage/repository-port.js +2 -0
  117. package/dist/src/storage/repository-port.js.map +1 -0
  118. package/dist/src/storage/sqlite-repository-adapter.d.ts +33 -0
  119. package/dist/src/storage/sqlite-repository-adapter.js +66 -0
  120. package/dist/src/storage/sqlite-repository-adapter.js.map +1 -0
  121. package/dist/src/storage/sqlite-repository.d.ts +73 -0
  122. package/dist/src/storage/sqlite-repository.js +652 -0
  123. package/dist/src/storage/sqlite-repository.js.map +1 -0
  124. package/dist/src/time/selector-resolver.d.ts +34 -0
  125. package/dist/src/time/selector-resolver.js +364 -0
  126. package/dist/src/time/selector-resolver.js.map +1 -0
  127. package/package.json +42 -3
@@ -0,0 +1,740 @@
1
+ import { isPunchWeapon } from './telemetry-events.js';
2
+ function integer(value) {
3
+ return Math.round(value).toLocaleString('zh-CN');
4
+ }
5
+ function kilometers(value) {
6
+ return `${(value / 1_000).toFixed(1)}km`;
7
+ }
8
+ function armorLabel(value) {
9
+ const normalized = (value ?? '').toLowerCase();
10
+ if (normalized.includes('head'))
11
+ return '头盔';
12
+ if (normalized.includes('vest') || normalized.includes('armor'))
13
+ return '防弹衣';
14
+ return '护甲';
15
+ }
16
+ function playerName(facts, playerId) {
17
+ return facts.players.find((player) => player.playerId === playerId)?.playerName ?? playerId;
18
+ }
19
+ function availableEvidence(facts) {
20
+ return new Map(facts.evidence.map((item) => [item.id, item]));
21
+ }
22
+ /** Turn raw event IDs and derived fact IDs into IDs present in ReviewEvidence. */
23
+ export function resolveFunEvidenceIds(facts, ids) {
24
+ const evidence = availableEvidence(facts);
25
+ const byEvent = new Map();
26
+ for (const item of facts.evidence) {
27
+ for (const eventId of item.eventIds) {
28
+ const references = byEvent.get(eventId) ?? [];
29
+ references.push(item.id);
30
+ byEvent.set(eventId, references);
31
+ }
32
+ }
33
+ const resolved = [];
34
+ for (const id of ids) {
35
+ if (evidence.has(id)) {
36
+ resolved.push(id);
37
+ continue;
38
+ }
39
+ const derived = `evidence-${id}`;
40
+ if (evidence.has(derived)) {
41
+ resolved.push(derived);
42
+ continue;
43
+ }
44
+ resolved.push(...(byEvent.get(id) ?? []));
45
+ }
46
+ return [...new Set(resolved)];
47
+ }
48
+ function makeFunEvent(facts, input) {
49
+ const factIds = [...new Set(input.factIds.filter(Boolean))];
50
+ const evidenceIds = resolveFunEvidenceIds(facts, input.evidenceIds);
51
+ if (!factIds.length || !evidenceIds.length)
52
+ return null;
53
+ return {
54
+ ...input,
55
+ type: input.type ?? 'FUN_EVENT',
56
+ factIds,
57
+ evidenceIds,
58
+ funScore: Math.max(0, Math.min(100, Math.round(input.funScore))),
59
+ targetPlayerIds: [...new Set(input.targetPlayerIds)],
60
+ tags: [...new Set(input.tags)],
61
+ };
62
+ }
63
+ function specialEvent(facts, event) {
64
+ if (!event.playerId)
65
+ return null;
66
+ const name = playerName(facts, event.playerId);
67
+ const values = event.facts;
68
+ const pickupEvents = Number(values.pickupEvents ?? 0);
69
+ const shots = Number(values.shots ?? 0);
70
+ const hits = Number(values.hits ?? 0);
71
+ const kills = Number(values.kills ?? 0);
72
+ const descriptors = {
73
+ ROCKET_UNUSED: {
74
+ type: 'ROCKET_UNUSED',
75
+ title: '🎒 军火收藏家',
76
+ text: `${name}\n火箭筒拾取${pickupEvents}次 · 发射0`,
77
+ score: 72,
78
+ category: 'heavy_weapon',
79
+ },
80
+ ROCKET_MISS: {
81
+ type: 'ROCKET_ALL_MISS',
82
+ title: '🎯 火力支援(理论上)',
83
+ text: `${name}\n火箭筒发射${shots}次 · 未检测到命中`,
84
+ score: 64,
85
+ category: 'heavy_weapon',
86
+ },
87
+ ROCKET_ALL_MISS: {
88
+ type: 'ROCKET_ALL_MISS',
89
+ title: '🎯 火力支援(理论上)',
90
+ text: `${name}\n火箭筒发射${shots}次 · 未检测到命中`,
91
+ score: 64,
92
+ category: 'heavy_weapon',
93
+ },
94
+ ROCKET_MULTI_KILL: {
95
+ type: 'ROCKET_MULTI_KILL',
96
+ title: '💥 一炮多响',
97
+ text: `${name}\n一发火箭筒关联${kills}次击杀`,
98
+ score: Math.min(99, 82 + kills * 4),
99
+ category: 'heavy_weapon',
100
+ },
101
+ ROCKET_VEHICLE_MULTI_KILL: {
102
+ type: 'ROCKET_VEHICLE_MULTI_KILL',
103
+ title: `☢️ 一炮${kills}响`,
104
+ text: `${name}\nPanzerfaust摧毁载具 · ${kills}杀`,
105
+ score: 100,
106
+ category: 'heavy_weapon',
107
+ },
108
+ ROCKET_VEHICLE_DESTROY: {
109
+ type: 'ROCKET_VEHICLE_DESTROY',
110
+ title: '🚙 火箭筒拆车',
111
+ text: `${name}\nPanzerfaust摧毁${Number(values.vehiclesDestroyed ?? 0)}辆载具`,
112
+ score: 78,
113
+ category: 'heavy_weapon',
114
+ },
115
+ ROCKET_HIT: {
116
+ type: 'ROCKET_HIT',
117
+ title: '🚀 火箭筒开张',
118
+ text: `${name}\n火箭筒${shots}发 · ${hits}次命中记录`,
119
+ score: Math.min(90, 65 + hits * 5),
120
+ category: 'heavy_weapon',
121
+ },
122
+ MULTI_KNOCK: {
123
+ type: 'MULTI_KNOCK',
124
+ title: '⚡ 倒地制造机',
125
+ text: `${name}\n一波团战造成${Number(values.knocks ?? 0)}次倒地`,
126
+ score: Math.min(92, 70 + Number(values.knocks ?? 0) * 5),
127
+ category: 'combat',
128
+ },
129
+ CLUTCH: {
130
+ type: 'CLUTCH',
131
+ title: '🏆 收割现场',
132
+ text: `${name}\n团战内完成${kills}次击杀`,
133
+ score: Math.min(94, 76 + kills * 5),
134
+ category: 'combat',
135
+ },
136
+ REVIVE_CHAIN: {
137
+ type: 'REVIVE_CHAIN',
138
+ title: '❤️ 急救站站长',
139
+ text: `${name}\n连续完成${Number(values.revives ?? 0)}次救援`,
140
+ score: Math.min(86, 55 + Number(values.revives ?? 0) * 6),
141
+ category: 'support',
142
+ },
143
+ VEHICLE_DESTROY: {
144
+ type: 'VEHICLE_DESTROY',
145
+ title: '🚙 车库拆迁队',
146
+ text: `${name}\n摧毁${Number(values.vehiclesDestroyed ?? 0)}辆载具`,
147
+ score: Math.min(90, 64 + Number(values.vehiclesDestroyed ?? 0) * 8),
148
+ category: 'vehicle',
149
+ },
150
+ };
151
+ const descriptor = descriptors[event.type];
152
+ if (!descriptor)
153
+ return null;
154
+ return makeFunEvent(facts, {
155
+ id: `fun-event-${event.id}`,
156
+ type: descriptor.type,
157
+ ...(event.playerId ? { actorPlayerId: event.playerId } : {}),
158
+ targetPlayerIds: [],
159
+ factIds: [event.id],
160
+ evidenceIds: [`evidence-${event.id}`, ...event.evidenceIds],
161
+ confidence: 'DERIVED',
162
+ funScore: descriptor.score,
163
+ category: descriptor.category,
164
+ title: descriptor.title,
165
+ text: descriptor.text,
166
+ facts: event.facts,
167
+ tags: ['special_event', descriptor.category],
168
+ dedupGroup: `special:${event.type}`,
169
+ });
170
+ }
171
+ function vehicleEvent(facts, vehicle, type, title, text, score, tags) {
172
+ const factId = vehicle.id ?? `vehicle-${vehicle.playerId}`;
173
+ return makeFunEvent(facts, {
174
+ id: `fun-event-vehicle-${type}-${vehicle.playerId}`,
175
+ actorPlayerId: vehicle.playerId,
176
+ targetPlayerIds: [],
177
+ factIds: [factId],
178
+ evidenceIds: vehicle.evidenceIds,
179
+ confidence: 'DERIVED',
180
+ funScore: score,
181
+ category: 'vehicle',
182
+ title,
183
+ text,
184
+ facts: {
185
+ rideDistance: Math.round(vehicle.rideDistance),
186
+ driveDistance: Math.round(vehicle.driveDistance),
187
+ maxSpeed: Math.round(vehicle.maxSpeed),
188
+ },
189
+ tags: ['vehicle', ...tags],
190
+ dedupGroup: 'vehicle-travel',
191
+ });
192
+ }
193
+ function aggregateTeamDamage(facts, source) {
194
+ const byActor = new Map();
195
+ for (const fact of facts.teamDamage ?? []) {
196
+ if (fact.source !== source)
197
+ continue;
198
+ const bucket = byActor.get(fact.actorPlayerId) ?? [];
199
+ bucket.push(fact);
200
+ byActor.set(fact.actorPlayerId, bucket);
201
+ }
202
+ const events = [];
203
+ for (const [actorId, actorFacts] of byActor) {
204
+ // A melee bucket can contain punches and weapon strikes. Generate the
205
+ // punch event from confirmed fist hits only, so a pan hit is never shown
206
+ // as another punch.
207
+ const factGroups = source === 'MELEE'
208
+ ? [
209
+ actorFacts.filter((fact) => isPunchWeapon(fact.weapon ?? null, fact.damageTypeCategory ?? null)),
210
+ actorFacts.filter((fact) => !isPunchWeapon(fact.weapon ?? null, fact.damageTypeCategory ?? null)),
211
+ ]
212
+ : [actorFacts];
213
+ for (const factsForEvent of factGroups) {
214
+ const totalHits = factsForEvent.reduce((sum, fact) => sum + fact.hitCount, 0);
215
+ const totalDamage = factsForEvent.reduce((sum, fact) => sum + fact.damage, 0);
216
+ if (totalHits <= 0)
217
+ continue;
218
+ const isPunching = source === 'MELEE' &&
219
+ factsForEvent.every((fact) => isPunchWeapon(fact.weapon ?? null, fact.damageTypeCategory ?? null));
220
+ const targetText = factsForEvent
221
+ .slice()
222
+ .sort((left, right) => right.hitCount - left.hitCount ||
223
+ left.victimPlayerId.localeCompare(right.victimPlayerId))
224
+ .map((fact) => `${playerName(facts, fact.victimPlayerId)} ${fact.hitCount}${isPunching ? '拳' : '次'}`)
225
+ .join(' · ');
226
+ const phase = factsForEvent[0]?.phase;
227
+ const phasePrefix = phase === 'pre_match' ? '赛前' : phase === 'in_match' ? '正赛' : '';
228
+ const descriptor = source === 'MELEE'
229
+ ? isPunching
230
+ ? {
231
+ type: 'TEAMMATE_PUNCHING',
232
+ title: '🥊 队内拳王',
233
+ text: `${playerName(facts, actorId)}\n${phasePrefix}给${targetText}`,
234
+ score: Math.min(78, 28 + totalHits * 8),
235
+ category: 'teammate',
236
+ }
237
+ : {
238
+ type: 'TEAM_MELEE_DAMAGE',
239
+ title: '🥊 队内近战',
240
+ text: `${playerName(facts, actorId)}\n对队友造成${integer(totalDamage)}点近战伤害`,
241
+ score: Math.min(64, 24 + totalDamage / 10),
242
+ category: 'teammate',
243
+ }
244
+ : source === 'GUN'
245
+ ? {
246
+ type: 'TEAM_GUN_DAMAGE',
247
+ title: '🔫 队友误伤',
248
+ text: `${playerName(facts, actorId)}\n对队友造成${integer(totalDamage)}点枪械伤害`,
249
+ score: Math.min(70, 25 + totalDamage / 10),
250
+ category: 'teammate',
251
+ }
252
+ : source === 'EXPLOSIVE'
253
+ ? {
254
+ type: 'TEAM_EXPLOSIVE_DAMAGE',
255
+ title: '💣 队内爆破',
256
+ text: `${playerName(facts, actorId)}\n对队友造成${integer(totalDamage)}点爆炸伤害`,
257
+ score: Math.min(76, 30 + totalDamage / 8),
258
+ category: 'teammate',
259
+ }
260
+ : {
261
+ type: 'TEAM_VEHICLE_DAMAGE',
262
+ title: '🚗 车辆误伤',
263
+ text: `对队友造成${integer(totalDamage)}点车辆伤害`,
264
+ score: Math.min(68, 28 + totalDamage / 10),
265
+ category: 'vehicle',
266
+ };
267
+ const confirmedDriver = source !== 'VEHICLE' ||
268
+ (facts.teamVehicleEvents ?? []).some((item) => item.actorPlayerId === actorId &&
269
+ item.driverConfirmed &&
270
+ factsForEvent.some((fact) => !fact.vehicleId || !item.vehicleId || fact.vehicleId === item.vehicleId));
271
+ const event = makeFunEvent(facts, {
272
+ id: `fun-event-team-damage-${source}-${actorId}-${isPunching ? 'punch' : 'other'}`,
273
+ type: descriptor.type,
274
+ ...(source === 'VEHICLE' && !confirmedDriver ? {} : { actorPlayerId: actorId }),
275
+ targetPlayerIds: factsForEvent.map((fact) => fact.victimPlayerId),
276
+ factIds: factsForEvent.map((fact) => fact.id),
277
+ evidenceIds: factsForEvent.flatMap((fact) => [`evidence-${fact.id}`, ...fact.evidenceIds]),
278
+ confidence: 'CONFIRMED',
279
+ funScore: descriptor.score,
280
+ category: descriptor.category,
281
+ title: descriptor.title,
282
+ text: descriptor.text,
283
+ facts: {
284
+ hitCount: totalHits,
285
+ damage: Math.round(totalDamage),
286
+ source,
287
+ ...(phase ? { phase } : {}),
288
+ ...(source === 'VEHICLE' ? { driverConfirmed: confirmedDriver } : {}),
289
+ },
290
+ tags: ['team_damage', source.toLowerCase(), ...(isPunching ? ['punching'] : [])],
291
+ dedupGroup: `team-damage:${source}:${isPunching ? 'punch' : 'other'}:${actorId}`,
292
+ });
293
+ if (event)
294
+ events.push(event);
295
+ }
296
+ }
297
+ return events;
298
+ }
299
+ function teamVehicleEvents(facts) {
300
+ const grouped = new Map();
301
+ for (const item of facts.teamVehicleEvents ?? []) {
302
+ const key = `${item.driverConfirmed ? item.actorPlayerId : 'unknown'}:${item.type}:${item.phase ?? 'unknown'}`;
303
+ const bucket = grouped.get(key) ?? [];
304
+ bucket.push(item);
305
+ grouped.set(key, bucket);
306
+ }
307
+ const result = [];
308
+ for (const [key, items] of grouped) {
309
+ const actorId = items[0]?.actorPlayerId;
310
+ const driverConfirmed = items.every((item) => item.driverConfirmed);
311
+ const hits = items.filter((item) => item.type === 'HIT');
312
+ const knocks = items.filter((item) => item.type === 'KNOCK');
313
+ const kills = items.filter((item) => item.type === 'KILL');
314
+ const targetIds = [...new Set(items.map((item) => item.victimPlayerId))];
315
+ const semanticType = items[0]?.type ?? 'HIT';
316
+ const count = items.length;
317
+ const detailText = items
318
+ .slice()
319
+ .sort((left, right) => left.victimPlayerId.localeCompare(right.victimPlayerId))
320
+ .map((item) => playerName(facts, item.victimPlayerId))
321
+ .join('、');
322
+ const confirmedText = semanticType === 'HIT'
323
+ ? `开车蹭到${detailText || '队友'} ${count}次`
324
+ : semanticType === 'KNOCK'
325
+ ? `开车撞倒${detailText || '队友'} ${count}人`
326
+ : `开车撞死${detailText || '队友'} ${count}人`;
327
+ const uncertainText = semanticType === 'HIT'
328
+ ? `车辆对${detailText || '队友'}造成 ${count} 次碰撞伤害`
329
+ : semanticType === 'KNOCK'
330
+ ? `车辆撞倒队友${detailText || ''} ${count}人`
331
+ : `车辆撞死队友${detailText || ''} ${count}人`;
332
+ const title = driverConfirmed
333
+ ? '🚗 肇事司机'
334
+ : semanticType === 'HIT'
335
+ ? '🚗 车辆误伤'
336
+ : '🚗 车辆事故';
337
+ const event = makeFunEvent(facts, {
338
+ id: `fun-event-vehicle-team-${key.replace(/[^a-z0-9:_-]/giu, '_')}`,
339
+ type: `VEHICLE_TEAM_${semanticType}`,
340
+ ...(driverConfirmed && actorId ? { actorPlayerId: actorId } : {}),
341
+ targetPlayerIds: targetIds,
342
+ factIds: items.map((item) => item.id),
343
+ evidenceIds: items.flatMap((item) => [`evidence-${item.id}`, ...item.evidenceIds]),
344
+ confidence: 'CONFIRMED',
345
+ funScore: Math.min(100, 58 + knocks.length * 14 + kills.length * 24 + hits.length * 4),
346
+ category: 'vehicle',
347
+ title,
348
+ text: driverConfirmed && actorId
349
+ ? `${playerName(facts, actorId)}\n${confirmedText}`
350
+ : uncertainText,
351
+ facts: {
352
+ hits: hits.length,
353
+ knocks: knocks.length,
354
+ kills: kills.length,
355
+ driverConfirmed,
356
+ ...(items[0]?.phase ? { phase: items[0].phase } : {}),
357
+ },
358
+ tags: [
359
+ 'vehicle',
360
+ 'team_damage',
361
+ ...(driverConfirmed ? ['driver_confirmed'] : ['driver_unconfirmed']),
362
+ ],
363
+ dedupGroup: `vehicle-team:${semanticType}:${driverConfirmed ? (actorId ?? 'unknown') : 'unknown'}`,
364
+ suppresses: ['TEAM_VEHICLE_DAMAGE'],
365
+ });
366
+ if (event)
367
+ result.push(event);
368
+ }
369
+ return result;
370
+ }
371
+ function flashEvents(facts) {
372
+ return (facts.flash ?? [])
373
+ .filter((item) => item.uses > 0)
374
+ .map((item) => makeFunEvent(facts, {
375
+ id: `fun-event-flash-${item.playerId}`,
376
+ type: 'FLASH_USED',
377
+ actorPlayerId: item.playerId,
378
+ targetPlayerIds: [],
379
+ factIds: [item.id ?? `flash-${item.playerId}`],
380
+ evidenceIds: [`evidence-flash-${item.playerId}`, ...item.evidenceIds],
381
+ confidence: 'CONFIRMED',
382
+ funScore: Math.min(80, 35 + item.uses * 7),
383
+ category: 'utility',
384
+ title: '💡 灯光师',
385
+ text: `${playerName(facts, item.playerId)}\n本局使用${item.uses}颗闪光弹`,
386
+ facts: { uses: item.uses },
387
+ tags: ['flash', 'utility'],
388
+ dedupGroup: `flash-use:${item.playerId}`,
389
+ }))
390
+ .filter((item) => item !== null);
391
+ }
392
+ function stunGunEvents(facts) {
393
+ return (facts.stunGuns ?? [])
394
+ .filter((item) => item.pickups > 0 || item.shots > 0)
395
+ .map((item) => makeFunEvent(facts, {
396
+ id: `fun-event-stun-gun-${item.playerId}`,
397
+ type: 'STUN_GUN_USED',
398
+ actorPlayerId: item.playerId,
399
+ targetPlayerIds: [],
400
+ factIds: [item.id],
401
+ evidenceIds: [`evidence-${item.id}`, ...item.evidenceIds],
402
+ confidence: 'CONFIRMED',
403
+ funScore: Math.min(80, 40 + item.shots * 12 + item.confirmedHits * 12),
404
+ category: 'utility',
405
+ title: '⚡ 电击枪出场',
406
+ text: `${playerName(facts, item.playerId)}\n拾取${item.pickups}次 · 开火${item.shots}次 · ${item.confirmedHits > 0 ? `确认命中${item.confirmedHits}次` : '未确认命中对象'}`,
407
+ facts: {
408
+ pickups: item.pickups,
409
+ shots: item.shots,
410
+ confirmedHits: item.confirmedHits,
411
+ teammateHits: item.teammateHits,
412
+ enemyHits: item.enemyHits,
413
+ unknownOutcomes: item.unknownOutcomes,
414
+ },
415
+ tags: ['stun_gun', 'utility'],
416
+ dedupGroup: `stun-gun:${item.playerId}`,
417
+ }))
418
+ .filter((item) => item !== null);
419
+ }
420
+ function vehicleImpactEvents(facts) {
421
+ return (facts.vehicleImpacts ?? [])
422
+ .filter((item) => item.wheelsDestroyed >= 3 || item.vehicleDestroyed > 0)
423
+ .map((item) => makeFunEvent(facts, {
424
+ id: `fun-event-vehicle-impact-${item.id}`,
425
+ type: item.wheelsDestroyed >= 4 ? 'VEHICLE_FOUR_WHEELS' : 'VEHICLE_IMPACT_CHAIN',
426
+ actorPlayerId: item.playerId,
427
+ targetPlayerIds: [],
428
+ factIds: [item.id],
429
+ evidenceIds: [`evidence-${item.id}`, ...item.evidenceIds],
430
+ confidence: 'CONFIRMED',
431
+ funScore: Math.min(100, 70 + item.wheelsDestroyed * 5 + item.vehicleDestroyed * 8 + item.knocks * 4),
432
+ category: 'vehicle',
433
+ title: item.wheelsDestroyed >= 4 ? '☢️ 一炮四轮' : '🚗 拆车连招',
434
+ text: `${playerName(facts, item.playerId)}\n${item.wheelsDestroyed > 0 ? `摧毁${item.wheelsDestroyed}个轮胎` : ''}${item.vehicleDamage > 0 ? ` · ${Math.round(item.vehicleDamage)}载具伤害` : ''}${item.playerDamage > 0 ? ` · ${Math.round(item.playerDamage)}人体伤害` : ''}${item.knocks > 0 ? ` · ${item.knocks}倒地` : ''}${item.vehicleDestroyed > 0 ? ' · 载具被摧毁' : ''}`,
435
+ facts: {
436
+ wheelsDestroyed: item.wheelsDestroyed,
437
+ vehicleDamage: Math.round(item.vehicleDamage),
438
+ playerDamage: Math.round(item.playerDamage),
439
+ knocks: item.knocks,
440
+ kills: item.kills,
441
+ vehicleDestroyed: item.vehicleDestroyed,
442
+ },
443
+ tags: ['vehicle', 'impact_chain', ...(item.wheelsDestroyed >= 4 ? ['four_wheels'] : [])],
444
+ dedupGroup: 'vehicle-impact-chain',
445
+ }))
446
+ .filter((item) => item !== null);
447
+ }
448
+ function armorBreakEvents(facts) {
449
+ return (facts.armorBreaks ?? [])
450
+ .filter((item) => item.followUp !== null)
451
+ .map((item) => makeFunEvent(facts, {
452
+ id: `fun-event-armor-break-${item.id}`,
453
+ type: 'ARMOR_BREAK_FOLLOW_UP',
454
+ actorPlayerId: item.actorPlayerId,
455
+ targetPlayerIds: item.victimPlayerId ? [item.victimPlayerId] : [],
456
+ factIds: [item.id],
457
+ evidenceIds: [`evidence-${item.id}`, ...item.evidenceIds],
458
+ confidence: 'CONFIRMED',
459
+ funScore: item.followUp === 'KILL' ? 88 : 76,
460
+ category: 'combat',
461
+ title: item.followUp === 'KILL'
462
+ ? `🎯 破${armorLabel(item.armorSlot)}后击杀`
463
+ : `🎯 破${armorLabel(item.armorSlot)}后击倒`,
464
+ text: `${playerName(facts, item.actorPlayerId)}\n${item.weapon ?? '攻击'}破${armorLabel(item.armorSlot)}后${item.followUp === 'KILL' ? '完成击杀' : '造成倒地'}`,
465
+ facts: {
466
+ armorItem: item.armorItem,
467
+ armorSlot: item.armorSlot,
468
+ weapon: item.weapon,
469
+ followUp: item.followUp,
470
+ distanceMeters: item.distanceMeters,
471
+ },
472
+ tags: ['armor_break', 'follow_up'],
473
+ dedupGroup: `armor-break-follow-up:${item.actorPlayerId}`,
474
+ }))
475
+ .filter((item) => item !== null);
476
+ }
477
+ function vehicleEvents(facts) {
478
+ const vehicles = facts.vehicles.filter((item) => item.evidenceIds.length > 0);
479
+ const result = [];
480
+ const drivers = vehicles
481
+ .filter((item) => item.driverConfirmed && item.driveDistance > 0)
482
+ .sort((left, right) => right.driveDistance - left.driveDistance || left.playerId.localeCompare(right.playerId));
483
+ const driver = drivers[0];
484
+ if (driver) {
485
+ const event = vehicleEvent(facts, driver, 'top-driver', '🚕 滴滴司机', `${playerName(facts, driver.playerId)}\n驾驶${kilometers(driver.driveDistance)} · 最高${Math.round(driver.maxSpeed)}km/h`, Math.min(82, 45 + driver.driveDistance / 1_000), ['driver']);
486
+ if (event)
487
+ result.push({ ...event, type: 'TOP_DRIVER', dedupGroup: 'vehicle-travel' });
488
+ }
489
+ const passengers = vehicles
490
+ .filter((item) => item.passengerConfirmed && item.rideDistance > 0)
491
+ .sort((left, right) => right.rideDistance - left.rideDistance || left.playerId.localeCompare(right.playerId));
492
+ const passenger = passengers[0];
493
+ if (passenger) {
494
+ const event = vehicleEvent(facts, passenger, 'top-passenger', '🛋 尊贵乘客', `${playerName(facts, passenger.playerId)}\n乘车${kilometers(passenger.rideDistance)}`, Math.min(76, 40 + passenger.rideDistance / 1_000), ['passenger']);
495
+ if (event)
496
+ result.push({ ...event, type: 'TOP_PASSENGER' });
497
+ }
498
+ const longest = [...vehicles]
499
+ .filter((item) => item.rideDistance >= 1_000)
500
+ .sort((left, right) => right.rideDistance - left.rideDistance || left.playerId.localeCompare(right.playerId))[0];
501
+ if (longest) {
502
+ const event = vehicleEvent(facts, longest, 'longest-ride', '🚌 公路旅行家', `${playerName(facts, longest.playerId)}\n乘车${kilometers(longest.rideDistance)}`, Math.min(68, 30 + longest.rideDistance / 2_000), ['ride']);
503
+ if (event)
504
+ result.push({ ...event, type: 'LONGEST_RIDE' });
505
+ }
506
+ return result;
507
+ }
508
+ function playerEvents(facts) {
509
+ const players = facts.players.filter((player) => player.matchPresence !== 'not_recorded');
510
+ const result = [];
511
+ const teamDamage = facts.squad.damage;
512
+ const summaryEvidence = (player) => [
513
+ `player-summary-${facts.match.matchId}-${player.playerId}`,
514
+ ];
515
+ const topDamage = [...players].sort((left, right) => right.damage - left.damage || left.playerId.localeCompare(right.playerId))[0];
516
+ if (topDamage && topDamage.damage >= Math.max(300, teamDamage * 0.4)) {
517
+ const event = makeFunEvent(facts, {
518
+ id: `fun-event-high-damage-${topDamage.playerId}`,
519
+ type: 'HIGH_DAMAGE',
520
+ actorPlayerId: topDamage.playerId,
521
+ targetPlayerIds: [],
522
+ factIds: [`player-summary-${facts.match.matchId}-${topDamage.playerId}`],
523
+ evidenceIds: summaryEvidence(topDamage),
524
+ confidence: 'DERIVED',
525
+ funScore: Math.min(75, 40 + topDamage.damage / 50),
526
+ category: 'combat',
527
+ title: '💥 火力压制',
528
+ text: `${topDamage.playerName}\n${integer(topDamage.damage)}伤害`,
529
+ facts: { damage: Math.round(topDamage.damage) },
530
+ tags: ['combat', 'damage'],
531
+ dedupGroup: `damage-output:${topDamage.playerId}`,
532
+ });
533
+ if (event)
534
+ result.push(event);
535
+ }
536
+ for (const player of players) {
537
+ if (player.damage >= 500 && player.kills <= 1) {
538
+ const event = makeFunEvent(facts, {
539
+ id: `fun-event-high-damage-low-kills-${player.playerId}`,
540
+ type: 'HIGH_DAMAGE_LOW_KILLS',
541
+ actorPlayerId: player.playerId,
542
+ targetPlayerIds: [],
543
+ factIds: [`player-summary-${facts.match.matchId}-${player.playerId}`],
544
+ evidenceIds: summaryEvidence(player),
545
+ confidence: 'DERIVED',
546
+ funScore: Math.min(88, 65 + player.damage / 100),
547
+ category: 'combat',
548
+ title: '📈 数据刷子',
549
+ text: `${player.playerName}\n${integer(player.damage)}伤害 · ${player.kills}杀`,
550
+ facts: { damage: Math.round(player.damage), kills: player.kills },
551
+ tags: ['combat', 'damage', 'low_kills'],
552
+ dedupGroup: `damage-output:${player.playerId}`,
553
+ suppresses: ['HIGH_DAMAGE'],
554
+ });
555
+ if (event)
556
+ result.push(event);
557
+ }
558
+ if (player.kills >= 2 && player.damage <= Math.max(250, teamDamage * 0.18)) {
559
+ const event = makeFunEvent(facts, {
560
+ id: `fun-event-low-damage-high-kills-${player.playerId}`,
561
+ type: 'LOW_DAMAGE_HIGH_KILLS',
562
+ actorPlayerId: player.playerId,
563
+ targetPlayerIds: [],
564
+ factIds: [`player-summary-${facts.match.matchId}-${player.playerId}`],
565
+ evidenceIds: summaryEvidence(player),
566
+ confidence: 'DERIVED',
567
+ funScore: Math.min(88, 70 + player.kills * 4),
568
+ category: 'combat',
569
+ title: '🧹 收割机',
570
+ text: `${player.playerName}\n${integer(player.damage)}伤害 · ${player.kills}杀`,
571
+ facts: { damage: Math.round(player.damage), kills: player.kills },
572
+ tags: ['combat', 'low_damage', 'kills'],
573
+ dedupGroup: `damage-output:${player.playerId}`,
574
+ });
575
+ if (event)
576
+ result.push(event);
577
+ }
578
+ if (player.dbnos >= 3 && player.kills / player.dbnos <= 0.4) {
579
+ const event = makeFunEvent(facts, {
580
+ id: `fun-event-knock-conversion-${player.playerId}`,
581
+ type: 'HIGH_KNOCK_LOW_KILL_CONVERSION',
582
+ actorPlayerId: player.playerId,
583
+ targetPlayerIds: [],
584
+ factIds: [`player-summary-${facts.match.matchId}-${player.playerId}`],
585
+ evidenceIds: summaryEvidence(player),
586
+ confidence: 'DERIVED',
587
+ funScore: Math.min(86, 52 + player.dbnos * 5),
588
+ category: 'combat',
589
+ title: '🫠 白打王',
590
+ text: `${player.playerName}\n${player.dbnos}倒地 · ${player.kills}杀`,
591
+ facts: { dbnos: player.dbnos, kills: player.kills },
592
+ tags: ['combat', 'knocks', 'conversion'],
593
+ dedupGroup: `knock-conversion:${player.playerId}`,
594
+ });
595
+ if (event)
596
+ result.push(event);
597
+ }
598
+ if (player.kills === 0 &&
599
+ player.assists === 0 &&
600
+ player.dbnos === 0 &&
601
+ player.revives === 0 &&
602
+ player.damage === 0 &&
603
+ player.keyOperations.length === 0) {
604
+ const event = makeFunEvent(facts, {
605
+ id: `fun-event-no-combat-${player.playerId}`,
606
+ type: 'NO_COMBAT_PRESENCE',
607
+ actorPlayerId: player.playerId,
608
+ targetPlayerIds: [],
609
+ factIds: [`player-summary-${facts.match.matchId}-${player.playerId}`],
610
+ evidenceIds: summaryEvidence(player),
611
+ confidence: 'DERIVED',
612
+ funScore: 58,
613
+ category: 'combat',
614
+ title: '👻 全场隐身',
615
+ text: `${player.playerName}\n0杀 · 0助 · 0倒地 · 0救援 · 0伤害`,
616
+ facts: { kills: 0, assists: 0, dbnos: 0, revives: 0, damage: 0 },
617
+ tags: ['combat', 'no_presence'],
618
+ dedupGroup: `combat-presence:${player.playerId}`,
619
+ });
620
+ if (event)
621
+ result.push(event);
622
+ }
623
+ }
624
+ const topRevives = [...players].sort((left, right) => right.revives - left.revives || left.playerId.localeCompare(right.playerId))[0];
625
+ const secondRevives = [...players].sort((left, right) => right.revives - left.revives || left.playerId.localeCompare(right.playerId))[1];
626
+ if (topRevives && topRevives.revives >= 2 && topRevives.revives > (secondRevives?.revives ?? 0)) {
627
+ const event = makeFunEvent(facts, {
628
+ id: `fun-event-most-revives-${topRevives.playerId}`,
629
+ type: 'MOST_REVIVES',
630
+ actorPlayerId: topRevives.playerId,
631
+ targetPlayerIds: [],
632
+ factIds: [`player-summary-${facts.match.matchId}-${topRevives.playerId}`],
633
+ evidenceIds: summaryEvidence(topRevives),
634
+ confidence: 'DERIVED',
635
+ funScore: Math.min(82, 48 + topRevives.revives * 7),
636
+ category: 'support',
637
+ title: '🩺 战地医生',
638
+ text: `${topRevives.playerName}\n${topRevives.revives}次救援`,
639
+ facts: { revives: topRevives.revives },
640
+ tags: ['support', 'revive'],
641
+ dedupGroup: `support-ranking:revives:${topRevives.playerId}`,
642
+ });
643
+ if (event)
644
+ result.push(event);
645
+ }
646
+ const topAssists = [...players].sort((left, right) => right.assists - left.assists || left.playerId.localeCompare(right.playerId))[0];
647
+ const secondAssists = [...players].sort((left, right) => right.assists - left.assists || left.playerId.localeCompare(right.playerId))[1];
648
+ if (topAssists && topAssists.assists >= 2 && topAssists.assists > (secondAssists?.assists ?? 0)) {
649
+ const event = makeFunEvent(facts, {
650
+ id: `fun-event-most-assists-${topAssists.playerId}`,
651
+ type: 'MOST_ASSISTS',
652
+ actorPlayerId: topAssists.playerId,
653
+ targetPlayerIds: [],
654
+ factIds: [`player-summary-${facts.match.matchId}-${topAssists.playerId}`],
655
+ evidenceIds: summaryEvidence(topAssists),
656
+ confidence: 'DERIVED',
657
+ funScore: Math.min(78, 44 + topAssists.assists * 6),
658
+ category: 'support',
659
+ title: '🤝 真兄弟',
660
+ text: `${topAssists.playerName}\n${topAssists.assists}次助攻`,
661
+ facts: { assists: topAssists.assists },
662
+ tags: ['support', 'assist'],
663
+ dedupGroup: `support-ranking:assists:${topAssists.playerId}`,
664
+ });
665
+ if (event)
666
+ result.push(event);
667
+ }
668
+ return result;
669
+ }
670
+ function keyOperationEvents(facts) {
671
+ const specialEvents = facts.specialEvents;
672
+ const descriptors = {
673
+ ENTRY: { title: '🔥 开团发动机', category: 'combat', score: 62 },
674
+ MULTI_KNOCK: { title: '⚡ 倒地制造机', category: 'combat', score: 82 },
675
+ CLUTCH: { title: '🏆 收割现场', category: 'combat', score: 84 },
676
+ TRADE: { title: '🔁 补枪及时', category: 'combat', score: 70 },
677
+ SUPPORT: { title: '🛡️ 火力支援', category: 'support', score: 62 },
678
+ REVIVE: { title: '❤️ 救援及时', category: 'support', score: 70 },
679
+ DAMAGE: { title: '💥 输出担当', category: 'combat', score: 68 },
680
+ VEHICLE: { title: '🚗 载具作战', category: 'vehicle', score: 70 },
681
+ HEAVY_WEAPON: { title: '🚀 重火力出场', category: 'heavy_weapon', score: 74 },
682
+ };
683
+ const result = [];
684
+ for (const player of facts.players) {
685
+ for (const operation of player.keyOperations) {
686
+ const descriptor = descriptors[operation.type];
687
+ if (!descriptor)
688
+ continue;
689
+ const fightId = operation.facts.fightId;
690
+ const coveredBySpecial = specialEvents.some((event) => event.playerId === player.playerId &&
691
+ event.type === operation.type &&
692
+ (fightId === undefined || event.facts.fightId === fightId));
693
+ if (coveredBySpecial)
694
+ continue;
695
+ const event = makeFunEvent(facts, {
696
+ id: `fun-event-operation-${operation.id}`,
697
+ type: `KEY_OPERATION_${operation.type}`,
698
+ actorPlayerId: player.playerId,
699
+ targetPlayerIds: [],
700
+ factIds: [operation.id],
701
+ evidenceIds: [`evidence-${operation.id}`, ...operation.evidenceIds],
702
+ confidence: 'DERIVED',
703
+ funScore: descriptor.score,
704
+ category: descriptor.category,
705
+ title: descriptor.title,
706
+ text: `${player.playerName}\n${operation.impact}`,
707
+ facts: operation.facts,
708
+ tags: ['key_operation', descriptor.category],
709
+ dedupGroup: `key-operation:${operation.type}:${player.playerId}`,
710
+ });
711
+ if (event)
712
+ result.push(event);
713
+ }
714
+ }
715
+ return result;
716
+ }
717
+ /** Generate one structured event per reliable fact pattern; combos are added later. */
718
+ export function generateBaseFunEvents(facts) {
719
+ return [
720
+ ...facts.specialEvents.map((event) => specialEvent(facts, event)),
721
+ ...aggregateTeamDamage(facts, 'MELEE'),
722
+ ...aggregateTeamDamage(facts, 'GUN'),
723
+ ...aggregateTeamDamage(facts, 'VEHICLE'),
724
+ ...aggregateTeamDamage(facts, 'EXPLOSIVE'),
725
+ ...teamVehicleEvents(facts),
726
+ ...flashEvents(facts),
727
+ ...stunGunEvents(facts),
728
+ ...vehicleImpactEvents(facts),
729
+ ...armorBreakEvents(facts),
730
+ ...vehicleEvents(facts),
731
+ ...playerEvents(facts),
732
+ ...keyOperationEvents(facts),
733
+ ].filter((event) => event !== null && event.evidenceIds.length > 0);
734
+ }
735
+ export class FunEventCandidateGenerator {
736
+ generate(facts) {
737
+ return generateBaseFunEvents(facts);
738
+ }
739
+ }
740
+ //# sourceMappingURL=fun-event-generator.js.map