diff --git a/Makefile b/Makefile
index 8d0e3a1..37106db 100644
--- a/Makefile
+++ b/Makefile
@@ -5,3 +5,7 @@ fmt:
.PHONY: lint
lint:
ruff check lambda/
+
+.PHONY: gen-priority-file
+gen-priority-file:
+ ./gen_item_list_priority.py -i ../fgoscdata/hash_drop.json -o viewer/src/data/item_list_priority.json
diff --git a/SPEC.md b/SPEC.md
index e20af68..3f3be4a 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -217,6 +217,38 @@ FGO イベント報告データを集計し、アイテムドロップ率を算
- `合計周回数 = 有効な報告の runcount の総和` (アイテムごとに異なりうる。NaN 報告を除くため)
- `ドロップ率 = 合計ドロップ数 / 合計周回数`
+### 5.4 アイテム表示優先度 (`item_list_priority.json`)
+
+`viewer/src/data/item_list_priority.json` に配置され、viewer が TypeScript import でビルド時バンドルする。`gen_item_list_priority.py`(`make gen-priority-file`)で生成する。
+
+各エントリのフィールド:
+
+| フィールド | 型 | 説明 |
+|---|---|---|
+| `id` | number | アイテム ID |
+| `rarity` | number | レアリティ |
+| `shortname` | string | アイテム名(報告 JSON の `items` キーと完全一致) |
+| `dropPriority` | number | 表示優先度(大きいほど上位) |
+
+#### 表示順序
+
+`dropPriority` 降順 → 同値の場合は `id` 降順でソートする。
+
+#### フィルタリング(集計テーブルへの適用)
+
+以下をすべて満たすアイテムは集計テーブル(素材・イベントアイテム・ポイント・QP)の表示対象にならない:
+
+1. このリストに `shortname` が存在しない
+2. 添字つきイベントアイテム `(xN)` でない
+3. ポイント `ポイント(+N)` でない
+4. QP `QP(+N)` でない
+
+報告一覧のカラムはこのフィルタの影響を受けない(未知アイテムもカラムとして表示される)。
+
+#### 報告者サマリ アコーディオン明細
+
+未知アイテム(上記フィルタ対象)は既知アイテムの後ろに順不同で末尾表示する。
+
## 6. 中間 JSON 出力フォーマット
集計 Lambda が S3 に出力する。クエストごとに 1ファイル。
diff --git a/gen_item_list_priority.py b/gen_item_list_priority.py
new file mode 100755
index 0000000..e519a9a
--- /dev/null
+++ b/gen_item_list_priority.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+
+import argparse
+import json
+import logging
+import sys
+
+logger = logging.getLogger(__name__)
+
+
+def make_item_dict(item: dict) -> dict:
+ return {
+ "id": item["id"],
+ "rarity": item.get("rarity", 0),
+ "shortname": item["shortname"],
+ "dropPriority": item["dropPriority"],
+ }
+
+
+def main(args: argparse.Namespace):
+ items = json.load(args.input)
+ filtered_items = []
+
+ # 特攻礼装 9005
+ # ボーナス礼装 9005
+ # 泥礼装 9005
+ # 礼装 9005
+ # ★4EXP礼装 9004
+ # ★3EXP礼装 9003
+ ce_cache: dict[str, dict] = {}
+
+ for item in items:
+ if "shortname" not in item:
+ continue
+ item_id = item["id"]
+ if item_id < 10_000_000:
+ # 礼装のデータは1つだけあればよいので、同一の shortname であれば id が大きい方を優先する
+ if item["shortname"] in ["特攻礼装", "ボーナス礼装", "泥礼装", "礼装", "★4EXP礼装", "★3EXP礼装"]:
+ if item["shortname"] in ce_cache:
+ cached_item = ce_cache[item["shortname"]]
+ if item["id"] > cached_item["id"]:
+ ce_cache[item["shortname"]] = item
+ else:
+ pass
+ else:
+ ce_cache[item["shortname"]] = item
+
+ # filtered_items への追加は最後にまとめて行うので、ここではスキップ
+ continue
+
+ # 種火はレアリティ 4, 5 のみを対象とする
+ if item["type"] == "Exp. UP" and item["rarity"] < 4:
+ continue
+
+ # 上記規則に当てはまらない礼装は無視
+ if item["type"] == "Craft Essence":
+ logger.warning(f"Unknown Craft Essence: {item['name']} (id: {item['id']})")
+ continue
+
+ filtered_items.append(make_item_dict(item))
+
+ # 礼装のデータを filtered_items に追加
+ for item in ce_cache.values():
+ filtered_items.append(make_item_dict(item))
+
+ sorted_items = sorted(filtered_items, key=lambda x: x["dropPriority"], reverse=True)
+
+ if args.output_format == "tsv":
+ print("id\trarity\tshortname\tdropPriority", file=args.output)
+ for item in sorted_items:
+ print(f"{item['id']}\t{item['rarity']}\t{item['shortname']}\t{item['dropPriority']}", file=args.output)
+ else:
+ print(json.dumps(sorted_items, indent=2, ensure_ascii=False), file=args.output)
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--input", "-i", type=argparse.FileType("r"), default=sys.stdin)
+ parser.add_argument("--output", "-o", type=argparse.FileType("w"), default=sys.stdout)
+ parser.add_argument("--output-format", "-f", choices=["json", "tsv"], default="json")
+ return parser.parse_args()
+
+
+if __name__ == "__main__":
+ args = parse_args()
+ main(args)
diff --git a/viewer/src/components/ReporterSummary.tsx b/viewer/src/components/ReporterSummary.tsx
index db5d14b..70ac38a 100644
--- a/viewer/src/components/ReporterSummary.tsx
+++ b/viewer/src/components/ReporterSummary.tsx
@@ -4,6 +4,7 @@ import { formatTimestamp } from "../formatters";
import { useFetchData } from "../hooks/useFetchData";
import { useFixedSortState } from "../hooks/useSortState";
import { useToggleSet } from "../hooks/useToggleSet";
+import { sortItemNames } from "../itemPriority";
import type { ReportDetail, SortKey } from "../reporterSummaryUtils";
import { aggregateReporters, DEFAULT_SORT, sortRows } from "../reporterSummaryUtils";
import type { ExclusionsMap, Quest, QuestData } from "../types";
@@ -51,7 +52,7 @@ function DetailTable({ details }: { details: ReportDetail[] }) {
itemNames.add(key);
}
}
- const itemCols = [...itemNames];
+ const itemCols = sortItemNames([...itemNames]);
return (
diff --git a/viewer/src/components/SummaryTable.tsx b/viewer/src/components/SummaryTable.tsx
index 858d0bb..aa8a67a 100644
--- a/viewer/src/components/SummaryTable.tsx
+++ b/viewer/src/components/SummaryTable.tsx
@@ -1,4 +1,5 @@
import { MAX_EVENT_BONUS } from "../constants";
+import { compareByDropPriority } from "../itemPriority";
import {
calcEventItemExpected,
classifyStats,
@@ -107,7 +108,8 @@ function BonusTable({ title, items }: { title: string; items: ItemStats[] }) {
export function SummaryTable({ stats }: Props) {
if (stats.length === 0) return データなし
;
- const { normal, eventItems, points, qp } = classifyStats(stats);
+ const { normal: rawNormal, eventItems, points, qp } = classifyStats(stats);
+ const normal = [...rawNormal].sort((a, b) => compareByDropPriority(a.itemName, b.itemName));
return (
<>
diff --git a/viewer/src/data/item_list_priority.json b/viewer/src/data/item_list_priority.json
new file mode 100644
index 0000000..fa93742
--- /dev/null
+++ b/viewer/src/data/item_list_priority.json
@@ -0,0 +1,866 @@
+[
+ {
+ "id": 5,
+ "rarity": 0,
+ "shortname": "報酬QP",
+ "dropPriority": 9018
+ },
+ {
+ "id": 100,
+ "rarity": 3,
+ "shortname": "金林檎",
+ "dropPriority": 9014
+ },
+ {
+ "id": 2109,
+ "rarity": 3,
+ "shortname": "EX2足跡",
+ "dropPriority": 9014
+ },
+ {
+ "id": 2108,
+ "rarity": 3,
+ "shortname": "EX1足跡",
+ "dropPriority": 9013
+ },
+ {
+ "id": 101,
+ "rarity": 2,
+ "shortname": "銀林檎",
+ "dropPriority": 9013
+ },
+ {
+ "id": 2107,
+ "rarity": 3,
+ "shortname": "狂足跡",
+ "dropPriority": 9012
+ },
+ {
+ "id": 104,
+ "rarity": 2,
+ "shortname": "青林檎",
+ "dropPriority": 9012
+ },
+ {
+ "id": 2106,
+ "rarity": 3,
+ "shortname": "殺足跡",
+ "dropPriority": 9011
+ },
+ {
+ "id": 102,
+ "rarity": 1,
+ "shortname": "銅林檎",
+ "dropPriority": 9011
+ },
+ {
+ "id": 2105,
+ "rarity": 3,
+ "shortname": "術足跡",
+ "dropPriority": 9010
+ },
+ {
+ "id": 2104,
+ "rarity": 3,
+ "shortname": "騎足跡",
+ "dropPriority": 9009
+ },
+ {
+ "id": 2103,
+ "rarity": 3,
+ "shortname": "槍足跡",
+ "dropPriority": 9008
+ },
+ {
+ "id": 2102,
+ "rarity": 3,
+ "shortname": "弓足跡",
+ "dropPriority": 9007
+ },
+ {
+ "id": 2101,
+ "rarity": 3,
+ "shortname": "剣足跡",
+ "dropPriority": 9006
+ },
+ {
+ "id": 2000,
+ "rarity": 3,
+ "shortname": "足跡",
+ "dropPriority": 9005
+ },
+ {
+ "id": 9409200,
+ "rarity": 5,
+ "shortname": "礼装",
+ "dropPriority": 9005
+ },
+ {
+ "id": 9408780,
+ "rarity": 5,
+ "shortname": "特攻礼装",
+ "dropPriority": 9005
+ },
+ {
+ "id": 9405310,
+ "rarity": 5,
+ "shortname": "ボーナス礼装",
+ "dropPriority": 9005
+ },
+ {
+ "id": 9408790,
+ "rarity": 5,
+ "shortname": "泥礼装",
+ "dropPriority": 9005
+ },
+ {
+ "id": 9809570,
+ "rarity": 4,
+ "shortname": "★4EXP礼装",
+ "dropPriority": 9004
+ },
+ {
+ "id": 9809580,
+ "rarity": 3,
+ "shortname": "★3EXP礼装",
+ "dropPriority": 9003
+ },
+ {
+ "id": 6563,
+ "rarity": 3,
+ "shortname": "箱",
+ "dropPriority": 8514
+ },
+ {
+ "id": 6562,
+ "rarity": 3,
+ "shortname": "聖水",
+ "dropPriority": 8513
+ },
+ {
+ "id": 6560,
+ "rarity": 3,
+ "shortname": "月光",
+ "dropPriority": 8511
+ },
+ {
+ "id": 6558,
+ "rarity": 3,
+ "shortname": "釜",
+ "dropPriority": 8510
+ },
+ {
+ "id": 6548,
+ "rarity": 3,
+ "shortname": "鬼灯",
+ "dropPriority": 8509
+ },
+ {
+ "id": 6544,
+ "rarity": 3,
+ "shortname": "カケラ",
+ "dropPriority": 8508
+ },
+ {
+ "id": 6542,
+ "rarity": 3,
+ "shortname": "卵",
+ "dropPriority": 8507
+ },
+ {
+ "id": 6540,
+ "rarity": 3,
+ "shortname": "鏡",
+ "dropPriority": 8506
+ },
+ {
+ "id": 6531,
+ "rarity": 3,
+ "shortname": "神酒",
+ "dropPriority": 8505
+ },
+ {
+ "id": 6529,
+ "rarity": 3,
+ "shortname": "胆石",
+ "dropPriority": 8504
+ },
+ {
+ "id": 6525,
+ "rarity": 3,
+ "shortname": "スカラベ",
+ "dropPriority": 8503
+ },
+ {
+ "id": 6518,
+ "rarity": 3,
+ "shortname": "根",
+ "dropPriority": 8502
+ },
+ {
+ "id": 6506,
+ "rarity": 3,
+ "shortname": "逆鱗",
+ "dropPriority": 8501
+ },
+ {
+ "id": 6517,
+ "rarity": 3,
+ "shortname": "心臓",
+ "dropPriority": 8500
+ },
+ {
+ "id": 6546,
+ "rarity": 3,
+ "shortname": "実",
+ "dropPriority": 8407
+ },
+ {
+ "id": 6539,
+ "rarity": 3,
+ "shortname": "炉心",
+ "dropPriority": 8406
+ },
+ {
+ "id": 6528,
+ "rarity": 3,
+ "shortname": "産毛",
+ "dropPriority": 8405
+ },
+ {
+ "id": 6523,
+ "rarity": 3,
+ "shortname": "ランプ",
+ "dropPriority": 8404
+ },
+ {
+ "id": 6521,
+ "rarity": 3,
+ "shortname": "脂",
+ "dropPriority": 8403
+ },
+ {
+ "id": 6520,
+ "rarity": 3,
+ "shortname": "涙石",
+ "dropPriority": 8402
+ },
+ {
+ "id": 6519,
+ "rarity": 3,
+ "shortname": "幼角",
+ "dropPriority": 8401
+ },
+ {
+ "id": 6507,
+ "rarity": 3,
+ "shortname": "爪",
+ "dropPriority": 8400
+ },
+ {
+ "id": 6561,
+ "rarity": 2,
+ "shortname": "レンズ",
+ "dropPriority": 8324
+ },
+ {
+ "id": 6559,
+ "rarity": 2,
+ "shortname": "キューブ",
+ "dropPriority": 8323
+ },
+ {
+ "id": 6557,
+ "rarity": 2,
+ "shortname": "花",
+ "dropPriority": 8322
+ },
+ {
+ "id": 6556,
+ "rarity": 2,
+ "shortname": "エーテル",
+ "dropPriority": 8321
+ },
+ {
+ "id": 6553,
+ "rarity": 2,
+ "shortname": "皮",
+ "dropPriority": 8320
+ },
+ {
+ "id": 6550,
+ "rarity": 2,
+ "shortname": "鱗粉",
+ "dropPriority": 8319
+ },
+ {
+ "id": 6547,
+ "rarity": 2,
+ "shortname": "糸玉",
+ "dropPriority": 8318
+ },
+ {
+ "id": 6545,
+ "rarity": 2,
+ "shortname": "霊子",
+ "dropPriority": 8317
+ },
+ {
+ "id": 6543,
+ "rarity": 2,
+ "shortname": "冠",
+ "dropPriority": 8316
+ },
+ {
+ "id": 6541,
+ "rarity": 2,
+ "shortname": "矢尻",
+ "dropPriority": 8315
+ },
+ {
+ "id": 6538,
+ "rarity": 2,
+ "shortname": "鈴",
+ "dropPriority": 8314
+ },
+ {
+ "id": 6536,
+ "rarity": 2,
+ "shortname": "オーロラ",
+ "dropPriority": 8313
+ },
+ {
+ "id": 6537,
+ "rarity": 2,
+ "shortname": "指輪",
+ "dropPriority": 8312
+ },
+ {
+ "id": 6535,
+ "rarity": 2,
+ "shortname": "結氷",
+ "dropPriority": 8311
+ },
+ {
+ "id": 6532,
+ "rarity": 2,
+ "shortname": "勾玉",
+ "dropPriority": 8310
+ },
+ {
+ "id": 6524,
+ "rarity": 2,
+ "shortname": "勲章",
+ "dropPriority": 8309
+ },
+ {
+ "id": 6526,
+ "rarity": 2,
+ "shortname": "貝殻",
+ "dropPriority": 8308
+ },
+ {
+ "id": 6509,
+ "rarity": 2,
+ "shortname": "蛇玉",
+ "dropPriority": 8307
+ },
+ {
+ "id": 6501,
+ "rarity": 2,
+ "shortname": "羽根",
+ "dropPriority": 8306
+ },
+ {
+ "id": 6513,
+ "rarity": 2,
+ "shortname": "蹄鉄",
+ "dropPriority": 8305
+ },
+ {
+ "id": 6514,
+ "rarity": 2,
+ "shortname": "ホム",
+ "dropPriority": 8304
+ },
+ {
+ "id": 6511,
+ "rarity": 2,
+ "shortname": "頁",
+ "dropPriority": 8303
+ },
+ {
+ "id": 6510,
+ "rarity": 2,
+ "shortname": "歯車",
+ "dropPriority": 8302
+ },
+ {
+ "id": 6515,
+ "rarity": 2,
+ "shortname": "八連",
+ "dropPriority": 8301
+ },
+ {
+ "id": 6508,
+ "rarity": 2,
+ "shortname": "ランタン",
+ "dropPriority": 8300
+ },
+ {
+ "id": 6502,
+ "rarity": 2,
+ "shortname": "種",
+ "dropPriority": 8203
+ },
+ {
+ "id": 6527,
+ "rarity": 1,
+ "shortname": "毒針",
+ "dropPriority": 8202
+ },
+ {
+ "id": 6505,
+ "rarity": 1,
+ "shortname": "塵",
+ "dropPriority": 8201
+ },
+ {
+ "id": 6512,
+ "rarity": 1,
+ "shortname": "牙",
+ "dropPriority": 8200
+ },
+ {
+ "id": 6555,
+ "rarity": 1,
+ "shortname": "残滓",
+ "dropPriority": 8110
+ },
+ {
+ "id": 6554,
+ "rarity": 1,
+ "shortname": "刃",
+ "dropPriority": 8109
+ },
+ {
+ "id": 6552,
+ "rarity": 1,
+ "shortname": "灰",
+ "dropPriority": 8108
+ },
+ {
+ "id": 6551,
+ "rarity": 1,
+ "shortname": "剣",
+ "dropPriority": 8107
+ },
+ {
+ "id": 6549,
+ "rarity": 1,
+ "shortname": "小鐘",
+ "dropPriority": 8106
+ },
+ {
+ "id": 6534,
+ "rarity": 1,
+ "shortname": "火薬",
+ "dropPriority": 8105
+ },
+ {
+ "id": 6533,
+ "rarity": 1,
+ "shortname": "鉄杭",
+ "dropPriority": 8104
+ },
+ {
+ "id": 6530,
+ "rarity": 1,
+ "shortname": "髄液",
+ "dropPriority": 8103
+ },
+ {
+ "id": 6522,
+ "rarity": 1,
+ "shortname": "鎖",
+ "dropPriority": 8102
+ },
+ {
+ "id": 6516,
+ "rarity": 1,
+ "shortname": "骨",
+ "dropPriority": 8101
+ },
+ {
+ "id": 6503,
+ "rarity": 1,
+ "shortname": "証",
+ "dropPriority": 8100
+ },
+ {
+ "id": 63,
+ "rarity": 3,
+ "shortname": "EX2結晶",
+ "dropPriority": 8008
+ },
+ {
+ "id": 62,
+ "rarity": 3,
+ "shortname": "EX1結晶",
+ "dropPriority": 8007
+ },
+ {
+ "id": 61,
+ "rarity": 3,
+ "shortname": "狂結晶",
+ "dropPriority": 8006
+ },
+ {
+ "id": 60,
+ "rarity": 3,
+ "shortname": "殺結晶",
+ "dropPriority": 8005
+ },
+ {
+ "id": 59,
+ "rarity": 3,
+ "shortname": "術結晶",
+ "dropPriority": 8004
+ },
+ {
+ "id": 58,
+ "rarity": 3,
+ "shortname": "騎結晶",
+ "dropPriority": 8003
+ },
+ {
+ "id": 57,
+ "rarity": 3,
+ "shortname": "槍結晶",
+ "dropPriority": 8002
+ },
+ {
+ "id": 56,
+ "rarity": 3,
+ "shortname": "弓結晶",
+ "dropPriority": 8001
+ },
+ {
+ "id": 55,
+ "rarity": 3,
+ "shortname": "剣結晶",
+ "dropPriority": 8000
+ },
+ {
+ "id": 6201,
+ "rarity": 3,
+ "shortname": "剣秘",
+ "dropPriority": 6300
+ },
+ {
+ "id": 6202,
+ "rarity": 3,
+ "shortname": "弓秘",
+ "dropPriority": 6299
+ },
+ {
+ "id": 6203,
+ "rarity": 3,
+ "shortname": "槍秘",
+ "dropPriority": 6298
+ },
+ {
+ "id": 6204,
+ "rarity": 3,
+ "shortname": "騎秘",
+ "dropPriority": 6297
+ },
+ {
+ "id": 6205,
+ "rarity": 3,
+ "shortname": "術秘",
+ "dropPriority": 6296
+ },
+ {
+ "id": 6206,
+ "rarity": 3,
+ "shortname": "殺秘",
+ "dropPriority": 6295
+ },
+ {
+ "id": 6207,
+ "rarity": 3,
+ "shortname": "狂秘",
+ "dropPriority": 6294
+ },
+ {
+ "id": 6101,
+ "rarity": 2,
+ "shortname": "剣魔",
+ "dropPriority": 6200
+ },
+ {
+ "id": 6102,
+ "rarity": 2,
+ "shortname": "弓魔",
+ "dropPriority": 6199
+ },
+ {
+ "id": 6103,
+ "rarity": 2,
+ "shortname": "槍魔",
+ "dropPriority": 6198
+ },
+ {
+ "id": 6104,
+ "rarity": 2,
+ "shortname": "騎魔",
+ "dropPriority": 6197
+ },
+ {
+ "id": 6105,
+ "rarity": 2,
+ "shortname": "術魔",
+ "dropPriority": 6196
+ },
+ {
+ "id": 6106,
+ "rarity": 2,
+ "shortname": "殺魔",
+ "dropPriority": 6195
+ },
+ {
+ "id": 6107,
+ "rarity": 2,
+ "shortname": "狂魔",
+ "dropPriority": 6194
+ },
+ {
+ "id": 6001,
+ "rarity": 1,
+ "shortname": "剣輝",
+ "dropPriority": 6100
+ },
+ {
+ "id": 6002,
+ "rarity": 1,
+ "shortname": "弓輝",
+ "dropPriority": 6099
+ },
+ {
+ "id": 6003,
+ "rarity": 1,
+ "shortname": "槍輝",
+ "dropPriority": 6098
+ },
+ {
+ "id": 6004,
+ "rarity": 1,
+ "shortname": "騎輝",
+ "dropPriority": 6097
+ },
+ {
+ "id": 6005,
+ "rarity": 1,
+ "shortname": "術輝",
+ "dropPriority": 6096
+ },
+ {
+ "id": 6006,
+ "rarity": 1,
+ "shortname": "殺輝",
+ "dropPriority": 6095
+ },
+ {
+ "id": 6007,
+ "rarity": 1,
+ "shortname": "狂輝",
+ "dropPriority": 6094
+ },
+ {
+ "id": 7101,
+ "rarity": 3,
+ "shortname": "剣モ",
+ "dropPriority": 5300
+ },
+ {
+ "id": 7102,
+ "rarity": 3,
+ "shortname": "弓モ",
+ "dropPriority": 5299
+ },
+ {
+ "id": 7103,
+ "rarity": 3,
+ "shortname": "槍モ",
+ "dropPriority": 5298
+ },
+ {
+ "id": 7104,
+ "rarity": 3,
+ "shortname": "騎モ",
+ "dropPriority": 5297
+ },
+ {
+ "id": 7105,
+ "rarity": 3,
+ "shortname": "術モ",
+ "dropPriority": 5296
+ },
+ {
+ "id": 7106,
+ "rarity": 3,
+ "shortname": "殺モ",
+ "dropPriority": 5295
+ },
+ {
+ "id": 7107,
+ "rarity": 3,
+ "shortname": "狂モ",
+ "dropPriority": 5294
+ },
+ {
+ "id": 7001,
+ "rarity": 2,
+ "shortname": "剣ピ",
+ "dropPriority": 5200
+ },
+ {
+ "id": 7002,
+ "rarity": 2,
+ "shortname": "弓ピ",
+ "dropPriority": 5199
+ },
+ {
+ "id": 7003,
+ "rarity": 2,
+ "shortname": "槍ピ",
+ "dropPriority": 5198
+ },
+ {
+ "id": 7004,
+ "rarity": 2,
+ "shortname": "騎ピ",
+ "dropPriority": 5197
+ },
+ {
+ "id": 7005,
+ "rarity": 2,
+ "shortname": "術ピ",
+ "dropPriority": 5196
+ },
+ {
+ "id": 7006,
+ "rarity": 2,
+ "shortname": "殺ピ",
+ "dropPriority": 5195
+ },
+ {
+ "id": 7007,
+ "rarity": 2,
+ "shortname": "狂ピ",
+ "dropPriority": 5194
+ },
+ {
+ "id": 9700400,
+ "rarity": 4,
+ "shortname": "全猛火",
+ "dropPriority": 696
+ },
+ {
+ "id": 9700500,
+ "rarity": 5,
+ "shortname": "全業火",
+ "dropPriority": 695
+ },
+ {
+ "id": 9701400,
+ "rarity": 4,
+ "shortname": "剣猛火",
+ "dropPriority": 691
+ },
+ {
+ "id": 9701500,
+ "rarity": 5,
+ "shortname": "剣業火",
+ "dropPriority": 690
+ },
+ {
+ "id": 9702400,
+ "rarity": 4,
+ "shortname": "槍猛火",
+ "dropPriority": 686
+ },
+ {
+ "id": 9702500,
+ "rarity": 5,
+ "shortname": "槍業火",
+ "dropPriority": 685
+ },
+ {
+ "id": 9703400,
+ "rarity": 4,
+ "shortname": "弓猛火",
+ "dropPriority": 681
+ },
+ {
+ "id": 9703500,
+ "rarity": 5,
+ "shortname": "弓業火",
+ "dropPriority": 680
+ },
+ {
+ "id": 9704400,
+ "rarity": 4,
+ "shortname": "騎猛火",
+ "dropPriority": 676
+ },
+ {
+ "id": 9704500,
+ "rarity": 5,
+ "shortname": "騎業火",
+ "dropPriority": 675
+ },
+ {
+ "id": 9705400,
+ "rarity": 4,
+ "shortname": "術猛火",
+ "dropPriority": 671
+ },
+ {
+ "id": 9705500,
+ "rarity": 5,
+ "shortname": "術業火",
+ "dropPriority": 670
+ },
+ {
+ "id": 9706400,
+ "rarity": 4,
+ "shortname": "殺猛火",
+ "dropPriority": 666
+ },
+ {
+ "id": 9706500,
+ "rarity": 5,
+ "shortname": "殺業火",
+ "dropPriority": 665
+ },
+ {
+ "id": 9707400,
+ "rarity": 4,
+ "shortname": "狂猛火",
+ "dropPriority": 661
+ },
+ {
+ "id": 9707500,
+ "rarity": 5,
+ "shortname": "狂業火",
+ "dropPriority": 660
+ },
+ {
+ "id": 4,
+ "rarity": 0,
+ "shortname": "FP",
+ "dropPriority": 512
+ }
+]
diff --git a/viewer/src/itemPriority.test.ts b/viewer/src/itemPriority.test.ts
new file mode 100644
index 0000000..cf087e4
--- /dev/null
+++ b/viewer/src/itemPriority.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, test } from "vitest";
+import { compareByDropPriority, isKnownItem, sortItemNames } from "./itemPriority";
+
+describe("isKnownItem", () => {
+ test("item_list_priority.json に掲載されているアイテムは true を返す", () => {
+ expect(isKnownItem("心臓")).toBe(true);
+ expect(isKnownItem("灰")).toBe(true);
+ expect(isKnownItem("鉄杭")).toBe(true);
+ expect(isKnownItem("骨")).toBe(true);
+ expect(isKnownItem("礼装")).toBe(true);
+ });
+
+ test("リストにないアイテムは false を返す", () => {
+ expect(isKnownItem("未知のアイテム")).toBe(false);
+ expect(isKnownItem("")).toBe(false);
+ });
+
+ test("イベントアイテム形式のキーはリストに含まれないので false を返す", () => {
+ // item_list_priority.json には "(xN)" 形式のキーは存在しない
+ expect(isKnownItem("ぐん肥(x3)")).toBe(false);
+ });
+});
+
+describe("compareByDropPriority", () => {
+ test("dropPriority 降順でソートされる", () => {
+ // 心臓: dp=8500, 灰: dp=8108, 鉄杭: dp=8104, 骨: dp=8101
+ expect(compareByDropPriority("心臓", "灰")).toBeLessThan(0);
+ expect(compareByDropPriority("灰", "心臓")).toBeGreaterThan(0);
+ expect(compareByDropPriority("灰", "鉄杭")).toBeLessThan(0);
+ expect(compareByDropPriority("鉄杭", "骨")).toBeLessThan(0);
+ });
+
+ test("dropPriority が同じ場合は id 降順でソートされる", () => {
+ // 金林檎: id=100, dp=9014 / EX2足跡: id=2109, dp=9014
+ // id 降順なので EX2足跡 (2109) が前
+ expect(compareByDropPriority("EX2足跡", "金林檎")).toBeLessThan(0);
+ expect(compareByDropPriority("金林檎", "EX2足跡")).toBeGreaterThan(0);
+ });
+
+ test("同一アイテムは 0 を返す", () => {
+ expect(compareByDropPriority("心臓", "心臓")).toBe(0);
+ });
+
+ test("未知アイテムはリスト内アイテムの後ろに配置される", () => {
+ expect(compareByDropPriority("未知", "心臓")).toBeGreaterThan(0);
+ expect(compareByDropPriority("心臓", "未知")).toBeLessThan(0);
+ });
+
+ test("未知アイテム同士は順序を問わない(0 を返す)", () => {
+ expect(compareByDropPriority("未知A", "未知B")).toBe(0);
+ });
+});
+
+describe("sortItemNames", () => {
+ test("dropPriority 降順にソートされる", () => {
+ const items = ["鉄杭", "心臓", "骨", "灰"];
+ const sorted = sortItemNames(items);
+ // 心臓(8500) > 灰(8108) > 鉄杭(8104) > 骨(8101)
+ expect(sorted).toEqual(["心臓", "灰", "鉄杭", "骨"]);
+ });
+
+ test("リスト内アイテムが未知アイテムより前に並ぶ", () => {
+ const items = ["未知のアイテム", "心臓", "別の未知"];
+ const sorted = sortItemNames(items);
+ expect(sorted[0]).toBe("心臓");
+ // 未知アイテムは末尾(順序不定)
+ expect(sorted.slice(1)).toContain("未知のアイテム");
+ expect(sorted.slice(1)).toContain("別の未知");
+ });
+
+ test("元の配列を変更しない", () => {
+ const items = ["骨", "心臓", "灰"];
+ const original = [...items];
+ sortItemNames(items);
+ expect(items).toEqual(original);
+ });
+
+ test("空配列では空を返す", () => {
+ expect(sortItemNames([])).toEqual([]);
+ });
+});
diff --git a/viewer/src/itemPriority.ts b/viewer/src/itemPriority.ts
new file mode 100644
index 0000000..1d3d23b
--- /dev/null
+++ b/viewer/src/itemPriority.ts
@@ -0,0 +1,48 @@
+import itemListPriority from "./data/item_list_priority.json";
+
+interface PriorityEntry {
+ id: number;
+ rarity: number;
+ shortname: string;
+ dropPriority: number;
+}
+
+const entries = itemListPriority as PriorityEntry[];
+
+const priorityMap = new Map(
+ entries.map((e) => [e.shortname, { id: e.id, dropPriority: e.dropPriority }]),
+);
+
+/** item_list_priority.json に掲載されているアイテム名のセット */
+export const knownItemNames: Set = new Set(entries.map((e) => e.shortname));
+
+/** アイテム名が item_list_priority.json に掲載されているか判定する */
+export function isKnownItem(itemName: string): boolean {
+ return knownItemNames.has(itemName);
+}
+
+/**
+ * dropPriority 降順 → id 降順のコンパレータ。
+ * リストにないアイテムはリスト内アイテムの後ろに配置する。
+ */
+export function compareByDropPriority(a: string, b: string): number {
+ const pa = priorityMap.get(a);
+ const pb = priorityMap.get(b);
+
+ if (!pa && !pb) return 0;
+ if (!pb) return -1;
+ if (!pa) return 1;
+
+ if (pb.dropPriority !== pa.dropPriority) {
+ return pb.dropPriority - pa.dropPriority;
+ }
+ return pb.id - pa.id;
+}
+
+/**
+ * アイテム名配列を dropPriority 降順でソートして返す。
+ * リスト内アイテム → リスト外アイテム(順不同)の順になる。
+ */
+export function sortItemNames(itemNames: string[]): string[] {
+ return [...itemNames].sort(compareByDropPriority);
+}
diff --git a/viewer/src/summaryUtils.test.ts b/viewer/src/summaryUtils.test.ts
index 2de3d9b..4444ce5 100644
--- a/viewer/src/summaryUtils.test.ts
+++ b/viewer/src/summaryUtils.test.ts
@@ -54,11 +54,22 @@ describe("classifyStats", () => {
makeStats("QP(+150000)"),
];
const { normal, eventItems, points, qp } = classifyStats(stats);
+ // 鉄杭・骨は item_list_priority.json に掲載されているため normal に含まれる
expect(normal).toHaveLength(2);
expect(eventItems).toHaveLength(2);
expect(points).toHaveLength(1);
expect(qp).toHaveLength(1);
});
+
+ test("item_list_priority.json に掲載されていない通常アイテムは除外される", () => {
+ const stats = [makeStats("未知のアイテム"), makeStats("鉄杭"), makeStats("ぐん肥(x3)")];
+ const { normal, eventItems } = classifyStats(stats);
+ // 未知のアイテムは normal に含まれない
+ expect(normal).toHaveLength(1);
+ expect(normal[0].itemName).toBe("鉄杭");
+ // イベントアイテムはリストに関係なく分類される
+ expect(eventItems).toHaveLength(1);
+ });
});
describe("extractBaseName", () => {
diff --git a/viewer/src/summaryUtils.ts b/viewer/src/summaryUtils.ts
index 8cbac61..e7c7814 100644
--- a/viewer/src/summaryUtils.ts
+++ b/viewer/src/summaryUtils.ts
@@ -1,4 +1,5 @@
import { RE_EVENT_ITEM, RE_POINT, RE_QP } from "./constants";
+import { isKnownItem } from "./itemPriority";
import type { ItemStats } from "./types";
/**
@@ -20,9 +21,10 @@ export function classifyStats(stats: ItemStats[]) {
qp.push(s);
} else if (RE_POINT.test(s.itemName)) {
points.push(s);
- } else {
+ } else if (isKnownItem(s.itemName)) {
normal.push(s);
}
+ // 未知アイテムは全カテゴリから除外
}
return { normal, eventItems, points, qp };
diff --git a/viewer/tsconfig.json b/viewer/tsconfig.json
index 20fb0a0..def71f6 100644
--- a/viewer/tsconfig.json
+++ b/viewer/tsconfig.json
@@ -7,6 +7,7 @@
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,