import { Button, Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; import { Settings } from "../../Settings/Settings"; import { getRecordKeys } from "../../Types/Record"; import { Modal } from "../../ui/React/Modal"; import { ComplexPage } from "../../ui/Enums"; import { KEY } from "../../utils/KeyboardEventKey"; import { areDifferentKeyCombinations, convertKeyboardEventToKeyCombination, CurrentKeyBindings, DefaultKeyBindings, determineKeyBindingTypes, getKeyCombination, isKeyCombinationPressed, isSpoilerKeyBindingType, KeyBindingEvents, KeyBindingEventType, mergePlayerDefinedKeyBindings, parseKeyCombinationToString, SpoilerKeyBindingTypes, type KeyBindingType, type KeyCombination, } from "../../utils/KeyBindingUtils"; import { GameOptionsPage } from "./GameOptionsPage"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { knowAboutBitverse } from "../../BitNode/BitNodeUtils"; function determineConflictKeys( keyBindingType: KeyBindingType, isPrimary: boolean, newCombination: KeyCombination, ): Set { const conflicts: Set = determineKeyBindingTypes(CurrentKeyBindings, newCombination); // Check if the new combination is the same as the current key binding. if (conflicts.has(keyBindingType)) { const currentKeyBinding = getKeyCombination(CurrentKeyBindings, keyBindingType, isPrimary); if ( currentKeyBinding && currentKeyBinding.control === newCombination.control && currentKeyBinding.alt === newCombination.alt && currentKeyBinding.shift === newCombination.shift && currentKeyBinding.meta === newCombination.meta && currentKeyBinding.key === newCombination.key ) { conflicts.delete(keyBindingType); } } // Common single-key hotkeys. if ( isKeyCombinationPressed(newCombination, { key: KEY.ESC }) || isKeyCombinationPressed(newCombination, { key: KEY.ENTER }) || isKeyCombinationPressed(newCombination, { key: KEY.TAB }) ) { conflicts.add("Common hotkeys"); } // Copy - Paste - Cut if (window.navigator.userAgent.includes("Mac")) { if ( isKeyCombinationPressed(newCombination, { meta: true, key: KEY.C }) || isKeyCombinationPressed(newCombination, { meta: true, key: KEY.V }) || isKeyCombinationPressed(newCombination, { meta: true, key: KEY.X }) ) { conflicts.add("Common hotkeys"); } } else { if ( isKeyCombinationPressed(newCombination, { control: true, key: KEY.C }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.V }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.X }) ) { conflicts.add("Common hotkeys"); } } // Terminal-ClearScreen if (isKeyCombinationPressed(newCombination, { control: true, key: KEY.L })) { conflicts.add("Terminal-ClearScreen"); } // Bash hotkeys if ( Settings.EnableBashHotkeys && (isKeyCombinationPressed(newCombination, { control: true, key: KEY.M }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.P }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.C }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.A }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.E }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.B }) || isKeyCombinationPressed(newCombination, { alt: true, key: KEY.B }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.F }) || isKeyCombinationPressed(newCombination, { alt: true, key: KEY.F }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.H }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.D }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.W }) || isKeyCombinationPressed(newCombination, { alt: true, key: KEY.D }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.U }) || isKeyCombinationPressed(newCombination, { control: true, key: KEY.K })) ) { conflicts.add("Bash hotkeys"); } // Remove spoilers in the list if (!knowAboutBitverse()) { for (const conflict of conflicts) { if (!isSpoilerKeyBindingType(conflict)) { continue; } conflicts.delete(conflict); conflicts.add("Endgame content"); } } return conflicts; } export function isCustomKeyCombination(keyBindingType: KeyBindingType, isPrimary: boolean): boolean { const slot = isPrimary ? 0 : 1; // Check if the player sets a binding. if (!Settings.KeyBindings[keyBindingType] || !Settings.KeyBindings[keyBindingType][slot]) { return false; } // If there is not a default binding, this binding is a custom one. if (!DefaultKeyBindings[keyBindingType][slot]) { return true; } return areDifferentKeyCombinations( DefaultKeyBindings[keyBindingType][slot], Settings.KeyBindings[keyBindingType][slot], ); } function SettingUpKeyBindingModal({ open, onClose, keyBindingType, isPrimary, }: { open: boolean; onClose: () => void; keyBindingType: KeyBindingType; isPrimary: boolean; }): React.ReactElement { const [combination, setCombination] = useState(getKeyCombination(CurrentKeyBindings, keyBindingType, isPrimary)); const [conflicts, setConflicts] = useState( combination ? determineConflictKeys(keyBindingType, isPrimary, combination) : new Set(), ); const handler = useCallback( (event: KeyboardEvent) => { event.preventDefault(); if (event.getModifierState(event.key)) { return; } const newCombination = convertKeyboardEventToKeyCombination(event); setCombination(newCombination); setConflicts(determineConflictKeys(keyBindingType, isPrimary, newCombination)); }, [keyBindingType, isPrimary], ); useEffect(() => { const currentKeyCombination = getKeyCombination(CurrentKeyBindings, keyBindingType, isPrimary); setCombination(currentKeyCombination); setConflicts( currentKeyCombination ? determineConflictKeys(keyBindingType, isPrimary, currentKeyCombination) : new Set(), ); /** * Add/remove handlers and emit an event that notifies subscribers if the player is setting up key bindings. When * they are doing that, we need to stop processing key events. For example, if they are setting key bindings and * they press Alt+T, we need to save that setting instead of going to the terminal. * * The action of going to a different page is handled in src\Sidebar\ui\SidebarRoot.tsx. When checking simple cases * (focusing on working, being in BitVerse, etc.), we can use the Player object and Router.page(). However, checking * if the player is setting key bindings is not easy for code in SidebarRoot, especially if we want to decouple * their logic and keep the dependency chain simple. It's best to do that by using the event system. */ if (open) { document.addEventListener("keydown", handler); KeyBindingEvents.emit(KeyBindingEventType.StartSettingUp); } else { document.removeEventListener("keydown", handler); KeyBindingEvents.emit(KeyBindingEventType.StopSettingUp); } }, [open, keyBindingType, isPrimary, handler]); const onClickClear = () => { setCombination(null); setConflicts(new Set()); }; const onClickDefault = () => { const defaultKeyCombination = getKeyCombination(DefaultKeyBindings, keyBindingType, isPrimary); setCombination(defaultKeyCombination); setConflicts( defaultKeyCombination ? determineConflictKeys(keyBindingType, isPrimary, defaultKeyCombination) : new Set(), ); }; const onClickOK = () => { if (!Settings.KeyBindings[keyBindingType]) { Settings.KeyBindings[keyBindingType] = structuredClone(DefaultKeyBindings[keyBindingType]); } Settings.KeyBindings[keyBindingType][isPrimary ? 0 : 1] = combination; // Merge Settings.KeyBindings with DefaultKeyBindings. mergePlayerDefinedKeyBindings(Settings.KeyBindings); onClose(); }; const onClickCancel = () => { onClose(); }; return (
Press the key you would like to use {parseKeyCombinationToString(combination)} {conflicts.size === 0 ? "No conflicts detected" : `Conflicts: ${[...conflicts]}`}
); } export function KeyBindingPage(): React.ReactElement { const [popupOpen, setPopupOpen] = useState(false); const [keyBindingType, setKeyBindingType] = useState(ComplexPage.Options); const [isPrimary, setIsPrimary] = useState(true); const showModal = (keyBindingType: KeyBindingType, isPrimary: boolean) => { setPopupOpen(true); setKeyBindingType(keyBindingType); setIsPrimary(isPrimary); }; const onClickHowToUse = () => { dialogBoxCreate( <> You can assign up to 2 key combinations per "action". If a key combination is assigned to many actions, pressing that key combination will perform all those actions.
Some key combinations cannot be used. Your OS and browsers usually have some built-in key bindings that cannot be overridden. For example, on Windows, Windows+R always opens the "Run" dialog.
When you set up key bindings, the list of conflicts may contain "Endgame content". It means that the key combination is currently used for features that you have not unlocked.
On non-Apple keyboards, the "Windows" key (other names: win, start, super, meta, etc.) is shown as ⊞. On Apple keyboards, the command key is shown as ⌘. , ); }; const keyBindingRows = getRecordKeys(CurrentKeyBindings) .filter( (keyBindingType) => knowAboutBitverse() || !(SpoilerKeyBindingTypes as unknown as string[]).includes(keyBindingType), ) .map((keyBindingType) => { return ( {keyBindingType} {[0, 1].map((value) => { const isPrimary = value === 0; return ( ); })} ); }); return (
{keyBindingRows}
setPopupOpen(false)} keyBindingType={keyBindingType} isPrimary={isPrimary} />
); }