Newer
Older
rpg / apps / game / src / systems / PlayerController.ts
import {
    World,
    IsometricTileMap,
    findPath,
    isoToScreen,
    screenToIsoExact,
    moveTo,
    Container,
    Graphics,
    DEFAULT_ISO,
    type Entity,
    type Vec2
} from '@rpg/engine';

/**
 * Управление героем: путь по клику (A*), плавное движение по маршруту.
 * Спрайт — плейсхолдер из Graphics (пока нет арта); позиция — центр ромба тайла.
 */
export class PlayerController {
    readonly entity: Entity;
    readonly view: Container;

    /** Позиция в виртуальных пикселях (источник истины, ноги героя). */
    private pos: Vec2;
    private path: { x: number; y: number }[] = [];
    private waypoint: Vec2 | null = null;
    /** Скорость в виртуальных пикселях/сек. */
    private speed = 56;

    constructor(world: World, private map: IsometricTileMap, startTile: { x: number; y: number }) {
        this.entity = world.createEntity();
        world.addComponent(this.entity, 'pos', { ...startTile });

        this.view = new Container();
        const body = new Graphics();
        // Плейсхолдер героя: тёмный плащ и шляпа звонаря.
        body.rect(-3, -9, 6, 9).fill(0x3b4a63);
        body.rect(-2, -3, 4, 3).fill(0x2a3548);
        body.rect(-3, -12, 6, 3).fill(0x8a6d4b);
        body.rect(-1, -11, 2, 1).fill(0x1c1c22);
        this.view.addChild(body);

        const p = isoToScreen(startTile.x, startTile.y);
        this.pos = { x: p.x, y: p.y + DEFAULT_ISO.tileH / 2 };
        this.view.position.set(Math.round(this.pos.x), Math.round(this.pos.y));
    }

    /** Кликом по миру выбран тайл — строим путь A* и начинаем движение. */
    onWorldClick(worldX: number, worldY: number): void {
        const tile = screenToIsoExact(
            worldX,
            worldY,
            this.map.data.width,
            this.map.data.height,
            DEFAULT_ISO
        );
        if (!tile) return;
        const path = findPath(this.map, this.currentTile(), tile, false);
        if (path && path.length > 0) {
            this.path = path;
            this.advanceWaypoint();
        }
    }

    update(dt: number): void {
        if (!this.waypoint) return;
        this.pos = moveTo(this.pos, this.waypoint, this.speed * dt);
        if (this.pos.x === this.waypoint.x && this.pos.y === this.waypoint.y) {
            this.advanceWaypoint();
        }
        this.view.position.set(Math.round(this.pos.x), Math.round(this.pos.y));
    }

    get moving(): boolean {
        return this.waypoint !== null;
    }

    /** Текущий тайл героя. */
    currentTile(): { x: number; y: number } {
        return (
            screenToIsoExact(
                this.pos.x,
                this.pos.y,
                this.map.data.width,
                this.map.data.height,
                DEFAULT_ISO
            ) ?? { x: 0, y: 0 }
        );
    }

    private advanceWaypoint(): void {
        const next = this.path.shift();
        if (next) {
            const p = isoToScreen(next.x, next.y);
            this.waypoint = { x: p.x, y: p.y + DEFAULT_ISO.tileH / 2 };
        } else {
            this.waypoint = null;
        }
    }
}