Add air movement controls for player in App.tsx and related files

This commit is contained in:
2026-04-12 02:06:00 +02:00
parent 41dd4dc434
commit 8aba9a02d7
9 changed files with 138 additions and 2 deletions

View File

@@ -10878,6 +10878,52 @@ export function App({ store, initialStatusMessage }: AppProps) {
}
/>
</label>
<label className="form-field form-field--toggle">
<span className="label">Air Move: Jump</span>
<input
data-testid="player-start-movement-air-move-jump-enabled"
type="checkbox"
checked={
playerStartMovementTemplateDraft.jump.moveWhileJumping
}
onChange={(event) =>
commitPlayerStartMovementTemplateDraft(
{
jump: {
moveWhileJumping:
event.currentTarget.checked
}
},
{
schedule: true
}
)
}
/>
</label>
<label className="form-field form-field--toggle">
<span className="label">Air Move: Fall</span>
<input
data-testid="player-start-movement-air-move-fall-enabled"
type="checkbox"
checked={
playerStartMovementTemplateDraft.jump.moveWhileFalling
}
onChange={(event) =>
commitPlayerStartMovementTemplateDraft(
{
jump: {
moveWhileFalling:
event.currentTarget.checked
}
},
{
schedule: true
}
)
}
/>
</label>
<label className="form-field form-field--toggle">
<span className="label">Bunny Hopping</span>
<input

View File

@@ -1359,6 +1359,16 @@ function readPlayerStartMovementTemplate(value: unknown, label: string) {
`${label}.jump.variableHeight`,
preset.jump.variableHeight
),
moveWhileJumping: readOptionalBoolean(
jump?.moveWhileJumping,
`${label}.jump.moveWhileJumping`,
preset.jump.moveWhileJumping
),
moveWhileFalling: readOptionalBoolean(
jump?.moveWhileFalling,
`${label}.jump.moveWhileFalling`,
preset.jump.moveWhileFalling
),
maxHoldMs:
jump?.maxHoldMs === undefined
? preset.jump.maxHoldMs

View File

