@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.
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/config/default-team.json +27 -0
- package/dist/src/config/chicken-index.d.ts +9 -0
- package/dist/src/config/chicken-index.js +37 -0
- package/dist/src/config/chicken-index.js.map +1 -0
- package/dist/src/config/team.d.ts +22 -0
- package/dist/src/config/team.js +69 -0
- package/dist/src/config/team.js.map +1 -0
- package/dist/src/context/context-store.d.ts +40 -0
- package/dist/src/context/context-store.js +119 -0
- package/dist/src/context/context-store.js.map +1 -0
- package/dist/src/context/team-context.d.ts +13 -0
- package/dist/src/context/team-context.js +16 -0
- package/dist/src/context/team-context.js.map +1 -0
- package/dist/src/core.d.ts +23 -0
- package/dist/src/core.js +24 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/data/model.d.ts +123 -0
- package/dist/src/data/model.js +100 -0
- package/dist/src/data/model.js.map +1 -0
- package/dist/src/data/pubg-api-client.d.ts +59 -0
- package/dist/src/data/pubg-api-client.js +441 -0
- package/dist/src/data/pubg-api-client.js.map +1 -0
- package/dist/src/engine/query-engine.d.ts +20 -0
- package/dist/src/engine/query-engine.js +765 -0
- package/dist/src/engine/query-engine.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +28 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/migration/importer.d.ts +35 -0
- package/dist/src/migration/importer.js +229 -0
- package/dist/src/migration/importer.js.map +1 -0
- package/dist/src/review/combat-scope.d.ts +11 -0
- package/dist/src/review/combat-scope.js +101 -0
- package/dist/src/review/combat-scope.js.map +1 -0
- package/dist/src/review/event-combination-engine.d.ts +17 -0
- package/dist/src/review/event-combination-engine.js +534 -0
- package/dist/src/review/event-combination-engine.js.map +1 -0
- package/dist/src/review/fight-detector.d.ts +4 -0
- package/dist/src/review/fight-detector.js +138 -0
- package/dist/src/review/fight-detector.js.map +1 -0
- package/dist/src/review/fight-integrity-validator.d.ts +13 -0
- package/dist/src/review/fight-integrity-validator.js +134 -0
- package/dist/src/review/fight-integrity-validator.js.map +1 -0
- package/dist/src/review/fun-candidate-generator.d.ts +9 -0
- package/dist/src/review/fun-candidate-generator.js +267 -0
- package/dist/src/review/fun-candidate-generator.js.map +1 -0
- package/dist/src/review/fun-event-generator.d.ts +8 -0
- package/dist/src/review/fun-event-generator.js +740 -0
- package/dist/src/review/fun-event-generator.js.map +1 -0
- package/dist/src/review/fun-intelligence.d.ts +13 -0
- package/dist/src/review/fun-intelligence.js +20 -0
- package/dist/src/review/fun-intelligence.js.map +1 -0
- package/dist/src/review/fun-ranker.d.ts +13 -0
- package/dist/src/review/fun-ranker.js +68 -0
- package/dist/src/review/fun-ranker.js.map +1 -0
- package/dist/src/review/heavy-weapon-intelligence.d.ts +5 -0
- package/dist/src/review/heavy-weapon-intelligence.js +215 -0
- package/dist/src/review/heavy-weapon-intelligence.js.map +1 -0
- package/dist/src/review/key-operation-detector.d.ts +3 -0
- package/dist/src/review/key-operation-detector.js +126 -0
- package/dist/src/review/key-operation-detector.js.map +1 -0
- package/dist/src/review/match-selector.d.ts +18 -0
- package/dist/src/review/match-selector.js +202 -0
- package/dist/src/review/match-selector.js.map +1 -0
- package/dist/src/review/review-analyzer.d.ts +2 -0
- package/dist/src/review/review-analyzer.js +652 -0
- package/dist/src/review/review-analyzer.js.map +1 -0
- package/dist/src/review/review-facts.d.ts +17 -0
- package/dist/src/review/review-facts.js +560 -0
- package/dist/src/review/review-facts.js.map +1 -0
- package/dist/src/review/review-share.d.ts +48 -0
- package/dist/src/review/review-share.js +37 -0
- package/dist/src/review/review-share.js.map +1 -0
- package/dist/src/review/selection-store.d.ts +25 -0
- package/dist/src/review/selection-store.js +80 -0
- package/dist/src/review/selection-store.js.map +1 -0
- package/dist/src/review/session-review.d.ts +72 -0
- package/dist/src/review/session-review.js +492 -0
- package/dist/src/review/session-review.js.map +1 -0
- package/dist/src/review/special-events.d.ts +3 -0
- package/dist/src/review/special-events.js +258 -0
- package/dist/src/review/special-events.js.map +1 -0
- package/dist/src/review/subgraph.d.ts +26 -0
- package/dist/src/review/subgraph.js +146 -0
- package/dist/src/review/subgraph.js.map +1 -0
- package/dist/src/review/supplemental-intelligence.d.ts +15 -0
- package/dist/src/review/supplemental-intelligence.js +431 -0
- package/dist/src/review/supplemental-intelligence.js.map +1 -0
- package/dist/src/review/team-damage.d.ts +8 -0
- package/dist/src/review/team-damage.js +180 -0
- package/dist/src/review/team-damage.js.map +1 -0
- package/dist/src/review/telemetry-events.d.ts +72 -0
- package/dist/src/review/telemetry-events.js +573 -0
- package/dist/src/review/telemetry-events.js.map +1 -0
- package/dist/src/review/telemetry.d.ts +86 -0
- package/dist/src/review/telemetry.js +266 -0
- package/dist/src/review/telemetry.js.map +1 -0
- package/dist/src/review/types.d.ts +501 -0
- package/dist/src/review/types.js +2 -0
- package/dist/src/review/types.js.map +1 -0
- package/dist/src/review/vehicle-intelligence.d.ts +3 -0
- package/dist/src/review/vehicle-intelligence.js +148 -0
- package/dist/src/review/vehicle-intelligence.js.map +1 -0
- package/dist/src/schema/query.d.ts +876 -0
- package/dist/src/schema/query.js +185 -0
- package/dist/src/schema/query.js.map +1 -0
- package/dist/src/schema/status.d.ts +66 -0
- package/dist/src/schema/status.js +49 -0
- package/dist/src/schema/status.js.map +1 -0
- package/dist/src/service/pubg-domain-service.d.ts +192 -0
- package/dist/src/service/pubg-domain-service.js +1598 -0
- package/dist/src/service/pubg-domain-service.js.map +1 -0
- package/dist/src/storage/repository-port.d.ts +84 -0
- package/dist/src/storage/repository-port.js +2 -0
- package/dist/src/storage/repository-port.js.map +1 -0
- package/dist/src/storage/sqlite-repository-adapter.d.ts +33 -0
- package/dist/src/storage/sqlite-repository-adapter.js +66 -0
- package/dist/src/storage/sqlite-repository-adapter.js.map +1 -0
- package/dist/src/storage/sqlite-repository.d.ts +73 -0
- package/dist/src/storage/sqlite-repository.js +652 -0
- package/dist/src/storage/sqlite-repository.js.map +1 -0
- package/dist/src/time/selector-resolver.d.ts +34 -0
- package/dist/src/time/selector-resolver.js +364 -0
- package/dist/src/time/selector-resolver.js.map +1 -0
- 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
|