@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,652 @@
1
+ import { selectKeyFights } from './fight-detector.js';
2
+ import { generateFunCandidates } from './fun-candidate-generator.js';
3
+ import { generateFunEvents } from './fun-intelligence.js';
4
+ import { meleeKindOf } from './telemetry-events.js';
5
+ function playerName(facts, playerId) {
6
+ return facts.players.find((player) => player.playerId === playerId)?.playerName ?? playerId;
7
+ }
8
+ function operationLabel(type) {
9
+ const labels = {
10
+ ENTRY: '开团',
11
+ MULTI_KNOCK: '多次倒地',
12
+ CLUTCH: '关键收割',
13
+ FLANK: '侧翼',
14
+ TRADE: '补枪交换',
15
+ SUPPORT: '火力支援',
16
+ REVIVE: '救援',
17
+ DAMAGE: '输出',
18
+ POSITIONING_RISK: '站位风险',
19
+ MISTAKE: '失误',
20
+ VEHICLE: '载具作战',
21
+ HEAVY_WEAPON: '重武器',
22
+ };
23
+ return labels[type] ?? type;
24
+ }
25
+ function operationTime(value) {
26
+ return value ?? Number.POSITIVE_INFINITY;
27
+ }
28
+ function hasBaseContribution(player) {
29
+ return (player.kills > 0 ||
30
+ player.assists > 0 ||
31
+ player.dbnos > 0 ||
32
+ player.revives > 0 ||
33
+ player.damage > 0);
34
+ }
35
+ function percentage(value, total) {
36
+ if (!Number.isFinite(value) || !Number.isFinite(total) || total <= 0)
37
+ return null;
38
+ return `${((value / total) * 100).toFixed(1)}%`;
39
+ }
40
+ function clock(seconds) {
41
+ if (seconds === null || !Number.isFinite(seconds))
42
+ return '--:--';
43
+ const total = Math.max(0, Math.round(seconds));
44
+ return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
45
+ }
46
+ function integer(value) {
47
+ return Math.round(value).toLocaleString('zh-CN');
48
+ }
49
+ function weaponLabel(value) {
50
+ const normalized = value
51
+ .replace(/^Item_Weapon_/iu, '')
52
+ .replace(/^Weapon_/iu, '')
53
+ .replace(/^Weap/iu, '')
54
+ .replace(/_C$/u, '');
55
+ return ({
56
+ AUGA3: 'AUG',
57
+ BerylM762: 'Beryl M762',
58
+ Kar98k: 'Kar98k',
59
+ M24: 'M24',
60
+ RPD: 'RPD',
61
+ PanzerFaust100M: 'Panzerfaust',
62
+ }[normalized] ?? normalized);
63
+ }
64
+ function armorLabel(value) {
65
+ const normalized = (value ?? '').toLowerCase();
66
+ if (normalized.includes('head'))
67
+ return '头盔';
68
+ if (normalized.includes('vest') || normalized.includes('armor'))
69
+ return '防弹衣';
70
+ return '护甲';
71
+ }
72
+ function playerWeapons(facts, playerId) {
73
+ return facts.weapons
74
+ .filter((weapon) => weapon.playerId === playerId && weapon.evidenceIds.length > 0)
75
+ .sort((left, right) => right.damage - left.damage || right.kills - left.kills || right.hits - left.hits);
76
+ }
77
+ function playerTeamDamage(facts, playerId, direction) {
78
+ return (facts.teamDamage ?? []).filter((fact) => direction === 'outgoing' ? fact.actorPlayerId === playerId : fact.victimPlayerId === playerId);
79
+ }
80
+ function meleeActionText(facts, playerId, direction) {
81
+ const factsForPlayer = playerTeamDamage(facts, playerId, direction).filter((fact) => fact.source === 'MELEE');
82
+ if (!factsForPlayer.length)
83
+ return null;
84
+ const counts = new Map();
85
+ for (const fact of factsForPlayer) {
86
+ const kind = fact.meleeKind ?? meleeKindOf(fact.weapon ?? null, fact.damageTypeCategory ?? null);
87
+ counts.set(kind, (counts.get(kind) ?? 0) + fact.hitCount);
88
+ }
89
+ const labels = { KICK: '脚', PUNCH: '拳', OTHER: '近战' };
90
+ const actions = [...counts.entries()]
91
+ .sort(([left], [right]) => (({ KICK: 1, PUNCH: 2, OTHER: 3 })[left] ?? 9) -
92
+ ({ KICK: 1, PUNCH: 2, OTHER: 3 }[right] ?? 9))
93
+ .map(([kind, count]) => `${count}${labels[kind] ?? '次'}`)
94
+ .join(' + ');
95
+ const totalDamage = factsForPlayer.reduce((sum, fact) => sum + fact.damage, 0);
96
+ return `${actions}、${Math.round(totalDamage * 100) / 100}点`;
97
+ }
98
+ function recoveryText(facts, playerId) {
99
+ const item = facts.recovery?.find((entry) => entry.playerId === playerId);
100
+ if (!item)
101
+ return null;
102
+ const parts = [
103
+ item.bandages ? `绷带${item.bandages}` : '',
104
+ item.firstAids ? `急救${item.firstAids}` : '',
105
+ item.medKits ? `医疗箱${item.medKits}` : '',
106
+ item.energyDrinks ? `能量${item.energyDrinks}` : '',
107
+ item.painkillers ? `止痛${item.painkillers}` : '',
108
+ ].filter(Boolean);
109
+ return parts.length ? parts.join('、') : null;
110
+ }
111
+ function playerEvidenceIds(facts, playerId) {
112
+ const ids = [
113
+ `player-summary-${facts.match.matchId}-${playerId}`,
114
+ ...(facts.players
115
+ .find((player) => player.playerId === playerId)
116
+ ?.keyOperations.flatMap((operation) => operation.evidenceIds) ?? []),
117
+ ...facts.weapons
118
+ .filter((item) => item.playerId === playerId)
119
+ .flatMap((item) => item.evidenceIds),
120
+ ...(facts.teamDamage
121
+ ?.filter((item) => item.actorPlayerId === playerId || item.victimPlayerId === playerId)
122
+ .flatMap((item) => item.evidenceIds) ?? []),
123
+ ...(facts.lootActivity
124
+ ?.filter((item) => item.playerId === playerId)
125
+ .flatMap((item) => item.evidenceIds) ?? []),
126
+ ...(facts.environment
127
+ ?.filter((item) => item.playerId === playerId)
128
+ .flatMap((item) => item.evidenceIds) ?? []),
129
+ ...(facts.vehicleImpacts
130
+ ?.filter((item) => item.playerId === playerId)
131
+ .flatMap((item) => item.evidenceIds) ?? []),
132
+ ];
133
+ const available = new Set(facts.evidence.map((item) => item.id));
134
+ return [...new Set(ids)].flatMap((id) => available.has(id) ? [id] : available.has(`evidence-${id}`) ? [`evidence-${id}`] : []);
135
+ }
136
+ function fightNumber(facts, fight) {
137
+ return Math.max(1, facts.fights.findIndex((candidate) => candidate.id === fight.id) + 1);
138
+ }
139
+ function fightLabel(facts, fight) {
140
+ return `第${fightNumber(facts, fight)}波团战`;
141
+ }
142
+ function operationForFight(facts, fight, type) {
143
+ return (facts.players
144
+ .flatMap((player) => player.keyOperations)
145
+ .filter((operation) => operation.facts.fightId === fight.id && operation.type === type)
146
+ .sort((left, right) => right.impactScore - left.impactScore ||
147
+ operationTime(left.time) - operationTime(right.time))[0] ?? null);
148
+ }
149
+ function isKeyPlayerSpecialEvent(type) {
150
+ return [
151
+ 'MULTI_KNOCK',
152
+ 'CLUTCH',
153
+ 'REVIVE_CHAIN',
154
+ 'ROCKET_HIT',
155
+ 'ROCKET_MULTI_KILL',
156
+ 'ROCKET_VEHICLE_DESTROY',
157
+ 'ROCKET_VEHICLE_MULTI_KILL',
158
+ 'VEHICLE_DESTROY',
159
+ 'VEHICLE_KILL',
160
+ ].includes(type);
161
+ }
162
+ function commentaryForPlayer(facts, player) {
163
+ const operations = [...player.keyOperations].sort((left, right) => right.impactScore - left.impactScore || operationTime(left.time) - operationTime(right.time));
164
+ const weapons = playerWeapons(facts, player.playerId);
165
+ const topWeapon = weapons[0];
166
+ const topDamage = facts.players
167
+ .filter((item) => item.matchPresence !== 'not_recorded')
168
+ .sort((left, right) => right.damage - left.damage || right.kills - left.kills)[0];
169
+ const fights = facts.fights
170
+ .filter((fight) => fight.eventCount > 0 &&
171
+ (fight.participants.includes(player.playerId) ||
172
+ fight.keyPlayers.includes(player.playerId)))
173
+ .sort((left, right) => right.importanceScore - left.importanceScore || right.teamDamage - left.teamDamage);
174
+ const outgoing = playerTeamDamage(facts, player.playerId, 'outgoing');
175
+ const meleeOut = meleeActionText(facts, player.playerId, 'outgoing');
176
+ const meleeIn = meleeActionText(facts, player.playerId, 'incoming');
177
+ const recovery = recoveryText(facts, player.playerId);
178
+ const loot = facts.lootActivity?.find((item) => item.playerId === player.playerId);
179
+ const environment = facts.environment?.find((item) => item.playerId === player.playerId);
180
+ const strengths = [
181
+ ...operations.slice(0, 2).map((operation) => operation.impact),
182
+ ...(topWeapon && topWeapon.damage > 0
183
+ ? [`${weaponLabel(topWeapon.weapon)}贡献${integer(topWeapon.damage)}伤害`]
184
+ : []),
185
+ ].slice(0, 3);
186
+ const improvements = [];
187
+ if (player.matchPresence === 'not_recorded') {
188
+ return {
189
+ playerId: player.playerId,
190
+ roleConfidence: player.roleConfidence,
191
+ text: '-',
192
+ strengths: [],
193
+ improvements: [],
194
+ operationIds: [],
195
+ };
196
+ }
197
+ if (!operations.length &&
198
+ !hasBaseContribution(player) &&
199
+ !topWeapon &&
200
+ !meleeOut &&
201
+ !recovery &&
202
+ !loot &&
203
+ !environment) {
204
+ return {
205
+ playerId: player.playerId,
206
+ roleConfidence: player.roleConfidence,
207
+ text: '本局未检测到有效关键贡献。',
208
+ strengths: [],
209
+ improvements: ['本局未检测到有效关键贡献,暂不做更强的负面推断'],
210
+ operationIds: [],
211
+ };
212
+ }
213
+ const share = percentage(player.damage, facts.squad.damage);
214
+ const role = player.kills >= 2
215
+ ? `终结位打出${player.kills}杀/${player.dbnos}倒地`
216
+ : topDamage?.playerId === player.playerId && player.damage > 0
217
+ ? `火力炮台打出${integer(player.damage)}伤害`
218
+ : player.revives > 0
219
+ ? `支援位完成${player.revives}次救援`
220
+ : player.assists > 0
221
+ ? `连接位拿到${player.assists}次助攻`
222
+ : player.damage > 0
223
+ ? `持续输出${integer(player.damage)}伤害`
224
+ : player.matchRole
225
+ ? `主要承担${player.matchRole}`
226
+ : '参与了本局战斗';
227
+ const details = [];
228
+ if (share && player.damage > 0)
229
+ details.push(`占队伍${share}`);
230
+ if (player.dbnos > 0)
231
+ details.push(`倒地转化${player.kills}/${player.dbnos}`);
232
+ if (player.assists > 0)
233
+ details.push(`${player.assists}助攻`);
234
+ if (player.revives > 0)
235
+ details.push(`${player.revives}救援`);
236
+ if (topWeapon && (topWeapon.damage > 0 || topWeapon.hits > 0 || topWeapon.kills > 0)) {
237
+ details.push(`主力${weaponLabel(topWeapon.weapon)} ${topWeapon.hits}次命中记录/${integer(topWeapon.damage)}伤害${topWeapon.kills ? `/${topWeapon.kills}杀` : ''}`);
238
+ }
239
+ const heavy = player.heavyWeapons.filter((item) => item.evidenceIds.length > 0 &&
240
+ (item.shots > 0 ||
241
+ item.hits > 0 ||
242
+ item.kills > 0 ||
243
+ item.knocks > 0 ||
244
+ item.vehiclesDestroyed > 0));
245
+ if (heavy.length) {
246
+ const item = heavy[0];
247
+ details.push(`重火力${weaponLabel(item.weapon)} ${item.shots}发/${item.hits}次命中记录${item.kills ? `/${item.kills}杀` : ''}${item.vehicleDamage > 0 ? `,${integer(item.vehicleDamage)}载具伤害` : ''}`);
248
+ }
249
+ const armorBreak = facts.armorBreaks?.find((item) => item.actorPlayerId === player.playerId && item.followUp !== null);
250
+ if (armorBreak)
251
+ details.push(`${weaponLabel(armorBreak.weapon ?? '攻击')}破${armorLabel(armorBreak.armorSlot)}后${armorBreak.followUp === 'KILL' ? '接击杀' : '接倒地'}`);
252
+ const vehicleImpact = facts.vehicleImpacts?.find((item) => item.playerId === player.playerId &&
253
+ (item.wheelsDestroyed > 0 || item.vehicleDestroyed > 0 || item.vehicleDamage > 0));
254
+ if (vehicleImpact)
255
+ details.push(`载具链${vehicleImpact.wheelsDestroyed ? `打掉${vehicleImpact.wheelsDestroyed}个轮胎` : ''}${vehicleImpact.vehicleDamage > 0 ? `、${integer(vehicleImpact.vehicleDamage)}载具伤害` : ''}`);
256
+ const flash = facts.flash?.find((item) => item.playerId === player.playerId && item.uses > 0);
257
+ if (flash)
258
+ details.push(`闪光弹${flash.uses}颗${facts.stunGuns?.some((item) => item.playerId === player.playerId && item.confirmedHits > 0) ? '' : ',未确认命中对象'}`);
259
+ if (recovery)
260
+ details.push(`恢复${recovery}`);
261
+ if (loot)
262
+ details.push(`原始拾取${loot.pickupEvents}/丢弃${loot.dropEvents}/搜包${loot.lootBoxPickups}`);
263
+ if (environment) {
264
+ const destroyed = (environment.destroyedObjects ?? []).reduce((sum, item) => sum + item.count, 0);
265
+ if (destroyed || environment.vaults || environment.doorOpens)
266
+ details.push(`环境破坏${destroyed}项、开门${environment.doorOpens}、翻越${environment.vaults}`);
267
+ }
268
+ if (fights[0])
269
+ details.push(`第${fightNumber(facts, fights[0])}波${fights[0].result === 'WIN' ? '赢下' : fights[0].result === 'LOSS' ? '崩盘' : '接战'}${Math.round(fights[0].teamDamage)}伤害`);
270
+ let text = `${role};${details.length ? `${details.join(',')}。` : '有明确参战记录。'}`;
271
+ if (player.matchRole === '开团/信息' && player.kills === 0)
272
+ improvements.push('开团后继续报点并跟进补枪,把先手优势转成击杀');
273
+ if (player.heavyWeapons.some((item) => item.pickupEvents > 0 && item.shots === 0))
274
+ improvements.push('重武器捡了不打就是背包装饰,进接触区前先确认弹药和射界');
275
+ if (player.dbnos > player.kills)
276
+ improvements.push(`倒地${player.dbnos}次只转化${player.kills}杀,别把敌人打跪后把收口权交出去`);
277
+ if (player.kills === 0 &&
278
+ player.dbnos === 0 &&
279
+ player.damage >= Math.max(120, facts.squad.damage * 0.3))
280
+ improvements.push('输出有了,击杀和倒地是空白;打靶分不等于团战收口,下一次把补枪做完');
281
+ if (player.assists > 0 && player.kills === 0 && player.dbnos === 0)
282
+ improvements.push('助攻只能证明碰到过战斗,先把一条残血和一个倒地兑现成击杀');
283
+ if (meleeOut) {
284
+ const meleeDamage = outgoing
285
+ .filter((item) => item.source === 'MELEE')
286
+ .reduce((sum, item) => sum + item.damage, 0);
287
+ improvements.push(meleeDamage >= 80
288
+ ? `对队友打出${meleeOut}近战伤害,脚拳全落在自己人身上;别把队友当沙袋,正赛不是练拳房`
289
+ : `对队友造成${meleeOut}近战伤害,贴脸前先看准目标,别再把队伍容错踢掉`);
290
+ }
291
+ if (meleeIn)
292
+ text += ` 你也承受了队友${meleeIn}的近战伤害。`;
293
+ const explosiveDamage = outgoing
294
+ .filter((item) => item.source === 'EXPLOSIVE')
295
+ .reduce((sum, item) => sum + item.damage, 0);
296
+ if (explosiveDamage > 0)
297
+ improvements.push(`投掷物误伤队友${integer(explosiveDamage)}点,落雷前先确认队友脚下不是爆破区`);
298
+ const vehicleTeamImpact = (facts.teamVehicleEvents ?? []).filter((item) => item.actorPlayerId === player.playerId &&
299
+ item.driverConfirmed &&
300
+ ['KNOCK', 'KILL'].includes(item.type));
301
+ if (vehicleTeamImpact.length)
302
+ improvements.push(`已确认驾驶碰撞造成队友${vehicleTeamImpact.length}次${vehicleTeamImpact.some((item) => item.type === 'KILL') ? '击杀' : '倒地'};别开车了,方向盘不是复活币`);
303
+ const finalLoss = facts.fights
304
+ .filter((fight) => fight.result === 'LOSS')
305
+ .sort((left, right) => right.end - left.end)[0];
306
+ if (finalLoss &&
307
+ finalLoss.participants.includes(player.playerId) &&
308
+ finalLoss.teamKills === 0 &&
309
+ finalLoss.receivedKills > 0) {
310
+ improvements.push(`末战第${fightNumber(facts, finalLoss)}波打出${Math.round(finalLoss.teamDamage)}伤害却零击杀,终局要先报倒地、补枪、收缩,别各打各的`);
311
+ }
312
+ if (improvements.length)
313
+ text += ` 锐评:${improvements.slice(0, 3).join(';')}。`;
314
+ return {
315
+ playerId: player.playerId,
316
+ ...(player.matchRole ? { role: player.matchRole } : {}),
317
+ roleConfidence: player.roleConfidence,
318
+ text,
319
+ strengths,
320
+ improvements,
321
+ operationIds: operations.map((operation) => operation.id),
322
+ };
323
+ }
324
+ function turningPointEvidence(fight, ...extra) {
325
+ return [...new Set([...fight.evidenceIds, ...extra.flat()])];
326
+ }
327
+ function mainFightPoint(facts, fight) {
328
+ const entry = operationForFight(facts, fight, 'ENTRY');
329
+ const multiKnock = operationForFight(facts, fight, 'MULTI_KNOCK');
330
+ const clutch = operationForFight(facts, fight, 'CLUTCH');
331
+ const clauses = [];
332
+ if (entry)
333
+ clauses.push(`${playerName(facts, entry.playerId)}先手开团`);
334
+ if (multiKnock)
335
+ clauses.push(`${playerName(facts, multiKnock.playerId)}打出${String(multiKnock.facts.knocks ?? fight.teamKnocks)}次倒地`);
336
+ if (clutch)
337
+ clauses.push(`${playerName(facts, clutch.playerId)}完成${String(clutch.facts.kills ?? fight.teamKills)}次击杀`);
338
+ const result = fight.result === 'TRADE'
339
+ ? `拿到${fight.teamKills}杀/${fight.teamKnocks}倒地,但付出${fight.receivedKills}次被击杀,属于高收益交换而不是无伤碾压`
340
+ : `我方${fight.result === 'WIN' ? '赢下' : '完成'}这波,${fight.teamKills}杀/${fight.teamKnocks}倒地,承受${fight.receivedKills}次被击杀`;
341
+ return {
342
+ id: `turning-point-${fight.id}`,
343
+ type: fight.result === 'TRADE'
344
+ ? 'POWER_SPIKE'
345
+ : fight.result === 'WIN'
346
+ ? 'CLEAN_WIN'
347
+ : 'POWER_SPIKE',
348
+ time: fight.start,
349
+ title: fight.result === 'TRADE'
350
+ ? '主战团:高收益交换'
351
+ : fight.result === 'WIN'
352
+ ? '主战团:成功收口'
353
+ : '主战团:战斗窗口',
354
+ text: `${fightLabel(facts, fight)} ${clock(fight.start)}–${clock(fight.end)}:${clauses.length ? `${clauses.join(',')};` : ''}${result}。全波造成${Math.round(fight.teamDamage)}伤害。`,
355
+ impact: fight.teamKills > fight.receivedKills
356
+ ? 'positive'
357
+ : fight.teamKills < fight.receivedKills
358
+ ? 'negative'
359
+ : 'neutral',
360
+ facts: {
361
+ fightId: fight.id,
362
+ teamKills: fight.teamKills,
363
+ teamKnocks: fight.teamKnocks,
364
+ teamDamage: Math.round(fight.teamDamage),
365
+ receivedKills: fight.receivedKills,
366
+ receivedKnocks: fight.receivedKnocks,
367
+ },
368
+ evidenceIds: turningPointEvidence(fight, entry?.evidenceIds ?? [], multiKnock?.evidenceIds ?? [], clutch?.evidenceIds ?? []),
369
+ };
370
+ }
371
+ function buildTurningPoints(facts) {
372
+ if (!facts.fightIntegrity.pass)
373
+ return [];
374
+ const fights = facts.fights.filter((fight) => fight.eventCount > 0);
375
+ if (!fights.length)
376
+ return [];
377
+ const result = [];
378
+ const main = [...fights].sort((left, right) => right.importanceScore - left.importanceScore || left.start - right.start)[0];
379
+ if (main)
380
+ result.push(mainFightPoint(facts, main));
381
+ const cleanWin = [...fights]
382
+ .filter((fight) => fight.id !== main?.id && fight.result === 'WIN')
383
+ .sort((left, right) => right.importanceScore - left.importanceScore || left.start - right.start)[0];
384
+ if (cleanWin) {
385
+ result.push({
386
+ id: `turning-point-${cleanWin.id}`,
387
+ type: 'CLEAN_WIN',
388
+ time: cleanWin.start,
389
+ title: '小团战:无伤收口',
390
+ text: `${fightLabel(facts, cleanWin)} ${clock(cleanWin.start)}–${clock(cleanWin.end)}完成${cleanWin.teamKills}次击杀、${cleanWin.teamKnocks}次倒地和${Math.round(cleanWin.teamDamage)}伤害,遥测未记录我方被击杀。`,
391
+ impact: 'positive',
392
+ facts: {
393
+ fightId: cleanWin.id,
394
+ teamKills: cleanWin.teamKills,
395
+ teamKnocks: cleanWin.teamKnocks,
396
+ teamDamage: Math.round(cleanWin.teamDamage),
397
+ },
398
+ evidenceIds: [...cleanWin.evidenceIds],
399
+ });
400
+ }
401
+ const finalLoss = [...fights]
402
+ .filter((fight) => fight.result === 'LOSS')
403
+ .sort((left, right) => right.end - left.end)[0];
404
+ if (finalLoss && finalLoss.id !== main?.id && finalLoss.id !== cleanWin?.id) {
405
+ result.push({
406
+ id: `turning-point-${finalLoss.id}`,
407
+ type: 'FINAL_LOSS',
408
+ time: finalLoss.start,
409
+ title: '末战:有输出但没收口',
410
+ text: `${fightLabel(facts, finalLoss)} ${clock(finalLoss.start)}–${clock(finalLoss.end)}造成${Math.round(finalLoss.teamDamage)}伤害、${finalLoss.teamKnocks}次倒地,却没有击杀;同时我方被击杀${finalLoss.receivedKills}次,最终以${facts.match.placement === null ? '未知名次' : `#${facts.match.placement}`}结束。`,
411
+ impact: 'negative',
412
+ facts: {
413
+ fightId: finalLoss.id,
414
+ teamDamage: Math.round(finalLoss.teamDamage),
415
+ teamKnocks: finalLoss.teamKnocks,
416
+ teamKills: finalLoss.teamKills,
417
+ receivedKills: finalLoss.receivedKills,
418
+ },
419
+ evidenceIds: [...finalLoss.evidenceIds],
420
+ });
421
+ }
422
+ const friendlyDamage = facts.teamDamage ?? [];
423
+ const totalFriendlyDamage = friendlyDamage.reduce((sum, item) => sum + item.damage, 0);
424
+ if (totalFriendlyDamage > 0) {
425
+ result.push({
426
+ id: 'turning-point-teamwork-risk',
427
+ type: 'TEAMWORK_RISK',
428
+ time: friendlyDamage.flatMap((item) => item.timestamps).sort((left, right) => left - right)[0] ??
429
+ friendlyDamage[0]?.timestamp ??
430
+ null,
431
+ title: '协同风险:队友误伤',
432
+ text: `遥测记录队友误伤${Math.round(totalFriendlyDamage)}伤害(${friendlyDamage.reduce((sum, item) => sum + item.hitCount, 0)}次命中),包含近战和投掷物;这会压缩后续战斗容错。`,
433
+ impact: 'negative',
434
+ facts: {
435
+ friendlyDamage: Math.round(totalFriendlyDamage),
436
+ friendlyHits: friendlyDamage.reduce((sum, item) => sum + item.hitCount, 0),
437
+ },
438
+ evidenceIds: [...new Set(friendlyDamage.flatMap((item) => item.evidenceIds))],
439
+ });
440
+ }
441
+ return result.slice(0, 4);
442
+ }
443
+ function buildTeamStory(facts, keyFights) {
444
+ const recorded = facts.players.filter((player) => player.matchPresence !== 'not_recorded');
445
+ const top = [...recorded].sort((left, right) => right.damage - left.damage || right.kills - left.kills)[0];
446
+ const share = top ? percentage(top.damage, facts.squad.damage) : null;
447
+ const main = keyFights[0];
448
+ const finalLoss = facts.fightIntegrity.pass
449
+ ? [...facts.fights]
450
+ .filter((fight) => fight.result === 'LOSS')
451
+ .sort((left, right) => right.end - left.end)[0]
452
+ : undefined;
453
+ const sentences = [];
454
+ if (top && share)
455
+ sentences.push(`火力主要集中在${top.playerName}(${Math.round(top.damage)}伤害,占队伍${share})`);
456
+ if (main) {
457
+ const entry = operationForFight(facts, main, 'ENTRY');
458
+ const clutch = operationForFight(facts, main, 'CLUTCH');
459
+ sentences.push(`${fightLabel(facts, main)}由${entry ? playerName(facts, entry.playerId) : '队伍'}${entry ? '先手打开' : '接手'},打出${main.teamKills}杀/${main.teamKnocks}倒地,但付出${main.receivedKills}次被击杀${clutch ? `,${playerName(facts, clutch.playerId)}完成了${String(clutch.facts.kills ?? main.teamKills)}次收割` : ''}`);
460
+ }
461
+ const cleanWin = keyFights.find((fight) => fight.result === 'WIN');
462
+ if (cleanWin && cleanWin !== main)
463
+ sentences.push(`${fightLabel(facts, cleanWin)}完成${cleanWin.teamKills}杀且没有我方被击杀`);
464
+ if (finalLoss)
465
+ sentences.push(`末战${fightLabel(facts, finalLoss)}虽打出${finalLoss.teamKnocks}次倒地和${Math.round(finalLoss.teamDamage)}伤害,却没有击杀,优势没有延续到终局`);
466
+ return sentences.length
467
+ ? `${sentences.join(';')}。`
468
+ : '当前遥测不足以串起完整战局,只展示已经通过校验的基础事实。';
469
+ }
470
+ function buildActionPlan(facts, keyFights) {
471
+ const actions = [];
472
+ const main = keyFights.find((fight) => fight.result === 'TRADE' && fight.teamKills > fight.receivedKills);
473
+ if (main)
474
+ actions.push(`${fightLabel(facts, main)}已经拿到${main.teamKills}杀/${main.teamKnocks}倒地,但付出${main.receivedKills}次被击杀;下次第一轮倒地后优先报点、补枪并收缩。`);
475
+ const finalLoss = facts.fightIntegrity.pass
476
+ ? [...facts.fights]
477
+ .filter((fight) => fight.result === 'LOSS')
478
+ .sort((left, right) => right.end - left.end)[0]
479
+ : undefined;
480
+ if (finalLoss && finalLoss.teamKnocks > 0 && finalLoss.teamKills === 0)
481
+ actions.push(`末战${fightLabel(facts, finalLoss)}有${finalLoss.teamKnocks}次倒地和${Math.round(finalLoss.teamDamage)}伤害却没有击杀;把“倒地→击杀”设为第一指令,再分散找角度。`);
482
+ const active = facts.players.filter((player) => player.matchPresence !== 'not_recorded' && hasBaseContribution(player));
483
+ const top = [...active].sort((left, right) => right.damage - left.damage)[0];
484
+ const share = top ? percentage(top.damage, facts.squad.damage) : null;
485
+ if (top && share && Number.parseFloat(share) >= 75 && active.length > 1)
486
+ actions.push(`${top.playerName}贡献了队伍${share}伤害;下一局让其余队员明确承担补枪、侧翼或投掷物压制,避免火力单核。`);
487
+ const friendlyDamage = (facts.teamDamage ?? []).reduce((sum, item) => sum + item.damage, 0);
488
+ if (friendlyDamage > 0)
489
+ actions.push(`队友误伤合计${Math.round(friendlyDamage)}伤害;投掷物和近战接触前先确认落点与目标。`);
490
+ const receivedKnocks = facts.fights.reduce((sum, fight) => sum + fight.receivedKnocks, 0);
491
+ if (facts.squad.revives === 0 && receivedKnocks > 0)
492
+ actions.push(`本局没有救援记录,团战中至少有${receivedKnocks}次我方被击倒;下次先封烟/拉开再救。`);
493
+ return [...new Set(actions)].slice(0, 5);
494
+ }
495
+ function buildImprovements(facts, keyFights) {
496
+ const improvements = [];
497
+ const main = keyFights.find((fight) => fight.result === 'TRADE' && fight.teamKills > fight.receivedKills);
498
+ if (main)
499
+ improvements.push(`${fightLabel(facts, main)}虽取得${main.teamKills}杀/${main.teamKnocks}倒地,但付出${main.receivedKills}次被击杀,优势没有做到低损收口。`);
500
+ const finalLoss = facts.fightIntegrity.pass
501
+ ? [...facts.fights]
502
+ .filter((fight) => fight.result === 'LOSS')
503
+ .sort((left, right) => right.end - left.end)[0]
504
+ : undefined;
505
+ if (finalLoss && finalLoss.teamKnocks > 0 && finalLoss.teamKills === 0)
506
+ improvements.push(`末战${fightLabel(facts, finalLoss)}有${finalLoss.teamKnocks}次倒地和${Math.round(finalLoss.teamDamage)}伤害,却没有转化为击杀。`);
507
+ const active = facts.players.filter((player) => player.matchPresence !== 'not_recorded' && hasBaseContribution(player));
508
+ const top = [...active].sort((left, right) => right.damage - left.damage)[0];
509
+ const share = top ? percentage(top.damage, facts.squad.damage) : null;
510
+ if (top && share && Number.parseFloat(share) >= 75 && active.length > 1)
511
+ improvements.push(`${top.playerName}贡献队伍${share}伤害,其他队员的补枪、侧翼或投掷物贡献不足。`);
512
+ const friendlyDamage = (facts.teamDamage ?? []).reduce((sum, item) => sum + item.damage, 0);
513
+ if (friendlyDamage > 0)
514
+ improvements.push(`记录到${Math.round(friendlyDamage)}点队友误伤,近战和投掷物的协同安全需要加强。`);
515
+ if (facts.squad.revives === 0 && facts.fights.some((fight) => fight.receivedKnocks > 0))
516
+ improvements.push('本局有我方被击倒但没有救援记录,需要改善封烟、拉开和救援节奏。');
517
+ return [...new Set(improvements)].slice(0, 5);
518
+ }
519
+ function buildAwards(facts) {
520
+ const active = facts.players.filter((player) => player.matchPresence !== 'not_recorded');
521
+ if (!active.length)
522
+ return [];
523
+ const award = (playerId, title, text) => {
524
+ const evidenceIds = playerEvidenceIds(facts, playerId);
525
+ return evidenceIds.length ? { playerId, title, text, evidenceIds } : null;
526
+ };
527
+ const combatMvp = [...active].sort((left, right) => right.kills * 500 +
528
+ right.dbnos * 240 +
529
+ right.damage +
530
+ right.assists * 80 +
531
+ right.revives * 60 -
532
+ (left.kills * 500 +
533
+ left.dbnos * 240 +
534
+ left.damage +
535
+ left.assists * 80 +
536
+ left.revives * 60) ||
537
+ right.damage - left.damage ||
538
+ left.playerId.localeCompare(right.playerId))[0];
539
+ const awards = [];
540
+ if (combatMvp && (combatMvp.kills > 0 || combatMvp.dbnos > 0 || combatMvp.damage > 0)) {
541
+ const item = award(combatMvp.playerId, combatMvp.kills >= 2 ? '双杀镇魂奖' : '战场核心奖', `${combatMvp.kills}杀/${combatMvp.dbnos}倒地 · ${integer(combatMvp.damage)}伤害${combatMvp.revives ? ` · ${combatMvp.revives}救援` : ''}`);
542
+ if (item)
543
+ awards.push(item);
544
+ }
545
+ const firepower = [...active].sort((left, right) => right.damage - left.damage ||
546
+ right.kills - left.kills ||
547
+ left.playerId.localeCompare(right.playerId))[0];
548
+ if (firepower && firepower.damage > 0 && firepower.playerId !== combatMvp?.playerId) {
549
+ const weapon = playerWeapons(facts, firepower.playerId)[0];
550
+ const item = award(firepower.playerId, '火力炮台奖', `队内最高${integer(firepower.damage)}伤害${weapon ? ` · ${weaponLabel(weapon.weapon)}为主力` : ''}`);
551
+ if (item)
552
+ awards.push(item);
553
+ }
554
+ const garbageCollector = [...(facts.lootActivity ?? [])]
555
+ .filter((item) => active.some((player) => player.playerId === item.playerId))
556
+ .sort((left, right) => right.pickupEvents + right.lootBoxPickups - (left.pickupEvents + left.lootBoxPickups) ||
557
+ left.playerId.localeCompare(right.playerId))[0];
558
+ if (garbageCollector && garbageCollector.pickupEvents + garbageCollector.lootBoxPickups > 0) {
559
+ const item = award(garbageCollector.playerId, '垃圾佬奖', `原始拾取${garbageCollector.pickupEvents}次 · 搜包${garbageCollector.lootBoxPickups}次 · 丢弃${garbageCollector.dropEvents}次`);
560
+ if (item)
561
+ awards.push(item);
562
+ }
563
+ const medic = [...active].sort((left, right) => right.revives - left.revives || left.playerId.localeCompare(right.playerId))[0];
564
+ if (medic &&
565
+ medic.revives > 0 &&
566
+ !awards.some((item) => item.playerId === medic.playerId && item.title.includes('救援'))) {
567
+ const item = award(medic.playerId, '救援承重奖', `完成${medic.revives}次救援,把队伍从倒地线拽回来`);
568
+ if (item)
569
+ awards.push(item);
570
+ }
571
+ return awards.slice(0, 4);
572
+ }
573
+ export function analyzeMatchReview(facts) {
574
+ const fightAnalyticsValid = facts.fightIntegrity.pass;
575
+ const keyFights = fightAnalyticsValid ? selectKeyFights(facts.fights, 3) : [];
576
+ const keyPlayers = facts.players
577
+ .map((player) => ({
578
+ player,
579
+ score: player.keyOperations.reduce((sum, operation) => sum + operation.impactScore, 0) +
580
+ (fightAnalyticsValid
581
+ ? facts.fights
582
+ .filter((fight) => fight.keyPlayers.includes(player.playerId))
583
+ .reduce((sum, fight) => sum + Math.min(300, fight.importanceScore / 10), 0)
584
+ : 0) +
585
+ facts.specialEvents
586
+ .filter((event) => event.playerId === player.playerId && isKeyPlayerSpecialEvent(event.type))
587
+ .reduce((sum, event) => sum + event.impactScore, 0),
588
+ }))
589
+ .filter((item) => item.score > 0 && item.player.matchPresence !== 'not_recorded')
590
+ .sort((left, right) => right.score - left.score || left.player.playerId.localeCompare(right.player.playerId))
591
+ .slice(0, 2)
592
+ .map((item) => playerName(facts, item.player.playerId));
593
+ const playerCommentary = facts.players.map((player) => commentaryForPlayer(facts, player));
594
+ const turningPoints = buildTurningPoints(facts);
595
+ const teamStory = buildTeamStory(facts, keyFights);
596
+ const actionPlan = buildActionPlan(facts, keyFights);
597
+ const good = [];
598
+ const improvements = buildImprovements(facts, keyFights);
599
+ const main = keyFights.find((fight) => fight.result === 'TRADE' || fight.result === 'WIN');
600
+ const cleanWin = keyFights.find((fight) => fight.result === 'WIN');
601
+ const top = [...facts.players]
602
+ .filter((player) => player.matchPresence !== 'not_recorded')
603
+ .sort((left, right) => right.damage - left.damage || right.kills - left.kills)[0];
604
+ if (facts.match.placement === 1)
605
+ good.push('最终拿到第一名,基础战绩确认吃鸡');
606
+ if (main && main.teamKills > 0)
607
+ good.push(`${fightLabel(facts, main)}打出${main.teamKills}杀、${main.teamKnocks}倒地和${Math.round(main.teamDamage)}伤害`);
608
+ if (cleanWin)
609
+ good.push(`${fightLabel(facts, cleanWin)}完成${cleanWin.teamKills}杀且没有我方被击杀`);
610
+ if (top && top.damage > 0) {
611
+ const share = percentage(top.damage, facts.squad.damage);
612
+ if (share)
613
+ good.push(`${top.playerName}贡献队伍${share}伤害,承担了主要火力`);
614
+ }
615
+ const heavy = facts.heavyWeapons.find((item) => item.shots > 0 || item.kills > 0 || item.knocks > 0 || item.vehiclesDestroyed > 0);
616
+ if (heavy)
617
+ good.push(`${heavy.weapon}记录${heavy.shots}发、${heavy.hits}次命中记录,带来${heavy.kills}杀/${heavy.knocks}倒地${heavy.vehiclesDestroyed ? `并摧毁${heavy.vehiclesDestroyed}辆载具` : ''}`);
618
+ if (facts.squad.revives > 0)
619
+ good.push(`队伍完成${facts.squad.revives}次救援,保留了回合容错`);
620
+ if (!keyFights.length && facts.match.placement !== 1 && facts.fightIntegrity.pass)
621
+ improvements.push('本局没有足够的团队级高影响团战证据');
622
+ if (!facts.fightIntegrity.pass)
623
+ improvements.push('详细团战数据未通过一致性校验,已停止展示团战结论');
624
+ const summary = main
625
+ ? `这把最终 #${facts.match.placement ?? '未知'};${fightLabel(facts, main)}打出${main.teamKills}杀/${main.teamKnocks}倒地,但付出${main.receivedKills}次被击杀${facts.fights.some((fight) => fight.result === 'LOSS') ? ',末战没有完成收口' : ''}。`
626
+ : facts.match.placement === 1
627
+ ? `这把以 #1 收尾,${keyPlayers.length ? `${keyPlayers.join('、')}的有证据关键操作贡献最突出` : '但没有足够的高光操作证据展开'}`
628
+ : keyFights.length
629
+ ? `这把最终 #${facts.match.placement ?? '未知'},复盘重点是${keyFights
630
+ .slice(0, 2)
631
+ .map((fight) => fightLabel(facts, fight))
632
+ .join('、')}。`
633
+ : `这把最终 #${facts.match.placement ?? '未知'},目前只有基础战绩或团战数据未通过校验。`;
634
+ const funCandidates = generateFunCandidates(facts);
635
+ const funEvents = generateFunEvents(facts);
636
+ return {
637
+ summary,
638
+ teamStory,
639
+ turningPoints,
640
+ actionPlan,
641
+ playerCommentary,
642
+ keyFights,
643
+ awards: buildAwards(facts),
644
+ good: [...new Set(good)],
645
+ improvements: [...new Set(improvements)],
646
+ keyPlayers,
647
+ fun: funEvents.map((item) => `${item.title}\n${item.text}`),
648
+ funCandidates,
649
+ funEvents,
650
+ };
651
+ }
652
+ //# sourceMappingURL=review-analyzer.js.map