import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; import type { Tables } from "~/db/tables"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import { split, startsWith } from "~/utils/strings"; import { assertType } from "~/utils/types"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; import { SendouButton } from "./elements/Button"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import { CrossIcon } from "./icons/Cross"; import styles from "./MapPoolSelector.module.css"; export type MapPoolSelectorProps = { mapPool: MapPool; preselectedMapPool?: MapPool; handleRemoval?: () => void; handleMapPoolChange: ( mapPool: MapPool, event?: Pick, ) => void; className?: string; title?: string; modesToInclude?: ModeShort[]; info?: React.ReactNode; footer?: React.ReactNode; /** Enables clear button, template selection, and toggling a whole stage */ allowBulkEdit?: boolean; hideBanned?: boolean; }; export function MapPoolSelector({ mapPool, preselectedMapPool, handleMapPoolChange, handleRemoval, className, title, modesToInclude, info, footer, allowBulkEdit = false, hideBanned = false, }: MapPoolSelectorProps) { const { t } = useTranslation(); const [template, setTemplate] = React.useState( detectTemplate(mapPool), ); const handleStageModesChange = (newMapPool: MapPool) => { setTemplate(detectTemplate(newMapPool)); handleMapPoolChange(newMapPool); }; const handleClear = () => { setTemplate("none"); handleMapPoolChange(MapPool.EMPTY); }; const handleTemplateChange = (template: MapPoolTemplateValue) => { setTemplate(template); if (template === "none") { return; } if (startsWith(template, "preset:")) { const [, presetId] = split(template, ":"); handleMapPoolChange(MapPool[presetId]); return; } assertType(); }; return (
{Boolean(title) && {title}} {Boolean(handleRemoval || allowBulkEdit) && (
{handleRemoval && ( {t("actions.remove")} )} {allowBulkEdit && ( {t("actions.clear")} )}
)}
{allowBulkEdit && (
)} {info} {footer}
); } export type MapPoolStagesProps = { mapPool: MapPool; handleMapPoolChange?: (newMapPool: MapPool) => void; allowBulkEdit?: boolean; modesToInclude?: ModeShort[]; preselectedMapPool?: MapPool; hideBanned?: boolean; }; export function MapPoolStages({ mapPool, handleMapPoolChange, allowBulkEdit = false, modesToInclude, preselectedMapPool, hideBanned = false, }: MapPoolStagesProps) { const { t } = useTranslation(["game-misc", "common"]); const isPresentational = !handleMapPoolChange; const stageRowIsVisible = (stageId: StageId) => { if (!isPresentational) return true; return mapPool.hasStage(stageId); }; const handleModeChange = ({ mode, stageId, }: { mode: ModeShort; stageId: StageId; }) => { const newMapPool = mapPool.parsed[mode].includes(stageId) ? new MapPool({ ...mapPool.parsed, [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), }) : new MapPool({ ...mapPool.parsed, [mode]: [...mapPool.parsed[mode], stageId], }); handleMapPoolChange?.(newMapPool); }; const handleStageClear = (stageId: StageId) => { const newMapPool = new MapPool({ TW: mapPool.parsed.TW.filter((id) => id !== stageId), SZ: mapPool.parsed.SZ.filter((id) => id !== stageId), TC: mapPool.parsed.TC.filter((id) => id !== stageId), RM: mapPool.parsed.RM.filter((id) => id !== stageId), CB: mapPool.parsed.CB.filter((id) => id !== stageId), }); handleMapPoolChange?.(newMapPool); }; const handleStageAdd = (stageId: StageId) => { const newMapPool = new MapPool({ TW: [...mapPool.parsed.TW, stageId], SZ: [...mapPool.parsed.SZ, stageId], TC: [...mapPool.parsed.TC, stageId], RM: [...mapPool.parsed.RM, stageId], CB: [...mapPool.parsed.CB, stageId], }); handleMapPoolChange?.(newMapPool); }; const id = React.useId(); return (
{stageIds.filter(stageRowIsVisible).map((stageId) => (
{/** biome-ignore lint/a11y/useSemanticElements: todo */}
{t(`game-misc:STAGE_${stageId}`)}
{modesShort .filter( (mode) => !modesToInclude || modesToInclude.includes(mode), ) .map((mode) => { const selected = mapPool.has({ stageId, mode }); if (isPresentational && !selected) return null; if (isPresentational && selected) { return ( {t(`game-misc:MODE_LONG_${mode}`)} ); } const preselected = preselectedMapPool?.has({ stageId, mode, }); return ( ); })} {!isPresentational && allowBulkEdit && (mapPool.hasStage(stageId) ? ( handleStageClear(stageId)} icon={} variant="minimal" aria-label={t("common:actions.remove")} size="small" /> ) : ( handleStageAdd(stageId)} icon={ } variant="minimal" aria-label={t("common:actions.selectAll")} size="small" /> ))}
))}
); } type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort]; type MapPoolTemplateValue = "none" | `preset:${MapModePresetId}`; function detectTemplate(mapPool: MapPool): MapPoolTemplateValue { for (const presetId of presetIds) { if (MapPool[presetId].serialized === mapPool.serialized) { return `preset:${presetId}`; } } return "none"; } type MapPoolTemplateSelectProps = { value: MapPoolTemplateValue; handleChange: (newValue: MapPoolTemplateValue) => void; recentEvents?: Pick[]; }; function MapPoolTemplateSelect({ handleChange, value, recentEvents, }: MapPoolTemplateSelectProps) { const { t } = useTranslation(["game-misc", "common"]); return ( ); }