import {
IsometricTileMap,
findPath,
isoToScreen,
screenToIsoExact,
moveTo,
FrameAnimation,
Container,
Sprite,
Texture,
DEFAULT_ISO,
type Vec2
} from '@rpg/engine';
/** Кадры героя по направлениям. side смотрит влево; вправо — флип. */
export interface HeroTextures {
down: [Texture, Texture];
up: [Texture, Texture];
side: [Texture, Texture];
}
type Facing = 'down' | 'up' | 'left' | 'right';
/**
* Управление героем: путь по клику (A*), плавное движение по маршруту,
* покадровая анимация ходьбы по направлению движения.
*/
export class PlayerController {
readonly view: Container;
private sprite: Sprite;
private anim: FrameAnimation;
private facing: Facing = 'down';
private textures: HeroTextures;
/** Позиция в виртуальных пикселях (источник истины, ноги героя). */
private pos: Vec2;
private path: { x: number; y: number }[] = [];
private waypoint: Vec2 | null = null;
/** Скорость в виртуальных пикселях/сек. */
private speed = 56;
constructor(
private map: IsometricTileMap,
textures: HeroTextures,
startTile: { x: number; y: number }
) {
this.textures = textures;
this.view = new Container();
this.sprite = new Sprite(textures.down[0]);
this.sprite.anchor.set(0.5, 1); // ноги в точке позиции
this.view.addChild(this.sprite);
this.anim = new FrameAnimation(this.sprite, textures.down, 6, true);
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();
return;
}
this.faceMovement();
this.anim.update(dt);
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 faceMovement(): void {
if (!this.waypoint) return;
const dx = this.waypoint.x - this.pos.x;
const dy = this.waypoint.y - this.pos.y;
let facing: Facing = this.facing;
if (Math.abs(dx) >= Math.abs(dy)) {
facing = dx >= 0 ? 'right' : 'left';
} else {
facing = dy >= 0 ? 'down' : 'up';
}
if (facing !== this.facing) {
this.facing = facing;
const frames =
facing === 'down'
? this.textures.down
: facing === 'up'
? this.textures.up
: this.textures.side;
this.anim.setFrames([...frames], false);
this.sprite.scale.x = facing === 'right' ? -1 : 1;
}
}
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;
}
}
}