import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react"; import { useTranslation } from "react-i18next"; import { TauriService, type GoldMapperMapping, } from "../../services/TauriService"; import { useUI, useConfig, useAudio } from "../../context/LauncherContext"; const KEY_FALLBACK = [ "KEY_A", "KEY_D", "KEY_S", "KEY_W", "KEY_SPACE", "KEY_RETURN", "KEY_ESCAPE", "KEY_LSHIFT", "KEY_LCTRL", ]; const MOUSE_IDS = ["MOUSE_LEFT", "MOUSE_MIDDLE", "MOUSE_RIGHT"]; const CONTROLLER_FALLBACK = [ "PAD_A", "PAD_B", "PAD_X", "PAD_Y", "PAD_LB", "PAD_RB", "PAD_BACK", "PAD_START", "PAD_LTHUMB", "PAD_RTHUMB", "PAD_DPAD_UP", "PAD_DPAD_DOWN", "PAD_DPAD_LEFT", "PAD_DPAD_RIGHT", ]; const DINPUT_ROWS: GoldMapperMapping[] = CONTROLLER_FALLBACK.map( (target, i) => ({ from: `DINPUT_${i}`, to: target }), ); const displayName = (id: string) => id.replace(/^(KEY|PAD)_/, "").replace(/_/g, " "); type Row = | { kind: "reset"; key: string } | { kind: "enable"; key: string } | { kind: "header"; key: string; label: string } | { kind: "bind"; key: string; id: string; label: string }; const stoneButtonStyle = (highlighted: boolean) => ({ backgroundImage: highlighted ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" as const, }); const GoldMapperView = memo(function GoldMapperView() { const { t } = useTranslation(); const { setActiveView } = useUI(); const { goldmapperEnabled, setGoldmapperEnabled } = useConfig(); const { playPressSound, playBackSound } = useAudio(); const [keyboardIds, setKeyboardIds] = useState(KEY_FALLBACK); const [controllerIds, setControllerIds] = useState(CONTROLLER_FALLBACK); const [binds, setBinds] = useState>({}); const [editing, setEditing] = useState(null); const [modalFocusIndex, setModalFocusIndex] = useState(0); const [keyInput, setKeyInput] = useState(""); const [keyInputError, setKeyInputError] = useState(null); const [focusIndex, setFocusIndex] = useState(null); const containerRef = useRef(null); useEffect(() => { TauriService.goldMapperGetDefaults() .then((defaults) => { setKeyboardIds( defaults.filter((m) => m.from.startsWith("KEY_")).map((m) => m.from), ); setControllerIds( Array.from( new Set( defaults .filter((m) => m.from.startsWith("DINPUT_")) .map((m) => m.to), ), ), ); }) .catch(console.error); TauriService.goldMapperLoadConfig() .then((rows) => { const loaded: Record = {}; for (const m of rows) { if (/^(KEY|MOUSE|PAD)_/.test(m.from)) { loaded[m.from] = m.to; } } setBinds(loaded); }) .catch(console.error); }, []); const getTo = useCallback((id: string) => binds[id] ?? id, [binds]); const mouseLabel = (id: string) => { if (id === "MOUSE_LEFT") return t("goldMapper.leftClick"); if (id === "MOUSE_MIDDLE") return t("goldMapper.middleClick"); return t("goldMapper.rightClick"); }; const buildPayload = useCallback( (nextBinds: Record): GoldMapperMapping[] => { const rows: GoldMapperMapping[] = []; for (const id of [...keyboardIds, ...MOUSE_IDS, ...controllerIds]) { const to = nextBinds[id] ?? id; if (to !== id) { rows.push({ from: id, to }); } } rows.push(...DINPUT_ROWS); return rows; }, [keyboardIds, controllerIds], ); const handleResetToDefaults = useCallback(() => { playPressSound(); setBinds({}); console.log("[GoldMapper] reset to defaults"); TauriService.goldMapperResetConfig().catch(console.error); }, [playPressSound]); const handleToggleEnabled = useCallback(() => { playPressSound(); setGoldmapperEnabled(!goldmapperEnabled); }, [playPressSound, goldmapperEnabled, setGoldmapperEnabled]); const handleBack = useCallback(() => { playBackSound(); setActiveView("settings"); }, [playBackSound, setActiveView]); const openBind = useCallback( (id: string) => { playPressSound(); setEditing(id); setModalFocusIndex(0); setKeyInput(/^KEY_/.test(getTo(id)) ? displayName(getTo(id)) : ""); setKeyInputError(null); }, [playPressSound, getTo], ); const closeModal = useCallback(() => { playBackSound(); (document.activeElement as HTMLElement | null)?.blur(); setEditing(null); }, [playBackSound]); const pickTarget = useCallback( (sourceId: string, targetId: string) => { playPressSound(); const next = { ...binds, [sourceId]: targetId }; setBinds(next); const payload = buildPayload(next); console.log( "[GoldMapper] saving config:", JSON.stringify({ mappings: payload }), ); TauriService.goldMapperSaveConfig(payload).catch(console.error); (document.activeElement as HTMLElement | null)?.blur(); setEditing(null); }, [playPressSound, binds, buildPayload], ); const submitKeyInput = useCallback(() => { if (editing === null) return; const norm = keyInput .trim() .toUpperCase() .replace(/\s+/g, "_") .replace(/^KEY_/, ""); if (!norm || !keyboardIds.includes(`KEY_${norm}`)) { setKeyInputError(t("goldMapper.unknownKeyName")); return; } pickTarget(editing, `KEY_${norm}`); }, [editing, keyInput, keyboardIds, pickTarget, t]); const rows: Row[] = useMemo(() => { const list: Row[] = [{ kind: "reset", key: "reset" }]; list.push({ kind: "enable", key: "enable" }); list.push({ kind: "header", key: "controller_header", label: t("goldMapper.controller"), }); for (const id of controllerIds) { list.push({ kind: "bind", key: `controller_${id}`, id, label: displayName(id), }); } list.push({ kind: "header", key: "mouse_header", label: t("goldMapper.mouse"), }); for (const id of MOUSE_IDS) { list.push({ kind: "bind", key: `mouse_${id}`, id, label: mouseLabel(id), }); } list.push({ kind: "header", key: "keyboard_header", label: t("goldMapper.keyboard"), }); for (const id of keyboardIds) { list.push({ kind: "bind", key: `keyboard_${id}`, id, label: displayName(id), }); } return list; }, [controllerIds, keyboardIds, t, mouseLabel]); const focusableCount = rows.filter((r) => r.kind !== "header").length; const modalItemCount = MOUSE_IDS.length + controllerIds.length + 2; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (editing !== null) { const activeTag = document.activeElement?.tagName; if (activeTag === "INPUT") { if (e.key === "Escape") { closeModal(); } return; } if (e.key === "Escape") { closeModal(); return; } if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault(); setModalFocusIndex((prev) => (prev + 1) % modalItemCount); } else if (e.key === "ArrowUp") { e.preventDefault(); setModalFocusIndex( (prev) => (prev - 1 + modalItemCount) % modalItemCount, ); } else if (e.key === "Enter") { e.preventDefault(); if (modalFocusIndex < MOUSE_IDS.length + controllerIds.length) { const allTargets = [...MOUSE_IDS, ...controllerIds]; pickTarget(editing, allTargets[modalFocusIndex]); } else if ( modalFocusIndex === MOUSE_IDS.length + controllerIds.length ) { submitKeyInput(); } else { closeModal(); } } return; } if (e.key === "Escape") { handleBack(); return; } if (e.key === "ArrowDown") { e.preventDefault(); setFocusIndex((prev) => prev === null || prev >= focusableCount - 1 ? 0 : prev + 1, ); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusIndex((prev) => prev === null || prev <= 0 ? focusableCount - 1 : prev - 1, ); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [ focusableCount, handleBack, editing, closeModal, modalItemCount, modalFocusIndex, controllerIds.length, pickTarget, submitKeyInput, ]); useEffect(() => { if (focusIndex === null || editing !== null) return; const el = containerRef.current?.querySelector( `[data-focus-index="${focusIndex}"]`, ) as HTMLElement | null; el?.focus(); }, [focusIndex, editing]); useEffect(() => { if (editing === null) return; const el = document.querySelector( `[data-modal-index="${modalFocusIndex}"]`, ) as HTMLElement | null; el?.focus(); }, [modalFocusIndex, editing]); let focusCounter = 0; const nextFocusIndex = () => { const idx = focusCounter; focusCounter += 1; return idx; }; const renderRow = (row: Row) => { if (row.kind === "header") { return (
{row.label}
); } const isReset = row.kind === "reset"; const isEnable = row.kind === "enable"; const focusIdx = nextFocusIndex(); const focused = focusIndex === focusIdx; return ( ); }; return (
{rows.map(renderRow)}
{editing !== null && (
{ if (e.target === e.currentTarget) closeModal(); }} >

{t("goldMapper.assign", { name: displayName(editing) })}

{t("goldMapper.mouse")}

{MOUSE_IDS.map((id, i) => ( ))}

{t("goldMapper.controller")}

{controllerIds.map((id, i) => { const idx = MOUSE_IDS.length + i; return ( ); })}

{t("goldMapper.keyboard")}

{ setKeyInput(e.target.value); setKeyInputError(null); }} onFocus={() => setModalFocusIndex(MOUSE_IDS.length + controllerIds.length) } onMouseEnter={() => setModalFocusIndex(MOUSE_IDS.length + controllerIds.length) } onKeyDown={(e) => { if (e.key === "Enter") submitKeyInput(); }} placeholder={t("goldMapper.typeKeyName")} className={`mc-textinput w-full h-10 px-3 text-white text-base outline-none text-center font-[var(--font-base)] ${ keyInputError ? "" : "" }`} style={{ imageRendering: "pixelated" }} />
{keyInputError && (

{keyInputError}

)}
)}
); }); export default GoldMapperView;