Add air direction only setting for player movement
This commit is contained in:
@@ -10924,6 +10924,29 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Air Direction Only</span>
|
||||
<input
|
||||
data-testid="player-start-movement-air-direction-only-enabled"
|
||||
type="checkbox"
|
||||
checked={
|
||||
playerStartMovementTemplateDraft.jump.directionOnly
|
||||
}
|
||||
onChange={(event) =>
|
||||
commitPlayerStartMovementTemplateDraft(
|
||||
{
|
||||
jump: {
|
||||
directionOnly:
|
||||
event.currentTarget.checked
|
||||
}
|
||||
},
|
||||
{
|
||||
schedule: true
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Bunny Hopping</span>
|
||||
<input
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
MULTI_SCENE_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||
MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION,
|
||||
@@ -1369,6 +1370,11 @@ function readPlayerStartMovementTemplate(value: unknown, label: string) {
|
||||
`${label}.jump.moveWhileFalling`,
|
||||
preset.jump.moveWhileFalling
|
||||
),
|
||||
directionOnly: readOptionalBoolean(
|
||||
jump?.directionOnly,
|
||||
`${label}.jump.directionOnly`,
|
||||
preset.jump.directionOnly
|
||||
),
|
||||
maxHoldMs:
|
||||
jump?.maxHoldMs === undefined
|
||||
? preset.jump.maxHoldMs
|
||||
@@ -2866,6 +2872,7 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
||||
if (
|
||||
source.version !== SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== 33 &&
|
||||
source.version !== PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PROJECT_NAME_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION &&
|
||||
|
||||
@@ -1370,6 +1370,17 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isBoolean(entity.movementTemplate?.jump?.directionOnly)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
"invalid-player-start-air-direction-only",
|
||||
"Player Start air direction only setting must be a boolean.",
|
||||
`${path}.movementTemplate.jump.directionOnly`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isBoolean(entity.movementTemplate?.jump?.bunnyHop)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
type WorldSettings
|
||||
} from "./world-settings";
|
||||
|
||||
export const SCENE_DOCUMENT_VERSION = 34 as const;
|
||||
export const SCENE_DOCUMENT_VERSION = 35 as const;
|
||||
export const PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION =
|
||||
35 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;
|
||||
|
||||
@@ -149,6 +149,7 @@ export interface PlayerStartJumpSettings {
|
||||
maxHoldMs: number;
|
||||
moveWhileJumping: boolean;
|
||||
moveWhileFalling: boolean;
|
||||
directionOnly: boolean;
|
||||
bunnyHop: boolean;
|
||||
bunnyHopBoost: number;
|
||||
}
|
||||
@@ -305,6 +306,7 @@ 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_AIR_DIRECTION_ONLY = false;
|
||||
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;
|
||||
@@ -323,6 +325,7 @@ export const DEFAULT_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
|
||||
maxHoldMs: DEFAULT_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
|
||||
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
|
||||
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
|
||||
directionOnly: DEFAULT_PLAYER_START_AIR_DIRECTION_ONLY,
|
||||
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
|
||||
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
|
||||
};
|
||||
@@ -343,6 +346,7 @@ export const RESPONSIVE_PLAYER_START_JUMP_SETTINGS: PlayerStartJumpSettings = {
|
||||
maxHoldMs: RESPONSIVE_PLAYER_START_VARIABLE_JUMP_MAX_HOLD_MS,
|
||||
moveWhileJumping: DEFAULT_PLAYER_START_MOVE_WHILE_JUMPING,
|
||||
moveWhileFalling: DEFAULT_PLAYER_START_MOVE_WHILE_FALLING,
|
||||
directionOnly: DEFAULT_PLAYER_START_AIR_DIRECTION_ONLY,
|
||||
bunnyHop: DEFAULT_PLAYER_START_BUNNY_HOP,
|
||||
bunnyHopBoost: DEFAULT_PLAYER_START_BUNNY_HOP_BOOST
|
||||
};
|
||||
@@ -523,6 +527,7 @@ function clonePlayerStartJumpSettings(
|
||||
maxHoldMs: settings.maxHoldMs,
|
||||
moveWhileJumping: settings.moveWhileJumping,
|
||||
moveWhileFalling: settings.moveWhileFalling,
|
||||
directionOnly: settings.directionOnly,
|
||||
bunnyHop: settings.bunnyHop,
|
||||
bunnyHopBoost: settings.bunnyHopBoost
|
||||
};
|
||||
@@ -765,6 +770,8 @@ export function createPlayerStartMovementTemplate(
|
||||
overrides.jump?.moveWhileJumping ?? preset.jump.moveWhileJumping,
|
||||
moveWhileFalling:
|
||||
overrides.jump?.moveWhileFalling ?? preset.jump.moveWhileFalling,
|
||||
directionOnly:
|
||||
overrides.jump?.directionOnly ?? preset.jump.directionOnly,
|
||||
bunnyHop: overrides.jump?.bunnyHop ?? preset.jump.bunnyHop,
|
||||
bunnyHopBoost:
|
||||
overrides.jump?.bunnyHopBoost ?? preset.jump.bunnyHopBoost
|
||||
@@ -821,6 +828,10 @@ export function createPlayerStartMovementTemplate(
|
||||
jump.moveWhileFalling,
|
||||
"Player Start move while falling setting"
|
||||
);
|
||||
assertBoolean(
|
||||
jump.directionOnly,
|
||||
"Player Start air direction only setting"
|
||||
);
|
||||
assertBoolean(jump.bunnyHop, "Player Start bunny hop setting");
|
||||
assertNonNegativeFiniteNumber(
|
||||
jump.bunnyHopBoost,
|
||||
@@ -893,6 +904,8 @@ export function inferPlayerStartMovementTemplateKind(
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileJumping &&
|
||||
candidate.jump.moveWhileFalling ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileFalling &&
|
||||
candidate.jump.directionOnly ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.directionOnly &&
|
||||
candidate.jump.bunnyHop ===
|
||||
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHop &&
|
||||
candidate.jump.bunnyHopBoost ===
|
||||
@@ -951,6 +964,7 @@ export function arePlayerStartMovementTemplatesEqual(
|
||||
left.jump.maxHoldMs === right.jump.maxHoldMs &&
|
||||
left.jump.moveWhileJumping === right.jump.moveWhileJumping &&
|
||||
left.jump.moveWhileFalling === right.jump.moveWhileFalling &&
|
||||
left.jump.directionOnly === right.jump.directionOnly &&
|
||||
left.jump.bunnyHop === right.jump.bunnyHop &&
|
||||
left.jump.bunnyHopBoost === right.jump.bunnyHopBoost &&
|
||||
left.sprint.speedMultiplier === right.sprint.speedMultiplier &&
|
||||
|
||||
@@ -65,6 +65,7 @@ function cloneRuntimePlayerMovement(
|
||||
maxHoldMs: movement.jump.maxHoldMs,
|
||||
moveWhileJumping: movement.jump.moveWhileJumping,
|
||||
moveWhileFalling: movement.jump.moveWhileFalling,
|
||||
directionOnly: movement.jump.directionOnly,
|
||||
bunnyHop: movement.jump.bunnyHop,
|
||||
bunnyHopBoost: movement.jump.bunnyHopBoost
|
||||
},
|
||||
@@ -313,6 +314,7 @@ export class FirstPersonNavigationController implements NavigationController {
|
||||
dt,
|
||||
feetPosition: this.feetPosition,
|
||||
movementYawRadians: this.yawRadians,
|
||||
airDirectionYawRadians: this.yawRadians,
|
||||
standingShape: this.standingPlayerShape,
|
||||
verticalVelocity: this.verticalVelocity,
|
||||
previousLocomotionState: this.locomotionState,
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface StepPlayerLocomotionOptions {
|
||||
dt: number;
|
||||
feetPosition: Vec3;
|
||||
movementYawRadians: number;
|
||||
airDirectionYawRadians?: number;
|
||||
standingShape: FirstPersonPlayerShape;
|
||||
verticalVelocity: number;
|
||||
previousLocomotionState?: RuntimeLocomotionState;
|
||||
@@ -190,18 +191,50 @@ function computePlanarMotion(
|
||||
requestedPlanarSpeed: number,
|
||||
dt: number
|
||||
): { motion: Vec3; inputMagnitude: number } {
|
||||
const inputX = input.moveRight - input.moveLeft;
|
||||
const inputZ = input.moveForward - input.moveBackward;
|
||||
const rawMagnitude = Math.hypot(inputX, inputZ);
|
||||
const inputMagnitude = clampUnitInterval(rawMagnitude);
|
||||
const directionResult = computePlanarInputDirection(
|
||||
movementYawRadians,
|
||||
input
|
||||
);
|
||||
|
||||
if (rawMagnitude <= 0 || requestedPlanarSpeed <= 0 || dt <= 0) {
|
||||
if (
|
||||
directionResult.direction === null ||
|
||||
requestedPlanarSpeed <= 0 ||
|
||||
dt <= 0
|
||||
) {
|
||||
return {
|
||||
motion: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
inputMagnitude: directionResult.inputMagnitude
|
||||
};
|
||||
}
|
||||
|
||||
const planarDistance = requestedPlanarSpeed * dt;
|
||||
|
||||
return {
|
||||
motion: {
|
||||
x: directionResult.direction.x * planarDistance,
|
||||
y: 0,
|
||||
z: directionResult.direction.z * planarDistance
|
||||
},
|
||||
inputMagnitude: directionResult.inputMagnitude
|
||||
};
|
||||
}
|
||||
|
||||
function computePlanarInputDirection(
|
||||
movementYawRadians: number,
|
||||
input: PlayerStartActionInputState
|
||||
): { direction: Vec3 | null; inputMagnitude: number } {
|
||||
const inputX = input.moveRight - input.moveLeft;
|
||||
const inputZ = input.moveForward - input.moveBackward;
|
||||
const rawMagnitude = Math.hypot(inputX, inputZ);
|
||||
const inputMagnitude = clampUnitInterval(rawMagnitude);
|
||||
|
||||
if (rawMagnitude <= 0) {
|
||||
return {
|
||||
direction: null,
|
||||
inputMagnitude
|
||||
};
|
||||
}
|
||||
@@ -212,17 +245,24 @@ function computePlanarMotion(
|
||||
const forwardZ = Math.cos(movementYawRadians);
|
||||
const rightX = -Math.cos(movementYawRadians);
|
||||
const rightZ = Math.sin(movementYawRadians);
|
||||
const planarDistance = requestedPlanarSpeed * dt;
|
||||
const directionX =
|
||||
forwardX * normalizedInputZ + rightX * normalizedInputX;
|
||||
const directionZ =
|
||||
forwardZ * normalizedInputZ + rightZ * normalizedInputX;
|
||||
const directionMagnitude = Math.hypot(directionX, directionZ);
|
||||
|
||||
if (directionMagnitude <= 0) {
|
||||
return {
|
||||
direction: null,
|
||||
inputMagnitude
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
motion: {
|
||||
x:
|
||||
(forwardX * normalizedInputZ + rightX * normalizedInputX) *
|
||||
planarDistance,
|
||||
direction: {
|
||||
x: directionX / directionMagnitude,
|
||||
y: 0,
|
||||
z:
|
||||
(forwardZ * normalizedInputZ + rightZ * normalizedInputX) *
|
||||
planarDistance
|
||||
z: directionZ / directionMagnitude
|
||||
},
|
||||
inputMagnitude
|
||||
};
|
||||
@@ -251,6 +291,48 @@ function clearPlanarMovementInput(
|
||||
};
|
||||
}
|
||||
|
||||
function computeDirectionalAirMotion(options: {
|
||||
directionYawRadians: number;
|
||||
input: PlayerStartActionInputState;
|
||||
previousPlanarDisplacement: Vec3;
|
||||
dt: number;
|
||||
}): { motion: Vec3; inputMagnitude: number } {
|
||||
const directionResult = computePlanarInputDirection(
|
||||
options.directionYawRadians,
|
||||
options.input
|
||||
);
|
||||
const planarSpeed = computePlanarSpeedFromDisplacement(
|
||||
options.previousPlanarDisplacement,
|
||||
options.dt
|
||||
);
|
||||
|
||||
if (
|
||||
directionResult.direction === null ||
|
||||
planarSpeed <= 0 ||
|
||||
options.dt <= 0
|
||||
) {
|
||||
return {
|
||||
motion: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
inputMagnitude: directionResult.inputMagnitude
|
||||
};
|
||||
}
|
||||
|
||||
const planarDistance = planarSpeed * options.dt;
|
||||
|
||||
return {
|
||||
motion: {
|
||||
x: directionResult.direction.x * planarDistance,
|
||||
y: 0,
|
||||
z: directionResult.direction.z * planarDistance
|
||||
},
|
||||
inputMagnitude: directionResult.inputMagnitude
|
||||
};
|
||||
}
|
||||
|
||||
function isWaterLocomotionMode(
|
||||
locomotionMode: RuntimeLocomotionMode | null | undefined
|
||||
): boolean {
|
||||
@@ -502,6 +584,16 @@ export function stepPlayerLocomotion(
|
||||
const planarInput = airMovementAllowed
|
||||
? options.input
|
||||
: clearPlanarMovementInput(options.input);
|
||||
const directionalAirControlActive =
|
||||
!currentlyGrounded &&
|
||||
!jumpTriggered &&
|
||||
!currentSwimmableWater &&
|
||||
airMovementAllowed &&
|
||||
options.movement.jump.directionOnly;
|
||||
const previousPlanarSpeed = computePlanarSpeedFromDisplacement(
|
||||
options.previousPlanarDisplacement,
|
||||
options.dt
|
||||
);
|
||||
|
||||
const requestedPlanarSpeed =
|
||||
activeShape.mode !== "none" && !currentSwimmableWater
|
||||
@@ -511,12 +603,20 @@ export function stepPlayerLocomotion(
|
||||
? groundedRequestedPlanarSpeed
|
||||
: airborneRequestedPlanarSpeed
|
||||
: groundedRequestedPlanarSpeed;
|
||||
const planarMotionFromInput = computePlanarMotion(
|
||||
options.movementYawRadians,
|
||||
planarInput,
|
||||
requestedPlanarSpeed,
|
||||
options.dt
|
||||
);
|
||||
const planarMotionFromInput = directionalAirControlActive
|
||||
? computeDirectionalAirMotion({
|
||||
directionYawRadians:
|
||||
options.airDirectionYawRadians ?? options.movementYawRadians,
|
||||
input: planarInput,
|
||||
previousPlanarDisplacement: options.previousPlanarDisplacement,
|
||||
dt: options.dt
|
||||
})
|
||||
: computePlanarMotion(
|
||||
options.movementYawRadians,
|
||||
planarInput,
|
||||
requestedPlanarSpeed,
|
||||
options.dt
|
||||
);
|
||||
const preserveAirborneMomentum =
|
||||
activeShape.mode !== "none" &&
|
||||
!currentSwimmableWater &&
|
||||
@@ -710,11 +810,10 @@ export function stepPlayerLocomotion(
|
||||
sprinting,
|
||||
inputMagnitude: planarMotion.inputMagnitude,
|
||||
requestedPlanarSpeed: preserveAirborneMomentum
|
||||
? computePlanarSpeedFromDisplacement(
|
||||
options.previousPlanarDisplacement,
|
||||
options.dt
|
||||
)
|
||||
: requestedPlanarSpeed * planarMotion.inputMagnitude,
|
||||
? previousPlanarSpeed
|
||||
: (directionalAirControlActive
|
||||
? previousPlanarSpeed
|
||||
: requestedPlanarSpeed) * planarMotion.inputMagnitude,
|
||||
planarSpeed: actualPlanarSpeed,
|
||||
verticalVelocity,
|
||||
contact: resolveContactState(resolvedMotion, groundProbe, grounded)
|
||||
|
||||
@@ -287,6 +287,7 @@ function clonePlayerStartJumpSettings(
|
||||
maxHoldMs: jump.maxHoldMs,
|
||||
moveWhileJumping: jump.moveWhileJumping,
|
||||
moveWhileFalling: jump.moveWhileFalling,
|
||||
directionOnly: jump.directionOnly,
|
||||
bunnyHop: jump.bunnyHop,
|
||||
bunnyHopBoost: jump.bunnyHopBoost
|
||||
};
|
||||
|
||||
@@ -79,6 +79,7 @@ function cloneRuntimePlayerMovement(
|
||||
maxHoldMs: movement.jump.maxHoldMs,
|
||||
moveWhileJumping: movement.jump.moveWhileJumping,
|
||||
moveWhileFalling: movement.jump.moveWhileFalling,
|
||||
directionOnly: movement.jump.directionOnly,
|
||||
bunnyHop: movement.jump.bunnyHop,
|
||||
bunnyHopBoost: movement.jump.bunnyHopBoost
|
||||
},
|
||||
@@ -285,6 +286,7 @@ export class ThirdPersonNavigationController implements NavigationController {
|
||||
dt,
|
||||
feetPosition: this.feetPosition,
|
||||
movementYawRadians: this.cameraYawRadians,
|
||||
airDirectionYawRadians: this.yawRadians,
|
||||
standingShape: this.standingPlayerShape,
|
||||
verticalVelocity: this.verticalVelocity,
|
||||
previousLocomotionState: this.locomotionState,
|
||||
|
||||
@@ -132,7 +132,8 @@ describe("validateSceneDocument", () => {
|
||||
variableHeight: "yes",
|
||||
maxHoldMs: 0,
|
||||
moveWhileJumping: "yes",
|
||||
moveWhileFalling: 1
|
||||
moveWhileFalling: 1,
|
||||
directionOnly: "left"
|
||||
},
|
||||
sprint: {
|
||||
speedMultiplier: 0
|
||||
@@ -217,6 +218,9 @@ describe("validateSceneDocument", () => {
|
||||
expect.objectContaining({
|
||||
code: "invalid-player-start-move-while-falling"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-player-start-air-direction-only"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-player-start-sprint-speed-multiplier"
|
||||
}),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION,
|
||||
LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION,
|
||||
MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION,
|
||||
PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION,
|
||||
@@ -467,7 +468,8 @@ describe("scene document JSON", () => {
|
||||
variableHeight: true,
|
||||
maxHoldMs: 220,
|
||||
moveWhileJumping: false,
|
||||
moveWhileFalling: false
|
||||
moveWhileFalling: false,
|
||||
directionOnly: true
|
||||
},
|
||||
sprint: {
|
||||
speedMultiplier: 1.8
|
||||
@@ -951,6 +953,7 @@ describe("scene document JSON", () => {
|
||||
const {
|
||||
moveWhileJumping: _moveWhileJumping,
|
||||
moveWhileFalling: _moveWhileFalling,
|
||||
directionOnly: _directionOnly,
|
||||
...legacyJump
|
||||
} = playerStart.movementTemplate.jump;
|
||||
const legacyDocument = {
|
||||
@@ -975,6 +978,39 @@ describe("scene document JSON", () => {
|
||||
expect(migratedDocument.entities[playerStart.id]).toEqual(playerStart);
|
||||
});
|
||||
|
||||
it("migrates version 34 Player Start jump settings to include default air direction mode", () => {
|
||||
const playerStart = createPlayerStartEntity({
|
||||
id: "entity-player-start-legacy-air-direction",
|
||||
movementTemplate: {
|
||||
kind: "responsive"
|
||||
}
|
||||
});
|
||||
const {
|
||||
directionOnly: _directionOnly,
|
||||
...legacyJump
|
||||
} = playerStart.movementTemplate.jump;
|
||||
const legacyDocument = {
|
||||
...createEmptySceneDocument({
|
||||
name: "Legacy Player Air Direction Scene"
|
||||
}),
|
||||
version: PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION,
|
||||
entities: {
|
||||
[playerStart.id]: {
|
||||
...playerStart,
|
||||
movementTemplate: {
|
||||
...playerStart.movementTemplate,
|
||||
jump: legacyJump
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const migratedDocument = migrateSceneDocument(legacyDocument);
|
||||
|
||||
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
||||
expect(migratedDocument.entities[playerStart.id]).toEqual(playerStart);
|
||||
});
|
||||
|
||||
it("round-trips authored third-person Player Start navigation", () => {
|
||||
const playerStart = createPlayerStartEntity({
|
||||
id: "entity-player-start-third-person",
|
||||
|
||||
@@ -574,6 +574,70 @@ describe("player-locomotion", () => {
|
||||
expect(step?.locomotionState.planarSpeed).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it("reorients airborne movement using existing speed without adding more", () => {
|
||||
const step = stepPlayerLocomotion({
|
||||
dt: 0.1,
|
||||
feetPosition: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
},
|
||||
movementYawRadians: 0,
|
||||
airDirectionYawRadians: Math.PI / 2,
|
||||
standingShape: FIRST_PERSON_PLAYER_SHAPE,
|
||||
verticalVelocity: -2,
|
||||
previousLocomotionState: createIdleRuntimeLocomotionState("airborne"),
|
||||
previousPlanarDisplacement: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0.45
|
||||
},
|
||||
jumpBufferRemainingMs: 0,
|
||||
coyoteTimeRemainingMs: 0,
|
||||
jumpHoldRemainingMs: 0,
|
||||
crouched: false,
|
||||
wasJumpPressed: false,
|
||||
input: FORWARD_INPUT,
|
||||
movement: {
|
||||
...DEFAULT_MOVEMENT,
|
||||
jump: {
|
||||
...DEFAULT_MOVEMENT.jump,
|
||||
directionOnly: true
|
||||
}
|
||||
},
|
||||
resolveMotion: (feetPosition, motion): ResolvedPlayerMotion => ({
|
||||
feetPosition: {
|
||||
x: feetPosition.x + motion.x,
|
||||
y: feetPosition.y + motion.y,
|
||||
z: feetPosition.z + motion.z
|
||||
},
|
||||
grounded: false,
|
||||
collisionCount: 0,
|
||||
groundCollisionNormal: null,
|
||||
collidedAxes: {
|
||||
x: false,
|
||||
y: false,
|
||||
z: false
|
||||
}
|
||||
}),
|
||||
resolveVolumeState: () => createVolumeState(),
|
||||
probeGround: () => ({
|
||||
grounded: false,
|
||||
distance: null,
|
||||
normal: null,
|
||||
slopeDegrees: null
|
||||
}),
|
||||
canOccupyShape: () => true
|
||||
});
|
||||
|
||||
expect(step).not.toBeNull();
|
||||
expect(step?.locomotionState.locomotionMode).toBe("airborne");
|
||||
expect(step?.planarDisplacement.x).toBeCloseTo(0.45);
|
||||
expect(step?.planarDisplacement.z).toBeCloseTo(0);
|
||||
expect(step?.locomotionState.requestedPlanarSpeed).toBeCloseTo(4.5);
|
||||
expect(step?.locomotionState.planarSpeed).toBeCloseTo(4.5);
|
||||
});
|
||||
|
||||
it("sinks toward the water surface while keeping the head above water", () => {
|
||||
const step = stepPlayerLocomotion({
|
||||
dt: 0.1,
|
||||
|
||||
@@ -179,6 +179,9 @@ describe("Player Start inspector", () => {
|
||||
const variableJumpCheckbox = screen.getByTestId(
|
||||
"player-start-movement-variable-jump-enabled"
|
||||
);
|
||||
const airDirectionOnlyCheckbox = screen.getByTestId(
|
||||
"player-start-movement-air-direction-only-enabled"
|
||||
);
|
||||
const jumpBufferInput = screen.getByTestId(
|
||||
"player-start-movement-jump-buffer"
|
||||
);
|
||||
@@ -217,6 +220,10 @@ describe("Player Start inspector", () => {
|
||||
fireEvent.click(variableJumpCheckbox);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(airDirectionOnlyCheckbox);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(jumpBufferInput, {
|
||||
target: {
|
||||
@@ -233,7 +240,8 @@ describe("Player Start inspector", () => {
|
||||
moveSpeed: 5.7,
|
||||
jump: {
|
||||
bufferMs: 75,
|
||||
variableHeight: false
|
||||
variableHeight: false,
|
||||
directionOnly: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user