import { normalizeSearch, searchItems } from "../search"; export type TeamSearchTarget = | "all" | "team" | "character" | "weapon" | "artifact"; export interface TeamSearchValues { team: string[]; character: string[]; weapon: string[]; artifact: string[]; } export interface DpsTeam { rotationSeconds: string | number; members: Array<{ damage: string | number }>; } export type DpsOrder = "best" | "worst" | null; function teamDps(team: DpsTeam): number { const seconds = Number(team.rotationSeconds); if (!(seconds > 0)) return 0; return team.members.reduce((sum, member) => sum + Number(member.damage), 0) / seconds; } export function sortTeamsByDps( teams: readonly Team[], order: DpsOrder, ): Team[] { if (!order) return [...teams]; return teams .map((team, index) => ({ team, index })) .sort((left, right) => { const difference = order === "best" ? teamDps(right.team) - teamDps(left.team) : teamDps(left.team) - teamDps(right.team); return difference || left.index - right.index; }) .map(({ team }) => team); } export function matchesTeamSearch( values: TeamSearchValues, query: string, target: TeamSearchTarget, ): boolean { const normalizedQuery = normalizeSearch(query); if (!normalizedQuery) return true; const candidates = target === "all" ? Object.values(values).flat() : values[target]; return candidates.some((value) => normalizeSearch(value).includes(normalizedQuery), ); } export function searchTeams( teams: readonly T[], values: (team: T) => TeamSearchValues, query: string, target: TeamSearchTarget, ) { const candidates = (team: T) => { const fields = values(team); return target === "all" ? Object.values(fields).flat() : fields[target]; }; // Separate candidates retain the existing rule that a direct match must // occur within one name, rather than spanning two members or categories. const direct = teams.filter((team) => matchesTeamSearch(values(team), query, target)); if (direct.length || !query.trim()) return { items: direct, fuzzy: false }; return searchItems(teams, query, () => "", candidates); }