Add input bindings configuration for player start entity

This commit is contained in:
2026-04-11 12:15:03 +02:00
parent ec1f04e5cc
commit 190c696319

View File

@@ -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<PlayerStartInputBindings>(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) {
)}
</div>
</div>
<div className="form-section">
<div className="label">Input Bindings</div>
{PLAYER_START_MOVEMENT_ACTIONS.map((action) => (
<div
key={action}
className="vector-inputs vector-inputs--two"
>
<label className="form-field">
<span className="label">
{getPlayerStartMovementActionLabel(action)} Key
</span>
<select
data-testid={`player-start-keyboard-binding-${action}`}
className="select-input"
value={playerStartInputBindingsDraft.keyboard[action]}
onChange={(event) =>
handlePlayerStartKeyboardBindingChange(
action,
event.currentTarget
.value as PlayerStartKeyboardBindingCode
)
}
>
{PLAYER_START_KEYBOARD_BINDING_CODES.map((code) => (
<option key={code} value={code}>
{formatPlayerStartKeyboardBindingLabel(code)}
</option>
))}
</select>
</label>
<label className="form-field">
<span className="label">
{getPlayerStartMovementActionLabel(action)} Pad
</span>
<select
data-testid={`player-start-gamepad-binding-${action}`}
className="select-input"
value={playerStartInputBindingsDraft.gamepad[action]}
onChange={(event) =>
handlePlayerStartGamepadBindingChange(
action,
event.currentTarget
.value as PlayerStartGamepadBinding
)
}
>
{PLAYER_START_GAMEPAD_BINDINGS.map((binding) => (
<option key={binding} value={binding}>
{formatPlayerStartGamepadBindingLabel(binding)}
</option>
))}
</select>
</label>
</div>
))}
<div className="material-summary">
These bindings feed the same typed movement actions in
First Person and Third Person.
</div>
</div>
</>
) : null}