104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
PLAYER_GROUND_MIN_NORMAL_Y,
|
|
PLAYER_AIRBORNE_PRESENTATION_DELAY_MS,
|
|
PLAYER_JUMP_BUFFER_MS,
|
|
PLAYER_JUMP_COYOTE_MS,
|
|
bufferPlayerJump,
|
|
cancelBufferedPlayerJump,
|
|
characterVerticalMotion,
|
|
createCharacterVerticalPresentationTiming,
|
|
createPlayerJumpTiming,
|
|
isWalkableGroundHit,
|
|
updateCharacterVerticalPresentation,
|
|
updatePlayerJumpTiming,
|
|
} from "./playerJump";
|
|
|
|
describe("player jump timing", () => {
|
|
it("accepts only a nearby upward-facing ground hit", () => {
|
|
expect(isWalkableGroundHit({
|
|
timeOfImpact: 0.9,
|
|
normal: { y: PLAYER_GROUND_MIN_NORMAL_Y },
|
|
}, 0.95)).toBe(true);
|
|
expect(isWalkableGroundHit({ timeOfImpact: 0.96, normal: { y: 1 } }, 0.95)).toBe(false);
|
|
expect(isWalkableGroundHit({ timeOfImpact: 0.4, normal: { y: 0.2 } }, 0.95)).toBe(false);
|
|
});
|
|
|
|
it("buffers a press shortly before landing", () => {
|
|
const timing = createPlayerJumpTiming();
|
|
bufferPlayerJump(timing, 1_000);
|
|
expect(updatePlayerJumpTiming(timing, 1_000 + PLAYER_JUMP_BUFFER_MS - 1, true, -2)).toBe(true);
|
|
});
|
|
|
|
it("allows coyote time after leaving a ledge", () => {
|
|
const timing = createPlayerJumpTiming(2_000);
|
|
bufferPlayerJump(timing, 2_000 + PLAYER_JUMP_COYOTE_MS - 1);
|
|
expect(updatePlayerJumpTiming(
|
|
timing,
|
|
2_000 + PLAYER_JUMP_COYOTE_MS - 1,
|
|
false,
|
|
-0.2,
|
|
)).toBe(true);
|
|
});
|
|
|
|
it("cancels buffered input while gameplay is blocked", () => {
|
|
const timing = createPlayerJumpTiming(2_500);
|
|
bufferPlayerJump(timing, 2_500);
|
|
cancelBufferedPlayerJump(timing);
|
|
expect(updatePlayerJumpTiming(timing, 2_500, true, 0)).toBe(false);
|
|
});
|
|
|
|
it("does not double jump until a real landing", () => {
|
|
const timing = createPlayerJumpTiming(3_000);
|
|
bufferPlayerJump(timing, 3_000);
|
|
expect(updatePlayerJumpTiming(timing, 3_000, true, 0)).toBe(true);
|
|
|
|
bufferPlayerJump(timing, 3_040);
|
|
expect(updatePlayerJumpTiming(timing, 3_040, true, 6.2)).toBe(false);
|
|
expect(updatePlayerJumpTiming(timing, 3_300, false, -2)).toBe(false);
|
|
bufferPlayerJump(timing, 3_550);
|
|
expect(updatePlayerJumpTiming(timing, 3_600, true, -1)).toBe(true);
|
|
});
|
|
|
|
it("reports rising, falling, landing, and grounded presentation phases", () => {
|
|
expect(characterVerticalMotion(false, 2, 100, 0)).toBe("rising");
|
|
expect(characterVerticalMotion(false, -0.1, 100, 0)).toBe("falling");
|
|
expect(characterVerticalMotion(true, 0, 100, 150)).toBe("landing");
|
|
expect(characterVerticalMotion(true, 0, 150, 150)).toBe("grounded");
|
|
});
|
|
|
|
it("hides brief terrain gaps without playing a landing animation", () => {
|
|
const timing = createCharacterVerticalPresentationTiming();
|
|
expect(updateCharacterVerticalPresentation(timing, false, -0.1, 1_000, false))
|
|
.toBe("grounded");
|
|
expect(updateCharacterVerticalPresentation(
|
|
timing,
|
|
true,
|
|
0,
|
|
1_000 + PLAYER_AIRBORNE_PRESENTATION_DELAY_MS - 1,
|
|
false,
|
|
)).toBe("grounded");
|
|
});
|
|
|
|
it("presents real falls and intentional jumps, then lands once", () => {
|
|
const falling = createCharacterVerticalPresentationTiming();
|
|
expect(updateCharacterVerticalPresentation(falling, false, -1, 2_000, false))
|
|
.toBe("grounded");
|
|
expect(updateCharacterVerticalPresentation(
|
|
falling,
|
|
false,
|
|
-2,
|
|
2_000 + PLAYER_AIRBORNE_PRESENTATION_DELAY_MS,
|
|
false,
|
|
)).toBe("falling");
|
|
expect(updateCharacterVerticalPresentation(falling, true, 0, 2_200, false))
|
|
.toBe("landing");
|
|
expect(updateCharacterVerticalPresentation(falling, true, 0, 2_400, false))
|
|
.toBe("grounded");
|
|
|
|
const jumping = createCharacterVerticalPresentationTiming();
|
|
expect(updateCharacterVerticalPresentation(jumping, false, 7.2, 3_000, true))
|
|
.toBe("rising");
|
|
});
|
|
});
|