Add tests
This commit is contained in:
parent
d0265ad90e
commit
b6db7e8dc7
18 changed files with 494 additions and 109 deletions
84
frontend/src/scripts/screen-shake.ts
Normal file
84
frontend/src/scripts/screen-shake.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Camera impact effects fed by combat hits, sampled by the camera at render
|
||||
// time. Kept as a process-wide singleton so any view can feed it without
|
||||
// threading a camera reference through the deserialized object graph. Both
|
||||
// effects touch only the *rendered* view (centre offset + view-area zoom), never
|
||||
// the followed position or the simulation clock — so they can't drift the camera
|
||||
// off the player, and they're completely decoupled from prediction/netcode.
|
||||
//
|
||||
// Two effects:
|
||||
// - shake: a trauma model (Eiserloh) — the felt shake is trauma SQUARED so
|
||||
// small taps stay subtle while a kill punches hard; it bleeds off linearly.
|
||||
// - punch: a brief zoom-in on a kill that snaps back fast, for "weight".
|
||||
//
|
||||
// Honours prefers-reduced-motion: callers can fire freely and it simply no-ops
|
||||
// for players who have asked the OS to minimise motion.
|
||||
export abstract class ScreenShake {
|
||||
private static trauma = 0;
|
||||
private static punch = 0;
|
||||
|
||||
// Peak translation, in world units, at full trauma. The visible world is
|
||||
// ~3800 units wide, so this tops out at a few percent of the screen.
|
||||
private static readonly maxTranslation = 150;
|
||||
// Fraction the view zooms in at full punch (smaller view area = zoomed in).
|
||||
private static readonly maxZoom = 0.07;
|
||||
private static readonly traumaDecayPerSecond = 1.7;
|
||||
// Snaps back quickly so the punch reads as a sharp bite, not a slow drift.
|
||||
private static readonly punchDecayPerSecond = 6;
|
||||
|
||||
private static offsetXValue = 0;
|
||||
private static offsetYValue = 0;
|
||||
|
||||
private static get reducedMotion(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.matchMedia === 'function' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
}
|
||||
|
||||
public static add(trauma: number): void {
|
||||
if (this.reducedMotion) {
|
||||
return;
|
||||
}
|
||||
this.trauma = Math.min(1, this.trauma + trauma);
|
||||
}
|
||||
|
||||
public static addPunch(amount: number): void {
|
||||
if (this.reducedMotion) {
|
||||
return;
|
||||
}
|
||||
this.punch = Math.min(1, this.punch + amount);
|
||||
}
|
||||
|
||||
public static step(deltaTimeInSeconds: number): void {
|
||||
this.trauma = Math.max(
|
||||
0,
|
||||
this.trauma - this.traumaDecayPerSecond * deltaTimeInSeconds,
|
||||
);
|
||||
this.punch = Math.max(0, this.punch - this.punchDecayPerSecond * deltaTimeInSeconds);
|
||||
const shake = this.trauma * this.trauma;
|
||||
this.offsetXValue = this.maxTranslation * shake * (Math.random() * 2 - 1);
|
||||
this.offsetYValue = this.maxTranslation * shake * (Math.random() * 2 - 1);
|
||||
}
|
||||
|
||||
public static get offsetX(): number {
|
||||
return this.offsetXValue;
|
||||
}
|
||||
|
||||
public static get offsetY(): number {
|
||||
return this.offsetYValue;
|
||||
}
|
||||
|
||||
// Multiplier for the view-area size: <1 zooms in. Squared so the punch bites
|
||||
// sharply near its peak rather than ramping linearly.
|
||||
public static get viewScale(): number {
|
||||
return 1 - this.maxZoom * this.punch * this.punch;
|
||||
}
|
||||
|
||||
public static reset(): void {
|
||||
this.trauma = 0;
|
||||
this.punch = 0;
|
||||
this.offsetXValue = 0;
|
||||
this.offsetYValue = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue