Newer
Older
rpg / packages / engine / src / render / Camera.ts
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;
    private readonly shake = new Shake();

    constructor(
        private readonly viewWidth: number,
        private readonly viewHeight: number
    ) {}

    follow(targetX: number, targetY: number): void {
        this.x = targetX;
        this.y = targetY;
        this.clamp();
    }

    /** Запустить толчок: амплитуда в пикселях, длительность в секундах. */
    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;
        }
    }
}