Files
KateLegend2_proj/tests/logic/JumpController.test.ts
T
2026-05-06 08:17:32 +08:00

106 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
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);
});
});