fortnite-replay-analysis 1.0.5 → 1.0.8
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/README.md +89 -28
- package/index.js +104 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,56 +1,117 @@
|
|
|
1
1
|
# Fortnite Replay Analysis
|
|
2
2
|
|
|
3
|
-
Fortnite
|
|
3
|
+
Fortniteのリプレイファイルを解析して、プレイヤーデータを取得・集計・ソートできるNode.jsのモジュールです。
|
|
4
4
|
|
|
5
5
|
## 特徴
|
|
6
6
|
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* OS判定してC#でビルドされた自己完結バイナリを呼び出すから、高速解析できる
|
|
8
|
+
* botプレイヤーの除外や順位ソートなどオプション対応
|
|
9
|
+
* 複数マッチのスコアをパーティ単位でマージして集計可能
|
|
10
|
+
* 公式準拠でのスコアのソートも可能
|
|
11
11
|
|
|
12
12
|
## インストール
|
|
13
13
|
|
|
14
|
-
```
|
|
15
|
-
npm install fortnite-replay-analysis
|
|
14
|
+
```
|
|
15
|
+
npm install fortnite-replay-analysis@latest
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## 使い方
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
|
-
const { ReplayAnalysis, sortScores,
|
|
21
|
+
const { ReplayAnalysis, mergeScores, sortScores, calculateScore } = require('fortnite-replay-analysis');
|
|
22
|
+
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
// 1試合分のリプレイ解析(返り値はJSON形式)
|
|
26
|
+
// rawPlayerData: 元の解析結果、生データ
|
|
27
|
+
// processedPlayerInfo: bot除外や順位ソート済みのプレイヤーデータ
|
|
28
|
+
const { rawPlayerData, processedPlayerInfo } = await ReplayAnalysis('./path/to/replayDir', { bot: false, sort: true });
|
|
29
|
+
|
|
30
|
+
console.log('Raw Data:', rawPlayerData);
|
|
31
|
+
console.log('Processed Player Info:', processedPlayerInfo);
|
|
32
|
+
|
|
33
|
+
// 解析結果のスコア配列を公式準拠のルールでソートも可能
|
|
34
|
+
const sortedScores = sortScores(processedPlayerInfo);
|
|
35
|
+
|
|
36
|
+
// 複数マッチの解析結果をまとめたいときは、
|
|
37
|
+
// sortScoresでソート済みの配列を複数用意して
|
|
38
|
+
// mergeScoresに配列の配列として渡す
|
|
39
|
+
const mergedScores = mergeScores([
|
|
40
|
+
sortedScores, // 1試合目の結果
|
|
41
|
+
sortedScores2, // 2試合目の結果
|
|
42
|
+
// ...
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// マージ後の結果もsortScoresで再ソート可能
|
|
46
|
+
const finalSorted = sortScores(mergedScores);
|
|
47
|
+
|
|
48
|
+
console.log('Merged and Sorted:', finalSorted);
|
|
49
|
+
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(e);
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## calculateScoreの使い方
|
|
57
|
+
|
|
58
|
+
リプレイ解析済みの`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイル(ファイル名は任意でOK)から、大会形式のスコアを計算したいときに使える。
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
```js
|
|
61
|
+
const { calculateScore } = require('fortnite-replay-analysis');
|
|
62
|
+
|
|
63
|
+
const score = await calculateScore({
|
|
64
|
+
matchDataPath: './output/matchA1/playerInfo.json',
|
|
65
|
+
points: {
|
|
66
|
+
1: 11, 2: 6, 3: 5, 4: 4, 5: 3,
|
|
67
|
+
6: 2, 7: 1, 8: 1, 9: 1, 10: 1
|
|
68
|
+
},
|
|
69
|
+
killPointMultiplier: 1,
|
|
70
|
+
killCountUpperLimit: 10
|
|
26
71
|
});
|
|
72
|
+
|
|
73
|
+
console.log(score);
|
|
27
74
|
```
|
|
28
75
|
|
|
29
|
-
##
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
### `ReplayAnalysis(replayFileDir, options)`
|
|
30
79
|
|
|
31
|
-
|
|
80
|
+
* `replayFileDir`:リプレイファイルが入ったディレクトリのパス
|
|
81
|
+
* `options`:
|
|
32
82
|
|
|
33
|
-
*
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
83
|
+
* `bot`(boolean):botプレイヤーを結果に含めるか(デフォルトfalse)
|
|
84
|
+
* `sort`(boolean):順位でソートするか(デフォルトtrue)
|
|
85
|
+
* 返り値はPromiseで、`rawPlayerData`と`processedPlayerInfo`を含むオブジェクトを返す
|
|
36
86
|
|
|
37
|
-
### `mergeScores(scoreArrays
|
|
87
|
+
### `mergeScores(scoreArrays)`
|
|
38
88
|
|
|
39
|
-
*
|
|
89
|
+
* 複数マッチのスコア配列をパーティ単位でマージする
|
|
90
|
+
* 返り値はマージされたスコア配列
|
|
40
91
|
|
|
41
|
-
### `sortScores(
|
|
92
|
+
### `sortScores(scoreArray)`
|
|
42
93
|
|
|
43
|
-
*
|
|
94
|
+
* 公式準拠のルールでスコアをソートする
|
|
95
|
+
* 引数はマージ済みのスコア配列
|
|
44
96
|
|
|
45
|
-
|
|
97
|
+
### `calculateScore({ matchDataPath, points, killCountUpperLimit, killPointMultiplier })`
|
|
46
98
|
|
|
47
|
-
*
|
|
48
|
-
*
|
|
99
|
+
* `matchDataPath`:`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイルのパス(ファイル名は任意でOK)
|
|
100
|
+
* `points`:順位に対するポイント設定(例:{ 1: 11, 2: 6, ... })
|
|
101
|
+
* `killCountUpperLimit`:キル数制限(nullで無制限)
|
|
102
|
+
* `killPointMultiplier`:キル数倍率(例:1なら1キル1ポイント、2なら1キル2ポイント)
|
|
49
103
|
|
|
50
|
-
##
|
|
104
|
+
## 動作環境
|
|
51
105
|
|
|
52
|
-
|
|
106
|
+
* Node.js v22以上
|
|
107
|
+
* Windows / Linux対応(Macは未対応)
|
|
108
|
+
* C#で作られた自己完結バイナリが`CSproj/bin/Release/net8.0/`配下に同補されていること
|
|
53
109
|
|
|
54
|
-
##
|
|
110
|
+
## 注意事項
|
|
55
111
|
|
|
56
|
-
|
|
112
|
+
* リプレイファイルはディレクトリに1つ以上`.replay`ファイルが必要
|
|
113
|
+
* ディレクトリ内に複数ファイルある場合は現状最初の1つのみ処理される
|
|
114
|
+
* 何か問題起こっても俺は責任追わない
|
|
115
|
+
* このリポジトリをフォークする際は、GitHubの「Fork」ボタンからフォークしてください。
|
|
116
|
+
git cloneして新しく別リポジトリを作るのではなく、GitHub上のフォーク機能を使っていただけると、変更履歴を正しく追えます。
|
|
117
|
+
ご協力よろしくお願いします!
|
package/index.js
CHANGED
|
@@ -77,7 +77,7 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
77
77
|
Placement: player.Placement,
|
|
78
78
|
Kills: player.Kills,
|
|
79
79
|
TeamKills: player.TeamKills,
|
|
80
|
-
aliveTime: aliveTimeDecimal,
|
|
80
|
+
aliveTime: aliveTimeDecimal,
|
|
81
81
|
EpicId: player.EpicId,
|
|
82
82
|
PlayerName: player.PlayerName,
|
|
83
83
|
Platform: player.Platform,
|
|
@@ -103,6 +103,53 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
async function calculateScore({ matchDataPath, points, killCountUpperLimit = null, killPointMultiplier = 1 } = {}) {
|
|
107
|
+
if (!matchDataPath || !fs.existsSync(matchDataPath)) {
|
|
108
|
+
throw new Error(`Match data file not found: ${matchDataPath}`);
|
|
109
|
+
}
|
|
110
|
+
if (!points || typeof points !== 'object' || Object.keys(points).length === 0) {
|
|
111
|
+
throw new Error('Points configuration is required and must be a non-empty object.');
|
|
112
|
+
}
|
|
113
|
+
if (killCountUpperLimit !== null && (typeof killCountUpperLimit !== 'number' || killCountUpperLimit < 0)) {
|
|
114
|
+
throw new Error('killCountUpperLimit must be a non-negative number or null.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const playerInfo = JSON.parse(fs.readFileSync(path.join(matchDataPath), 'utf8'));
|
|
118
|
+
const partyScore = playerInfo.reduce((acc, player) => {
|
|
119
|
+
if (!acc[player.partyNumber]) {
|
|
120
|
+
const limitedKills = killCountUpperLimit == null
|
|
121
|
+
? (player.TeamKills || 0)
|
|
122
|
+
: Math.min(player.TeamKills || 0, killCountUpperLimit);
|
|
123
|
+
acc[player.partyNumber] = {
|
|
124
|
+
partyPlacement: player.Placement,
|
|
125
|
+
partyNumber: player.partyNumber,
|
|
126
|
+
partyKills: limitedKills,
|
|
127
|
+
partyKillsNoLimit: player.TeamKills || 0,
|
|
128
|
+
partyScore: (points[player.Placement] ?? 0) + ((limitedKills) * killPointMultiplier),
|
|
129
|
+
partyPoint: points[player.Placement] ?? 0,
|
|
130
|
+
partyVictoryRoyale: player.Placement === 1,
|
|
131
|
+
partyKillsList: [],
|
|
132
|
+
partyAliveTimeList: [],
|
|
133
|
+
partyMemberList: [],
|
|
134
|
+
partyMemberIdList: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// キル数の加算
|
|
139
|
+
acc[player.partyNumber].partyKillsList.push(player.Kills || 0);
|
|
140
|
+
acc[player.partyNumber].partyAliveTimeList.push(player.aliveTime || 0);
|
|
141
|
+
acc[player.partyNumber].partyMemberList.push(player.PlayerName);
|
|
142
|
+
acc[player.partyNumber].partyMemberIdList.push(player.EpicId);
|
|
143
|
+
|
|
144
|
+
return acc;
|
|
145
|
+
}, {});
|
|
146
|
+
|
|
147
|
+
let result = Object.values(partyScore);
|
|
148
|
+
result = sortScores(result);
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
106
153
|
function mergeScores(scoreArrays) { // 複数マッチの結果をマージしてパーティごとに集計
|
|
107
154
|
const map = new Map();
|
|
108
155
|
scoreArrays.forEach(scores =>
|
|
@@ -146,38 +193,47 @@ function mergeScores(scoreArrays) { // 複数マッチの結果をマージし
|
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
196
|
+
// リザルトとしてpoint, ビクロイ数, マッチ数, 平均撃破数, 平均順位, 合計生存時間を追加したい
|
|
197
|
+
if (!Array.isArray(arr) || arr.length === 0) return arr;
|
|
198
|
+
|
|
199
|
+
arr.forEach(p => {
|
|
200
|
+
const matchCount = (p.partyPlacementList || []).filter((placement, i, arr) =>
|
|
201
|
+
placement === 1 && p.partyNumber === (arr[i] || {}).partyNumber
|
|
202
|
+
).length || 1;
|
|
203
|
+
p.result = {
|
|
204
|
+
point: p.partyScore || 0,
|
|
205
|
+
victoryCount: p.partyVictoryRoyaleCount ?? (p.partyVictoryRoyale ? 1 : 0),
|
|
206
|
+
matchCount,
|
|
207
|
+
avgKills: (p.partyKills || 0) / matchCount,
|
|
208
|
+
avgPlacement: (p.partyPlacementList && p.partyPlacementList.length > 0)
|
|
209
|
+
? (p.partyPlacementList.reduce((s, x) => s + x, 0) / p.partyPlacementList.length)
|
|
210
|
+
: p.partyPlacement,
|
|
211
|
+
totalAliveTime: sumMaxAliveTime(p.partyAliveTimeList, p.partyAliveTimeByMatch),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
|
|
149
215
|
return arr.sort((a, b) => {
|
|
150
216
|
// 1. 累計獲得ポイント
|
|
151
|
-
if (b.
|
|
152
|
-
return b.
|
|
217
|
+
if (b.result.point !== a.result.point) {
|
|
218
|
+
return b.result.point - a.result.point;
|
|
153
219
|
}
|
|
154
220
|
// 2. セッション中の累計 Victory Royale 回数
|
|
155
|
-
if (b.
|
|
156
|
-
return b.
|
|
221
|
+
if (b.result.victoryCount !== a.result.victoryCount) {
|
|
222
|
+
return b.result.victoryCount - a.result.victoryCount;
|
|
157
223
|
}
|
|
158
224
|
|
|
159
|
-
// マッチ数(配置と生存時間の配列長を使う想定)
|
|
160
|
-
const aCount = (a.partyPlacementList || a.partyAliveTimeList || []).length || 1;
|
|
161
|
-
const bCount = (b.partyPlacementList || b.partyAliveTimeList || []).length || 1;
|
|
162
|
-
|
|
163
225
|
// 3. 平均撃破数
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (bAvgKills !== aAvgKills) {
|
|
167
|
-
return bAvgKills - aAvgKills;
|
|
226
|
+
if (b.result.avgKills !== a.result.avgKills) {
|
|
227
|
+
return b.result.avgKills - a.result.avgKills;
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
// 4. 平均順位(小さいほうが上位)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (aAvgPlacement !== bAvgPlacement) {
|
|
174
|
-
return aAvgPlacement - bAvgPlacement;
|
|
231
|
+
if (b.result.avgPlacement !== a.result.avgPlacement) {
|
|
232
|
+
return b.result.avgPlacement - a.result.avgPlacement;
|
|
175
233
|
}
|
|
176
234
|
|
|
177
235
|
// 5. 全マッチの合計生存時間
|
|
178
|
-
const
|
|
179
|
-
const bTime = sumMaxAliveTime(b.partyAliveTimeByMatch);
|
|
180
|
-
const cmp = bTime.comparedTo(aTime);
|
|
236
|
+
const cmp = b.result.totalAliveTime.comparedTo(a.result.totalAliveTime);
|
|
181
237
|
if (cmp !== 0) return cmp;
|
|
182
238
|
|
|
183
239
|
// 6. 最終手段:1マッチ目のパーティ番号が小さい順
|
|
@@ -185,17 +241,40 @@ function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
|
185
241
|
});
|
|
186
242
|
}
|
|
187
243
|
|
|
188
|
-
function sumMaxAliveTime(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
function sumMaxAliveTime(partyAliveTimeList, partyAliveTimeByMatch) {
|
|
245
|
+
if (Array.isArray(partyAliveTimeByMatch) && partyAliveTimeByMatch.length > 0) {
|
|
246
|
+
// 複数マッチ分の最大値を足す処理
|
|
247
|
+
return partyAliveTimeByMatch.reduce((sum, match) => {
|
|
248
|
+
if (!Array.isArray(match.times) || match.times.length === 0) return sum;
|
|
249
|
+
const maxTime = match.times.reduce(
|
|
250
|
+
(max, t) => {
|
|
251
|
+
const timeDec = new Decimal(t);
|
|
252
|
+
return timeDec.greaterThan(max) ? timeDec : max;
|
|
253
|
+
},
|
|
254
|
+
new Decimal(0)
|
|
255
|
+
);
|
|
193
256
|
return sum.plus(maxTime);
|
|
194
257
|
}, new Decimal(0));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// こっちは単一マッチ用。配列の最大値返すだけ
|
|
261
|
+
if (Array.isArray(partyAliveTimeList) && partyAliveTimeList.length > 0) {
|
|
262
|
+
const maxVal = partyAliveTimeList.reduce(
|
|
263
|
+
(max, t) => {
|
|
264
|
+
const timeDec = new Decimal(t);
|
|
265
|
+
return timeDec.greaterThan(max) ? timeDec : max;
|
|
266
|
+
},
|
|
267
|
+
new Decimal(0)
|
|
268
|
+
);
|
|
269
|
+
return maxVal;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return new Decimal(0);
|
|
195
273
|
}
|
|
196
274
|
|
|
197
275
|
module.exports = {
|
|
198
276
|
ReplayAnalysis,
|
|
277
|
+
calculateScore,
|
|
199
278
|
sortScores,
|
|
200
279
|
mergeScores
|
|
201
280
|
};
|