Files
KateLegend2_proj/assets/scripts/logic/CameraScroller.ts
T
2026-05-06 08:17:32 +08:00

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