first commmit

This commit is contained in:
jakciehan
2026-05-06 08:17:32 +08:00
commit 427a33c55b
269 changed files with 20906 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
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);
});
});