// AnimationQueue - Sequences card animations properly // Ensures animations play in order without overlap class AnimationQueue { constructor(cardManager, getSlotRect, getLocationRect, playSound) { this.cardManager = cardManager; this.getSlotRect = getSlotRect; // Function to get slot position this.getLocationRect = getLocationRect; // Function to get deck/discard position this.playSound = playSound || (() => {}); // Sound callback this.queue = []; this.processing = false; this.animationInProgress = false; // Timing configuration (ms) - use centralized TIMING config const T = window.TIMING || {}; this.timing = { flipDuration: T.card?.flip || 540, moveDuration: T.card?.move || 270, cardLift: T.card?.lift || 100, pauseAfterFlip: T.pause?.afterFlip || 144, pauseAfterDiscard: T.pause?.afterDiscard || 550, pauseBeforeNewCard: T.pause?.beforeNewCard || 150, pauseAfterSwapComplete: T.pause?.afterSwapComplete || 400, pauseBetweenAnimations: T.pause?.betweenAnimations || 90, pauseBeforeFlip: T.pause?.beforeFlip || 50, // Beat timing beatBase: T.beat?.base || 1000, beatVariance: T.beat?.variance || 200, fadeOut: T.beat?.fadeOut || 300, fadeIn: T.beat?.fadeIn || 300, }; } // Add movements to the queue and start processing async enqueue(movements, onComplete) { if (!movements || movements.length === 0) { if (onComplete) onComplete(); return; } // Add completion callback to last movement const movementsWithCallback = movements.map((m, i) => ({ ...m, onComplete: i === movements.length - 1 ? onComplete : null })); this.queue.push(...movementsWithCallback); if (!this.processing) { await this.processQueue(); } } // Process queued animations one at a time async processQueue() { if (this.processing) return; this.processing = true; this.animationInProgress = true; while (this.queue.length > 0) { const movement = this.queue.shift(); try { await this.animate(movement); } catch (e) { console.error('Animation error:', e); } // Callback after last movement if (movement.onComplete) { movement.onComplete(); } // Pause between animations if (this.queue.length > 0) { await this.delay(this.timing.pauseBetweenAnimations); } } this.processing = false; this.animationInProgress = false; } // Route to appropriate animation async animate(movement) { switch (movement.type) { case 'flip': await this.animateFlip(movement); break; case 'swap': await this.animateSwap(movement); break; case 'discard': await this.animateDiscard(movement); break; case 'draw-deck': await this.animateDrawDeck(movement); break; case 'draw-discard': await this.animateDrawDiscard(movement); break; } } // Animate a card flip async animateFlip(movement) { const { playerId, position, faceUp, card } = movement; // Get slot position const slotRect = this.getSlotRect(playerId, position); if (!slotRect || slotRect.width === 0 || slotRect.height === 0) { return; } // Create animation card at slot position const animCard = this.createAnimCard(); this.cardManager.cardLayer.appendChild(animCard); this.setCardPosition(animCard, slotRect); const inner = animCard.querySelector('.card-inner'); const front = animCard.querySelector('.card-face-front'); // Set up what we're flipping to (front face) this.setCardFront(front, card); // Start face down (flipped = showing back) inner.classList.add('flipped'); // Force a reflow to ensure the initial state is applied animCard.offsetHeight; // Animate the flip this.playSound('flip'); await this.delay(this.timing.pauseBeforeFlip); // Remove flipped to trigger animation to front inner.classList.remove('flipped'); await this.delay(this.timing.flipDuration); await this.delay(this.timing.pauseAfterFlip); // Clean up animCard.remove(); } // Animate a card swap - smooth continuous motion async animateSwap(movement) { const { playerId, position, oldCard, newCard } = movement; const slotRect = this.getSlotRect(playerId, position); const discardRect = this.getLocationRect('discard'); const holdingRect = this.getLocationRect('holding'); if (!slotRect || !discardRect || slotRect.width === 0) { return; } // Create animation cards const handCard = this.createAnimCard(); this.cardManager.cardLayer.appendChild(handCard); this.setCardPosition(handCard, slotRect); const handInner = handCard.querySelector('.card-inner'); const handFront = handCard.querySelector('.card-face-front'); const heldCard = this.createAnimCard(); this.cardManager.cardLayer.appendChild(heldCard); this.setCardPosition(heldCard, holdingRect || discardRect); const heldInner = heldCard.querySelector('.card-inner'); const heldFront = heldCard.querySelector('.card-face-front'); // Set up initial state this.setCardFront(handFront, oldCard); if (!oldCard.face_up) { handInner.classList.add('flipped'); } this.setCardFront(heldFront, newCard); heldInner.classList.remove('flipped'); // Step 1: If face-down, flip to reveal if (!oldCard.face_up) { this.playSound('flip'); handInner.classList.remove('flipped'); await this.delay(this.timing.flipDuration); } // Step 2: Quick crossfade swap handCard.classList.add('fade-out'); heldCard.classList.add('fade-out'); await this.delay(150); this.setCardPosition(handCard, discardRect); this.setCardPosition(heldCard, slotRect); this.playSound('card'); handCard.classList.remove('fade-out'); heldCard.classList.remove('fade-out'); handCard.classList.add('fade-in'); heldCard.classList.add('fade-in'); await this.delay(150); // Clean up handCard.remove(); heldCard.remove(); } // Create a temporary animation card element createAnimCard() { const card = document.createElement('div'); card.className = 'real-card anim-card'; card.innerHTML = `