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); }); });