Add unit tests for player climbing logic and helpers
This commit is contained in:
165
tests/unit/player-climbing.test.ts
Normal file
165
tests/unit/player-climbing.test.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createBoxBrush } from "../../src/document/brushes";
|
||||||
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||||
|
import { FIRST_PERSON_PLAYER_SHAPE } from "../../src/runtime-three/player-collision";
|
||||||
|
import {
|
||||||
|
computeClimbPlaneMovement,
|
||||||
|
isClimbableWallNormal,
|
||||||
|
resolvePlayerClimbSurface,
|
||||||
|
shouldEnterClimbing,
|
||||||
|
shouldExitClimbing
|
||||||
|
} from "../../src/runtime-three/player-climbing";
|
||||||
|
import type { PlayerStartActionInputState } from "../../src/runtime-three/player-input-bindings";
|
||||||
|
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
|
||||||
|
|
||||||
|
function createInputState(
|
||||||
|
overrides: Partial<PlayerStartActionInputState> = {}
|
||||||
|
): PlayerStartActionInputState {
|
||||||
|
return {
|
||||||
|
moveForward: 0,
|
||||||
|
moveBackward: 0,
|
||||||
|
moveLeft: 0,
|
||||||
|
moveRight: 0,
|
||||||
|
jump: 0,
|
||||||
|
sprint: 0,
|
||||||
|
crouch: 0,
|
||||||
|
interact: 0,
|
||||||
|
clearTarget: 0,
|
||||||
|
pauseTime: 0,
|
||||||
|
climb: 0,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("player climbing helpers", () => {
|
||||||
|
it("accepts wall-like normals and rejects floor or ceiling normals", () => {
|
||||||
|
expect(isClimbableWallNormal({ x: 0, y: 0, z: 1 })).toBe(true);
|
||||||
|
expect(isClimbableWallNormal({ x: 0, y: 1, z: 0 })).toBe(false);
|
||||||
|
expect(isClimbableWallNormal({ x: 0, y: -1, z: 0 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps forward/back input vertically and strafe input along the climb face", () => {
|
||||||
|
const upward = computeClimbPlaneMovement({
|
||||||
|
normal: { x: 0, y: 0, z: 1 },
|
||||||
|
input: createInputState({ moveForward: 1 }),
|
||||||
|
speedMetersPerSecond: 2,
|
||||||
|
dt: 0.5
|
||||||
|
});
|
||||||
|
const rightward = computeClimbPlaneMovement({
|
||||||
|
normal: { x: 0, y: 0, z: 1 },
|
||||||
|
input: createInputState({ moveRight: 1 }),
|
||||||
|
speedMetersPerSecond: 2,
|
||||||
|
dt: 0.5
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(upward.motion).toMatchObject({
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
z: 0
|
||||||
|
});
|
||||||
|
expect(upward.inputMagnitude).toBe(1);
|
||||||
|
expect(rightward.motion).toMatchObject({
|
||||||
|
x: 1,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
});
|
||||||
|
expect(rightward.inputMagnitude).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps climb entry and exit conditions explicit", () => {
|
||||||
|
const surface = {
|
||||||
|
brushId: "brush-wall",
|
||||||
|
faceId: "negZ",
|
||||||
|
point: { x: 0, y: 1, z: 0.75 },
|
||||||
|
normal: { x: 0, y: 0, z: -1 },
|
||||||
|
distance: 0.75
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shouldEnterClimbing({
|
||||||
|
climbInput: 1,
|
||||||
|
surface,
|
||||||
|
jumpPressed: false
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldEnterClimbing({
|
||||||
|
climbInput: 0,
|
||||||
|
surface,
|
||||||
|
jumpPressed: false
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldExitClimbing({
|
||||||
|
climbInput: 1,
|
||||||
|
surface,
|
||||||
|
jumpPressed: true
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldExitClimbing({
|
||||||
|
climbInput: 0,
|
||||||
|
surface,
|
||||||
|
jumpPressed: false
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves only authored climbable whitebox faces in front of the player", () => {
|
||||||
|
const brush = createBoxBrush({
|
||||||
|
id: "brush-climbable-wall",
|
||||||
|
center: {
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
z: 0.9
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
x: 4,
|
||||||
|
y: 2,
|
||||||
|
z: 0.25
|
||||||
|
}
|
||||||
|
});
|
||||||
|
brush.faces.negZ.climbable = true;
|
||||||
|
const runtimeScene = buildRuntimeSceneFromDocument({
|
||||||
|
...createEmptySceneDocument({ name: "Climb Surface Scene" }),
|
||||||
|
brushes: {
|
||||||
|
[brush.id]: brush
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePlayerClimbSurface({
|
||||||
|
runtimeScene,
|
||||||
|
feetPosition: { x: 0, y: 0, z: 0 },
|
||||||
|
facingDirection: { x: 0, y: 0, z: 1 },
|
||||||
|
shape: FIRST_PERSON_PLAYER_SHAPE
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
brushId: brush.id,
|
||||||
|
faceId: "negZ",
|
||||||
|
normal: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
brush.faces.negZ.climbable = false;
|
||||||
|
const runtimeSceneWithoutClimbableFace = buildRuntimeSceneFromDocument({
|
||||||
|
...createEmptySceneDocument({ name: "Non Climb Surface Scene" }),
|
||||||
|
brushes: {
|
||||||
|
[brush.id]: brush
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePlayerClimbSurface({
|
||||||
|
runtimeScene: runtimeSceneWithoutClimbableFace,
|
||||||
|
feetPosition: { x: 0, y: 0, z: 0 },
|
||||||
|
facingDirection: { x: 0, y: 0, z: 1 },
|
||||||
|
shape: FIRST_PERSON_PLAYER_SHAPE
|
||||||
|
})
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user