diff --git a/src/app/App.tsx b/src/app/App.tsx index c3958767..f9a7d689 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -190,6 +190,9 @@ import { DEFAULT_SOUND_EMITTER_MAX_DISTANCE, DEFAULT_TELEPORT_TARGET_YAW_DEGREES, PLAYER_START_COLLIDER_MODES, + PLAYER_START_GAMEPAD_BINDINGS, + PLAYER_START_KEYBOARD_BINDING_CODES, + PLAYER_START_MOVEMENT_ACTIONS, PLAYER_START_NAVIGATION_MODES, DEFAULT_SPOT_LIGHT_ANGLE_DEGREES, DEFAULT_SPOT_LIGHT_COLOR_HEX, @@ -198,8 +201,10 @@ import { DEFAULT_SPOT_LIGHT_INTENSITY, DEFAULT_TRIGGER_VOLUME_SIZE, areEntityInstancesEqual, + clonePlayerStartInputBindings, createInteractableEntity, createPointLightEntity, + createPlayerStartInputBindings, createPlayerStartEntity, createSceneEntryEntity, createSceneExitEntity, @@ -214,6 +219,10 @@ import { normalizeYawDegrees, normalizeInteractablePrompt, type PlayerStartColliderMode, + type PlayerStartGamepadBinding, + type PlayerStartInputBindings, + type PlayerStartKeyboardBindingCode, + type PlayerStartMovementAction, type PlayerStartNavigationMode, type EntityInstance, type EntityKind @@ -333,6 +342,75 @@ function getPlayerStartColliderModeDescription( } } +function getPlayerStartMovementActionLabel( + action: PlayerStartMovementAction +): string { + switch (action) { + case "moveForward": + return "Forward"; + case "moveBackward": + return "Backward"; + case "moveLeft": + return "Left"; + case "moveRight": + return "Right"; + } +} + +function formatPlayerStartKeyboardBindingLabel( + code: PlayerStartKeyboardBindingCode +): string { + switch (code) { + case "KeyW": + return "W"; + case "KeyA": + return "A"; + case "KeyS": + return "S"; + case "KeyD": + return "D"; + case "ArrowUp": + return "Arrow Up"; + case "ArrowLeft": + return "Arrow Left"; + case "ArrowDown": + return "Arrow Down"; + case "ArrowRight": + return "Arrow Right"; + case "KeyI": + return "I"; + case "KeyJ": + return "J"; + case "KeyK": + return "K"; + case "KeyL": + return "L"; + } +} + +function formatPlayerStartGamepadBindingLabel( + binding: PlayerStartGamepadBinding +): string { + switch (binding) { + case "leftStickUp": + return "Left Stick Up"; + case "leftStickDown": + return "Left Stick Down"; + case "leftStickLeft": + return "Left Stick Left"; + case "leftStickRight": + return "Left Stick Right"; + case "dpadUp": + return "D-Pad Up"; + case "dpadDown": + return "D-Pad Down"; + case "dpadLeft": + return "D-Pad Left"; + case "dpadRight": + return "D-Pad Right"; + } +} + const STARTER_MATERIAL_ORDER = new Map( STARTER_MATERIAL_LIBRARY.map((material, index) => [material.id, index]) ); @@ -1366,6 +1444,8 @@ export function App({ store, initialStatusMessage }: AppProps) { const [playerStartBoxSizeDraft, setPlayerStartBoxSizeDraft] = useState( createVec3Draft(DEFAULT_PLAYER_START_BOX_SIZE) ); + const [playerStartInputBindingsDraft, setPlayerStartInputBindingsDraft] = + useState(createPlayerStartInputBindings()); const [soundEmitterAudioAssetIdDraft, setSoundEmitterAudioAssetIdDraft] = useState(DEFAULT_SOUND_EMITTER_AUDIO_ASSET_ID ?? ""); const [soundEmitterVolumeDraft, setSoundEmitterVolumeDraft] = useState( @@ -1766,6 +1846,7 @@ export function App({ store, initialStatusMessage }: AppProps) { setPlayerStartBoxSizeDraft( createVec3Draft(DEFAULT_PLAYER_START_BOX_SIZE) ); + setPlayerStartInputBindingsDraft(createPlayerStartInputBindings()); setSoundEmitterAudioAssetIdDraft( DEFAULT_SOUND_EMITTER_AUDIO_ASSET_ID ?? "" ); @@ -1824,6 +1905,9 @@ export function App({ store, initialStatusMessage }: AppProps) { setPlayerStartBoxSizeDraft( createVec3Draft(selectedEntity.collider.boxSize) ); + setPlayerStartInputBindingsDraft( + clonePlayerStartInputBindings(selectedEntity.inputBindings) + ); break; case "sceneEntry": setSceneEntryYawDraft(String(selectedEntity.yawDegrees)); @@ -3835,6 +3919,7 @@ export function App({ store, initialStatusMessage }: AppProps) { overrides: { colliderMode?: PlayerStartColliderMode; navigationMode?: PlayerStartNavigationMode; + inputBindings?: PlayerStartInputBindings; } = {} ) => { if (selectedPlayerStart === null) { @@ -3852,12 +3937,15 @@ export function App({ store, initialStatusMessage }: AppProps) { overrides.navigationMode ?? playerStartNavigationModeDraft; const colliderMode = overrides.colliderMode ?? playerStartColliderModeDraft; + const inputBindings = + overrides.inputBindings ?? playerStartInputBindingsDraft; const nextEntity = createPlayerStartEntity({ id: selectedPlayerStart.id, name: selectedPlayerStart.name, position: snappedPosition, yawDegrees, navigationMode, + inputBindings, collider: { mode: colliderMode, eyeHeight: readPositiveNumberDraft( @@ -3889,6 +3977,46 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; + const handlePlayerStartKeyboardBindingChange = ( + action: PlayerStartMovementAction, + nextCode: PlayerStartKeyboardBindingCode + ) => { + const nextBindings = createPlayerStartInputBindings({ + keyboard: { + ...playerStartInputBindingsDraft.keyboard, + [action]: nextCode + } as PlayerStartInputBindings["keyboard"], + gamepad: playerStartInputBindingsDraft.gamepad + }); + + setPlayerStartInputBindingsDraft(nextBindings); + scheduleDraftCommit(() => + applyPlayerStartChange({ + inputBindings: nextBindings + }) + ); + }; + + const handlePlayerStartGamepadBindingChange = ( + action: PlayerStartMovementAction, + nextBinding: PlayerStartGamepadBinding + ) => { + const nextBindings = createPlayerStartInputBindings({ + keyboard: playerStartInputBindingsDraft.keyboard, + gamepad: { + ...playerStartInputBindingsDraft.gamepad, + [action]: nextBinding + } as PlayerStartInputBindings["gamepad"] + }); + + setPlayerStartInputBindingsDraft(nextBindings); + scheduleDraftCommit(() => + applyPlayerStartChange({ + inputBindings: nextBindings + }) + ); + }; + const applySceneEntryChange = () => { if (selectedSceneEntry === null) { setStatusMessage("Select a Scene Entry before editing it."); @@ -9968,6 +10096,68 @@ export function App({ store, initialStatusMessage }: AppProps) { )} + +
+
Input Bindings
+ {PLAYER_START_MOVEMENT_ACTIONS.map((action) => ( +
+ + +
+ ))} + +
+ These bindings feed the same typed movement actions in + First Person and Third Person. +
+
) : null}