Implement third-person climbing mechanics
This commit is contained in:
@@ -665,6 +665,238 @@ export class ThirdPersonNavigationController implements NavigationController {
|
||||
this.publishTelemetry();
|
||||
}
|
||||
|
||||
private resolveClimbProbeDirection(movementYawRadians: number): Vec3 {
|
||||
if (this.climbSurface !== null) {
|
||||
return {
|
||||
x: -this.climbSurface.normal.x,
|
||||
y: -this.climbSurface.normal.y,
|
||||
z: -this.climbSurface.normal.z
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
x: Math.sin(movementYawRadians),
|
||||
y: 0,
|
||||
z: Math.cos(movementYawRadians)
|
||||
};
|
||||
}
|
||||
|
||||
private createClimbingLocomotionState(options: {
|
||||
inputMagnitude: number;
|
||||
displacement: Vec3;
|
||||
dt: number;
|
||||
collisionCount: number;
|
||||
collidedAxes: { x: boolean; y: boolean; z: boolean };
|
||||
}): RuntimeLocomotionState {
|
||||
const speed =
|
||||
options.dt > 0
|
||||
? Math.hypot(
|
||||
options.displacement.x,
|
||||
options.displacement.y,
|
||||
options.displacement.z
|
||||
) / options.dt
|
||||
: 0;
|
||||
|
||||
return {
|
||||
locomotionMode: "climbing",
|
||||
airborneKind: null,
|
||||
gait: options.inputMagnitude > 0 ? "walk" : "idle",
|
||||
grounded: false,
|
||||
crouched: false,
|
||||
sprinting: false,
|
||||
inputMagnitude: options.inputMagnitude,
|
||||
requestedPlanarSpeed: CLIMB_SPEED_METERS_PER_SECOND,
|
||||
planarSpeed: speed,
|
||||
verticalVelocity: 0,
|
||||
contact: {
|
||||
collisionCount: options.collisionCount,
|
||||
collidedAxes: options.collidedAxes,
|
||||
groundNormal: null,
|
||||
groundDistance: null,
|
||||
slopeDegrees: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private stepClimbing(
|
||||
dt: number,
|
||||
inputState: ReturnType<typeof resolvePlayerStartActionInputs>,
|
||||
playerMovement: RuntimePlayerMovement,
|
||||
movementYawRadians: number
|
||||
): boolean {
|
||||
if (this.context === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const climbPressed = inputState.climb > CLIMB_INPUT_ACTIVE_THRESHOLD;
|
||||
const jumpPressed = inputState.jump > CLIMB_INPUT_ACTIVE_THRESHOLD;
|
||||
|
||||
if (!climbPressed) {
|
||||
this.climbLatchBlocked = false;
|
||||
}
|
||||
|
||||
const climbSurface =
|
||||
this.context.resolvePlayerClimbSurface?.(
|
||||
this.feetPosition,
|
||||
this.resolveClimbProbeDirection(movementYawRadians),
|
||||
this.standingPlayerShape,
|
||||
this.climbSurface
|
||||
) ?? null;
|
||||
|
||||
if (
|
||||
this.climbSurface !== null &&
|
||||
shouldExitClimbing({
|
||||
climbInput: inputState.climb,
|
||||
surface: climbSurface,
|
||||
jumpPressed
|
||||
})
|
||||
) {
|
||||
const exitSurface = this.climbSurface;
|
||||
this.climbSurface = null;
|
||||
|
||||
if (jumpPressed && playerMovement.capabilities.jump) {
|
||||
const detachMotion = {
|
||||
x: exitSurface.normal.x * 0.25,
|
||||
y: Math.max(0.05, playerMovement.jump.speed * 0.05),
|
||||
z: exitSurface.normal.z * 0.25
|
||||
};
|
||||
const resolvedMotion =
|
||||
this.context.resolveFirstPersonMotion(
|
||||
this.feetPosition,
|
||||
detachMotion,
|
||||
this.standingPlayerShape
|
||||
) ?? null;
|
||||
const nextFeetPosition =
|
||||
resolvedMotion?.feetPosition ?? {
|
||||
x: this.feetPosition.x + detachMotion.x,
|
||||
y: this.feetPosition.y + detachMotion.y,
|
||||
z: this.feetPosition.z + detachMotion.z
|
||||
};
|
||||
const displacement = {
|
||||
x: nextFeetPosition.x - this.feetPosition.x,
|
||||
y: nextFeetPosition.y - this.feetPosition.y,
|
||||
z: nextFeetPosition.z - this.feetPosition.z
|
||||
};
|
||||
|
||||
this.feetPosition = nextFeetPosition;
|
||||
this.activePlayerShape = cloneFirstPersonPlayerShape(
|
||||
this.standingPlayerShape
|
||||
);
|
||||
this.verticalVelocity = playerMovement.jump.speed;
|
||||
this.jumpBufferRemainingMs = 0;
|
||||
this.coyoteTimeRemainingMs = 0;
|
||||
this.jumpHoldRemainingMs = playerMovement.jump.variableHeight
|
||||
? playerMovement.jump.maxHoldMs
|
||||
: 0;
|
||||
this.jumpPressed = true;
|
||||
this.latestJumpStarted = true;
|
||||
this.latestHeadBump = false;
|
||||
this.climbLatchBlocked = true;
|
||||
this.previousPlanarDisplacement = displacement;
|
||||
this.grounded = false;
|
||||
this.inWaterVolume = false;
|
||||
this.inFogVolume =
|
||||
this.context.resolvePlayerVolumeState(this.feetPosition).inFog;
|
||||
this.smoothedFeetY = this.feetPosition.y;
|
||||
this.locomotionState = {
|
||||
...createIdleRuntimeLocomotionState("airborne"),
|
||||
airborneKind: "jumping",
|
||||
verticalVelocity: this.verticalVelocity,
|
||||
inputMagnitude: 0,
|
||||
requestedPlanarSpeed: playerMovement.moveSpeed,
|
||||
planarSpeed:
|
||||
dt > 0 ? Math.hypot(displacement.x, displacement.z) / dt : 0
|
||||
};
|
||||
this.updateCameraTransform(dt);
|
||||
this.publishTelemetry();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (climbSurface === null && climbPressed) {
|
||||
this.climbLatchBlocked = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.climbSurface === null &&
|
||||
(this.climbLatchBlocked ||
|
||||
!shouldEnterClimbing({
|
||||
climbInput: inputState.climb,
|
||||
surface: climbSurface,
|
||||
jumpPressed
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeSurface = climbSurface ?? this.climbSurface;
|
||||
|
||||
if (activeSurface === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.climbSurface = activeSurface;
|
||||
|
||||
const climbMovement = computeClimbPlaneMovement({
|
||||
normal: activeSurface.normal,
|
||||
input: inputState,
|
||||
speedMetersPerSecond: CLIMB_SPEED_METERS_PER_SECOND,
|
||||
dt
|
||||
});
|
||||
const resolvedMotion =
|
||||
this.context.resolveFirstPersonMotion(
|
||||
this.feetPosition,
|
||||
climbMovement.motion,
|
||||
this.standingPlayerShape
|
||||
) ?? null;
|
||||
const nextFeetPosition =
|
||||
resolvedMotion?.feetPosition ?? {
|
||||
x: this.feetPosition.x + climbMovement.motion.x,
|
||||
y: this.feetPosition.y + climbMovement.motion.y,
|
||||
z: this.feetPosition.z + climbMovement.motion.z
|
||||
};
|
||||
const displacement = {
|
||||
x: nextFeetPosition.x - this.feetPosition.x,
|
||||
y: nextFeetPosition.y - this.feetPosition.y,
|
||||
z: nextFeetPosition.z - this.feetPosition.z
|
||||
};
|
||||
const volumeState = this.context.resolvePlayerVolumeState(nextFeetPosition);
|
||||
|
||||
this.feetPosition = nextFeetPosition;
|
||||
this.activePlayerShape = cloneFirstPersonPlayerShape(
|
||||
this.standingPlayerShape
|
||||
);
|
||||
this.verticalVelocity = 0;
|
||||
this.jumpBufferRemainingMs = 0;
|
||||
this.coyoteTimeRemainingMs = 0;
|
||||
this.jumpHoldRemainingMs = 0;
|
||||
this.jumpPressed = jumpPressed;
|
||||
this.latestJumpStarted = false;
|
||||
this.latestHeadBump = false;
|
||||
this.previousPlanarDisplacement = displacement;
|
||||
this.grounded = false;
|
||||
this.inWaterVolume = volumeState.inWater;
|
||||
this.inFogVolume = volumeState.inFog;
|
||||
this.smoothedFeetY = this.feetPosition.y;
|
||||
this.locomotionState = this.createClimbingLocomotionState({
|
||||
inputMagnitude: climbMovement.inputMagnitude,
|
||||
displacement,
|
||||
dt,
|
||||
collisionCount: resolvedMotion?.collisionCount ?? 0,
|
||||
collidedAxes: resolvedMotion?.collidedAxes ?? {
|
||||
x: false,
|
||||
y: false,
|
||||
z: false
|
||||
}
|
||||
});
|
||||
|
||||
this.updateCameraTransform(dt);
|
||||
this.publishTelemetry();
|
||||
return true;
|
||||
}
|
||||
|
||||
private applyTargetLookOffsetDelta(yawDelta: number, pitchDelta: number) {
|
||||
const nextYaw = this.targetLookOffsetYawRadians + yawDelta;
|
||||
const nextPitch = this.targetLookOffsetPitchRadians + pitchDelta;
|
||||
|
||||
Reference in New Issue
Block a user