Skip to content

Commit

Permalink
support customizing hotkeys (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
RheingoldRiver authored Nov 3, 2023
1 parent a51be38 commit 918df74
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 117 deletions.
147 changes: 103 additions & 44 deletions src/components/GameStateProvider/GameStateProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@ import {
RotationDirection,
orientationReducer,
} from "./currentPentominoReducer";
import { DEFAULT_GAME_PREFERENCES } from "./gameConstants";
import {
DEFAULT_GAME_PREFERENCES,
DEFAULT_HOTKEYS,
DEFAULT_HOTKEY_MAP,
HotkeyMap,
HotkeyableAction,
Hotkeys,
} from "./gameConstants";
import { produce } from "immer";
import useHotkeyMap from "../../hooks/use-hotkey-map";
import { deserializeHotkeys, serializeHotkeys } from "./hotkeyMapState";

interface GameState {
grid: PlacedPentomino[][];
Expand Down Expand Up @@ -68,6 +77,9 @@ interface GameState {
updateDefaultRandomColors: (newDefault: boolean) => void;
defaultAddTerrain: boolean;
updateDefaultAddTerrain: (newDefault: boolean) => void;
hotkeys: Hotkeys;
hotkeyMap: HotkeyMap;
updateHotkeyMap: (nextMap: HotkeyMap) => void;
}

const DEFAULT_GAME_STATE: GameState = {
Expand Down Expand Up @@ -96,6 +108,9 @@ const DEFAULT_GAME_STATE: GameState = {
updateDefaultRandomColors: () => {},
defaultAddTerrain: true,
updateDefaultAddTerrain: () => {},
hotkeys: DEFAULT_HOTKEYS,
hotkeyMap: [],
updateHotkeyMap: () => {},
};

export const GameStateContext = createContext(DEFAULT_GAME_STATE);
Expand Down Expand Up @@ -343,39 +358,6 @@ export default function GameStateProvider({ children }: { children: ReactNode })
setActionHistory(nextActionHistory);
});

useHotkey(undefined, "ArrowLeft", () => {
setShowKeyboardIndicators(true);
updateGridCoords("y", -1);
});

useHotkey(undefined, "ArrowUp", () => {
setShowKeyboardIndicators(true);
updateGridCoords("x", -1);
});

useHotkey(undefined, "ArrowRight", () => {
setShowKeyboardIndicators(true);
updateGridCoords("y", 1);
});

useHotkey(undefined, "ArrowDown", () => {
setShowKeyboardIndicators(true);
updateGridCoords("x", 1);
});

useHotkey(undefined, "A", () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Left });
});
useHotkey(undefined, "D", () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Right });
});
useHotkey(undefined, "S", () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.X });
});
useHotkey(undefined, "W", () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.Y });
});

function updateToolbarPentomino(increment: number) {
const curIndex = ALL_PENTOMINO_NAMES.indexOf(currentPentomino.name);
const nextPentomino =
Expand All @@ -388,19 +370,93 @@ export default function GameStateProvider({ children }: { children: ReactNode })
orientationDispatch({ type: OrientationActionType.replace });
}

useHotkey(undefined, "E", () => {
setShowKeyboardIndicators(true);
updateToolbarPentomino(1);
const [hotkeyMap, setHotkeyMap] = useState<HotkeyMap>(() => {
const cachedData = window.localStorage.getItem("hotkeys");
if (!cachedData) return cloneDeep(DEFAULT_HOTKEY_MAP);
return deserializeHotkeys(cachedData);
});

useHotkey(undefined, "Q", () => {
setShowKeyboardIndicators(true);
updateToolbarPentomino(-1);
});
const updateHotkeyMap = (nextMap: HotkeyMap) => {
setHotkeyMap(nextMap);
window.localStorage.setItem("hotkeys", serializeHotkeys(nextMap));
};

useHotkey(undefined, "Enter", () => {
clickBoard(currentGridCoords.x, currentGridCoords.y);
});
const hotkeys = {
[HotkeyableAction.ReflectY]: {
action: () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.Y });
},
text: "Reflect horizontally",
},
[HotkeyableAction.ReflectX]: {
action: () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.X });
},
text: "Reflect vertically",
},
[HotkeyableAction.RotateLeft]: {
action: () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Left });
},
text: "Rotate left (counter-clockwise)",
},
[HotkeyableAction.RotateRight]: {
action: () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Right });
},
text: "Rotate right (clockwise)",
},
[HotkeyableAction.TilePrev]: {
action: () => {
setShowKeyboardIndicators(true);
updateToolbarPentomino(-1);
},
text: "Select previous tile",
},
[HotkeyableAction.TileNext]: {
action: () => {
setShowKeyboardIndicators(true);
updateToolbarPentomino(1);
},
text: "Select next tile",
},
[HotkeyableAction.ClickBoard]: {
action: () => {
clickBoard(currentGridCoords.x, currentGridCoords.y);
},
text: "Place or remove tile",
},
[HotkeyableAction.GridUp]: {
action: () => {
setShowKeyboardIndicators(true);
updateGridCoords("x", -1);
},
text: "Move grid cursor up",
},
[HotkeyableAction.GridRight]: {
action: () => {
setShowKeyboardIndicators(true);
updateGridCoords("y", 1);
},
text: "Move grid cursor right",
},
[HotkeyableAction.GridDown]: {
action: () => {
setShowKeyboardIndicators(true);
updateGridCoords("x", 1);
},
text: "Move grid cursor down",
},
[HotkeyableAction.GridLeft]: {
action: () => {
setShowKeyboardIndicators(true);
updateGridCoords("y", -1);
},
text: "Move grid cursor left",
},
};

