diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index b10ff54..61ec7b2 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -44,12 +44,12 @@ This document describes the code that exists today in the repository. ## Editor System - `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`. -- `src/features/editor/components/EditorControls.tsx` renders the HTML editor control panel. -- `src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering. -- `src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls. -- `src/features/editor/controls/FlyController.tsx` provides player-style editor navigation. -- `src/features/editor/hooks/useEditorSceneData.ts` loads scene data and handles folder upload fallback. -- `src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo state. +- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel. +- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering. +- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls. +- `src/controls/editor/FlyController.tsx` provides player-style editor navigation. +- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback. +- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state. - `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing. - `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs. - `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types. @@ -63,9 +63,9 @@ This document describes the code that exists today in the repository. ## Current Limitations -- The repository is still a prototype, not the full intended game runtime. -- `src/world/debug/TestScene.tsx` is still part of the active scene composition. -- There is no central gameplay orchestrator such as `GameManager` yet. +- The repository is a prototype, not the full intended game runtime. +- `src/world/debug/TestScene.tsx` is part of the active scene composition. +- There is no central gameplay orchestrator such as `GameManager`. - Missions, zones, cinematics, and dialogue systems are not implemented. - The player uses octree collision and simple movement rules, not a complete gameplay physics stack. - Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API. diff --git a/docs/technical/editor.md b/docs/technical/editor.md index 9839d8f..42e2f60 100644 --- a/docs/technical/editor.md +++ b/docs/technical/editor.md @@ -20,18 +20,19 @@ src/ ├── pages/ │ └── editor/ │ └── EditorPage.tsx -├── features/ +├── components/ │ └── editor/ -│ ├── components/ -│ │ └── EditorControls.tsx -│ ├── controls/ -│ │ └── FlyController.tsx -│ ├── hooks/ -│ │ ├── useEditorHistory.ts -│ │ └── useEditorSceneData.ts -│ ├── scene/ -│ │ ├── EditorMap.tsx -│ │ └── EditorScene.tsx +│ ├── EditorControls.tsx +│ └── scene/ +│ ├── EditorMap.tsx +│ └── EditorScene.tsx +├── controls/ +│ └── editor/ +│ └── FlyController.tsx +├── hooks/ +│ └── editor/ +│ ├── useEditorHistory.ts +│ └── useEditorSceneData.ts ├── types/ │ └── editor.ts └── utils/ @@ -44,17 +45,17 @@ src/ `src/pages/editor/EditorPage.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle. -`src/features/editor/hooks/useEditorSceneData.ts` loads the default map data and handles folder uploads. +`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads. -`src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo history. +`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history. -`src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`. +`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`. -`src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls. +`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls. -`src/features/editor/components/EditorControls.tsx` renders the HTML control panel outside the canvas. +`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas. -`src/features/editor/controls/FlyController.tsx` provides editor movement controls for player-style navigation. +`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation. `src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files. @@ -138,6 +139,6 @@ Editor styles are in `src/index.css` under the `/* Editor page */` section. Clas ## Known Limitations - Uploaded model object URLs are not currently revoked after replacement or unmount. -- Large `map.json` files may need virtualization, culling, or LOD support later. -- There is no snap-to-grid, duplication, material editing, or object creation workflow yet. +- Large `map.json` files are not virtualized, culled, or LOD-managed. +- There is no snap-to-grid, duplication, material editing, or object creation workflow. - Save to Server is a Vite dev-server helper, not a production backend API. diff --git a/docs/technical/target-architecture.md b/docs/technical/target-architecture.md index 9ad9c0e..80efa9f 100644 --- a/docs/technical/target-architecture.md +++ b/docs/technical/target-architecture.md @@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project. ## Relationship To The Current Code - `docs/technical/architecture.md` is the source of truth for what exists now. -- This document is intentionally aspirational. +- This document describes intended direction, not implemented behavior. - If this document conflicts with the current implementation, the current implementation wins. ## Goals @@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project. - performance overlay - scene helpers - free camera and calibration controls - - temporary test scenes used during development + - debug test scenes used during development ### UI Layer - `src/components/ui/` should contain player-facing HTML overlays. -- Expected future examples: +- Candidate examples: - crosshair - loading flow - mission HUD @@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project. ### Gameplay Layer - As the project grows, gameplay state can move toward a clearer orchestration layer. -- Likely future concerns: +- Likely concerns: - missions - zones - cinematics @@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project. - Prefer direct, working code over speculative scaffolding. - Shared types should stay close to their domain until they have multiple real consumers. - Avoid creating new managers or service layers without an active runtime need. -- Debug-only runtime paths should be clearly marked and easy to remove later. +- Debug-only runtime paths should be clearly marked and easy to remove when obsolete. diff --git a/docs/user/editor.md b/docs/user/editor.md index c68b20b..1c92b74 100644 --- a/docs/user/editor.md +++ b/docs/user/editor.md @@ -72,12 +72,12 @@ This is useful for checking numeric transform values before saving or exporting. `Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint. -The button is hidden in production builds because production persistence is not implemented yet. +The button is hidden in production builds because production persistence is not implemented. ## Current Limitations - The editor only modifies existing nodes. -- It does not create or delete objects yet. +- It does not create or delete objects. - It does not edit model files or textures. - It does not provide production persistence. - Fallback cubes indicate missing models; they are editor placeholders, not exported assets. diff --git a/src/components/3d/InteractableObject.tsx b/src/components/3d/InteractableObject.tsx index bcd31ed..e69bdc3 100644 --- a/src/components/3d/InteractableObject.tsx +++ b/src/components/3d/InteractableObject.tsx @@ -14,7 +14,7 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { InteractionManager } from "@/stateManager/InteractionManager"; import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig"; import type { Vector3Tuple } from "@/types/3d"; -import type { InteractableHandle, InteractableKind } from "@/types/interaction"; +import type { InteractableHandle } from "@/types/interaction"; interface InteractableObjectBaseProps { label: string; @@ -37,46 +37,67 @@ type InteractableObjectProps = | TriggerInteractableObjectProps | GrabInteractableObjectProps; -type MutableInteractableHandle = { - kind: InteractableKind; - label: string; - onPress: () => void; - onRelease?: () => void; -}; - const _cameraPos = new THREE.Vector3(); const _cameraDir = new THREE.Vector3(); const _objectPos = new THREE.Vector3(); const _raycaster = new THREE.Raycaster(); +function createInteractableHandle( + props: InteractableObjectProps, +): InteractableHandle { + if (props.kind === "grab") { + return { + kind: props.kind, + label: props.label, + onPress: props.onPress, + onRelease: props.onRelease, + }; + } + + return { + kind: props.kind, + label: props.label, + onPress: props.onPress, + }; +} + export function InteractableObject( props: InteractableObjectProps, ): React.JSX.Element { const { kind, label, position, bodyRef, onPress, children } = props; - const onRelease = props.kind === "grab" ? props.onRelease : undefined; + const onRelease = props.kind === "grab" ? props.onRelease : null; const camera = useThree((state) => state.camera); const groupRef = useRef(null); const debugSphereRef = useRef(null); - const handle = useRef( - props.kind === "grab" - ? { kind: props.kind, label, onPress, onRelease: props.onRelease } - : { kind: props.kind, label, onPress }, - ); + const handle = useRef(createInteractableHandle(props)); useEffect(() => { - const current = handle.current as MutableInteractableHandle; - current.kind = kind; - current.label = label; - current.onPress = onPress; + const currentHandle = handle.current; + + if (currentHandle.kind === kind) { + currentHandle.label = label; + currentHandle.onPress = onPress; + + if (currentHandle.kind === "grab") { + if (!onRelease) return; + currentHandle.onRelease = onRelease; + } - if (kind === "grab" && onRelease) { - current.onRelease = onRelease; return; } - delete current.onRelease; - return undefined; + if (kind === "grab") { + if (!onRelease) return; + handle.current = { kind, label, onPress, onRelease }; + } else { + handle.current = { kind, label, onPress }; + } + + const manager = InteractionManager.getInstance(); + if (manager.getState().focused === currentHandle) { + manager.setFocused(handle.current); + } }, [kind, label, onPress, onRelease]); const setupInteractionDebugFolder = useCallback((folder: GUI) => { diff --git a/src/components/editor/EditorControls.tsx b/src/components/editor/EditorControls.tsx index f3db353..e68cd70 100644 --- a/src/components/editor/EditorControls.tsx +++ b/src/components/editor/EditorControls.tsx @@ -31,6 +31,20 @@ interface EditorControlsProps { isPlayerMode?: boolean; } +const TRANSFORM_OPTIONS = [ + { mode: "translate", label: "Translate", shortcut: "T", Icon: Move3D }, + { mode: "rotate", label: "Rotate", shortcut: "R", Icon: RotateCw }, + { mode: "scale", label: "Scale", shortcut: "S", Icon: Expand }, +] as const; + +const EDITOR_SHORTCUTS = [ + ["Click", "Select object"], + ["T / R / S", "Transform mode"], + ["Ctrl Z / Y", "Undo / redo"], + ["Esc", "Deselect"], + ["WASD", "Move when locked"], +] as const; + export function EditorControls({ transformMode, onTransformModeChange, @@ -69,33 +83,18 @@ export function EditorControls({
- - - + {TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => ( + + ))}
@@ -203,26 +202,12 @@ export function EditorControls({
-
-
Click
-
Select object
-
-
-
T / R / S
-
Transform mode
-
-
-
Ctrl Z / Y
-
Undo / redo
-
-
-
Esc
-
Deselect
-
-
-
WASD
-
Move when locked
-
+ {EDITOR_SHORTCUTS.map(([keys, description]) => ( +
+
{keys}
+
{description}
+
+ ))}
diff --git a/src/components/editor/scene/EditorMap.tsx b/src/components/editor/scene/EditorMap.tsx index 53cb125..8b94718 100644 --- a/src/components/editor/scene/EditorMap.tsx +++ b/src/components/editor/scene/EditorMap.tsx @@ -29,6 +29,12 @@ interface EditorNodeCommonProps { onHoverNode: (index: number | null) => void; } +interface EditorNodePointerHandlers { + onClick: (event: ThreeEvent) => void; + onPointerEnter: (event: ThreeEvent) => void; + onPointerLeave: (event: ThreeEvent) => void; +} + function applyNodeTransform(object: THREE.Object3D, node: MapNode): void { object.position.set(...node.position); object.rotation.set(...node.rotation); @@ -88,6 +94,36 @@ function cloneHighlightedMaterial( return clone; } +function getNodeHighlightColor( + isSelected: boolean, + isHovered: boolean, +): string | null { + if (isSelected) return "#ffffff"; + if (isHovered) return "#b8b8b8"; + return null; +} + +function createEditorNodePointerHandlers( + index: number, + onSelectNode: (index: number | null) => void, + onHoverNode: (index: number | null) => void, +): EditorNodePointerHandlers { + return { + onClick: (event) => { + event.stopPropagation(); + onSelectNode(index); + }, + onPointerEnter: (event) => { + event.stopPropagation(); + onHoverNode(index); + }, + onPointerLeave: (event) => { + event.stopPropagation(); + onHoverNode(null); + }, + }; +} + export function EditorMap({ sceneData, selectedNodeIndex, @@ -224,15 +260,16 @@ function EditorModelNode({ const { scene } = useGLTF(modelUrl); const sceneInstance = useMemo(() => scene.clone(true), [scene]); + const pointerHandlers = createEditorNodePointerHandlers( + index, + onSelectNode, + onHoverNode, + ); useRegisteredEditorNode(groupRef, index, node, objectsMapRef); useEffect(() => { if (!groupRef.current) return; - const highlightColor = isSelected - ? "#ffffff" - : isHovered - ? "#b8b8b8" - : null; + const highlightColor = getNodeHighlightColor(isSelected, isHovered); groupRef.current.traverse((child) => { if (!(child instanceof THREE.Mesh)) { @@ -288,18 +325,7 @@ function EditorModelNode({ position={node.position} rotation={node.rotation} scale={node.scale} - onClick={(e: ThreeEvent) => { - e.stopPropagation(); - onSelectNode(index); - }} - onPointerEnter={(e: ThreeEvent) => { - e.stopPropagation(); - onHoverNode(index); - }} - onPointerLeave={(e: ThreeEvent) => { - e.stopPropagation(); - onHoverNode(null); - }} + {...pointerHandlers} /> ); } @@ -314,9 +340,14 @@ function EditorFallbackNode({ onHoverNode, }: EditorNodeCommonProps) { const meshRef = useRef(null); + const pointerHandlers = createEditorNodePointerHandlers( + index, + onSelectNode, + onHoverNode, + ); useRegisteredEditorNode(meshRef, index, node, objectsMapRef); - const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f"; + const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f"; return ( ) => { - e.stopPropagation(); - onSelectNode(index); - }} - onPointerEnter={(e: ThreeEvent) => { - e.stopPropagation(); - onHoverNode(index); - }} - onPointerLeave={(e: ThreeEvent) => { - e.stopPropagation(); - onHoverNode(null); - }} + {...pointerHandlers} > diff --git a/src/contexts/docs/DocsLanguageContext.ts b/src/contexts/docs/DocsLanguageContext.ts index 295bb7f..60e6865 100644 --- a/src/contexts/docs/DocsLanguageContext.ts +++ b/src/contexts/docs/DocsLanguageContext.ts @@ -2,7 +2,7 @@ import { createContext } from "react"; export type DocsLanguage = "en" | "fr"; -export interface DocsLanguageContextValue { +interface DocsLanguageContextValue { language: DocsLanguage; toggleLanguage: () => void; } diff --git a/src/data/debug/debugConfig.ts b/src/data/debug/debugConfig.ts index 6f5e4fc..618ddc6 100644 --- a/src/data/debug/debugConfig.ts +++ b/src/data/debug/debugConfig.ts @@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16; export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15"; export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; -export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88; - export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05; export const DEBUG_CAMERA_MIN_DISTANCE = 100; export const DEBUG_CAMERA_MAX_DISTANCE = 1000; diff --git a/src/data/docs/docsSections.ts b/src/data/docs/docsSections.ts index ba46778..2fd4f9e 100644 --- a/src/data/docs/docsSections.ts +++ b/src/data/docs/docsSections.ts @@ -1,11 +1,11 @@ -export interface DocSection { +interface DocSection { path: string; title: string; subtitle: string; meta: string; } -export interface DocGroup { +interface DocGroup { label: string; sections: DocSection[]; } diff --git a/src/pages/editor/EditorPage.tsx b/src/pages/editor/EditorPage.tsx index 45666f6..e06cc51 100644 --- a/src/pages/editor/EditorPage.tsx +++ b/src/pages/editor/EditorPage.tsx @@ -4,7 +4,13 @@ import { EditorControls } from "@/components/editor/EditorControls"; import { EditorScene } from "@/components/editor/scene/EditorScene"; import { useEditorHistory } from "@/hooks/editor/useEditorHistory"; import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData"; -import type { MapNode, TransformMode } from "@/types/editor"; +import type { MapNode, SceneData, TransformMode } from "@/types/editor"; + +const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement"; + +function serializeMapNodes(sceneData: SceneData): string { + return JSON.stringify(sceneData.mapNodes, null, 2); +} export function EditorPage(): React.JSX.Element { const { @@ -46,7 +52,7 @@ export function EditorPage(): React.JSX.Element { const handleSaveToServer = useCallback(async () => { if (!sceneData) return; - const json = JSON.stringify(sceneData.mapNodes, null, 2); + const json = serializeMapNodes(sceneData); try { const response = await fetch("/api/save-map", { @@ -58,17 +64,17 @@ export function EditorPage(): React.JSX.Element { if (response.ok) { alert("Map enregistrée avec succès!"); } else { - alert("Erreur lors de l'enregistrement"); + alert(SAVE_ERROR_MESSAGE); } } catch (err) { console.error("Error saving map:", err); - alert("Erreur lors de l'enregistrement"); + alert(SAVE_ERROR_MESSAGE); } }, [sceneData]); const handleExportJson = useCallback(() => { if (!sceneData) return; - const json = JSON.stringify(sceneData.mapNodes, null, 2); + const json = serializeMapNodes(sceneData); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); diff --git a/src/utils/debug/scene/DebugCameraControls.tsx b/src/utils/debug/scene/DebugCameraControls.tsx index 0a0c9d8..8e4234c 100644 --- a/src/utils/debug/scene/DebugCameraControls.tsx +++ b/src/utils/debug/scene/DebugCameraControls.tsx @@ -8,12 +8,13 @@ import { PLAYER_EYE_HEIGHT, PLAYER_SPAWN_POSITION_GAME, } from "@/data/player/playerConfig"; +import type { Vector3Tuple } from "@/types/3d"; -const DEBUG_CAMERA_TARGET = [ +const DEBUG_CAMERA_TARGET: Vector3Tuple = [ PLAYER_SPAWN_POSITION_GAME[0], PLAYER_EYE_HEIGHT, PLAYER_SPAWN_POSITION_GAME[2], -] as const; +]; export function DebugCameraControls(): React.JSX.Element { return ( diff --git a/src/utils/loadMapSceneData.ts b/src/utils/loadMapSceneData.ts index 9ba2f8f..2f8f4f0 100644 --- a/src/utils/loadMapSceneData.ts +++ b/src/utils/loadMapSceneData.ts @@ -2,6 +2,7 @@ import type { MapNode, SceneData } from "@/types/editor"; const MAP_JSON_PATH = "/map.json"; const MODEL_FILE_NAME = "model.gltf"; +type ModelEntry = [modelName: string, modelUrl: string]; export async function loadMapSceneData(): Promise { const response = await fetch(MAP_JSON_PATH); @@ -29,7 +30,8 @@ async function loadMapModelUrls( try { const response = await fetch(modelUrl, { method: "HEAD" }); - return response.ok ? ([modelName, modelUrl] as const) : null; + const modelEntry: ModelEntry = [modelName, modelUrl]; + return response.ok ? modelEntry : null; } catch { return null; } diff --git a/src/world/Map.tsx b/src/world/Map.tsx deleted file mode 100644 index 532a780..0000000 --- a/src/world/Map.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useThree } from "@react-three/fiber"; -import { useGLTF } from "@react-three/drei"; -import * as THREE from "three"; -import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debug/debugConfig"; -import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode"; -import type { OctreeReadyHandler } from "@/types/3d"; -import { Debug } from "@/utils/debug/Debug"; - -const MAP_PATH = "/models/map/model.gltf"; - -interface MapProps { - onOctreeReady: OctreeReadyHandler; -} - -export function Map({ onOctreeReady }: MapProps): React.JSX.Element { - const { scene: gltfScene } = useGLTF(MAP_PATH); - const groupRef = useRef(null); - const boxHelpersRef = useRef([]); - const { scene } = useThree(); - - useOctreeGraphNode(groupRef, onOctreeReady); - - useEffect(() => { - const debug = Debug.getInstance(); - if (!debug.active || !groupRef.current) return; - - const helpers: THREE.BoxHelper[] = []; - - groupRef.current.traverse((child) => { - if (!(child instanceof THREE.Mesh)) return; - const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR); - scene.add(helper); - helpers.push(helper); - }); - - boxHelpersRef.current = helpers; - - return () => { - helpers.forEach((h) => { - scene.remove(h); - h.dispose(); - }); - boxHelpersRef.current = []; - }; - }, [scene]); - - return ( - - - - ); -} - -useGLTF.preload(MAP_PATH); diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index 0158143..67aedd8 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -54,6 +54,25 @@ const _up = new THREE.Vector3(0, 1, 0); const _translateVec = new THREE.Vector3(); const _collisionCorrection = new THREE.Vector3(); +function setMovementKey(keys: Keys, key: string, pressed: boolean): boolean { + switch (key.toLowerCase()) { + case MOVE_FORWARD_KEY: + keys.forward = pressed; + return true; + case MOVE_BACKWARD_KEY: + keys.backward = pressed; + return true; + case MOVE_LEFT_KEY: + keys.left = pressed; + return true; + case MOVE_RIGHT_KEY: + keys.right = pressed; + return true; + default: + return false; + } +} + export function PlayerController({ octree, spawnPosition, @@ -89,51 +108,29 @@ export function PlayerController({ const interaction = InteractionManager.getInstance(); const handleKeyDown = (event: KeyboardEvent): void => { - switch (event.key.toLowerCase()) { - case MOVE_FORWARD_KEY: - keys.current.forward = true; - break; - case MOVE_BACKWARD_KEY: - keys.current.backward = true; - break; - case MOVE_LEFT_KEY: - keys.current.left = true; - break; - case MOVE_RIGHT_KEY: - keys.current.right = true; - break; - case JUMP_KEY: - wantsJump.current = true; - break; - case INTERACT_KEY: - if (interaction.getState().focused?.kind === "trigger") { - interaction.pressInteract(); - } - break; - default: - return; + if (setMovementKey(keys.current, event.key, true)) { + event.preventDefault(); + return; + } + + if (event.key === JUMP_KEY) { + wantsJump.current = true; + event.preventDefault(); + return; + } + + if (event.key.toLowerCase() === INTERACT_KEY) { + if (interaction.getState().focused?.kind === "trigger") { + interaction.pressInteract(); + } + event.preventDefault(); } - event.preventDefault(); }; const handleKeyUp = (event: KeyboardEvent): void => { - switch (event.key.toLowerCase()) { - case MOVE_FORWARD_KEY: - keys.current.forward = false; - break; - case MOVE_BACKWARD_KEY: - keys.current.backward = false; - break; - case MOVE_LEFT_KEY: - keys.current.left = false; - break; - case MOVE_RIGHT_KEY: - keys.current.right = false; - break; - default: - return; + if (setMovementKey(keys.current, event.key, false)) { + event.preventDefault(); } - event.preventDefault(); }; const handleMouseDown = (event: MouseEvent): void => {