107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { PlayerMotionModel } from '@logic/PlayerMotionModel';
|
||
import { JumpController, PARABOLIC_HORIZONTAL_SPEED, heightToImpulse } from '@logic/JumpController';
|
||
import {
|
||
JUMP_HEIGHT_STANDARD,
|
||
JUMP_HEIGHT_CHARGED,
|
||
JUMP_HEIGHT_YELLOW,
|
||
JUMP_PREPARE_DELAY_MS,
|
||
PlayerColorState,
|
||
} from '@common/Constants';
|
||
|
||
function newPair(color: PlayerColorState = PlayerColorState.Red) {
|
||
const motion = new PlayerMotionModel({
|
||
aabb: { x: 0, y: 16, w: 16, h: 32 },
|
||
platforms: [{ topY: 0, leftX: -500, rightX: 500 }],
|
||
initialColorState: color,
|
||
levelLengthPx: 2000,
|
||
});
|
||
motion.update(0.016); // settle on ground
|
||
const jump = new JumpController(motion);
|
||
return { motion, jump };
|
||
}
|
||
|
||
describe('JumpController — press / release lifecycle (req 2.2, 2.3, 2.4, 2.8)', () => {
|
||
it('refuses to press when airborne (req 2.4)', () => {
|
||
const { motion, jump } = newPair();
|
||
motion.applyJumpImpulse(500); // lift off manually
|
||
const res = jump.pressJump(0);
|
||
expect(res.reason).toBe('airborne');
|
||
expect(jump.isButtonEnabled()).toBe(false);
|
||
});
|
||
|
||
it('standard-press + quick release → standard jump height', () => {
|
||
const { jump } = newPair();
|
||
jump.pressJump(0);
|
||
const res = jump.releaseJump(100, 'horizontal');
|
||
expect(res.height).toBe(JUMP_HEIGHT_STANDARD);
|
||
expect(res.phase).toBe('crouching');
|
||
});
|
||
|
||
it('holding ≥500ms produces the charged jump (req 2.3)', () => {
|
||
const { jump } = newPair();
|
||
jump.pressJump(0);
|
||
const res = jump.releaseJump(600, 'horizontal');
|
||
expect(res.height).toBe(JUMP_HEIGHT_CHARGED);
|
||
});
|
||
|
||
it('yellow color state uses 300px baseline jump (req 2.2)', () => {
|
||
const { jump } = newPair(PlayerColorState.Yellow);
|
||
jump.pressJump(0);
|
||
const res = jump.releaseJump(50, 'horizontal', PlayerColorState.Yellow);
|
||
expect(res.height).toBe(JUMP_HEIGHT_YELLOW);
|
||
});
|
||
});
|
||
|
||
describe('JumpController — parabolic impulse (req 2.5)', () => {
|
||
it('parabolic_right imparts +PARABOLIC_HORIZONTAL_SPEED', () => {
|
||
const { jump } = newPair();
|
||
jump.pressJump(0);
|
||
const res = jump.releaseJump(100, 'parabolic_right');
|
||
expect(res.horizontalImpulse).toBe(PARABOLIC_HORIZONTAL_SPEED);
|
||
});
|
||
|
||
it('parabolic_left imparts −PARABOLIC_HORIZONTAL_SPEED', () => {
|
||
const { jump } = newPair();
|
||
jump.pressJump(0);
|
||
const res = jump.releaseJump(100, 'parabolic_left');
|
||
expect(res.horizontalImpulse).toBe(-PARABOLIC_HORIZONTAL_SPEED);
|
||
});
|
||
});
|
||
|
||
describe('JumpController — crouch delay + launch + re-enable (req 2.8, 2.4)', () => {
|
||
it('does not apply impulse until after 150ms crouch delay', () => {
|
||
const { motion, jump } = newPair();
|
||
jump.pressJump(0);
|
||
jump.releaseJump(100, 'horizontal');
|
||
// Inside the crouch window — still grounded because impulse not applied.
|
||
jump.tick(200);
|
||
expect(motion.vy).toBe(0);
|
||
expect(motion.isGrounded).toBe(true);
|
||
jump.tick(100 + JUMP_PREPARE_DELAY_MS + 1);
|
||
expect(motion.vy).toBeGreaterThan(0);
|
||
expect(motion.isGrounded).toBe(false);
|
||
});
|
||
|
||
it('re-enables the jump button once the player lands again', () => {
|
||
const { motion, jump } = newPair();
|
||
jump.pressJump(0);
|
||
jump.releaseJump(50, 'horizontal');
|
||
jump.tick(50 + JUMP_PREPARE_DELAY_MS + 1);
|
||
// Let gravity bring the player back down.
|
||
for (let i = 0; i < 120; i++) motion.update(1 / 60);
|
||
jump.tick(1_000);
|
||
expect(motion.isGrounded).toBe(true);
|
||
expect(jump.isButtonEnabled()).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('heightToImpulse — physics math', () => {
|
||
it('computes impulse such that peak equals the requested height', () => {
|
||
const g = 2500;
|
||
const h = 250;
|
||
const v0 = heightToImpulse(h, g);
|
||
// At apex v=0 ⇒ t = v0/g ⇒ peak = v0 * t - 0.5 * g * t^2 ⇒ v0^2 / (2g)
|
||
expect((v0 * v0) / (2 * g)).toBeCloseTo(h, 3);
|
||
});
|
||
});
|