import { ILevelConfig, ScrollDirection } from '../data/Interfaces'; /** * Camera-scrolling model (task 7.1). * * Captures the level's camera/scrolling state without depending on `cc`. The * Cocos view layer maps `CameraScroller.offsetX / offsetY` into a `Camera` * component position every frame. * * Supported scroll modes (req 8.1-8.5, 8.8): * - `horizontal` — scroll never rewinds (森林/魔城). * - `horizontal_bi` — left/right both allowed (洞穴水路). * - `vertical` — scrolls upward as the player climbs (城壁). * * Values are in **landscape design pixels** (960x540 baseline). */ export interface ICameraConfig { /** Scroll direction, mirrors `ILevelConfig.scrollDirection`. */ direction: ScrollDirection; /** Horizontal level length (for `horizontal` and `horizontal_bi`). */ lengthX: number; /** Vertical level length (for `vertical`). */ lengthY?: number; /** Camera viewport (design px). */ viewportW: number; viewportH: number; } /** Four-layer parallax scroller (req 8.8). Speed ratios 1 : 2 : 4 : 4. */ export const PARALLAX_RATIOS = [1, 2, 4, 4] as const; export type ParallaxLayer = 'far' | 'mid' | 'near' | 'fx'; export const PARALLAX_LAYERS: ParallaxLayer[] = ['far', 'mid', 'near', 'fx']; export class CameraScroller { private _offsetX = 0; private _offsetY = 0; private readonly cfg: ICameraConfig; constructor(cfg: ICameraConfig) { this.cfg = cfg; } public get offsetX(): number { return this._offsetX; } public get offsetY(): number { return this._offsetY; } /** Camera target follows the player but never rewinds on `horizontal`. */ public followPlayer(playerX: number, playerY: number): void { const halfW = this.cfg.viewportW / 2; const halfH = this.cfg.viewportH / 2; if (this.cfg.direction === 'horizontal') { const desired = Math.max(0, playerX - halfW); this._offsetX = Math.min( Math.max(this._offsetX, desired), Math.max(0, this.cfg.lengthX - this.cfg.viewportW) ); } else if (this.cfg.direction === 'horizontal_bi') { const desired = Math.max(0, playerX - halfW); this._offsetX = Math.min(desired, Math.max(0, this.cfg.lengthX - this.cfg.viewportW)); } else if (this.cfg.direction === 'vertical') { const ly = this.cfg.lengthY ?? this.cfg.viewportH; const desiredY = Math.max(0, playerY - halfH); this._offsetY = Math.min(desiredY, Math.max(0, ly - this.cfg.viewportH)); } } /** Compute the world offset for a given parallax layer. */ public offsetForLayer(layer: ParallaxLayer): { x: number; y: number } { const ratio = PARALLAX_RATIOS[PARALLAX_LAYERS.indexOf(layer)]; return { x: this._offsetX / ratio, y: this._offsetY / ratio }; } /** Return the level's culling rect in world coordinates. */ public cullRect(): { leftX: number; rightX: number; topY: number; bottomY: number } { return { leftX: this._offsetX, rightX: this._offsetX + this.cfg.viewportW, topY: this._offsetY + this.cfg.viewportH, bottomY: this._offsetY, }; } } /** Build a CameraScroller from a level config. */ export function cameraFromLevel(level: ILevelConfig, viewportW = 960, viewportH = 540): CameraScroller { return new CameraScroller({ direction: level.scrollDirection, lengthX: level.levelLengthPx, lengthY: level.scrollDirection === 'vertical' ? level.levelLengthPx : undefined, viewportW, viewportH, }); }