207 lines
5.4 KiB
JavaScript
207 lines
5.4 KiB
JavaScript
/**
|
|
* Joystick.js
|
|
* Virtual joystick component for touch-based directional control.
|
|
* Positioned at the bottom-left of the screen; overlaid as a separate layer
|
|
* above the map when they intersect.
|
|
*/
|
|
|
|
const {
|
|
DIRECTION,
|
|
SCREEN_WIDTH,
|
|
SCREEN_HEIGHT,
|
|
} = require('../base/GameGlobal');
|
|
|
|
class Joystick {
|
|
constructor() {
|
|
// Position and size — anchored to screen bottom-left, shifted slightly
|
|
// towards the upper-right for comfortable thumb reach
|
|
this.radius = 50;
|
|
this.innerRadius = 20;
|
|
const padding = this.radius + 30;
|
|
this.cx = padding + 15; // left edge + rightward offset
|
|
this.cy = SCREEN_HEIGHT - padding - 30; // bottom edge + upward offset
|
|
|
|
// State
|
|
this._active = false;
|
|
this._touchId = null;
|
|
this._touchX = 0;
|
|
this._touchY = 0;
|
|
this._direction = -1; // -1 = no direction
|
|
this._dx = 0;
|
|
this._dy = 0;
|
|
|
|
// Touch area (larger than visual for easier use)
|
|
this._touchAreaRadius = this.radius * 2;
|
|
}
|
|
|
|
/**
|
|
* Handle touch events.
|
|
* @param {string} eventType
|
|
* @param {Touch} touch - Single touch object.
|
|
* @returns {boolean} Whether this joystick consumed the touch.
|
|
*/
|
|
handleTouch(eventType, touch) {
|
|
const tx = touch.clientX;
|
|
const ty = touch.clientY;
|
|
|
|
if (eventType === 'touchstart') {
|
|
// Check if touch is within joystick area
|
|
const dist = Math.sqrt((tx - this.cx) ** 2 + (ty - this.cy) ** 2);
|
|
if (dist <= this._touchAreaRadius) {
|
|
this._active = true;
|
|
this._touchId = touch.identifier;
|
|
this._updateDirection(tx, ty);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (eventType === 'touchmove') {
|
|
if (this._active && touch.identifier === this._touchId) {
|
|
this._updateDirection(tx, ty);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (eventType === 'touchend') {
|
|
if (this._active && touch.identifier === this._touchId) {
|
|
this._active = false;
|
|
this._touchId = null;
|
|
this._direction = -1;
|
|
this._dx = 0;
|
|
this._dy = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculate direction from touch position.
|
|
* @private
|
|
*/
|
|
_updateDirection(tx, ty) {
|
|
this._touchX = tx;
|
|
this._touchY = ty;
|
|
|
|
const dx = tx - this.cx;
|
|
const dy = ty - this.cy;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < 10) {
|
|
this._direction = -1;
|
|
this._dx = 0;
|
|
this._dy = 0;
|
|
return;
|
|
}
|
|
|
|
// Clamp to radius for visual
|
|
const clampDist = Math.min(dist, this.radius);
|
|
this._dx = (dx / dist) * clampDist;
|
|
this._dy = (dy / dist) * clampDist;
|
|
|
|
// Determine 4-direction based on angle
|
|
const angle = Math.atan2(dy, dx);
|
|
// Right: -45° to 45°, Down: 45° to 135°, Left: 135° to -135°, Up: -135° to -45°
|
|
if (angle >= -Math.PI / 4 && angle < Math.PI / 4) {
|
|
this._direction = DIRECTION.RIGHT;
|
|
} else if (angle >= Math.PI / 4 && angle < Math.PI * 3 / 4) {
|
|
this._direction = DIRECTION.DOWN;
|
|
} else if (angle >= -Math.PI * 3 / 4 && angle < -Math.PI / 4) {
|
|
this._direction = DIRECTION.UP;
|
|
} else {
|
|
this._direction = DIRECTION.LEFT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the joystick.
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
render(ctx) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.3;
|
|
|
|
// Outer circle
|
|
ctx.fillStyle = '#333333';
|
|
ctx.strokeStyle = '#666666';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// Direction indicators
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.fillStyle = '#FFFFFF';
|
|
const arrowSize = 8;
|
|
// Up arrow
|
|
this._drawArrow(ctx, this.cx, this.cy - this.radius * 0.6, DIRECTION.UP, arrowSize);
|
|
// Down arrow
|
|
this._drawArrow(ctx, this.cx, this.cy + this.radius * 0.6, DIRECTION.DOWN, arrowSize);
|
|
// Left arrow
|
|
this._drawArrow(ctx, this.cx - this.radius * 0.6, this.cy, DIRECTION.LEFT, arrowSize);
|
|
// Right arrow
|
|
this._drawArrow(ctx, this.cx + this.radius * 0.6, this.cy, DIRECTION.RIGHT, arrowSize);
|
|
|
|
// Inner knob
|
|
ctx.globalAlpha = 0.6;
|
|
const knobX = this._active ? this.cx + this._dx : this.cx;
|
|
const knobY = this._active ? this.cy + this._dy : this.cy;
|
|
|
|
ctx.fillStyle = this._active ? '#FFD700' : '#888888';
|
|
ctx.beginPath();
|
|
ctx.arc(knobX, knobY, this.innerRadius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Draw a small directional arrow.
|
|
* @private
|
|
*/
|
|
_drawArrow(ctx, x, y, dir, size) {
|
|
ctx.beginPath();
|
|
switch (dir) {
|
|
case DIRECTION.UP:
|
|
ctx.moveTo(x, y - size);
|
|
ctx.lineTo(x - size, y + size);
|
|
ctx.lineTo(x + size, y + size);
|
|
break;
|
|
case DIRECTION.DOWN:
|
|
ctx.moveTo(x, y + size);
|
|
ctx.lineTo(x - size, y - size);
|
|
ctx.lineTo(x + size, y - size);
|
|
break;
|
|
case DIRECTION.LEFT:
|
|
ctx.moveTo(x - size, y);
|
|
ctx.lineTo(x + size, y - size);
|
|
ctx.lineTo(x + size, y + size);
|
|
break;
|
|
case DIRECTION.RIGHT:
|
|
ctx.moveTo(x + size, y);
|
|
ctx.lineTo(x - size, y - size);
|
|
ctx.lineTo(x - size, y + size);
|
|
break;
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
/** Current direction (-1 if idle). */
|
|
get direction() {
|
|
return this._direction;
|
|
}
|
|
|
|
/** Whether the joystick is being touched. */
|
|
get active() {
|
|
return this._active;
|
|
}
|
|
}
|
|
|
|
module.exports = Joystick;
|