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

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