useHotkeyMap(hotkeyMap, hotkeys);

return (
<GameStateContext.Provider
Expand Down Expand Up @@ -430,6 +486,9 @@ export default function GameStateProvider({ children }: { children: ReactNode })
updateDefaultRandomColors,
defaultAddTerrain,
updateDefaultAddTerrain,
hotkeys,
hotkeyMap,
updateHotkeyMap,
}}
>
{children}
Expand Down
57 changes: 57 additions & 0 deletions src/components/GameStateProvider/gameConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,60 @@ export const DEFAULT_GAME_PREFERENCES = {
defaultRandomColors: false,
defaultAddTerrain: true,
};

export enum HotkeyableAction {
ReflectY,
ReflectX,
RotateLeft,
RotateRight,
TilePrev,
TileNext,
GridUp,
GridRight,
GridDown,
GridLeft,
ClickBoard,
}

export type Hotkeys = Record<
HotkeyableAction,
{
action: () => void;
text: string;
}
>;

export interface HotkeyMapEntry {
keybind: string;
action: HotkeyableAction;
}

export type HotkeyMap = HotkeyMapEntry[];

export const DEFAULT_HOTKEYS: Hotkeys = {
[HotkeyableAction.ReflectY]: { action: () => {}, text: "" },
[HotkeyableAction.ReflectX]: { action: () => {}, text: "" },
[HotkeyableAction.RotateLeft]: { action: () => {}, text: "" },
[HotkeyableAction.RotateRight]: { action: () => {}, text: "" },
[HotkeyableAction.TilePrev]: { action: () => {}, text: "" },
[HotkeyableAction.TileNext]: { action: () => {}, text: "" },
[HotkeyableAction.GridUp]: { action: () => {}, text: "" },
[HotkeyableAction.GridRight]: { action: () => {}, text: "" },
[HotkeyableAction.GridDown]: { action: () => {}, text: "" },
[HotkeyableAction.GridLeft]: { action: () => {}, text: "" },
[HotkeyableAction.ClickBoard]: { action: () => {}, text: "" },
};

export const DEFAULT_HOTKEY_MAP: HotkeyMap = [
{ keybind: "A", action: HotkeyableAction.RotateLeft },
{ keybind: "D", action: HotkeyableAction.RotateRight },
{ keybind: "S", action: HotkeyableAction.ReflectX },
{ keybind: "W", action: HotkeyableAction.ReflectY },
{ keybind: "ArrowUp", action: HotkeyableAction.GridUp },
{ keybind: "ArrowDown", action: HotkeyableAction.GridDown },
{ keybind: "ArrowLeft", action: HotkeyableAction.GridLeft },
{ keybind: "ArrowRight", action: HotkeyableAction.GridRight },
{ keybind: "Enter", action: HotkeyableAction.ClickBoard },
{ keybind: "Q", action: HotkeyableAction.TilePrev },
{ keybind: "E", action: HotkeyableAction.TileNext },
];
19 changes: 19 additions & 0 deletions src/components/GameStateProvider/hotkeyMapState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HotkeyMap } from "./gameConstants";

const OUTER_SEP = ";;;";
const INNER_SEP = ":::";

export const serializeHotkeys = (hotkeyMap: HotkeyMap): string => {
return hotkeyMap.map((key) => `${key.action}${INNER_SEP}${key.keybind}`).join(OUTER_SEP);
};

export const deserializeHotkeys = (val: string): HotkeyMap => {
const t = val.split(OUTER_SEP);
return t.map((s) => {
const [a, k] = s.split(INNER_SEP);
return {
action: parseInt(a),
keybind: k,
};
});
};
Loading

0 comments on commit 918df74

Please sign in to comment.