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.
Files changed (5) hide show
  1. package/LICENSE +30 -0
  2. package/README.ja.md +124 -0
  3. package/README.md +89 -89
  4. package/index.js +71 -52
  5. 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
- # Fortnite Replay Analysis
1
+ ## 🌐 Language
2
2
 
3
- Fortniteのリプレイファイルを解析して、プレイヤーデータを取得・集計・ソートできるNode.jsのモジュールです。
3
+ - [English](./README.md)
4
+ - [日本語](./README.ja.md)
4
5
 
5
- ## 特徴
6
+ # Fortnite Replay Analysis
6
7
 
7
- * OS判定してC#でビルドされた自己完結バイナリを呼び出すから、高速解析できる
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
- npm install fortnite-replay-analysis@latest
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
- ```js
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 { 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
- ## calculateScoreの使い方
57
-
58
- リプレイ解析済みの`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイル(ファイル名は任意でOK)から、大会形式のスコアを計算したいときに使える。
23
+ ## Usage
59
24
 
60
25
  ```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
71
- });
72
-
73
- console.log(score);
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(replayFileDir, options)`
70
+ ### `ReplayAnalysis(inputPath, options)`
79
71
 
80
- * `replayFileDir`:リプレイファイルが入ったディレクトリのパス
81
- * `options`:
72
+ * `inputPath`: Path to a directory or a `.replay` file.
73
+ * `options` (optional):
82
74
 
83
- * `bot`(boolean):botプレイヤーを結果に含めるか(デフォルトfalse
84
- * `sort`(boolean):順位でソートするか(デフォルトtrue
85
- * 返り値はPromiseで、`rawPlayerData`と`processedPlayerInfo`を含むオブジェクトを返す
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
- ### `mergeScores(scoreArrays)`
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
- ### `calculateScore({ matchDataPath, points, killCountUpperLimit, killPointMultiplier })`
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
- * `matchDataPath`:`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイルのパス(ファイル名は任意でOK)
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
- * Node.js v22以上
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
- * リプレイファイルはディレクトリに1つ以上`.replay`ファイルが必要
113
- * ディレクトリ内に複数ファイルある場合は現状最初の1つのみ処理される
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(replayFileDir, { bot = false, sort = true } = {}) { // Fortniteのリプレイファイルを解析してプレイヤーデータを返す
20
+ function ReplayAnalysis(inputPath, { bot = false, sort = true } = {}) { // Fortniteのリプレイファイルを解析してプレイヤーデータを返す
21
21
  return new Promise((resolve, reject) => {
22
22
 
23
- let replayFiles;
23
+ let replayFilePath;
24
+
24
25
  try {
25
- replayFiles = fs.readdirSync(replayFileDir).filter(f => f.endsWith('.replay'));
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 read directory: ${e.message}`));
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({ matchDataPath, points, killCountUpperLimit = null, killPointMultiplier = 1 } = {}) {
118
- if (!matchDataPath || !fs.existsSync(matchDataPath)) {
119
- throw new Error(`Match data file not found: ${matchDataPath}`);
120
- }
121
- if (!points || typeof points !== 'object' || Object.keys(points).length === 0) {
122
- throw new Error('Points configuration is required and must be a non-empty object.');
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 (killCountUpperLimit !== null && (typeof killCountUpperLimit !== 'number' || killCountUpperLimit < 0)) {
125
- throw new Error('killCountUpperLimit must be a non-negative number or null.');
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
- partyMemberList: [...p.partyMemberList],
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()).map(p => ({
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.partyPlacementList || []).filter((placement, i, arr) =>
212
- placement === 1 && p.partyNumber === (arr[i] || {}).partyNumber
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
- avgKills: (p.partyKills || 0) / matchCount,
219
- avgPlacement: (p.partyPlacementList && p.partyPlacementList.length > 0)
220
- ? (p.partyPlacementList.reduce((s, x) => s + x, 0) / p.partyPlacementList.length)
221
- : p.partyPlacement,
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.result.point !== a.result.point) {
229
- return b.result.point - a.result.point;
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.result.victoryCount !== a.result.victoryCount) {
233
- return b.result.victoryCount - a.result.victoryCount;
241
+ if (b.summary.victoryCount !== a.summary.victoryCount) {
242
+ return b.summary.victoryCount - a.summary.victoryCount;
234
243
  }
235
244
 
236
245
  // 3. 平均撃破数
237
- if (b.result.avgKills !== a.result.avgKills) {
238
- return b.result.avgKills - a.result.avgKills;
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
- if (b.result.avgPlacement !== a.result.avgPlacement) {
243
- return b.result.avgPlacement - a.result.avgPlacement;
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 cmp = b.result.totalAliveTime.comparedTo(a.result.totalAliveTime);
248
- if (cmp !== 0) return cmp;
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
- return a.partyNumber - b.partyNumber;
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.0.9",
4
- "description": "Fortniteのリプレイ解析をNode.jsから呼べる自己完結型C#バイナリラッパー",
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",