99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
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,
|
|
});
|
|
}
|