import { CameraScroller, PARALLAX_LAYERS, PARALLAX_RATIOS, cameraFromLevel } from '@logic/CameraScroller'; import { LevelMgr } from '@logic/LevelMgr'; import { EnemyType, ILevelConfig } from '@data/Interfaces'; const HORIZONTAL_LEVEL: ILevelConfig = { id: '1-1', chapter: 1, displayName: '初始森林', sceneTheme: 'forest', scrollDirection: 'horizontal', timeLimitSec: 75, objective: { kind: 'kill_count', enemy: EnemyType.YaoFang, count: 3 }, levelLengthPx: 3840, bgm: 'bgm_forest', enemySpawns: [], }; describe('CameraScroller — horizontal (req 8.1)', () => { it('camera scrolls forward as player advances', () => { const cam = cameraFromLevel(HORIZONTAL_LEVEL, 960, 540); cam.followPlayer(480, 270); expect(cam.offsetX).toBe(0); cam.followPlayer(900, 270); expect(cam.offsetX).toBe(420); }); it('camera never rewinds on horizontal scroll (req 8.1)', () => { const cam = cameraFromLevel(HORIZONTAL_LEVEL); cam.followPlayer(1500, 270); const forward = cam.offsetX; cam.followPlayer(100, 270); expect(cam.offsetX).toBe(forward); // did not rewind }); it('camera stops at level end', () => { const cam = cameraFromLevel(HORIZONTAL_LEVEL); cam.followPlayer(10_000, 270); expect(cam.offsetX).toBe(HORIZONTAL_LEVEL.levelLengthPx - 960); }); }); describe('CameraScroller — parallax layers (req 8.8 — 1:2:4:4)', () => { it('exposes 4 layers with ratios 1,2,4,4', () => { expect(PARALLAX_LAYERS).toEqual(['far', 'mid', 'near', 'fx']); expect([...PARALLAX_RATIOS]).toEqual([1, 2, 4, 4]); }); it('far/mid/near produce progressively smaller offsets', () => { const cam = cameraFromLevel(HORIZONTAL_LEVEL); cam.followPlayer(1200, 270); const far = cam.offsetForLayer('far'); const mid = cam.offsetForLayer('mid'); const near = cam.offsetForLayer('near'); expect(far.x).toBeGreaterThan(mid.x); expect(mid.x).toBeGreaterThan(near.x); }); }); describe('CameraScroller — bi-directional + vertical', () => { it('horizontal_bi rewinds when the player walks backward', () => { const cam = new CameraScroller({ direction: 'horizontal_bi', lengthX: 4800, viewportW: 960, viewportH: 540, }); cam.followPlayer(2000, 0); cam.followPlayer(400, 0); expect(cam.offsetX).toBe(0); }); it('vertical rising clamps at top', () => { const cam = new CameraScroller({ direction: 'vertical', lengthX: 960, lengthY: 3240, viewportW: 960, viewportH: 540, }); cam.followPlayer(480, 6000); expect(cam.offsetY).toBe(3240 - 540); }); }); describe('LevelMgr — objective / timer / result', () => { it('reports victory when kill objective is met', () => { const lv = new LevelMgr(HORIZONTAL_LEVEL); lv.onEnemyKilled(EnemyType.YaoFang); lv.onEnemyKilled(EnemyType.YaoFang); lv.onEnemyKilled(EnemyType.YaoFang); expect(lv.tick(0.016)).toBe('victory'); }); it('reports timeout when time-limit expires', () => { const lv = new LevelMgr(HORIZONTAL_LEVEL); expect(lv.tick(HORIZONTAL_LEVEL.timeLimitSec + 0.1)).toBe('timeout'); }); it('player_dead is terminal and outranks victory', () => { const lv = new LevelMgr(HORIZONTAL_LEVEL); lv.onPlayerDied(); expect(lv.tick(0.1)).toBe('player_dead'); }); it('reach_top objective', () => { const cfg: ILevelConfig = { ...HORIZONTAL_LEVEL, objective: { kind: 'reach_top' } }; const lv = new LevelMgr(cfg); lv.onReachedTop(); expect(lv.tick(0.016)).toBe('victory'); }); it('defeat_boss objective', () => { const cfg: ILevelConfig = { ...HORIZONTAL_LEVEL, objective: { kind: 'defeat_boss', bossId: 'shuang_huan_fang' }, }; const lv = new LevelMgr(cfg); lv.onBossKilled(); expect(lv.tick(0.016)).toBe('victory'); }); it('result() returns kills and remaining seconds', () => { const lv = new LevelMgr(HORIZONTAL_LEVEL); lv.onEnemyKilled(EnemyType.QingRen); lv.tick(10); const r = lv.result(); expect(r.kills[EnemyType.QingRen]).toBe(1); expect(r.remainingSec).toBeCloseTo(65, 1); }); });