import { Container, Graphics, Rectangle } from 'pixi.js';
/**
* Виртуальный джойстик для тач-устройств: рисуется в uiRoot, показывает
* базовый круг и ручку. Игра читает getVector() и двигает персонажа.
* Появляется там, где игрок коснулся экрана (полупрозрачно), исчезает при отпускании.
*/
export interface VirtualJoystickOptions {
/** Радиус базового круга (виртуальные пиксели). */
radius?: number;
/** Цвета. */
color?: number;
/** Отступ от краёв экрана, за который джойстик не заходит. */
margin?: number;
/** Полные размеры экрана (виртуальные), чтобы клэмпить позицию. */
screen: { width: number; height: number };
}
export class VirtualJoystick extends Container {
private base: Graphics;
private knob: Graphics;
private radius: number;
private margin: number;
private screen: { width: number; height: number };
private color: number;
private dragging = false;
/** Текущий вектор (-1..1 по каждой оси; длина ≤ 1). */
private vector = { x: 0, y: 0 };
constructor(options: VirtualJoystickOptions) {
super();
this.radius = options.radius ?? 28;
this.margin = options.margin ?? 12;
this.screen = options.screen;
this.color = options.color ?? 0x8899aa;
this.visible = false;
this.eventMode = 'static';
this.hitArea = new Rectangle(0, 0, this.screen.width, this.screen.height);
this.base = new Graphics();
this.knob = new Graphics();
this.addChild(this.base, this.knob);
this.drawStatic();
this.on('pointerdown', (e) => this.onDown(e.global.x, e.global.y));
this.on('pointermove', (e) => {
if (this.dragging) this.onMove(e.global.x, e.global.y);
});
this.on('pointerup', () => this.release());
this.on('pointerupoutside', () => this.release());
}
/** Вектор управления; активен ли джойстик сейчас. */
getVector(): { x: number; y: number } {
return { x: this.vector.x, y: this.vector.y };
}
get active(): boolean {
return this.dragging;
}
/** Обновить размеры экрана (при ресайзе). */
setScreen(width: number, height: number): void {
this.screen = { width, height };
}
private onDown(px: number, py: number): void {
this.dragging = true;
this.visible = true;
this.reposition(px, py);
this.updateKnob(px, py);
}
private onMove(px: number, py: number): void {
this.updateKnob(px, py);
}
private release(): void {
this.dragging = false;
this.visible = false;
this.vector = { x: 0, y: 0 };
}
private reposition(px: number, py: number): void {
const x = Math.min(Math.max(px, this.margin + this.radius), this.screen.width - this.margin - this.radius);
const y = Math.min(Math.max(py, this.margin + this.radius), this.screen.height - this.margin - this.radius);
this.position.set(x, y);
}
private updateKnob(px: number, py: number): void {
let dx = px - this.x;
let dy = py - this.y;
const len = Math.hypot(dx, dy);
const max = this.radius;
if (len > max) {
dx = (dx / len) * max;
dy = (dy / len) * max;
}
this.knob.position.set(dx, dy);
this.vector = { x: dx / max, y: dy / max };
}
private drawStatic(): void {
this.base.clear();
this.base.circle(0, 0, this.radius).stroke({ color: this.color, width: 1, alpha: 0.5 });
this.base.circle(0, 0, this.radius * 0.6).fill({ color: this.color, alpha: 0.08 });
this.knob.clear();
this.knob.circle(0, 0, this.radius * 0.35).fill({ color: this.color, alpha: 0.6 });
}
}