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; recommendation: Record<"weapon" | "artifact", AnalyticsItem[]>; } export interface RankedAnalyticsItem extends AnalyticsItem { count: number; } type AnalyticsSelection = { guideIds: Set; teamDivisors: Map }; 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 = 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(); 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), }, }; }