Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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ファイル。
Expand Down
86 changes: 86 additions & 0 deletions gen_item_list_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3

import argparse
import json
import logging
import sys

logger = logging.getLogger(__name__)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logger is created but never configured with basicConfig or a handler. When warnings are logged at line 57, they won't be displayed to the user unless the logging level is configured. Consider adding logging.basicConfig() in the main function or in the if name == "main" block to ensure warning messages are visible.

Copilot uses AI. Check for mistakes.


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)
3 changes: 2 additions & 1 deletion viewer/src/components/ReporterSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,7 +52,7 @@ function DetailTable({ details }: { details: ReportDetail[] }) {
itemNames.add(key);
}
}
const itemCols = [...itemNames];
const itemCols = sortItemNames([...itemNames]);

return (
<table style={{ ...tableStyle, marginBottom: "0.5rem", fontSize: "0.85rem" }}>
Expand Down
4 changes: 3 additions & 1 deletion viewer/src/components/SummaryTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MAX_EVENT_BONUS } from "../constants";
import { compareByDropPriority } from "../itemPriority";
import {
calcEventItemExpected,
classifyStats,
Expand Down Expand Up @@ -107,7 +108,8 @@ function BonusTable({ title, items }: { title: string; items: ItemStats[] }) {
export function SummaryTable({ stats }: Props) {
if (stats.length === 0) return <p>データなし</p>;

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 (
<>
Expand Down
Loading