@@ -1348,6 +1348,28 @@ function validatePlayerStartEntity(
);
}
if (!isBoolean(entity.movementTemplate?.jump?.moveWhileJumping)) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-player-start-move-while-jumping",
"Player Start move while jumping setting must be a boolean.",
`${path}.movementTemplate.jump.moveWhileJumping`
)
);
}
if (!isBoolean(entity.movementTemplate?.jump?.moveWhileFalling)) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-player-start-move-while-falling",
"Player Start move while falling setting must be a boolean.",
`${path}.movementTemplate.jump.moveWhileFalling`
)
);
}
if (!isBoolean(entity.movementTemplate?.jump?.bunnyHop)) {
diagnostics.push(
createDiagnostic(

View File

@@ -15,7 +15,8 @@ import {
type WorldSettings
} from "./world-settings";
export const SCENE_DOCUMENT_VERSION = 33 as const;
export const SCENE_DOCUMENT_VERSION = 34 as const;
export const PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION = 34 as const;
export const WHITEBOX_BEVEL_SCENE_DOCUMENT_VERSION = 32 as const;
export const PLAYER_START_LOCOMOTION_CORE_SCENE_DOCUMENT_VERSION = 32 as const;
export const PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION =

View File

@@ -147,6 +147,8 @@ export interface PlayerStartJumpSettings {
coyoteTimeMs: number;
variableHeight: boolean;
maxHoldMs: number;
moveWhileJumping: boolean;
moveWhileFalling: boolean;
bunnyHop: boolean;
bunnyHopBoost: number;
}
@@ -301,6 +303,8 @@ export const DEFAULT_PLAYER_START_JUMP_BUFFER_MS = 0;
export const DEFAULT_PLAYER_START_COYOTE_TIME_MS = 0;
export const DEFAULT_PLAYER_START_VARIABLE_JUMP_HEIGHT = false;
export const DEFAULT_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS = 160;
export const DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING = true;
export const DEFAULT_PLAYER_START_MOVE_WHILE_FALLING = true;
export const DEFAULT_PLAYER_START_BUNNY_HOP = false;
export const DEFAULT_PLAYER_START_BUNNY_HOP_BOOST = 0.05;
export const DEFAULT_PLAYER_START_SPRINT_SPEED_MULTIPLIER = 1.65;
@@ -317,6 +321,8 @@ export const DEFAULT_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
coyoteTimeMs: DEFAULT_PLAYER_START_COYOTE_TIME_MS,
variableHeight: DEFAULT_PLAYER_START_VARIABLE_JUMP_HEIGHT,
maxHoldMs: DEFAULT_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
};
@@ -335,6 +341,8 @@ export const RESPONSIVE_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
coyoteTimeMs: RESPONSIVE_PLAYER_START_COYOTE_TIME_MS,
variableHeight: true,
maxHoldMs: RESPONSIVE_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
};
@@ -513,6 +521,8 @@ function clonePlayerStartJumpSettings(
coyoteTimeMs: settings.coyoteTimeMs,
variableHeight: settings.variableHeight,
maxHoldMs: settings.maxHoldMs,
moveWhileJumping: settings.moveWhileJumping,
moveWhileFalling: settings.moveWhileFalling,
bunnyHop: settings.bunnyHop,
bunnyHopBoost: settings.bunnyHopBoost
};
@@ -751,6 +761,10 @@ export function createPlayerStartMovementTemplate(
variableHeight:
overrides.jump?.variableHeight ?? preset.jump.variableHeight,
maxHoldMs: overrides.jump?.maxHoldMs ?? preset.jump.maxHoldMs,
moveWhileJumping:
overrides.jump?.moveWhileJumping ?? preset.jump.moveWhileJumping,
moveWhileFalling:
overrides.jump?.moveWhileFalling ?? preset.jump.moveWhileFalling,
bunnyHop: overrides.jump?.bunnyHop ?? preset.jump.bunnyHop,
bunnyHopBoost:
overrides.jump?.bunnyHopBoost ?? preset.jump.bunnyHopBoost
@@ -799,6 +813,14 @@ export function createPlayerStartMovementTemplate(
jump.maxHoldMs,
"Player Start variable jump max hold milliseconds"
);
assertBoolean(
jump.moveWhileJumping,
"Player Start move while jumping setting"
);
assertBoolean(
jump.moveWhileFalling,
"Player Start move while falling setting"
);
assertBoolean(jump.bunnyHop, "Player Start bunny hop setting");
assertNonNegativeFiniteNumber(
jump.bunnyHopBoost,
@@ -867,6 +889,10 @@ export function inferPlayerStartMovementTemplateKind(
createPlayerStartMovementTemplate({ kind: presetKind }).jump.variableHeight &&
candidate.jump.maxHoldMs ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.maxHoldMs &&
candidate.jump.moveWhileJumping ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileJumping &&
candidate.jump.moveWhileFalling ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileFalling &&
candidate.jump.bunnyHop ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHop &&
candidate.jump.bunnyHopBoost ===
@@ -923,6 +949,8 @@ export function arePlayerStartMovementTemplatesEqual(
left.jump.coyoteTimeMs === right.jump.coyoteTimeMs &&
left.jump.variableHeight === right.jump.variableHeight &&
left.jump.maxHoldMs === right.jump.maxHoldMs &&
left.jump.moveWhileJumping === right.jump.moveWhileJumping &&
left.jump.moveWhileFalling === right.jump.moveWhileFalling &&
left.jump.bunnyHop === right.jump.bunnyHop &&
left.jump.bunnyHopBoost === right.jump.bunnyHopBoost &&
left.sprint.speedMultiplier === right.sprint.speedMultiplier &&

View File

@@ -63,6 +63,8 @@ function cloneRuntimePlayerMovement(
coyoteTimeMs: movement.jump.coyoteTimeMs,
variableHeight: movement.jump.variableHeight,
maxHoldMs: movement.jump.maxHoldMs,
moveWhileJumping: movement.jump.moveWhileJumping,
moveWhileFalling: movement.jump.moveWhileFalling,
bunnyHop: movement.jump.bunnyHop,
bunnyHopBoost: movement.jump.bunnyHopBoost
},

View File

@@ -239,6 +239,18 @@ function computePlanarSpeedFromDisplacement(
return Math.hypot(displacement.x, displacement.z) / dt;
}
function clearPlanarMovementInput(
input: PlayerStartActionInputState
): PlayerStartActionInputState {
return {
...input,
moveForward: 0,
moveBackward: 0,
moveLeft: 0,
moveRight: 0
};
}
function isWaterLocomotionMode(
locomotionMode: RuntimeLocomotionMode | null | undefined
): boolean {
@@ -480,6 +492,17 @@ export function stepPlayerLocomotion(
coyoteTimeRemainingMs = 0;
}
const airMovementAllowed =
currentlyGrounded ||
jumpTriggered ||
currentSwimmableWater ||
(options.verticalVelocity > VERTICAL_ASCENT_EPSILON
? options.movement.jump.moveWhileJumping
: options.movement.jump.moveWhileFalling);
const planarInput = airMovementAllowed
? options.input
: clearPlanarMovementInput(options.input);
const requestedPlanarSpeed =
activeShape.mode !== "none" && !currentSwimmableWater
? jumpTriggered
@@ -490,7 +513,7 @@ export function stepPlayerLocomotion(
: groundedRequestedPlanarSpeed;
const planarMotionFromInput = computePlanarMotion(
options.movementYawRadians,
options.input,
planarInput,
requestedPlanarSpeed,
options.dt
);

View File

@@ -285,6 +285,8 @@ function clonePlayerStartJumpSettings(
coyoteTimeMs: jump.coyoteTimeMs,
variableHeight: jump.variableHeight,
maxHoldMs: jump.maxHoldMs,
moveWhileJumping: jump.moveWhileJumping,
moveWhileFalling: jump.moveWhileFalling,
bunnyHop: jump.bunnyHop,
bunnyHopBoost: jump.bunnyHopBoost
};

View File

@@ -77,6 +77,8 @@ function cloneRuntimePlayerMovement(
coyoteTimeMs: movement.jump.coyoteTimeMs,
variableHeight: movement.jump.variableHeight,
maxHoldMs: movement.jump.maxHoldMs,
moveWhileJumping: movement.jump.moveWhileJumping,
moveWhileFalling: movement.jump.moveWhileFalling,
bunnyHop: movement.jump.bunnyHop,
bunnyHopBoost: movement.jump.bunnyHopBoost
},