diff --git a/ANIMATION_IDEAS.md b/ANIMATION_IDEAS.md new file mode 100644 index 0000000..ced0dc2 --- /dev/null +++ b/ANIMATION_IDEAS.md @@ -0,0 +1,286 @@ +# Royal Family - Animation Ideas Brainstorm + +This document contains animation ideas organized by complexity, from simple quick-win animations to larger entertainment-focused effects. + +## ✅ Already Implemented (Tier 1 - Highest Priority) + +These animations are now live in the game: + +### Card Movement +- **Card Flip/Reveal** - Cards flip when drawn from deck (styles.css:1459) +- **Card Slide from Deck** - Smooth slide animation from deck to hand (styles.css:1476) +- **Card Slide from Field** - Slide from field pile to hand (styles.css:1492) +- **Enhanced Card Hover** - Better lift effect on interactive cards (styles.css:1666) + +### Combat Feedback +- **Castle Damage Shake** - Castle shakes when taking damage (styles.css:1526) +- **Damage Flash** - Red flash overlay on damaged castles (styles.css:1544) +- **Raid Impact** - Explosion-style effect on raided castles (styles.css:1560) +- **Fortification Battle** - Clash animation when attacking fortifications (styles.css:1588) + +### Phase Transitions +- **Phase Banners** - Dynamic banners announcing phase changes (styles.css:1601) + - Draw Phase: 🎴 + - Action Phase: ⚔️ + - Round Start: 🎯 + - Raid Success: 💥 + - Assassin Attack: 🗡️ + +### Special Effects +- **Royal Entrance** - Flourish animation when bringing royals to power (styles.css:1727) +- **Alliance Activation** - Beam effect when alliance castle activates (styles.css:1774) +- **Castle Destruction** - Crumbling effect for destroyed castles (styles.css:1747) +- **Number Pop** - Stats pop when changing (styles.css:1694) + +--- + +## 🎯 Small & Simple Animations (Quick Wins) + +### Card Interactions +5. **Card Glow Border** - Color-coded glow (red/black) when selected +6. **Card Shake** - Gentle shake for invalid moves/locked cards (partially implemented) + +### Game Actions +7. **Persuasion Bar Fill** - Animated fill when adding persuasion points +8. **Threat Counter** - Red flash/pulse when threat points added +9. **Fortification Build** - Stack animation when soldiers added to fortification +10. **Squaring Up** - Quick crossfade/cancel animation when points cancel out + +### UI Feedback +11. **Button Press** - Scale down slightly on click (tactile feel) +12. **Message Banner** - Slide in from top for game messages, fade out +13. **Turn End Swoosh** - Quick transition effect between turns +14. **Hand Reorganize** - Cards smoothly reposition when hand changes +15. **Deck Shuffle** - Brief shuffle animation when deck is created + +--- + +## 🎭 Medium Complexity Animations + +### Combat & Conflict +16. **Battle Clash** - Cards collide in center when attacking fortifications +17. **Assassination Strike** - Dagger slash across card, then fade out +18. **Kidnap/Rescue** - Card swoops from one side to another +19. **Joker Reveal** - Dramatic spin/reveal determining the Age + +### Card Movement +20. **Deal Animation** - Cards fly out from deck to field positions during flop +21. **Discard Arc** - Cards arc gracefully to field piles +22. **Hand Fan** - Cards spread out in a fan when added to hand +23. **Card Return** - Smooth return animation for cancelled actions + +### Strategic Actions +24. **Persuasion Success** - Sparkles/shimmer when persuasion threshold reached +25. **Fortification Breach** - Crack/shatter effect when fortification destroyed +26. **Point Counter Tick-Up** - Animated number increment for persuasion/threat +27. **Chain Reactions** - Visual cascade when squaring up multiple times + +--- + +## 🎪 Large & Entertaining Animations + +### Epic Game Moments +28. **Enhanced Victory Celebration** - Confetti, castle sparkles, royal family cheers +29. **Dramatic Defeat** - Castle crumbles, screen darkens, sorrowful music cue +30. **Age Announcement** - Full-screen dramatic reveal: "Age of Oppression" with medieval banner unfurl +31. **Round Start Fanfare** - Medieval trumpets, cards dramatically dealt +32. **Critical Hit** - Slow-motion raid impact with particle effects +33. **Final Blow** - Slow-motion + zoom + impact for game-winning raid + +### Character Personality +34. **Enhanced AI Mood Changes** - Animated emoji transitions with personality effects + - Aggressive: Fire/rage effects + - Defensive: Shield glow + - Chaotic: Random swirls/sparkles + - Balanced: Calm aura +35. **Royal Portraits** - Animated portraits for Kings, Queens, Jacks (winking, nodding) +36. **Assassin Stealth** - Shadow figure creeps across screen during assassination +37. **Castle Expressions** - Castles react to damage (worried), fortification (confident) + +### Environmental Effects +38. **Weather System** - Light snow, rain, or sunshine based on game state +39. **Day/Night Cycle** - Background gradually shifts based on round number +40. **Battlefield Smoke** - Particles during intense combat sequences +41. **Magic Sparkles** - Trail effects following cards as they move +42. **Background Banners** - Animated flags waving for each family color + +### Combo & Special Moves +43. **Multi-Raid Combo** - Lightning effects connecting multiple successful raids +44. **Perfect Turn** - Golden glow effect for optimal strategic plays +45. **Comeback Moment** - Dramatic lighting change when losing player makes strong play +46. **Royal Family Complete** - Special effect when all K, Q, J in one castle +47. **Field Glow** - Pulsing highlights showing which field pile has best cards + +### Interactive Flourishes +48. **Card Trails** - Motion trails with family colors during movement +49. **Power Meter** - Charging animation showing attack/defense strength +50. **Split-Screen Tension** - Show both players' castles side-by-side during crucial moments + +### Cinematic Touches +51. **Camera Shake** - Screen shake on big impacts +52. **Zoom & Focus** - Camera zooms to important action areas +53. **Slow-Motion Highlights** - Critical moments play at 0.5x speed +54. **Replay System** - Animate the last action quickly if requested + +### Sound-Synchronized (Visual Cues) +55. **Card Sounds Visual** - Ripple effect when card dealt/played +56. **Medieval Bells** - Visual bell swing for round start +57. **Sword Clash** - Crossed swords appear during battles +58. **Crown Shine** - Royals have crown that glints periodically +59. **Castle Bells** - Ringing animation when castle takes damage + +--- + +## 📊 Implementation Priority Tiers + +### Priority Tier 2 (Polish & Personality) +Best next steps after Tier 1: +- Persuasion bar animated fill (#7) +- Deal animation for flop (#20) +- Discard arc animation (#21) +- Enhanced AI mood animations (#34) +- Assassination strike effect (#17) +- Joker reveal drama (#19) + +**Estimated effort:** 4-6 hours +**Impact:** High polish, makes game feel more premium + +### Priority Tier 3 (Entertainment Value) +For maximum fun factor: +- Environmental effects (#38-42) +- Combo celebrations (#43-47) +- Cinematic touches (#51-54) +- Royal portraits (#35) + +**Estimated effort:** 8-12 hours +**Impact:** Transforms game into entertainment experience + +--- + +## 🛠 Technical Implementation Notes + +### CSS Animations (Current Approach) +- Simple, performant, works on all browsers +- Easy to maintain and debug +- Good for transforms, opacity, scale +- Limited for complex sequences + +### Web Animations API (For Complex Effects) +```javascript +element.animate([ + { transform: 'translateX(0px)' }, + { transform: 'translateX(100px)' } +], { + duration: 500, + easing: 'ease-out' +}); +``` + +### Canvas for Particle Effects +For effects like: +- Confetti +- Sparkles +- Smoke +- Magic trails + +### SVG Animations +For: +- Shield effects +- Beam effects +- Complex paths + +### Performance Considerations +- Keep animations under 500ms for responsiveness +- Use `transform` and `opacity` for GPU acceleration +- Avoid animating `width`, `height`, `top`, `left` +- Consider `will-change` for frequently animated elements +- Test on mobile devices + +--- + +## 🎨 Animation Timing Guidelines + +| Action Type | Duration | Easing | +|-------------|----------|--------| +| Card draw | 400-600ms | ease-out | +| Card discard | 300-400ms | ease-in | +| Damage shake | 500-800ms | ease | +| Phase transition | 300-500ms | ease-in-out | +| Royal entrance | 600-800ms | ease-out | +| Castle destruction | 1000ms | ease-out | +| UI feedback | 200-300ms | ease | +| Celebration | 1500-2000ms | ease-in-out | + +--- + +## 💡 Future Ideas + +### Accessibility Options +- Toggle to reduce motion +- Speed controls (0.5x, 1x, 2x) +- Option to disable non-essential animations +- High contrast mode + +### Advanced Features +- Custom animation themes (medieval, modern, minimal) +- Player-selectable victory animations +- Seasonal themes (Halloween, Christmas) +- Unlockable animation effects + +### Educational Animations +- Tutorial mode with highlighted actions +- Strategy hints with visual indicators +- Move preview animations +- Undo/redo with animation replay + +--- + +## 📝 Notes for Implementation + +1. **Start Small**: Implement one animation category at a time +2. **Test Thoroughly**: Check animations on different screen sizes +3. **Get Feedback**: Players might find some animations distracting +4. **Performance First**: Don't sacrifice game responsiveness for flashy effects +5. **Consistency**: Keep animation style consistent across the game +6. **Accessibility**: Always provide option to reduce or disable animations + +--- + +## 🚀 Quick Start for Next Animations + +To add a new animation: + +1. **Define CSS keyframe** in styles.css: +```css +@keyframes myAnimation { + 0% { /* start state */ } + 100% { /* end state */ } +} +``` + +2. **Create CSS class**: +```css +.my-animation { + animation: myAnimation 0.5s ease; +} +``` + +3. **Add trigger in ui.js**: +```javascript +animateMyAction(element) { + element.classList.add('my-animation'); + setTimeout(() => { + element.classList.remove('my-animation'); + }, 500); +} +``` + +4. **Call from action handler**: +```javascript +this.animateMyAction(targetElement); +``` + +--- + +Last Updated: 2026-01-12 +Version: 1.0 diff --git a/game.js b/game.js index b9fffab..38c278a 100644 --- a/game.js +++ b/game.js @@ -1147,6 +1147,7 @@ class AIPlayer { this.game = game; this.playerNumber = playerNumber; this.thinkingDelay = 800; + this.ui = null; // Will be set by UI if animations are needed // Mood system this.mood = this.randomMood(); @@ -1461,9 +1462,17 @@ class AIPlayer { const chosen = this.pickFromSimilarOptions(decentOptions); if (chosen.type === 'deck') { - this.game.drawFromDeck(); + if (this.ui) { + await this.ui.aiDrawFromDeck(); + } else { + this.game.drawFromDeck(); + } } else { - this.game.drawFromField(chosen.index); + if (this.ui) { + await this.ui.aiDrawFromField(chosen.index); + } else { + this.game.drawFromField(chosen.index); + } } } @@ -1961,7 +1970,24 @@ class AIPlayer { } } - executeAction(action) { + async executeAction(action) { + // Use UI animation wrappers if available + if (this.ui) { + // For assassinate, we need to set up the target + if (action.type === 'assassinate' && action.targets && action.targets.length > 0) { + const target = action.targets.reduce((best, t) => + ROYAL_HIERARCHY[t.royal.value] > ROYAL_HIERARCHY[best.royal.value] ? t : best + ); + await this.ui.aiExecuteAction('assassinate', target); + return; + } + + // For other actions, pass them through + await this.ui.aiExecuteAction(action.type, action.castle, action.attackingCastle); + return; + } + + // Fallback: no UI animations switch (action.type) { case 'field': // Will trigger field-select phase @@ -1970,7 +1996,7 @@ class AIPlayer { case 'assassinate': // Pick the highest value royal to kill if (action.targets && action.targets.length > 0) { - const target = action.targets.reduce((best, t) => + const target = action.targets.reduce((best, t) => ROYAL_HIERARCHY[t.royal.value] > ROYAL_HIERARCHY[best.royal.value] ? t : best ); this.game.executeAction('assassinate', target); @@ -2038,7 +2064,11 @@ class AIPlayer { // Cover assassin if: we're fielding our own royal OR there's a royal we want on field if (isOurUsableRoyal || hasDesirableRoyalOnField) { this.game.phase = 'action'; - this.game.executeAction('field', assassinPileIndex); + if (this.ui) { + await this.ui.aiExecuteAction('field', assassinPileIndex); + } else { + this.game.executeAction('field', assassinPileIndex); + } return; } } @@ -2069,9 +2099,13 @@ class AIPlayer { pileOptions.sort((a, b) => b.coverValue - a.coverValue); const targetPile = pileOptions[0].index; - + this.game.phase = 'action'; - this.game.executeAction('field', targetPile); + if (this.ui) { + await this.ui.aiExecuteAction('field', targetPile); + } else { + this.game.executeAction('field', targetPile); + } } // Decide raid choice (damage is already applied when raid starts) diff --git a/styles.css b/styles.css index 0c8e7df..4f4c075 100644 --- a/styles.css +++ b/styles.css @@ -1452,3 +1452,387 @@ body::before { color: var(--gold); font-weight: bold; } + +/* ========== CARD MOVEMENT ANIMATIONS ========== */ + +/* Card flip animation when drawing from deck */ +@keyframes cardFlip { + 0% { + transform: rotateY(0deg); + } + 50% { + transform: rotateY(90deg); + } + 100% { + transform: rotateY(0deg); + } +} + +.card-flipping { + animation: cardFlip 0.6s ease; +} + +/* Card slide from deck to drawn slot */ +@keyframes slideFromDeck { + 0% { + transform: translate(-200px, 0) scale(0.8); + opacity: 0.5; + } + 100% { + transform: translate(0, 0) scale(1); + opacity: 1; + } +} + +.card-slide-from-deck { + animation: slideFromDeck 0.5s ease-out; +} + +/* Card slide from field pile to drawn slot */ +@keyframes slideFromField { + 0% { + transform: translate(var(--slide-x, 0), var(--slide-y, 0)) scale(0.9); + opacity: 0.7; + } + 100% { + transform: translate(0, 0) scale(1); + opacity: 1; + } +} + +.card-slide-from-field { + animation: slideFromField 0.5s ease-out; +} + +/* Card slide to field pile (discard) */ +@keyframes slideToField { + 0% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + 100% { + transform: translate(var(--slide-x, 0), var(--slide-y, 0)) scale(0.9) rotate(5deg); + opacity: 0.8; + } +} + +.card-slide-to-field { + animation: slideToField 0.4s ease-in; +} + +/* ========== COMBAT ANIMATIONS ========== */ + +/* Castle damage shake */ +@keyframes castleDamage { + 0%, 100% { transform: translateX(0); } + 10% { transform: translateX(-8px); } + 20% { transform: translateX(8px); } + 30% { transform: translateX(-8px); } + 40% { transform: translateX(8px); } + 50% { transform: translateX(-4px); } + 60% { transform: translateX(4px); } + 70% { transform: translateX(-2px); } + 80% { transform: translateX(2px); } + 90% { transform: translateX(0); } +} + +.castle-taking-damage { + animation: castleDamage 0.6s ease; +} + +/* Red flash overlay for damage */ +@keyframes damageFlash { + 0%, 100% { + box-shadow: 0 0 0 rgba(220, 38, 38, 0); + border-color: var(--border-subtle); + } + 20%, 60% { + box-shadow: 0 0 30px rgba(220, 38, 38, 0.8), inset 0 0 30px rgba(220, 38, 38, 0.3); + border-color: #dc2626; + } +} + +.castle-damage-flash { + animation: damageFlash 0.8s ease; +} + +/* Raid impact effect - explosion-style */ +@keyframes raidImpact { + 0% { + transform: scale(1); + filter: brightness(1); + } + 25% { + transform: scale(1.15); + filter: brightness(1.5); + } + 50% { + transform: scale(0.95); + filter: brightness(1.2); + } + 75% { + transform: scale(1.05); + filter: brightness(1); + } + 100% { + transform: scale(1); + filter: brightness(1); + } +} + +.castle-raid-impact { + animation: raidImpact 0.7s ease, damageFlash 0.8s ease; +} + +/* Battle clash effect on fortifications */ +@keyframes battleClash { + 0%, 100% { transform: rotate(90deg) scale(1); } + 25% { transform: rotate(85deg) scale(1.1); } + 50% { transform: rotate(95deg) scale(1.05); } + 75% { transform: rotate(88deg) scale(1.08); } +} + +.fortification-battle { + animation: battleClash 0.5s ease; +} + +/* ========== PHASE TRANSITION BANNER ========== */ + +.phase-banner { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: linear-gradient(135deg, rgba(26, 26, 36, 0.95), rgba(37, 37, 50, 0.95)); + border: 3px solid var(--gold); + border-radius: 12px; + padding: 30px 60px; + text-align: center; + z-index: 999; + box-shadow: 0 0 60px rgba(0, 0, 0, 0.9), 0 0 100px rgba(212, 175, 55, 0.3); + animation: phaseBannerSlide 1.2s ease; + pointer-events: none; +} + +@keyframes phaseBannerSlide { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + 20% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.1); + } + 80% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } +} + +.phase-banner-title { + font-family: 'Cinzel', serif; + font-size: 3rem; + color: var(--gold); + text-shadow: 0 0 20px rgba(212, 175, 55, 0.6); + margin-bottom: 10px; + letter-spacing: 0.1em; +} + +.phase-banner-subtitle { + font-size: 1.3rem; + color: var(--text-secondary); + font-style: italic; +} + +/* Action icons for phase banners */ +.phase-icon { + font-size: 4rem; + display: block; + margin-bottom: 10px; + filter: drop-shadow(0 0 10px rgba(212, 175, 55, 0.5)); +} + +/* ========== CARD INTERACTION HIGHLIGHTS ========== */ + +/* Hover lift for interactive cards */ +.card.clickable { + transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease; +} + +.card.clickable:hover { + transform: translateY(-8px) scale(1.05); + box-shadow: 0 12px 35px rgba(0, 0, 0, 0.8), 0 0 25px var(--gold); + filter: brightness(1.1); + z-index: 10; +} + +/* Card selected state */ +.card-selected { + transform: translateY(-4px) scale(1.03); + box-shadow: 0 0 30px var(--gold), 0 8px 30px rgba(0, 0, 0, 0.6); + border: 2px solid var(--gold); +} + +/* Invalid move shake */ +@keyframes invalidShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +.card-invalid { + animation: invalidShake 0.3s ease; +} + +/* ========== POINT COUNTER ANIMATIONS ========== */ + +/* Number tick-up animation */ +@keyframes numberPop { + 0% { transform: scale(1); } + 50% { transform: scale(1.3); color: var(--gold-light); } + 100% { transform: scale(1); } +} + +.number-pop { + animation: numberPop 0.4s ease; +} + +/* Persuasion/Threat point added effect */ +@keyframes pointAdded { + 0% { + transform: translateY(-10px) scale(0.5); + opacity: 0; + } + 50% { + transform: translateY(0) scale(1.1); + opacity: 1; + } + 100% { + transform: translateY(0) scale(1); + opacity: 1; + } +} + +.point-added { + animation: pointAdded 0.5s ease; +} + +/* ========== SPECIAL EFFECTS ========== */ + +/* Royal entrance flourish */ +@keyframes royalEntrance { + 0% { + opacity: 0; + transform: translateY(-30px) scale(0.8) rotate(-10deg); + } + 60% { + opacity: 1; + transform: translateY(5px) scale(1.1) rotate(2deg); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1) rotate(0deg); + } +} + +.royal-entering { + animation: royalEntrance 0.7s ease; +} + +/* Castle destruction effect */ +@keyframes castleDestroy { + 0% { + transform: scale(1) rotate(0deg); + opacity: 1; + } + 25% { + transform: scale(1.1) rotate(2deg); + } + 50% { + transform: scale(0.95) rotate(-2deg); + } + 75% { + transform: scale(1.05) rotate(1deg); + opacity: 0.7; + } + 100% { + transform: scale(0.8) rotate(0deg); + opacity: 0.3; + filter: grayscale(1); + } +} + +.castle-destroying { + animation: castleDestroy 1s ease-out forwards; +} + +/* Alliance activation beam */ +@keyframes allianceActivate { + 0% { + box-shadow: 0 0 0 rgba(212, 175, 55, 0); + } + 50% { + box-shadow: 0 0 60px rgba(212, 175, 55, 0.9), inset 0 0 40px rgba(212, 175, 55, 0.4); + } + 100% { + box-shadow: 0 0 30px rgba(212, 175, 55, 0.5), inset 0 0 20px rgba(212, 175, 55, 0.2); + } +} + +.alliance-activating { + animation: allianceActivate 1s ease; +} + +/* ========== VIEW TRANSITIONS ========== */ + +/* Configure view transition for card movements */ +::view-transition-old(card-moving), +::view-transition-new(card-moving), +::view-transition-old(card-moving-dest), +::view-transition-new(card-moving-dest) { + animation-duration: 0.4s; + animation-timing-function: ease-out; +} + +::view-transition-old(card-moving) { + animation-name: fade-out-scale; +} + +::view-transition-new(card-moving) { + animation-name: fade-in-scale; +} + +::view-transition-old(card-moving-dest) { + animation-name: fade-in-scale; +} + +::view-transition-new(card-moving-dest) { + animation-name: fade-out-scale; +} + +/* Create a view transition group that morphs between card-moving and card-moving-dest */ +::view-transition-group(card-moving) { + animation-duration: 0.4s; + animation-timing-function: ease-out; +} + +::view-transition-group(card-moving-dest) { + animation-duration: 0.4s; + animation-timing-function: ease-out; +} + +@keyframes fade-out-scale { + to { + opacity: 0.3; + } +} + +@keyframes fade-in-scale { + from { + opacity: 0.3; + } +} diff --git a/ui.js b/ui.js index 5697275..8dd33da 100644 --- a/ui.js +++ b/ui.js @@ -92,20 +92,21 @@ class GameUI { this.game.reset(); this.vsAI = vsAI; this.aiTurnInProgress = false; - + if (vsAI) { // AI controls player 2 (The Scarlett - Red) this.ai = new AIPlayer(this.game, 2); + this.ai.ui = this; // Give AI reference to UI for animations this.game.player2.name = 'The Crown'; // AI name } else { this.ai = null; } - + this.gameOverOverlay.classList.remove('active'); this.showScreen('game-screen'); this.game.startGame(); this.render(); - + // Check if AI should take first turn this.checkAITurn(); } @@ -598,10 +599,13 @@ class GameUI { btn.className = 'action-btn'; const pileCount = this.game.fieldPiles[i].length; btn.textContent = `Field ${i + 1} (${pileCount} card${pileCount !== 1 ? 's' : ''})`; - btn.addEventListener('click', () => { + btn.addEventListener('click', async () => { this.game.phase = 'action'; // Reset phase - this.game.executeAction('field', i); - this.render(); + + // Animate card to field pile with view transition + await this.animateCardToFieldPile(i, () => { + this.game.executeAction('field', i); + }); }); this.actionsList.appendChild(btn); } @@ -711,77 +715,172 @@ class GameUI { handleDeckClick() { if (this.game.phase !== 'draw') return; if (this.game.deck.length === 0) return; - + this.game.drawFromDeck(); this.render(); + + // Animate card draw with flip - no slide since we don't know deck position + const drawnCard = this.drawnCardSlot.querySelector('.card'); + if (drawnCard) { + drawnCard.classList.add('card-flipping'); + setTimeout(() => { + drawnCard.classList.remove('card-flipping'); + }, 600); + } } handleFieldClick(pileIndex) { if (this.game.phase !== 'draw') return; if (this.game.fieldPiles[pileIndex].length === 0) return; - + this.game.drawFromField(pileIndex); this.render(); + + // Animate card draw with flip + const drawnCard = this.drawnCardSlot.querySelector('.card'); + if (drawnCard) { + drawnCard.classList.add('card-flipping'); + setTimeout(() => { + drawnCard.classList.remove('card-flipping'); + }, 600); + } } - handleAction(action) { + async handleAction(action) { switch (action.type) { case 'field': // Show pile selection this.game.phase = 'field-select'; this.render(); return; + case 'persuade': - this.game.executeAction('persuade'); - break; + const wasActive = this.game.getPlayer(this.game.currentPlayer).allianceCastle.isActive; + const currentPlayer = this.game.getPlayer(this.game.currentPlayer); + const allianceCastle = currentPlayer.allianceCastle; + + // Animate card to persuasion track with view transition + await this.animateCardToPersuasionTrack(allianceCastle, currentPlayer, () => { + this.game.executeAction('persuade'); + }); + + // Animate alliance activation if it just became active + const isNowActive = this.game.getPlayer(this.game.currentPlayer).allianceCastle.isActive; + if (!wasActive && isNowActive) { + setTimeout(() => { + const player = this.game.getPlayer(this.game.currentPlayer); + const castlePrefix = this.getCastlePrefix(player, player.allianceCastle); + const castleEl = document.getElementById(`${castlePrefix}-castle`); + if (castleEl) this.animateAllianceActivation(castleEl); + }, 50); + } + return; + case 'threaten': - this.game.executeAction('threaten'); - break; + const opponent = this.game.getPlayer(this.game.currentPlayer === 1 ? 2 : 1); + const opponentAlliance = opponent.allianceCastle; + + // Animate card to opponent's persuasion track with view transition + await this.animateCardToPersuasionTrack(opponentAlliance, opponent, () => { + this.game.executeAction('threaten'); + }); + return; + case 'fortify': case 'upgrade-fortification': case 'repair-fortification': - this.game.executeAction(action.type, action.castle); - break; + // Animate card to fortification with view transition + await this.animateCardToFortification(action.castle, () => { + this.game.executeAction(action.type, action.castle); + }); + return; + case 'battle': - this.game.executeAction('battle', action.castle); - break; + // Animate battle attack with view transition + await this.animateCardToBattle(action.castle, () => { + this.game.executeAction('battle', action.castle); + }); + return; + case 'raid': this.game.executeAction('raid', action.castle, action.attackingCastle); - break; + this.render(); + // Animate raid impact + setTimeout(() => { + const player = this.game.getPlayer(this.game.currentPlayer === 1 ? 2 : 1); + const castlePrefix = this.getCastlePrefix(player, action.castle); + const castleEl = document.getElementById(`${castlePrefix}-castle`); + if (castleEl) this.animateCastleDamage(castleEl, true); + }, 50); + return; + case 'raid-no-damage': this.game.executeAction('raid-no-damage', action.castle); break; + case 'bring-to-power': - this.game.executeAction('bring-to-power', action.castle); - break; + // Animate royal to castle with view transition + await this.animateCardToRoyalStack(action.castle, () => { + this.game.executeAction('bring-to-power', action.castle); + }); + return; + case 'assassinate': // Show target selection this.showTargetSelection(action.targets); return; } - + this.render(); } + // Helper to get castle prefix for animations + getCastlePrefix(player, castle) { + const prefix = player.id === 1 ? 'p1' : 'p2'; + const isAlliance = castle === player.allianceCastle; + return `${prefix}-${isAlliance ? 'alliance' : 'primary'}`; + } + showTargetSelection(targets) { this.actionsList.innerHTML = ''; - + const title = document.createElement('div'); title.style.cssText = 'color: var(--gold); margin-bottom: 8px;'; title.textContent = 'Select target to assassinate:'; this.actionsList.appendChild(title); - + targets.forEach(target => { const btn = document.createElement('button'); btn.className = 'action-btn danger'; btn.textContent = `${target.royal.value}${target.royal.suit} in ${SUIT_NAMES[target.castle.suit]} castle`; - btn.addEventListener('click', () => { - this.game.executeAction('assassinate', target); - this.render(); + btn.addEventListener('click', async () => { + // Find the target royal element + const targetPlayer = this.game.getPlayer(target.owner); + const castlePrefix = this.getCastlePrefix(targetPlayer, target.castle); + const royalsSlot = document.getElementById(`${castlePrefix}-royals`); + + // Find the specific royal card + const royalCards = royalsSlot?.querySelectorAll('.card'); + let targetRoyalElement = null; + if (royalCards) { + for (const card of royalCards) { + const valueText = card.querySelector('.card-value')?.textContent; + const suitText = card.querySelector('.card-suit')?.textContent; + if (valueText === target.royal.value && suitText === target.royal.suit) { + targetRoyalElement = card; + break; + } + } + } + + // Animate assassin to target with view transition + await this.animateAssassinToTarget(targetRoyalElement, () => { + this.game.executeAction('assassinate', target); + }); }); this.actionsList.appendChild(btn); }); - + const cancelBtn = document.createElement('button'); cancelBtn.className = 'action-btn'; cancelBtn.textContent = 'Cancel'; @@ -793,7 +892,7 @@ class GameUI { const winner = this.game.getPlayer(this.game.winner); const loser = this.game.getPlayer(this.game.winner === 1 ? 2 : 1); const playerWon = this.game.winner === 1; - + // Different messages based on who won if (this.vsAI) { if (playerWon) { @@ -822,9 +921,525 @@ class GameUI { `; this.gameOverOverlay.classList.remove('player-victory', 'ai-victory'); } - + this.gameOverOverlay.classList.add('active'); } + + // ========== ANIMATION HELPERS ========== + + // Animate an element by adding a class and removing it after animation completes + animateElement(element, animationClass, duration = 1000) { + if (!element) return Promise.resolve(); + + return new Promise((resolve) => { + element.classList.add(animationClass); + setTimeout(() => { + element.classList.remove(animationClass); + resolve(); + }, duration); + }); + } + + // Animate card draw from deck + animateCardDrawFromDeck() { + const drawnCard = this.drawnCardSlot.querySelector('.card'); + if (drawnCard) { + drawnCard.classList.add('card-slide-from-deck', 'card-flipping'); + setTimeout(() => { + drawnCard.classList.remove('card-slide-from-deck', 'card-flipping'); + }, 600); + } + } + + // Animate card draw from field using View Transition + animateCardDrawFromField(pileIndex) { + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const fieldPile = this.fieldPiles[pileIndex]; + const topCard = fieldPile?.querySelector('.card:last-child'); + + if (drawnCard && topCard && document.startViewTransition) { + // Mark both source and destination + topCard.style.viewTransitionName = 'card-moving'; + drawnCard.style.viewTransitionName = 'card-moving'; + + // Trigger view transition + const transition = document.startViewTransition(() => { + // The card is already moved, just update the DOM + // View transition will animate between the positions + }); + + transition.finished.then(() => { + // Clean up + if (topCard) topCard.style.viewTransitionName = ''; + if (drawnCard) drawnCard.style.viewTransitionName = ''; + }); + } else if (drawnCard) { + // Fallback: use CSS animation + drawnCard.classList.add('card-slide-from-deck'); + setTimeout(() => { + drawnCard.classList.remove('card-slide-from-deck'); + }, 500); + } + } + + // Animate castle taking damage + animateCastleDamage(castleElement, isRaid = false) { + if (!castleElement) return; + + const animClass = isRaid ? 'castle-raid-impact' : 'castle-taking-damage'; + castleElement.classList.add(animClass, 'castle-damage-flash'); + + setTimeout(() => { + castleElement.classList.remove(animClass, 'castle-damage-flash'); + }, 800); + } + + // Animate fortification taking damage + animateFortificationBattle(fortElement) { + if (!fortElement) return; + + fortElement.classList.add('fortification-battle'); + setTimeout(() => { + fortElement.classList.remove('fortification-battle'); + }, 500); + } + + // Animate royal entering castle + animateRoyalEntrance(royalElement) { + if (!royalElement) return; + + royalElement.classList.add('royal-entering'); + setTimeout(() => { + royalElement.classList.remove('royal-entering'); + }, 700); + } + + // Animate alliance activation + animateAllianceActivation(castleElement) { + if (!castleElement) return; + + castleElement.classList.add('alliance-activating'); + setTimeout(() => { + castleElement.classList.remove('alliance-activating'); + }, 1000); + } + + // Animate castle destruction + animateCastleDestruction(castleElement) { + if (!castleElement) return; + + castleElement.classList.add('castle-destroying'); + // Don't remove this class - it has 'forwards' in animation + } + + // Animate number/stat change + animateNumberChange(element) { + if (!element) return; + + element.classList.add('number-pop'); + setTimeout(() => { + element.classList.remove('number-pop'); + }, 400); + } + + // === AI Animation Wrappers === + // These methods allow the AI to trigger animations while executing game logic + + async aiDrawFromDeck() { + this.game.drawFromDeck(); + this.render(); + setTimeout(() => this.animateCardDrawFromDeck(), 50); + } + + async aiDrawFromField(pileIndex) { + // Don't use view transitions - causes conflicts with player event handlers + // Just draw and render, animation will happen via CSS + this.game.drawFromField(pileIndex); + this.render(); + await this.delay(300); // Small delay for visual feedback + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async aiExecuteAction(actionType, target = null, additionalTarget = null) { + // Handle field action with pile index + if (actionType === 'field' && typeof target === 'number') { + await this.animateCardToFieldPile(target, () => { + this.game.executeAction('field', target); + }); + return; + } + + // Handle assassinate with target object + if (actionType === 'assassinate' && target && target.royal) { + // Find the target royal element + const targetPlayer = this.game.getPlayer(target.owner); + const castlePrefix = this.getCastlePrefix(targetPlayer, target.castle); + const royalsSlot = document.getElementById(`${castlePrefix}-royals`); + + let targetRoyalElement = null; + const royalCards = royalsSlot?.querySelectorAll('.card'); + if (royalCards) { + for (const card of royalCards) { + const valueText = card.querySelector('.card-value')?.textContent; + const suitText = card.querySelector('.card-suit')?.textContent; + if (valueText === target.royal.value && suitText === target.royal.suit) { + targetRoyalElement = card; + break; + } + } + } + + await this.animateAssassinToTarget(targetRoyalElement, () => { + this.game.executeAction('assassinate', target); + }); + return; + } + + // For other actions, create action object + const action = { type: actionType, castle: target, attackingCastle: additionalTarget }; + + // Use the same handleAction logic + await this.handleAction(action); + } + + // Animate using View Transitions API + async animateWithViewTransition(callback) { + // Mark the drawn card for transition + const drawnCard = this.drawnCardSlot.querySelector('.card'); + if (drawnCard) { + drawnCard.style.viewTransitionName = 'card-moving'; + } + + // Use view transition + if (document.startViewTransition) { + const transition = document.startViewTransition(() => { + callback(); + }); + await transition.finished; + } else { + // Direct execution if not supported + callback(); + } + + // Clean up transition name + if (drawnCard) { + drawnCard.style.viewTransitionName = ''; + } + } + + // Mark destination element for view transition + markDestinationForTransition(element) { + if (element) { + element.style.viewTransitionName = 'card-moving'; + // Remove after a frame + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (element) { + element.style.viewTransitionName = ''; + } + }); + }); + } + } + + // Animate card to persuasion/threat track + async animateCardToPersuasionTrack(targetCastle, targetPlayer, callback) { + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const castlePrefix = this.getCastlePrefix(targetPlayer, targetCastle); + const targetBar = document.getElementById(`${castlePrefix}-persuasion-bar`); + + if (drawnCard && targetBar && document.startViewTransition) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.4s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to target + await new Promise(resolve => { + requestAnimationFrame(() => { + const targetRect = targetBar.getBoundingClientRect(); + clone.style.left = `${targetRect.left}px`; + clone.style.top = `${targetRect.top}px`; + clone.style.transform = 'scale(0.3)'; + clone.style.opacity = '0.5'; + + setTimeout(() => { + clone.remove(); + resolve(); + }, 400); + }); + }); + } else { + callback(); + this.render(); + } + } + + // Animate card to fortification + async animateCardToFortification(targetCastle, callback) { + const player = this.game.getPlayer(this.game.currentPlayer); + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const castlePrefix = this.getCastlePrefix(player, targetCastle); + const fortSlot = document.getElementById(`${castlePrefix}-fort`); + + if (drawnCard && fortSlot) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.5s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to fortification + await new Promise(resolve => { + requestAnimationFrame(() => { + const newFortSlot = document.getElementById(`${castlePrefix}-fort`); + if (newFortSlot) { + const targetRect = newFortSlot.getBoundingClientRect(); + clone.style.left = `${targetRect.left}px`; + clone.style.top = `${targetRect.top}px`; + clone.style.transform = 'rotate(90deg) scale(0.85)'; + } + + setTimeout(() => { + clone.remove(); + resolve(); + }, 500); + }); + }); + } else { + callback(); + this.render(); + } + } + + // Animate battle attack + async animateCardToBattle(targetCastle, callback) { + const opponent = this.game.getPlayer(this.game.currentPlayer === 1 ? 2 : 1); + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const castlePrefix = this.getCastlePrefix(opponent, targetCastle); + const fortSlot = document.getElementById(`${castlePrefix}-fort`); + + if (drawnCard && fortSlot) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.4s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to fortification + await new Promise(resolve => { + requestAnimationFrame(() => { + const newFortSlot = document.getElementById(`${castlePrefix}-fort`); + if (newFortSlot) { + const targetRect = newFortSlot.getBoundingClientRect(); + clone.style.left = `${targetRect.left}px`; + clone.style.top = `${targetRect.top}px`; + clone.style.opacity = '0'; + + // Animate fortification clash + setTimeout(() => this.animateFortificationBattle(newFortSlot), 200); + } + + setTimeout(() => { + clone.remove(); + resolve(); + }, 400); + }); + }); + } else { + callback(); + this.render(); + } + } + + // Animate royal to castle + async animateCardToRoyalStack(targetCastle, callback) { + const player = this.game.getPlayer(this.game.currentPlayer); + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const castlePrefix = this.getCastlePrefix(player, targetCastle); + const royalsSlot = document.getElementById(`${castlePrefix}-royals`); + + if (drawnCard && royalsSlot) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.5s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to royal stack + await new Promise(resolve => { + requestAnimationFrame(() => { + const newRoyalsSlot = document.getElementById(`${castlePrefix}-royals`); + if (newRoyalsSlot) { + const targetRect = newRoyalsSlot.getBoundingClientRect(); + clone.style.left = `${targetRect.left}px`; + clone.style.top = `${targetRect.top}px`; + clone.style.opacity = '0'; + + // Add entrance animation to the new royal card + const newRoyal = newRoyalsSlot.querySelector('.card:last-child'); + if (newRoyal) { + setTimeout(() => this.animateRoyalEntrance(newRoyal), 300); + } + } + + setTimeout(() => { + clone.remove(); + resolve(); + }, 500); + }); + }); + } else { + callback(); + this.render(); + } + } + + // Animate assassin to target + async animateAssassinToTarget(targetRoyalElement, callback) { + const drawnCard = this.drawnCardSlot.querySelector('.card'); + + if (drawnCard && targetRoyalElement) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.4s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to target royal + await new Promise(resolve => { + requestAnimationFrame(() => { + const targetRect = targetRoyalElement.getBoundingClientRect(); + clone.style.left = `${targetRect.left}px`; + clone.style.top = `${targetRect.top}px`; + clone.style.transform = 'scale(1.2)'; + clone.style.opacity = '0'; + + setTimeout(() => { + clone.remove(); + resolve(); + }, 400); + }); + }); + } else { + callback(); + this.render(); + } + } + + // Animate card to field pile + async animateCardToFieldPile(pileIndex, callback) { + const drawnCard = this.drawnCardSlot.querySelector('.card'); + const targetPile = this.fieldPiles[pileIndex]; + + if (drawnCard && targetPile) { + // Clone the card to animate + const clone = drawnCard.cloneNode(true); + clone.style.position = 'fixed'; + clone.style.zIndex = '1000'; + clone.style.pointerEvents = 'none'; + + const rect = drawnCard.getBoundingClientRect(); + clone.style.left = `${rect.left}px`; + clone.style.top = `${rect.top}px`; + clone.style.width = `${rect.width}px`; + clone.style.height = `${rect.height}px`; + clone.style.transition = 'all 0.4s ease-out'; + + document.body.appendChild(clone); + + // Execute action and render + callback(); + this.render(); + + // Animate clone to target pile + await new Promise(resolve => { + requestAnimationFrame(() => { + const pileRect = this.fieldPiles[pileIndex].getBoundingClientRect(); + clone.style.left = `${pileRect.left}px`; + clone.style.top = `${pileRect.top}px`; + clone.style.transform = 'scale(0.95) rotate(5deg)'; + + setTimeout(() => { + clone.remove(); + resolve(); + }, 400); + }); + }); + } else { + callback(); + this.render(); + } + } } // Initialize game when DOM is ready