import { Fragment, startTransition, useEffect, useMemo, useRef, useState, } from "react"; import { Combobox, ComboboxItem, ComboboxList, ComboboxPopover, ComboboxProvider, ComboboxGroup, ComboboxGroupLabel, useComboboxStore, } from "@ariakit/react"; import { matchSorter } from "match-sorter"; import { getMissionInfo, getMissionList, getSourceAndPath } from "../manifest"; import orderBy from "lodash.orderby"; const excludeMissions = new Set([ "SkiFree", "SkiFree_Daily", "SkiFree_Randomizer", ]); const sourceGroupNames: Record = { "missions.vl2": "Official", "TR2final105-client.vl2": "Team Rabbit 2", "z_mappacks/CTF/Classic_maps_v1.vl2": "Classic", "z_mappacks/CTF/DynamixFinalPack.vl2": "Official", "z_mappacks/CTF/KryMapPack_b3EDIT.vl2": "KryMapPack", "z_mappacks/CTF/S5maps.vl2": "S5", "z_mappacks/CTF/S8maps.vl2": "S8", "z_mappacks/CTF/TWL-MapPack.vl2": "TWL", "z_mappacks/CTF/TWL-MapPackEDIT.vl2": "TWL", "z_mappacks/CTF/TWL2-MapPack.vl2": "TWL2", "z_mappacks/CTF/TWL2-MapPackEDIT.vl2": "TWL2", "z_mappacks/TWL_T2arenaOfficialMaps.vl2": "Arena", "z_mappacks/z_DMP2-V0.6.vl2": "DMP2 (Discord Map Pack)", "z_mappacks/zDMP-4.7.3DX.vl2": "DMP (Discord Map Pack)", }; const dirGroupNames: Record = { "z_mappacks/DM": "DM", "z_mappacks/LCTF": "LCTF", "z_mappacks/Lak": "LakRabbit", }; interface MissionItem { resourcePath: string; missionName: string; displayName: string; sourcePath: string; groupName: string | null; missionTypes: string[]; } const getDirName = (sourcePath: string) => { const match = sourcePath.match(/^(.*)(\/[^/]+)$/); return match ? match[1] : ""; }; const allMissions: MissionItem[] = getMissionList() .filter((name) => !excludeMissions.has(name)) .map((missionName) => { const missionInfo = getMissionInfo(missionName); const [sourcePath] = getSourceAndPath(missionInfo.resourcePath); const sourceDir = getDirName(sourcePath); const groupName = sourceGroupNames[sourcePath] ?? dirGroupNames[sourceDir] ?? null; return { resourcePath: missionInfo.resourcePath, missionName, displayName: missionInfo.displayName, sourcePath, groupName, missionTypes: missionInfo.missionTypes, }; }); const missionsByName = new Map(allMissions.map((m) => [m.missionName, m])); function groupMissions(missions: MissionItem[]) { const groupMap = new Map(); for (const mission of missions) { const group = groupMap.get(mission.groupName) ?? []; group.push(mission); groupMap.set(mission.groupName, group); } groupMap.forEach((groupMissions, groupName) => { groupMap.set( groupName, orderBy( groupMissions, [(m) => (m.displayName || m.missionName).toLowerCase()], ["asc"], ), ); }); return orderBy( Array.from(groupMap.entries()), [ ([groupName]) => groupName === "Official" ? 0 : groupName == null ? 2 : 1, ([groupName]) => (groupName ? groupName.toLowerCase() : ""), ], ["asc", "asc"], ); } const defaultGroups = groupMissions(allMissions); const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform); function MissionItemContent({ mission }: { mission: MissionItem }) { return ( <> {mission.displayName || mission.missionName} {mission.missionTypes.length > 0 && ( {mission.missionTypes.map((type) => ( {type} ))} )} {mission.missionName} ); } export function MissionSelect({ value, onChange, }: { value: string; onChange: (missionName: string) => void; }) { const [searchValue, setSearchValue] = useState(""); const inputRef = useRef(null); const combobox = useComboboxStore({ resetValueOnHide: true, selectedValue: value, setSelectedValue: (newValue) => { if (newValue) onChange(newValue); }, setValue: (value) => { startTransition(() => setSearchValue(value)); }, }); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); inputRef.current?.focus(); combobox.show(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [combobox]); const selectedMission = missionsByName.get(value); // When searching, return flat list sorted by relevance; otherwise return grouped const filteredResults = useMemo(() => { if (!searchValue) return { type: "grouped" as const, groups: defaultGroups }; const matches = matchSorter(allMissions, searchValue, { keys: ["displayName", "missionName"], }); return { type: "flat" as const, missions: matches }; }, [searchValue]); const displayValue = selectedMission ? selectedMission.displayName || selectedMission.missionName : value; const noResults = filteredResults.type === "flat" ? filteredResults.missions.length === 0 : filteredResults.groups.length === 0; return (
{ document.exitPointerLock(); combobox.show(); }} /> {isMac ? "⌘K" : "^K"}
{filteredResults.type === "flat" ? filteredResults.missions.map((mission) => ( )) : filteredResults.groups.map(([groupName, missions]) => groupName ? ( {groupName} {missions.map((mission) => ( ))} ) : ( {missions.map((mission) => ( ))} ), )} {noResults && (
No missions found
)}
); }