import { Container } from 'pixi.js';
import { Shake } from './shake';
/**
* Камера виртуального разрешения: позиция — центр взгляда в мировых (виртуальных) пикселях.
* Округляет смещение до целого пикселя, чтобы пиксель-арт не «дрожал».
* Границы прокрутки — прямоугольник мира (может начинаться с отрицательных координат,
* например у изометрической карты, «ромб» которой уходит в минус по X).
* Поддерживает тряску (addShake) — движок вызывает update(dt) каждый тик.
*/
export interface CameraBounds {
x: number;
y: number;
width: number;
height: number;
}
export class Camera {
x = 0;
y = 0;
bounds: CameraBounds | null = null;
/**
* «Мёртвая зона» (виртуальные границы экрана): пока цель внутри окна,
* камера стоит; у края окна цель толкает камеру на величину выхода.
* null — камера всегда центрируется на цели.
*/
deadZone: { width: number; height: number } | null = null;
private readonly shake = new Shake();
constructor(
private readonly viewWidth: number,
private readonly viewHeight: number
) {}
follow(targetX: number, targetY: number): void {
const dz = this.deadZone;
if (dz) {
// Двигаем каждую ось только на величину выхода цели за окно.
const hw = dz.width / 2;
const hh = dz.height / 2;
const dxMin = this.x - hw;
const dxMax = this.x + hw;
if (targetX < dxMin) this.x -= dxMin - targetX;
else if (targetX > dxMax) this.x += targetX - dxMax;
const dyMin = this.y - hh;
const dyMax = this.y + hh;
if (targetY < dyMin) this.y -= dyMin - targetY;
else if (targetY > dyMax) this.y += targetY - dyMax;
} else {
this.x = targetX;
this.y = targetY;
}
this.clamp();
}
/** Мгновенно поставить камеру на точку (спавн, телепорт, смена локации). */
snap(targetX: number, targetY: number): void {
const dz = this.deadZone;
this.deadZone = null;
this.follow(targetX, targetY);
this.deadZone = dz;
}
/** Запустить толчок: амплитуда в пикселях, длительность в секундах. */
addShake(strength: number, duration: number): void {
this.shake.add(strength, duration);
}
clearShake(): void {
this.shake.clear();
}
get shaking(): boolean {
return this.shake.active;
}
/** Обновление тряски — движок вызывает каждый фиксированный шаг. */
update(dt: number): void {
this.shake.update(dt);
}
apply(container: Container): void {
const o = this.shake.offset;
const halfW = Math.floor(this.viewWidth / 2);
const halfH = Math.floor(this.viewHeight / 2);
container.position.set(
halfW - Math.round(this.x) + o.x,
halfH - Math.round(this.y) + o.y
);
}
private clamp(): void {
if (!this.bounds) return;
const halfW = this.viewWidth / 2;
const halfH = this.viewHeight / 2;
const b = this.bounds;
if (b.width > this.viewWidth) {
this.x = Math.min(Math.max(this.x, b.x + halfW), b.x + b.width - halfW);
} else {
this.x = b.x + b.width / 2;
}
if (b.height > this.viewHeight) {
this.y = Math.min(Math.max(this.y, b.y + halfH), b.y + b.height - halfH);
} else {
this.y = b.y + b.height / 2;
}
}
}