fortnite-replay-analysis 1.0.9 → 1.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 +30 -0
- package/README.ja.md +124 -0
- package/README.md +89 -89
- package/index.js +71 -52
- package/package.json +14 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 yuyutti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This project includes code from the following MIT-licensed project:
|
|
26
|
+
|
|
27
|
+
FortniteReplayDecompressor
|
|
28
|
+
https://github.com/Shiqan/FortniteReplayDecompressor
|
|
29
|
+
Copyright (c) 2021 Ferron
|
|
30
|
+
Licensed under the MIT License.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
## 🌐 Language
|
|
2
|
+
|
|
3
|
+
- [English](./README.md)
|
|
4
|
+
- [日本語](./README.ja.md)
|
|
5
|
+
|
|
6
|
+
# Fortnite Replay Analysis
|
|
7
|
+
|
|
8
|
+
FortniteのリプレイファイルをNode.jsで解析し、プレイヤーデータを取得・集計・ソートできるモジュールです。
|
|
9
|
+
|
|
10
|
+
## 特徴
|
|
11
|
+
|
|
12
|
+
* OS判定でビルド済みの自己完結バイナリを呼び出し、高速に解析できます。
|
|
13
|
+
* botプレイヤーの除外や順位ソートのオプションに対応しています。
|
|
14
|
+
* 複数マッチのスコアをパーティ単位でマージして集計できます。
|
|
15
|
+
* 公式準拠のルールでスコアをソートできます。
|
|
16
|
+
|
|
17
|
+
## インストール
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install fortnite-replay-analysis@latest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 使い方
|
|
24
|
+
|
|
25
|
+
以下は、1試合のリプレイ解析からスコア計算、複数マッチのマージまでを実行する例です。
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const {
|
|
29
|
+
ReplayAnalysis,
|
|
30
|
+
calculateScore,
|
|
31
|
+
sortScores,
|
|
32
|
+
mergeScores
|
|
33
|
+
} = require('fortnite-replay-analysis');
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
// リプレイ解析(ディレクトリ指定時は最初に見つけた .replay を処理、ファイル指定時はそのファイルを使用)
|
|
37
|
+
const {
|
|
38
|
+
rawReplayData,
|
|
39
|
+
rawPlayerData,
|
|
40
|
+
processedPlayerInfo
|
|
41
|
+
} = await ReplayAnalysis(
|
|
42
|
+
'./path/to/replayDirOrFile',
|
|
43
|
+
{ bot: false, sort: true }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
console.log('Raw Data:', rawPlayerData);
|
|
47
|
+
console.log('Processed Player Info:', processedPlayerInfo);
|
|
48
|
+
|
|
49
|
+
// 公式ルールでソート
|
|
50
|
+
const sortedScores = sortScores(processedPlayerInfo);
|
|
51
|
+
|
|
52
|
+
// ポイント&キル計算
|
|
53
|
+
const score = await calculateScore({
|
|
54
|
+
matchData: processedPlayerInfo,
|
|
55
|
+
points: { 1: 11, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2 },
|
|
56
|
+
killCountUpperLimit: 10, // 省略可能、デフォルト null(無制限)
|
|
57
|
+
killPointMultiplier: 1 // 1撃破あたりの倍率(1の場合1撃破1pt, 2の場合1撃破2ポイント)、省略可能、デフォルト 1
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log('Score:', score);
|
|
61
|
+
|
|
62
|
+
// 複数マッチのマージと再ソート
|
|
63
|
+
const merged = mergeScores([ sortedScores, sortedScores2 ]);
|
|
64
|
+
const finalSorted = sortScores(merged);
|
|
65
|
+
|
|
66
|
+
console.log('Merged & Sorted:', finalSorted);
|
|
67
|
+
})();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### `ReplayAnalysis(inputPath, options)`
|
|
73
|
+
|
|
74
|
+
* `inputPath`: .replayファイルがあるディレクトリまたはファイルのパス
|
|
75
|
+
* `options`(省略可):
|
|
76
|
+
|
|
77
|
+
* `bot`(boolean): botプレイヤーを含めるか(デフォルト: `false`)
|
|
78
|
+
* `sort`(boolean): 順位でソートするか(デフォルト: `true`)
|
|
79
|
+
* 戻り値: Promise<{
|
|
80
|
+
rawReplayData: Object,
|
|
81
|
+
rawPlayerData: Array,
|
|
82
|
+
processedPlayerInfo: Array
|
|
83
|
+
}>
|
|
84
|
+
|
|
85
|
+
### `calculateScore({ matchData, points, killCountUpperLimit, killPointMultiplier })`
|
|
86
|
+
|
|
87
|
+
* `matchData`: `ReplayAnalysis`の`processedPlayerInfo`配列、またはそのJSONファイルへのパス
|
|
88
|
+
* `points`: 順位ごとのポイント設定オブジェクト(例: `{1:11,2:6,...}`)
|
|
89
|
+
* `killCountUpperLimit`: キル数の上限(省略可能、デフォルト: `null` で無制限)
|
|
90
|
+
* `killPointMultiplier`: 1撃破あたりの倍率(1の場合1撃破1pt, 2の場合1撃破2ポイント)、省略可能、デフォルト: `1`
|
|
91
|
+
* 戻り値: Promise(パーティごとの集計結果)
|
|
92
|
+
|
|
93
|
+
### `sortScores(scoreArray)`
|
|
94
|
+
|
|
95
|
+
* 公式準拠のルールでスコアをソートして返します。
|
|
96
|
+
* 引数: `calculateScore`や`mergeScores`の戻り値として得られる配列
|
|
97
|
+
* ソート順:
|
|
98
|
+
|
|
99
|
+
1. 累計ポイント降順
|
|
100
|
+
2. Victory Royale 回数降順
|
|
101
|
+
3. 平均撃破数降順
|
|
102
|
+
4. 平均順位昇順
|
|
103
|
+
5. 合計生存時間降順
|
|
104
|
+
6. 最初のパーティ番号昇順
|
|
105
|
+
|
|
106
|
+
### `mergeScores(scoreArrays)`
|
|
107
|
+
|
|
108
|
+
* 複数マッチ分のスコア配列をパーティ単位でマージします。
|
|
109
|
+
* 引数: ソート済みスコア配列の配列(例: `[sorted1, sorted2, ...]`)
|
|
110
|
+
* 戻り値: マージ後のスコア配列
|
|
111
|
+
|
|
112
|
+
## 注意事項
|
|
113
|
+
|
|
114
|
+
* ディレクトリ指定時は最初に見つけた `.replay` を処理します。
|
|
115
|
+
* 直接ファイルを指定した場合はそのファイルを処理し、`.replay` が存在しない場合でも最初に見つけたものを使用します。
|
|
116
|
+
* 本ツールの利用により発生した問題について、開発者は一切の責任を負いません。
|
|
117
|
+
* フォークする場合は、GitHub の「Fork」機能を利用してください(clone → 新規リポジトリ作成は非推奨です)。
|
|
118
|
+
|
|
119
|
+
## 🔗 使用ライブラリ
|
|
120
|
+
|
|
121
|
+
このプロジェクトは以下のオープンソースライブラリを使用しています:
|
|
122
|
+
|
|
123
|
+
- [FortniteReplayDecompressor](https://github.com/Shiqan/FortniteReplayDecompressor)
|
|
124
|
+
© Shiqan — 本プロジェクトは MIT ライセンスのもとで利用しています。
|
package/README.md
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
|
|
1
|
+
## 🌐 Language
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- [English](./README.md)
|
|
4
|
+
- [日本語](./README.ja.md)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
# Fortnite Replay Analysis
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
* botプレイヤーの除外や順位ソートなどオプション対応
|
|
9
|
-
* 複数マッチのスコアをパーティ単位でマージして集計可能
|
|
10
|
-
* 公式準拠でのスコアのソートも可能
|
|
8
|
+
Fortnite Replay Analysis is a Node.js module for reading Fortnite replay files, extracting player data, and ranking results.
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## Features
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
* Detects the operating system and invokes a prebuilt, self-contained binary for fast parsing.
|
|
13
|
+
* Supports excluding bot players and optional placement sorting.
|
|
14
|
+
* Merges scores across multiple matches by party.
|
|
15
|
+
* Sorts scores following the official Fortnite scoring rules.
|
|
17
16
|
|
|
18
|
-
##
|
|
17
|
+
## Installation
|
|
19
18
|
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(async () => {
|
|
24
|
-
try {
|
|
25
|
-
// 1試合分のリプレイ解析(返り値はJSON形式)
|
|
26
|
-
// rawPlayerData: 元の解析結果、生データ
|
|
27
|
-
// processedPlayerInfo: bot除外や順位ソート済みのプレイヤーデータ
|
|
28
|
-
const { rawReplayData, 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
|
-
})();
|
|
19
|
+
```bash
|
|
20
|
+
npm install fortnite-replay-analysis@latest
|
|
54
21
|
```
|
|
55
22
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
リプレイ解析済みの`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイル(ファイル名は任意でOK)から、大会形式のスコアを計算したいときに使える。
|
|
23
|
+
## Usage
|
|
59
24
|
|
|
60
25
|
```js
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
26
|
+
const {
|
|
27
|
+
ReplayAnalysis,
|
|
28
|
+
calculateScore,
|
|
29
|
+
sortScores,
|
|
30
|
+
mergeScores
|
|
31
|
+
} = require('fortnite-replay-analysis');
|
|
32
|
+
|
|
33
|
+
(async () => {
|
|
34
|
+
// Parse a single match (directory: first .replay file; file: specific .replay)
|
|
35
|
+
const {
|
|
36
|
+
rawReplayData,
|
|
37
|
+
rawPlayerData,
|
|
38
|
+
processedPlayerInfo
|
|
39
|
+
} = await ReplayAnalysis(
|
|
40
|
+
'./path/to/replayDirOrFile',
|
|
41
|
+
{ bot: false, sort: true }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
console.log('Raw Data:', rawPlayerData);
|
|
45
|
+
console.log('Processed Player Info:', processedPlayerInfo);
|
|
46
|
+
|
|
47
|
+
// Sort by official rules
|
|
48
|
+
const sortedScores = sortScores(processedPlayerInfo);
|
|
49
|
+
|
|
50
|
+
// Calculate points & kills
|
|
51
|
+
const score = await calculateScore({
|
|
52
|
+
matchData: processedPlayerInfo,
|
|
53
|
+
points: { 1: 11, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2 },
|
|
54
|
+
killCountUpperLimit: 10, // optional, default null (no limit)
|
|
55
|
+
killPointMultiplier: 1 // points per kill multiplier, optional, default 1
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log('Score:', score);
|
|
59
|
+
|
|
60
|
+
// Merge and re-sort multiple matches
|
|
61
|
+
const merged = mergeScores([sortedScores, sortedScores2]);
|
|
62
|
+
const finalSorted = sortScores(merged);
|
|
63
|
+
|
|
64
|
+
console.log('Merged & Sorted:', finalSorted);
|
|
65
|
+
})();
|
|
74
66
|
```
|
|
75
67
|
|
|
76
68
|
## API
|
|
77
69
|
|
|
78
|
-
### `ReplayAnalysis(
|
|
70
|
+
### `ReplayAnalysis(inputPath, options)`
|
|
79
71
|
|
|
80
|
-
* `
|
|
81
|
-
* `options
|
|
72
|
+
* `inputPath`: Path to a directory or a `.replay` file.
|
|
73
|
+
* `options` (optional):
|
|
82
74
|
|
|
83
|
-
* `bot
|
|
84
|
-
* `sort
|
|
85
|
-
*
|
|
75
|
+
* `bot` (boolean): Include bot players (default: `false`).
|
|
76
|
+
* `sort` (boolean): Sort by placement (default: `true`).
|
|
77
|
+
* Returns: `Promise<{ rawReplayData: Object, rawPlayerData: Array, processedPlayerInfo: Array }>`
|
|
86
78
|
|
|
87
|
-
### `
|
|
79
|
+
### `calculateScore({ matchData, points, killCountUpperLimit, killPointMultiplier })`
|
|
88
80
|
|
|
89
|
-
*
|
|
90
|
-
*
|
|
81
|
+
* `matchData`: The `processedPlayerInfo` array from `ReplayAnalysis`, or a path to its JSON file.
|
|
82
|
+
* `points`: Object mapping placement to points (e.g., `{1:11,2:6,...}`).
|
|
83
|
+
* `killCountUpperLimit`: Upper limit for kills (optional, default `null` for unlimited).
|
|
84
|
+
* `killPointMultiplier`: Points multiplier per kill (optional, default `1`).
|
|
85
|
+
* Returns: `Promise<Array>` of aggregated results per party.
|
|
91
86
|
|
|
92
87
|
### `sortScores(scoreArray)`
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
Sorts scores according to official Fortnite rules:
|
|
90
|
+
|
|
91
|
+
1. Total points (descending)
|
|
92
|
+
2. Victory Royale count (descending)
|
|
93
|
+
3. Average kills (descending)
|
|
94
|
+
4. Average placement (ascending)
|
|
95
|
+
5. Total survival time (descending)
|
|
96
|
+
6. First party number (ascending)
|
|
97
|
+
|
|
98
|
+
### `mergeScores(scoreArrays)`
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
* Merges multiple sorted score arrays by party.
|
|
101
|
+
* `scoreArrays`: Array of sorted score arrays (e.g., `[sorted1, sorted2, ...]`).
|
|
102
|
+
* Returns: Merged score array.
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
* `points`:順位に対するポイント設定(例:{ 1: 11, 2: 6, ... })
|
|
101
|
-
* `killCountUpperLimit`:キル数制限(nullで無制限)
|
|
102
|
-
* `killPointMultiplier`:キル数倍率(例:1なら1キル1ポイント、2なら1キル2ポイント)
|
|
104
|
+
## Notes
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
* When a directory is provided, the first `.replay` file found will be processed.
|
|
107
|
+
* When a file is specified, that file will be processed; if no `.replay` is found, the first one in the directory is used.
|
|
108
|
+
* This software is provided without any warranty. Use it at your own risk.
|
|
109
|
+
* When forking this repository, please use GitHub’s "Fork" feature to retain commit history.
|
|
110
|
+
* I’m not very good at English, so the translation might be incorrect.
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
* Windows / Linux対応(Macは未対応)
|
|
108
|
-
* C#で作られた自己完結バイナリが`CSproj/bin/Release/net8.0/`配下に同補されていること
|
|
112
|
+
## 🔗 Acknowledgements
|
|
109
113
|
|
|
110
|
-
|
|
114
|
+
This project uses the following open-source library:
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* 何か問題起こっても俺は責任追わない
|
|
115
|
-
* このリポジトリをフォークする際は、GitHubの「Fork」ボタンからフォークしてください。
|
|
116
|
-
git cloneして新しく別リポジトリを作るのではなく、GitHub上のフォーク機能を使っていただけると、変更履歴を正しく追えます。
|
|
117
|
-
ご協力よろしくお願いします!
|
|
116
|
+
- [FortniteReplayDecompressor](https://github.com/Shiqan/FortniteReplayDecompressor)
|
|
117
|
+
© Shiqan — Licensed under the MIT License.
|
package/index.js
CHANGED
|
@@ -17,24 +17,27 @@ function getBinaryPath() { // OS判定して自己完結バイナリの実行フ
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function ReplayAnalysis(
|
|
20
|
+
function ReplayAnalysis(inputPath, { bot = false, sort = true } = {}) { // Fortniteのリプレイファイルを解析してプレイヤーデータを返す
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
22
|
|
|
23
|
-
let
|
|
23
|
+
let replayFilePath;
|
|
24
|
+
|
|
24
25
|
try {
|
|
25
|
-
|
|
26
|
+
const stat = fs.statSync(inputPath);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
const replayFiles = fs.readdirSync(inputPath).filter(f => f.endsWith('.replay'));
|
|
29
|
+
if (replayFiles.length === 0) {
|
|
30
|
+
return reject(new Error(`No .replay files found in directory: ${inputPath}`));
|
|
31
|
+
}
|
|
32
|
+
replayFilePath = path.join(inputPath, replayFiles[0]);
|
|
33
|
+
} else if (stat.isFile()) {
|
|
34
|
+
replayFilePath = inputPath;
|
|
35
|
+
} else {
|
|
36
|
+
return reject(new Error(`Invalid input path: ${inputPath}`));
|
|
37
|
+
}
|
|
26
38
|
} catch (e) {
|
|
27
|
-
reject(new Error(`Failed to
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (replayFiles.length === 0) {
|
|
32
|
-
reject(new Error(`No replay file found in directory: ${replayFileDir}`));
|
|
33
|
-
return;
|
|
39
|
+
return reject(new Error(`Failed to access path: ${e.message}`));
|
|
34
40
|
}
|
|
35
|
-
|
|
36
|
-
// とりあえず1個目のファイルを処理(複数ある場合は要拡張)
|
|
37
|
-
const replayFilePath = path.join(replayFileDir, replayFiles[0]);
|
|
38
41
|
const binPath = getBinaryPath();
|
|
39
42
|
|
|
40
43
|
execFile(binPath, [replayFilePath], (error, stdout, stderr) => {
|
|
@@ -114,18 +117,24 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
async function calculateScore({
|
|
118
|
-
if (!
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
throw new Error(
|
|
120
|
+
async function calculateScore({ matchData, points, killCountUpperLimit = null, killPointMultiplier = 1 } = {}) {
|
|
121
|
+
if (!matchData) throw new Error('matchData is required');
|
|
122
|
+
|
|
123
|
+
let playerInfo;
|
|
124
|
+
if (typeof matchData === 'string') {
|
|
125
|
+
if (!fs.existsSync(matchData)) throw new Error(`Match data path does not exist: ${matchData}`);
|
|
126
|
+
const rawData = fs.readFileSync(matchData, 'utf8');
|
|
127
|
+
try {
|
|
128
|
+
playerInfo = JSON.parse(rawData);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(`Failed to parse JSON from file: ${e.message}`);
|
|
131
|
+
}
|
|
123
132
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
133
|
+
else if (Array.isArray(matchData)) playerInfo = matchData;
|
|
134
|
+
else {
|
|
135
|
+
throw new Error('matchData must be either a file path (string) or parsed JSON array');
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
const playerInfo = JSON.parse(fs.readFileSync(path.join(matchDataPath), 'utf8'));
|
|
129
138
|
const partyScore = playerInfo.reduce((acc, player) => {
|
|
130
139
|
if (!acc[player.partyNumber]) {
|
|
131
140
|
const limitedKills = killCountUpperLimit == null
|
|
@@ -168,39 +177,38 @@ function mergeScores(scoreArrays) { // 複数マッチの結果をマージし
|
|
|
168
177
|
const key = JSON.stringify([...p.partyMemberIdList].sort());
|
|
169
178
|
if (!map.has(key)) {
|
|
170
179
|
map.set(key, {
|
|
171
|
-
partyPlacement: null,
|
|
172
|
-
partyNumber: p.partyNumber,
|
|
173
180
|
partyScore: p.partyScore,
|
|
174
181
|
partyPoint: p.partyPoint,
|
|
175
182
|
partyKills: p.partyKills,
|
|
176
|
-
|
|
177
|
-
matchList: [p.matchName],
|
|
183
|
+
partyKillsNoLimit: p.partyKillsNoLimit,
|
|
178
184
|
partyVictoryRoyaleCount: p.partyVictoryRoyale ? 1 : 0,
|
|
185
|
+
matchList: [p.matchName],
|
|
186
|
+
partyMemberList: [...p.partyMemberList],
|
|
179
187
|
partyAliveTimeByMatch: [
|
|
180
188
|
{ match: p.matchName, times: [...(p.partyAliveTimeList || [])] }
|
|
181
189
|
],
|
|
182
|
-
partyPlacementList: [p.partyPlacement]
|
|
190
|
+
partyPlacementList: [p.partyPlacement],
|
|
191
|
+
matchs: { [p.matchName]: { ...p } }
|
|
183
192
|
});
|
|
184
193
|
} else {
|
|
185
194
|
const ex = map.get(key);
|
|
186
|
-
ex.matchList.push(p.matchName);
|
|
187
195
|
ex.partyScore += p.partyScore;
|
|
188
|
-
ex.partyKills += p.partyKills;
|
|
189
196
|
ex.partyPoint += p.partyPoint;
|
|
197
|
+
ex.partyKills += p.partyKills;
|
|
198
|
+
ex.partyKillsNoLimit += p.partyKillsNoLimit;
|
|
190
199
|
ex.partyVictoryRoyaleCount += p.partyVictoryRoyale ? 1 : 0;
|
|
200
|
+
ex.matchList.push(p.matchName);
|
|
191
201
|
ex.partyAliveTimeByMatch.push({
|
|
192
202
|
match: p.matchName,
|
|
193
203
|
times: [...(p.partyAliveTimeList || [])]
|
|
194
204
|
});
|
|
195
205
|
ex.partyPlacementList.push(p.partyPlacement);
|
|
206
|
+
ex.matchs[p.matchName] = { ...p };
|
|
196
207
|
}
|
|
197
208
|
})
|
|
198
209
|
);
|
|
199
210
|
|
|
200
|
-
return Array.from(map.values())
|
|
201
|
-
...p,
|
|
202
|
-
partyPlacement: p.partyPlacementList.reduce((a, b) => a + b, 0) / p.partyPlacementList.length
|
|
203
|
-
}));
|
|
211
|
+
return Array.from(map.values());
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
@@ -208,47 +216,58 @@ function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
|
208
216
|
if (!Array.isArray(arr) || arr.length === 0) return arr;
|
|
209
217
|
|
|
210
218
|
arr.forEach(p => {
|
|
211
|
-
const matchCount = (p.
|
|
212
|
-
|
|
213
|
-
).length || 1;
|
|
214
|
-
p.result = {
|
|
219
|
+
const matchCount = (p.matchList || []).length || 1;
|
|
220
|
+
p.summary = {
|
|
215
221
|
point: p.partyScore || 0,
|
|
216
222
|
victoryCount: p.partyVictoryRoyaleCount ?? (p.partyVictoryRoyale ? 1 : 0),
|
|
217
223
|
matchCount,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
? (p.
|
|
221
|
-
: p.
|
|
224
|
+
// Decimalを使って計算
|
|
225
|
+
avgKills: matchCount > 0
|
|
226
|
+
? new Decimal(p.partyKills || 0).dividedBy(matchCount)
|
|
227
|
+
: new Decimal(p.partyKills || 0),
|
|
228
|
+
avgPlacement: Array.isArray(p.partyPlacementList) && p.partyPlacementList.length > 0 && matchCount > 0
|
|
229
|
+
? new Decimal(p.partyPlacementList.reduce((sum, val) => sum + val, 0)).dividedBy(matchCount)
|
|
230
|
+
: new Decimal(p.partyPlacement || 0),
|
|
222
231
|
totalAliveTime: sumMaxAliveTime(p.partyAliveTimeList, p.partyAliveTimeByMatch),
|
|
223
232
|
};
|
|
224
233
|
});
|
|
225
234
|
|
|
226
235
|
return arr.sort((a, b) => {
|
|
227
236
|
// 1. 累計獲得ポイント
|
|
228
|
-
if (b.
|
|
229
|
-
return b.
|
|
237
|
+
if (b.summary.point !== a.summary.point) {
|
|
238
|
+
return b.summary.point - a.summary.point;
|
|
230
239
|
}
|
|
231
240
|
// 2. セッション中の累計 Victory Royale 回数
|
|
232
|
-
if (b.
|
|
233
|
-
return b.
|
|
241
|
+
if (b.summary.victoryCount !== a.summary.victoryCount) {
|
|
242
|
+
return b.summary.victoryCount - a.summary.victoryCount;
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
// 3. 平均撃破数
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
const cmpAvgKills = b.summary.avgKills.comparedTo(a.summary.avgKills);
|
|
247
|
+
if (cmpAvgKills !== 0) {
|
|
248
|
+
return cmpAvgKills;
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
// 4. 平均順位(小さいほうが上位)
|
|
242
|
-
|
|
243
|
-
|
|
252
|
+
const cmpAvgPlacement = b.summary.avgPlacement.comparedTo(a.summary.avgPlacement);
|
|
253
|
+
if (cmpAvgPlacement !== 0) {
|
|
254
|
+
return cmpAvgPlacement;
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
// 5. 全マッチの合計生存時間
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
258
|
+
const cmpTime = b.summary.totalAliveTime.comparedTo(a.summary.totalAliveTime);
|
|
259
|
+
if (cmpTime !== 0) {
|
|
260
|
+
return cmpTime;
|
|
261
|
+
}
|
|
249
262
|
|
|
250
263
|
// 6. 最終手段:1マッチ目のパーティ番号が小さい順
|
|
251
|
-
|
|
264
|
+
const numA = Array.isArray(a.matchList) && a.matchList.length > 0
|
|
265
|
+
? a.matchs[a.matchList[0]].partyNumber
|
|
266
|
+
: a.partyNumber;
|
|
267
|
+
const numB = Array.isArray(b.matchList) && b.matchList.length > 0
|
|
268
|
+
? b.matchs[b.matchList[0]].partyNumber
|
|
269
|
+
: b.partyNumber;
|
|
270
|
+
return numA - numB;
|
|
252
271
|
});
|
|
253
272
|
}
|
|
254
273
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fortnite-replay-analysis",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Fortnite
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Fortnite replay analysis tool (Node.js用Fortniteリプレイ解析バイナリラッパー)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/yuyutti/Fortnite_Replay_Analysis"
|
|
@@ -16,7 +16,18 @@
|
|
|
16
16
|
"analysis",
|
|
17
17
|
"decimal",
|
|
18
18
|
"csharp",
|
|
19
|
-
"nodejs"
|
|
19
|
+
"nodejs",
|
|
20
|
+
"C#",
|
|
21
|
+
"fortnite-replay",
|
|
22
|
+
"fortnite解析",
|
|
23
|
+
"リプレイ解析",
|
|
24
|
+
"自動スコア計算",
|
|
25
|
+
"tournament",
|
|
26
|
+
"game-analysis",
|
|
27
|
+
"epicgames",
|
|
28
|
+
"fortnite-scoring",
|
|
29
|
+
"match-result",
|
|
30
|
+
"yuyutti"
|
|
20
31
|
],
|
|
21
32
|
"author": "yuyutti",
|
|
22
33
|
"license": "MIT",
|