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"); }); });