107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
import { PlayerMotionModel, IPlatform, DEFAULT_GRAVITY } from '@logic/PlayerMotionModel';
|
|
import { MOVE_SPEED, PlayerColorState } from '@common/Constants';
|
|
|
|
function makeGroundPlatform(): IPlatform {
|
|
return { topY: 0, leftX: -1000, rightX: 1000 };
|
|
}
|
|
|
|
function makeModel(color: PlayerColorState = PlayerColorState.Red) {
|
|
return new PlayerMotionModel({
|
|
aabb: { x: 0, y: 16, w: 16, h: 32 }, // character 16x32 resting on y=0 ground
|
|
platforms: [makeGroundPlatform()],
|
|
initialColorState: color,
|
|
});
|
|
}
|
|
|
|
describe('PlayerMotionModel — horizontal movement (req 2.1, 5.1-5.2)', () => {
|
|
it('stands still on initialisation', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
expect(m.vx).toBe(0);
|
|
expect(m.isGrounded).toBe(true);
|
|
});
|
|
|
|
it('moves at 100 px/s in red state', () => {
|
|
const m = makeModel(PlayerColorState.Red);
|
|
m.update(0.016); // settle
|
|
m.setHorizontalInput(1);
|
|
m.update(1);
|
|
expect(m.vx).toBe(MOVE_SPEED[PlayerColorState.Red]);
|
|
expect(m.aabb.x).toBeCloseTo(100, 1);
|
|
});
|
|
|
|
it('moves at 150 px/s in yellow state', () => {
|
|
const m = makeModel(PlayerColorState.Yellow);
|
|
m.update(0.016);
|
|
m.setHorizontalInput(-1);
|
|
m.update(1);
|
|
expect(m.vx).toBe(-MOVE_SPEED[PlayerColorState.Yellow]);
|
|
});
|
|
|
|
it('reflects speed immediately when setColorState is called mid-run', () => {
|
|
const m = makeModel(PlayerColorState.Red);
|
|
m.update(0.016);
|
|
m.setHorizontalInput(1);
|
|
m.setColorState(PlayerColorState.Yellow);
|
|
m.update(1);
|
|
expect(m.vx).toBe(150);
|
|
});
|
|
});
|
|
|
|
describe('PlayerMotionModel — jump / gravity (req 2.4, 13.4)', () => {
|
|
it('applyJumpImpulse is rejected when in the air (req 2.4)', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
expect(m.applyJumpImpulse(600)).toBe(true); // first jump succeeds
|
|
expect(m.isGrounded).toBe(false);
|
|
expect(m.applyJumpImpulse(600)).toBe(false); // second jump in air denied
|
|
});
|
|
|
|
it('gravity pulls the player back to the ground', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
m.applyJumpImpulse(600);
|
|
// Simulate ~1 second of flight — gravity reclaims the player.
|
|
for (let i = 0; i < 120; i++) m.update(1 / 60);
|
|
expect(m.isGrounded).toBe(true);
|
|
expect(m.vy).toBe(0);
|
|
});
|
|
|
|
it('preserves mid-air vx (起跳定型 — req 13.4)', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
// Build horizontal speed + jump.
|
|
m.setHorizontalInput(1);
|
|
m.update(0.016);
|
|
m.applyJumpImpulse(500);
|
|
const airVx = m.vx;
|
|
// Even if the player now tries to flip direction while airborne, vx stays put.
|
|
m.setHorizontalInput(-1);
|
|
m.update(0.05);
|
|
expect(m.vx).toBe(airVx);
|
|
});
|
|
|
|
it('applyHorizontalImpulse overrides vx (for parabolic jumps, req 2.5)', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
m.applyJumpImpulse(600);
|
|
m.applyHorizontalImpulse(120);
|
|
m.update(0.016);
|
|
expect(m.vx).toBe(120);
|
|
});
|
|
});
|
|
|
|
describe('PlayerMotionModel — platform switching', () => {
|
|
it('setPlatforms clears grounded and re-settles on next update', () => {
|
|
const m = makeModel();
|
|
m.update(0.016);
|
|
expect(m.isGrounded).toBe(true);
|
|
m.setPlatforms([{ topY: -500, leftX: -10, rightX: 10 }]);
|
|
expect(m.isGrounded).toBe(false);
|
|
});
|
|
|
|
it('gravity constant matches the documented default', () => {
|
|
expect(DEFAULT_GRAVITY).toBe(2500);
|
|
});
|
|
});
|