85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
export type AnalyticsKind = "character" | "weapon" | "artifact";
|
|
export type AnalyticsCategory = "team" | "recommendation";
|
|
|
|
export interface AnalyticsItem {
|
|
key: string;
|
|
name: string;
|
|
imageUrl: string | null;
|
|
rarity: number | null;
|
|
guideCount: number;
|
|
stygianCount: number;
|
|
contributions: Array<{ guideId: string; teamSignature: string | null; count: number }>;
|
|
}
|
|
|
|
export interface AnalyticsGuide {
|
|
id: string;
|
|
source: "guide" | "stygian";
|
|
name: string;
|
|
}
|
|
|
|
export interface AnalyticsTeamGroup {
|
|
guideId: string;
|
|
signature: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface PublicAnalytics {
|
|
guides: AnalyticsGuide[];
|
|
teamGroups: AnalyticsTeamGroup[];
|
|
guideCount: number;
|
|
stygianScheduleCount: number;
|
|
guideTeamCount: number;
|
|
stygianTeamCount: number;
|
|
team: Record<AnalyticsKind, AnalyticsItem[]>;
|
|
recommendation: Record<"weapon" | "artifact", AnalyticsItem[]>;
|
|
}
|
|
|
|
export interface RankedAnalyticsItem extends AnalyticsItem {
|
|
count: number;
|
|
}
|
|
|
|
type AnalyticsSelection = { guideIds: Set<string>; teamDivisors: Map<string, number> };
|
|
|
|
export function rankAnalyticsItems(items: AnalyticsItem[], includeStygian: boolean, selection?: AnalyticsSelection): RankedAnalyticsItem[] {
|
|
return items.map((item) => ({
|
|
...item,
|
|
count: selection ? item.contributions.reduce((count, entry) => {
|
|
if (!selection.guideIds.has(entry.guideId)) return count;
|
|
const divisor = entry.teamSignature === null ? 1 : selection.teamDivisors.get(`${entry.guideId}:${entry.teamSignature}`);
|
|
return divisor ? count + entry.count / divisor : count;
|
|
}, 0) : item.guideCount + (includeStygian ? item.stygianCount : 0),
|
|
})).filter((item) => item.count > 0).sort((a, b) =>
|
|
b.count - a.count || a.name.localeCompare(b.name, "th") || a.key.localeCompare(b.key, "en"),
|
|
);
|
|
}
|
|
|
|
export function selectAnalytics(data: PublicAnalytics, includeStygian: boolean, excludedGuideIds: ReadonlySet<string> = new Set(), ignoreRepeatedTeams = false) {
|
|
const includedGuides = data.guides.filter((guide) => !excludedGuideIds.has(guide.id) && (includeStygian || guide.source === "guide"));
|
|
const selection: AnalyticsSelection = { guideIds: new Set(includedGuides.map((guide) => guide.id)), teamDivisors: new Map() };
|
|
const seen = new Set<string>();
|
|
let teamCount = 0;
|
|
// Choose one copy deterministically after applying guide exclusions.
|
|
for (const group of [...data.teamGroups].sort((a, b) => a.guideId.localeCompare(b.guideId))) {
|
|
if (!selection.guideIds.has(group.guideId)) continue;
|
|
if (ignoreRepeatedTeams && seen.has(group.signature)) continue;
|
|
seen.add(group.signature);
|
|
selection.teamDivisors.set(`${group.guideId}:${group.signature}`, ignoreRepeatedTeams ? group.count : 1);
|
|
teamCount += ignoreRepeatedTeams ? 1 : group.count;
|
|
}
|
|
const team = {
|
|
character: rankAnalyticsItems(data.team.character, includeStygian, selection),
|
|
weapon: rankAnalyticsItems(data.team.weapon, includeStygian, selection),
|
|
artifact: rankAnalyticsItems(data.team.artifact, includeStygian, selection),
|
|
};
|
|
return {
|
|
guideCount: includedGuides.filter((guide) => guide.source === "guide").length,
|
|
stygianScheduleCount: includedGuides.filter((guide) => guide.source === "stygian").length,
|
|
teamCount,
|
|
team,
|
|
recommendation: {
|
|
weapon: rankAnalyticsItems(data.recommendation.weapon, includeStygian, selection),
|
|
artifact: rankAnalyticsItems(data.recommendation.artifact, includeStygian, selection),
|
|
},
|
|
};
|
|
}
|