You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
import {
|
|
CAMERA_FOLLOW_LERP,
|
|
CAMERA_LERP_REFERENCE_MS,
|
|
SHAKE_MAGNITUDE,
|
|
SHAKE_DURATION_MS,
|
|
} from '../shared/constants';
|
|
|
|
/**
|
|
* Camera controller with soft follow and screen shake.
|
|
*
|
|
* The camera tracks a target position (the hero's screen-space position)
|
|
* and applies a smooth lerp to follow. Screen shake is applied as an
|
|
* additive offset that decays over time.
|
|
*/
|
|
export class Camera {
|
|
/** Current camera position (center of view in world-screen space) */
|
|
x = 0;
|
|
y = 0;
|
|
|
|
/** Target position to follow */
|
|
private targetX = 0;
|
|
private targetY = 0;
|
|
|
|
/** Shake state */
|
|
private shakeTimeRemaining = 0;
|
|
private shakeMagnitude = SHAKE_MAGNITUDE;
|
|
private shakeOffsetX = 0;
|
|
private shakeOffsetY = 0;
|
|
|
|
/** Lerp factor (0..1) */
|
|
private lerpFactor: number;
|
|
|
|
constructor(lerpFactor = CAMERA_FOLLOW_LERP) {
|
|
this.lerpFactor = lerpFactor;
|
|
}
|
|
|
|
/** Set the target position for the camera to follow */
|
|
setTarget(x: number, y: number): void {
|
|
this.targetX = x;
|
|
this.targetY = y;
|
|
}
|
|
|
|
/** Snap the camera instantly to the target (no lerp) */
|
|
snapToTarget(): void {
|
|
this.x = this.targetX;
|
|
this.y = this.targetY;
|
|
}
|
|
|
|
/** Trigger a screen shake effect (e.g., on hit) */
|
|
shake(magnitude = SHAKE_MAGNITUDE, durationMs = SHAKE_DURATION_MS): void {
|
|
this.shakeMagnitude = magnitude;
|
|
this.shakeTimeRemaining = durationMs;
|
|
}
|
|
|
|
/** Update camera position. Call once per frame with delta time in ms. */
|
|
update(dtMs: number): void {
|
|
// Exponential smoothing; same factor as legacy per-frame lerp at ~60 Hz reference.
|
|
const k =
|
|
1 -
|
|
Math.pow(1 - this.lerpFactor, dtMs / CAMERA_LERP_REFERENCE_MS);
|
|
this.x += (this.targetX - this.x) * k;
|
|
this.y += (this.targetY - this.y) * k;
|
|
|
|
// Update screen shake
|
|
if (this.shakeTimeRemaining > 0) {
|
|
this.shakeTimeRemaining -= dtMs;
|
|
const intensity = Math.max(0, this.shakeTimeRemaining / SHAKE_DURATION_MS);
|
|
const mag = this.shakeMagnitude * intensity;
|
|
this.shakeOffsetX = (Math.random() * 2 - 1) * mag;
|
|
this.shakeOffsetY = (Math.random() * 2 - 1) * mag;
|
|
} else {
|
|
this.shakeOffsetX = 0;
|
|
this.shakeOffsetY = 0;
|
|
}
|
|
}
|
|
|
|
/** Get the final camera X including shake offset */
|
|
get finalX(): number {
|
|
return this.x + this.shakeOffsetX;
|
|
}
|
|
|
|
/** Get the final camera Y including shake offset */
|
|
get finalY(): number {
|
|
return this.y + this.shakeOffsetY;
|
|
}
|
|
|
|
/**
|
|
* Apply camera transform to a PixiJS container.
|
|
* The container is shifted so the camera target appears at screen center.
|
|
*/
|
|
applyTo(container: { x: number; y: number }, screenWidth: number, screenHeight: number): void {
|
|
container.x = Math.round(screenWidth / 2 - this.finalX);
|
|
container.y = Math.round(screenHeight / 2 - this.finalY);
|
|
}
|
|
}
|