/** * DragDropHandler.js - Handles drag-and-drop and click-to-move interactions * Provides both desktop and mobile-friendly move input */ export class DragDropHandler { constructor(game, renderer) { this.game = game; this.renderer = renderer; this.enabled = true; this.draggedPiece = null; this.selectedPiece = null; } /** * Setup all event listeners */ setupEventListeners() { const board = this.renderer.boardElement; // Drag and drop events board.addEventListener('dragstart', (e) => this.onDragStart(e)); board.addEventListener('dragover', (e) => this.onDragOver(e)); board.addEventListener('drop', (e) => this.onDrop(e)); board.addEventListener('dragend', (e) => this.onDragEnd(e)); // Click events (for click-to-move and mobile) board.addEventListener('click', (e) => this.onClick(e)); // Touch events for mobile board.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: false }); board.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: false }); board.addEventListener('touchend', (e) => this.onTouchEnd(e)); } /** * Handle drag start * @param {DragEvent} e - Drag event */ onDragStart(e) { if (!this.enabled) return; const pieceEl = e.target; if (!pieceEl.classList.contains('piece')) return; const square = pieceEl.parentElement; const row = parseInt(square.dataset.row); const col = parseInt(square.dataset.col); const piece = this.game.board.getPiece(row, col); // Only allow dragging pieces of current turn if (!piece || piece.color !== this.game.currentTurn) { e.preventDefault(); return; } e.dataTransfer.setData('text/plain', JSON.stringify({ row, col })); e.dataTransfer.effectAllowed = 'move'; this.draggedPiece = { piece, row, col }; // Highlight legal moves const legalMoves = this.game.getLegalMoves(piece); this.renderer.highlightMoves(legalMoves); // Add dragging class pieceEl.classList.add('dragging'); } /** * Handle drag over * @param {DragEvent} e - Drag event */ onDragOver(e) { if (!this.enabled) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; // Highlight drop target const square = e.target.closest('.square'); if (square) { this.renderer.boardElement.querySelectorAll('.drop-target').forEach(s => { s.classList.remove('drop-target'); }); square.classList.add('drop-target'); } } /** * Handle drop * @param {DragEvent} e - Drag event */ onDrop(e) { if (!this.enabled) return; e.preventDefault(); const square = e.target.closest('.square'); if (!square) return; const toRow = parseInt(square.dataset.row); const toCol = parseInt(square.dataset.col); let from; try { from = JSON.parse(e.dataTransfer.getData('text/plain')); } catch (err) { return; } // Attempt move const result = this.game.makeMove(from.row, from.col, toRow, toCol); if (result.success) { // Re-render board this.renderer.renderBoard(this.game.board, this.game.gameState); // Show check indicator if in check if (result.gameStatus === 'check') { this.renderer.showCheckIndicator(this.game.currentTurn, this.game.board); } } else { // Show error feedback this.showError(result.error); } } /** * Handle drag end * @param {DragEvent} e - Drag event */ onDragEnd(e) { if (!this.enabled) return; // Clean up const pieceEl = e.target; pieceEl.classList.remove('dragging'); this.renderer.clearHighlights(); this.renderer.boardElement.querySelectorAll('.drop-target').forEach(s => { s.classList.remove('drop-target'); }); this.draggedPiece = null; } /** * Handle click (for click-to-move) * @param {MouseEvent} e - Click event */ onClick(e) { if (!this.enabled) return; const square = e.target.closest('.square'); if (!square) return; const row = parseInt(square.dataset.row); const col = parseInt(square.dataset.col); if (!this.selectedPiece) { // First click - select piece const piece = this.game.board.getPiece(row, col); if (piece && piece.color === this.game.currentTurn) { this.selectedPiece = { piece, row, col }; this.renderer.selectSquare(row, col); // Show legal moves const legalMoves = this.game.getLegalMoves(piece); this.renderer.highlightMoves(legalMoves); } } else { // Second click - attempt move const result = this.game.makeMove( this.selectedPiece.row, this.selectedPiece.col, row, col ); if (result.success) { // Re-render board this.renderer.renderBoard(this.game.board, this.game.gameState); // Show check indicator if in check if (result.gameStatus === 'check') { this.renderer.showCheckIndicator(this.game.currentTurn, this.game.board); } } else { // Check if clicking another piece of same color const newPiece = this.game.board.getPiece(row, col); if (newPiece && newPiece.color === this.game.currentTurn) { // Select new piece this.selectedPiece = { piece: newPiece, row, col }; this.renderer.deselectSquare(); this.renderer.selectSquare(row, col); const legalMoves = this.game.getLegalMoves(newPiece); this.renderer.highlightMoves(legalMoves); return; } this.showError(result.error); } // Clear selection this.selectedPiece = null; this.renderer.deselectSquare(); this.renderer.clearHighlights(); } } /** * Handle touch start (mobile) * @param {TouchEvent} e - Touch event */ onTouchStart(e) { if (!this.enabled) return; const touch = e.touches[0]; const element = document.elementFromPoint(touch.clientX, touch.clientY); const square = element?.closest('.square'); if (!square) return; e.preventDefault(); const row = parseInt(square.dataset.row); const col = parseInt(square.dataset.col); const piece = this.game.board.getPiece(row, col); if (piece && piece.color === this.game.currentTurn) { this.selectedPiece = { piece, row, col }; this.renderer.selectSquare(row, col); const legalMoves = this.game.getLegalMoves(piece); this.renderer.highlightMoves(legalMoves); } } /** * Handle touch move (mobile) * @param {TouchEvent} e - Touch event */ onTouchMove(e) { if (!this.enabled || !this.selectedPiece) return; e.preventDefault(); const touch = e.touches[0]; const element = document.elementFromPoint(touch.clientX, touch.clientY); const square = element?.closest('.square'); // Highlight potential drop target this.renderer.boardElement.querySelectorAll('.drop-target').forEach(s => { s.classList.remove('drop-target'); }); if (square) { square.classList.add('drop-target'); } } /** * Handle touch end (mobile) * @param {TouchEvent} e - Touch event */ onTouchEnd(e) { if (!this.enabled || !this.selectedPiece) return; e.preventDefault(); const touch = e.changedTouches[0]; const element = document.elementFromPoint(touch.clientX, touch.clientY); const square = element?.closest('.square'); if (square) { const toRow = parseInt(square.dataset.row); const toCol = parseInt(square.dataset.col); const result = this.game.makeMove( this.selectedPiece.row, this.selectedPiece.col, toRow, toCol ); if (result.success) { this.renderer.renderBoard(this.game.board, this.game.gameState); if (result.gameStatus === 'check') { this.renderer.showCheckIndicator(this.game.currentTurn, this.game.board); } } else { this.showError(result.error); } } // Clear selection this.selectedPiece = null; this.renderer.deselectSquare(); this.renderer.clearHighlights(); this.renderer.boardElement.querySelectorAll('.drop-target').forEach(s => { s.classList.remove('drop-target'); }); } /** * Enable drag and drop */ enable() { this.enabled = true; } /** * Disable drag and drop */ disable() { this.enabled = false; this.selectedPiece = null; this.draggedPiece = null; this.renderer.clearAllHighlights(); } /** * Show error message to user * @param {string} message - Error message */ showError(message) { // This could be enhanced with a proper UI notification system console.warn('Move error:', message); // Flash the board briefly this.renderer.boardElement.classList.add('error-shake'); setTimeout(() => { this.renderer.boardElement.classList.remove('error-shake'); }, 500); } }