import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party' export type GridItemType = 'character' | 'weapon' | 'summon' export type GridItem = GridCharacter | GridWeapon | GridSummon export interface Position { container: string position: number } export interface DragSource extends Position { type: GridItemType } export interface DraggedItem { type: GridItemType data: GridItem source: DragSource } export interface DropTarget extends Position { type: GridItemType } export interface DragOperation { id: string type: 'move' | 'swap' | 'reorder' timestamp: number source: { container: string position: number itemId: string type?: GridItemType } target: { container: string position: number itemId?: string type?: GridItemType } status: 'pending' | 'synced' | 'failed' retryCount: number } export interface TouchState { touchStartPos: { x: number; y: number } | null touchStartTime: number longPressTimer: number | null touchThreshold: number longPressDuration: number currentTouch: Touch | null } export interface DragDropState { isDragging: boolean draggedItem: DraggedItem | null hoveredOver: DropTarget | null validDrop: boolean dragPreview: HTMLElement | null operationQueue: DragOperation[] lastError: Error | null touchState: TouchState } export interface DragDropHandlers { onDrop?: (from: DragSource, to: DropTarget, item: GridItem) => void onSwap?: (from: DragSource, to: DropTarget, fromItem: GridItem, toItem: GridItem) => void onValidate?: (from: DragSource, to: DropTarget) => boolean onLocalUpdate?: (operation: DragOperation) => void } export function createDragDropContext(handlers: DragDropHandlers = {}) { let state = $state({ isDragging: false, draggedItem: null, hoveredOver: null, validDrop: false, dragPreview: null, operationQueue: [], lastError: null, touchState: { touchStartPos: null, touchStartTime: 0, longPressTimer: null, touchThreshold: 10, longPressDuration: 500, currentTouch: null } }) function detectItemType(item: GridItem): GridItemType { if ('character' in item) return 'character' if ('weapon' in item) return 'weapon' if ('summon' in item) return 'summon' throw new Error('Unknown item type') } function handlePointerDown(e: PointerEvent, item: GridItem, source: Position, type: GridItemType) { if (e.pointerType === 'touch') { initiateTouchDrag(e, item, source, type) } else { startDrag(item, { ...source, type }) } } function initiateTouchDrag(e: PointerEvent, item: GridItem, source: Position, type: GridItemType) { state.touchState.touchStartPos = { x: e.clientX, y: e.clientY } state.touchState.touchStartTime = Date.now() state.touchState.longPressTimer = window.setTimeout(() => { startDrag(item, { ...source, type }) if ('vibrate' in navigator) { navigator.vibrate(50) } }, state.touchState.longPressDuration) } function handlePointerMove(e: PointerEvent) { if (!state.touchState.touchStartPos) return const distance = Math.sqrt( Math.pow(e.clientX - state.touchState.touchStartPos.x, 2) + Math.pow(e.clientY - state.touchState.touchStartPos.y, 2) ) if (distance > state.touchState.touchThreshold && state.touchState.longPressTimer) { clearTimeout(state.touchState.longPressTimer) state.touchState.longPressTimer = null } } function handlePointerUp() { if (state.touchState.longPressTimer) { clearTimeout(state.touchState.longPressTimer) state.touchState.longPressTimer = null } state.touchState.touchStartPos = null } function startDrag(item: GridItem, source: DragSource) { try { console.group('🚀 Drag Start') console.log('Item:', item) console.log('Source:', source) console.groupEnd() state.isDragging = true state.draggedItem = { type: source.type, data: item, source } createDragPreview(item) } catch (error) { handleDragError(error as Error) } } function createDragPreview(item: GridItem) { const preview = document.createElement('div') preview.className = 'drag-preview' preview.style.position = 'fixed' preview.style.pointerEvents = 'none' preview.style.zIndex = '10000' preview.style.opacity = '0.8' const itemName = 'character' in item ? item.character.name : 'weapon' in item ? item.weapon.name : 'summon' in item ? item.summon.name : 'Unknown' preview.innerHTML = `
${itemName || 'Item'}
` document.body.appendChild(preview) state.dragPreview = preview } function updateHover(target: DropTarget | null) { state.hoveredOver = target if (target && state.draggedItem) { const isValid = validateDrop(state.draggedItem.source, target) state.validDrop = isValid } else { state.validDrop = false } } function determineOperationType(source: Position, target: Position, targetHasItem: boolean): 'move' | 'swap' | 'reorder' { if (source.position === target.position && source.container === target.container) return 'reorder' if (targetHasItem) return 'swap' return 'move' } function endDrag(targetItem?: GridItem) { try { console.group('🏁 Drag End') console.log('Final state:', { ...state }) if (state.validDrop && state.draggedItem && state.hoveredOver) { const operation: DragOperation = { id: crypto.randomUUID(), type: determineOperationType(state.draggedItem.source, state.hoveredOver, !!targetItem), timestamp: Date.now(), source: { container: state.draggedItem.source.container, position: state.draggedItem.source.position, itemId: state.draggedItem.data.id, type: state.draggedItem.source.type }, target: { container: state.hoveredOver.container, position: state.hoveredOver.position, itemId: targetItem?.id || undefined, type: state.hoveredOver.type }, status: 'pending', retryCount: 0 } state.operationQueue.push(operation) console.log('📝 Operation queued:', operation) handlers.onLocalUpdate?.(operation) } console.groupEnd() } catch (error) { handleDragError(error as Error) } finally { cleanupDragState() } } function handleDragError(error: Error) { console.error('🔥 Drag operation failed:', error) state.lastError = error cleanupDragState() } function cleanupDragState() { state.isDragging = false state.draggedItem = null state.hoveredOver = null state.validDrop = false if (state.dragPreview) { state.dragPreview.remove() state.dragPreview = null } if (state.touchState.longPressTimer) { clearTimeout(state.touchState.longPressTimer) } state.touchState = { ...state.touchState, touchStartPos: null, longPressTimer: null, currentTouch: null } } function validateDrop(source: DragSource, target: DropTarget): boolean { console.group('🎯 Drop Validation') console.log('Source:', source) console.log('Target:', target) // Can't drop on self if (source.container === target.container && source.position === target.position) { console.log('❌ Cannot drop on self') console.groupEnd() return false } // Type mismatch check if (source.type !== target.type) { console.log('❌ Type mismatch:', source.type, 'vs', target.type) console.groupEnd() return false } // Custom validation if (handlers.onValidate) { const customValid = handlers.onValidate(source, target) console.log(customValid ? '✅ Custom validation passed' : '❌ Custom validation failed') console.groupEnd() return customValid } console.log('✅ Drop allowed') console.groupEnd() return true } function handleDrop(target: DropTarget, targetItem?: GridItem) { if (!state.draggedItem || !state.validDrop) { console.log('❌ Invalid drop attempt') return false } const source = state.draggedItem.source const item = state.draggedItem.data console.group('💧 Handle Drop') console.log('From:', source) console.log('To:', target) console.log('Item:', item) console.log('Target Item:', targetItem) if (targetItem) { // Swap items console.log('🔄 Swapping items') handlers.onSwap?.(source, target, item, targetItem) } else { // Move to empty slot console.log('📦 Moving to empty slot') handlers.onDrop?.(source, target, item) } console.groupEnd() endDrag(targetItem) return true } function updateDragPreviewPosition(x: number, y: number) { if (state.dragPreview) { state.dragPreview.style.left = `${x + 10}px` state.dragPreview.style.top = `${y - 20}px` } } function getQueuedOperations() { return state.operationQueue.filter(op => op.status === 'pending') } function clearQueue() { state.operationQueue = [] } return { get state() { return state }, startDrag, updateHover, endDrag, validateDrop, handleDrop, handlePointerDown, handlePointerMove, handlePointerUp, updateDragPreviewPosition, getQueuedOperations, clearQueue } } export type DragDropContext = ReturnType