# Animation System Documentation

This document captures the patterns, principles, and practices for implementing animations in this boardgame.io + bgio-effects project.

## 1. General Principles for boardgame.io/bgio-effects Animations

### The Critical Insight: State vs Phase Delays

**`updateStateAfterEffects: true` delays STATE updates but NOT PHASE transitions.**

This is the most important thing to understand:

- When you call `effects.someEffect()` with a duration of 3 seconds, and then `events.endPhase()`:
  - The **phase changes IMMEDIATELY** (synchronously)
  - The **state update is delayed** for 3 seconds
  - The UI sees the old state for 3 seconds, then receives the new state
  
This creates a timing problem: buttons become enabled (because phase changed) before animations complete (while state is still delayed).

### How Effects Work

1. **Effects are declarative timing markers**, not imperative animations
2. All game logic runs **synchronously and atomically** within a move/phase handler
3. The order of state changes within a move doesn't matter for animation timing
4. What matters is: call `effects.something()` **before** `events.endPhase()` or ending the move

### The Animation Flow

```
Player Action
  ↓
Move executes (all state changes happen)
  ↓
Effect triggered (e.g., effects.gridAdvance())
  ↓
Move/phase ends
  ↓
[Framework holds the updated state]
  ↓
UI receives effect event → starts animation
  ↓
[Animation plays for N seconds]
  ↓
[After N seconds] Framework releases updated state
  ↓
UI re-renders with new state (which matches where animation ended)
```

### Key Principles

1. **State changes happen immediately in game logic** - don't try to delay them
2. **Effects just signal "animate this for N seconds"** - they don't control state
3. **Phases change immediately** - can't be delayed by effects
4. **UI must block input during animations** - can't rely on phases alone
5. **Animation timing must be centralized** - one source of truth for durations

### When Animations Work Well

- **During opponent's turn** - buttons naturally disabled by phase check
- **During a separate "animating" phase** - phase system blocks input
- **With UI-level animation locks** - explicit `isAnimating` flags

### Common Pitfall

❌ **Don't do this:**
```javascript
effects.myAnimation();
events.endPhase(); // Phase changes immediately, buttons enabled!
```

The phase changes right away, so players can click buttons during the animation, causing chaos.

✅ **Do this instead:**
Add UI-level blocking:
```javascript
// In game logic
effects.myAnimation();
events.endPhase(); // Phase changes, but...

// In UI
useEffectListener('myAnimation', () => {
  setIsAnimating(true); // Block input
  setTimeout(() => setIsAnimating(false), DURATION);
});

// In buttons
disabled={ctx.phase !== 'playerTurn' || isAnimating}
```

## 2. One-Time Setup for Animation System

### Step 1: Create Constants File

Create `src/game/constants.js` to centralize all animation timings:

```javascript
// Animation durations (in seconds for effects plugin, milliseconds for JS code)
export const MY_ANIMATION_DURATION_SECONDS = 1;
export const MY_ANIMATION_DURATION_MS = MY_ANIMATION_DURATION_SECONDS * 1000;

// Other game constants
export const GRID_ROWS = 10;
export const GRID_COLS = 7;
```

**Why both formats?**
- `_SECONDS` - for bgio-effects plugin configuration
- `_MS` - for JavaScript `setTimeout()` calls in React components

### Step 2: Enable updateStateAfterEffects

In your board component wrapper:

```javascript
export default EffectsBoardWrapper(Board, {
  updateStateAfterEffects: true  // CRITICAL: Must be true!
});
```

### Step 3: Set Up UI Animation Lock System

Add an `isAnimating` flag at the Board level to block all player input during animations:

```javascript
import { useEffectListener } from 'bgio-effects/react';
import { ANIMATION_DURATION_MS } from '../game/constants';

const Board = ({ G, ctx, moves, events }) => {
  const [isAnimating, setIsAnimating] = useState(false);

  // Listen for animations that should block player input
  useEffectListener('gridAdvance', () => {
    setIsAnimating(true);
    setTimeout(() => setIsAnimating(false), GRID_ADVANCE_DURATION_MS);
  }, []);

  // Pass isAnimating down to all interactive components
  // Use it to disable buttons, grid clicks, etc.
};
```

**How many `isAnimating` flags do you need?**

**One is usually sufficient!** 

- If animations are **sequential** (one after another), a single flag works perfectly
- If animations **never overlap**, a single flag prevents all input during any animation
- Only need multiple flags if you want to allow **some** interactions during **specific** animations

For most games: **Start with one global `isAnimating` flag**. Only add more if you have a specific need to allow partial interaction.

**Do you need a listener for every animation?**

**NO!** Only listen to animations that should **block player input**:

- ✅ Grid/enemy turn animations (system taking action)
- ✅ Phase transition animations (changing game mode)
- ❌ Decorative effects (particles, punches, score popups)
- ❌ Player action feedback (these happen during player's turn)

In a typical game, you might have 50+ animations but only 2-3 that block input.

**What if blocking animations can overlap?**

If you have multiple blocking animations that could fire simultaneously, use **reference counting** instead of simple timeouts:

```javascript
const animationCount = useRef(0);

const startBlockingAnimation = (duration) => {
  animationCount.current++;
  setIsAnimating(true);
  setTimeout(() => {
    animationCount.current--;
    if (animationCount.current === 0) {
      setIsAnimating(false);
    }
  }, duration);
};

useEffectListener('animation1', () => {
  startBlockingAnimation(DURATION1_MS);
});

useEffectListener('animation2', () => {
  startBlockingAnimation(DURATION2_MS);
});
```

This prevents the "early unlock" bug where the first animation's timeout re-enables input while the second animation is still running.

**For most turn-based games:** Blocking animations are sequential (grid moves, then player acts, then grid moves), so simple `setTimeout` is fine.

### Step 4: Block All Interactive Elements

Every interactive element needs to check `isAnimating`:

```javascript
// Buttons
<button 
  disabled={ctx.phase !== 'playerTurn' || isAnimating}
  onClick={...}
>

// Grid cells
const handleClick = () => {
  if (ctx.phase !== 'playerTurn' || isAnimating) return;
  // ... handle click
};

// Custom controls
const handleAction = () => {
  if (isAnimating) return;
  // ... handle action
};
```

**Pass `isAnimating` down through props** to all components that need it.

## 3. Adding a New Animation (Step-by-Step Checklist)

### Step 1: Add Timing Constants

In `src/game/constants.js`:

```javascript
export const NEW_ANIMATION_DURATION_SECONDS = 0.5;
export const NEW_ANIMATION_DURATION_MS = NEW_ANIMATION_DURATION_SECONDS * 1000;
```

### Step 2: Register the Effect

In your game definition's `EffectsPlugin` configuration:

```javascript
const configuredEffectsPlugin = EffectsPlugin({
  effects: {
    newAnimation: {
      duration: NEW_ANIMATION_DURATION_SECONDS,
      create: (data) => data // data will be passed to UI
    }
  }
});
```

### Step 3: Trigger the Effect in Game Logic

In the move or phase handler where the action happens:

```javascript
moves: {
  myMove: ({ G, events, effects }, ...args) => {
    // 1. Make all state changes
    G.something = newValue;
    
    // 2. Trigger animation effect BEFORE ending turn/phase
    effects.newAnimation({ whatever: 'data', you: 'need' });
    
    // 3. End turn/phase
    events.setPhase('nextPhase');
  }
}
```

**Critical order:**
1. Make state changes (order doesn't matter)
2. Call `effects.newAnimation()`
3. Call `events.endPhase()` or end the move

### Step 4: Listen for the Effect in UI

If using the global `isAnimating` pattern, add a listener at the Board level:

```javascript
useEffectListener('newAnimation', (data) => {
  setIsAnimating(true);
  setTimeout(() => setIsAnimating(false), NEW_ANIMATION_DURATION_MS);
}, []);
```

If this animation should block input, you're done! The existing `isAnimating` prop will disable everything.

### Step 5: Implement the Visual Animation

Create a component or add to an existing one:

```javascript
const AnimatedThing = ({ row, col, tile }) => {
  const [isAnimating, setIsAnimating] = useState(false);

  useEffectListener('newAnimation', (data) => {
    // Check if this component should animate
    if (data.row === row && data.col === col) {
      setIsAnimating(true);
      setTimeout(() => setIsAnimating(false), NEW_ANIMATION_DURATION_MS);
    }
  }, [row, col]);

  const style = {
    ...(isAnimating && {
      animation: `myAnimation ${NEW_ANIMATION_DURATION_MS}ms ease-out forwards`,
    }),
  };

  return (
    <>
      {isAnimating && (
        <style>
          {`
            @keyframes myAnimation {
              from { transform: scale(1); }
              to { transform: scale(1.5); }
            }
          `}
        </style>
      )}
      <div style={style}>
        {/* content */}
      </div>
    </>
  );
};
```

### Step 6: Test

1. Verify animation plays
2. Try spamming buttons during animation - should be disabled
3. Check console for timing issues
4. Adjust duration constant if needed (only one place to change!)

## 4. Application to This Codebase

### File Structure

```
src/
  game/
    constants.js              # All animation durations
    CollectorGame.js          # Effect definitions, move handlers
  components/
    Board.jsx                 # isGridAnimating flag, effect listeners
    Grid.jsx                  # Passes isGridAnimating to children
    GridCell.jsx              # Checks isGridAnimating before clicks
    AnimatedTile.jsx          # Visual animations for tiles
    MagnetAnimation.jsx       # Visual animation for magnet sweep
```

### Current Effects

#### 1. `magnetSweep` Effect

**Purpose:** Animate magnet sweeping across a row and grabbing metal(s)

**Defined in:** `src/game/CollectorGame.js`
```javascript
magnetSweep: {
  duration: MAGNET_SWEEP_DURATION_SECONDS,
  create: (data) => data // { row, side, grabbedMetals }
}
```

**Triggered in:** `moves.magnet()` in `CollectorGame.js`
```javascript
effects.magnetSweep({ row, side, grabbedMetals });
events.setPhase('gridTurn');
```

**Animated by:**
- `MagnetAnimation.jsx` - renders red circle sweeping across row
- `AnimatedTile.jsx` - each grabbed tile punches and disappears when magnet reaches it

**Timing calculation:**
```javascript
// In AnimatedTile.jsx
const timeToReachTile = (distanceToTile / totalDistance) * MAGNET_SWEEP_DURATION_MS;
```

This calculates when the magnet reaches each specific tile, so sound/animation triggers at the right moment.

#### 2. `gridAdvance` Effect

**Purpose:** Animate all tiles sliding down one row when grid advances

**Defined in:** `src/game/CollectorGame.js`
```javascript
gridAdvance: {
  duration: GRID_ADVANCE_DURATION_SECONDS,
  create: () => ({})
}
```

**Triggered in:** `phases.gridTurn.onBegin()` in `CollectorGame.js`
```javascript
advanceGrid(G, effects);
G.cellHints = computeCellHints(G.grid);
effects.gridAdvance();
events.endPhase();
```

**Animated by:**
- `AnimatedTile.jsx` - all tiles get `slideDown` animation

**UI Blocking:**
- `Board.jsx` listens and sets `isGridAnimating = true`
- All buttons check `isGridAnimating` before allowing clicks
- Grid cells and magnet launchers check before handling input

### Where Effects Should Be Triggered

**Rule:** Trigger effects in the **same move/phase handler** where the state changes happen, **before** calling `events.endPhase()` or ending the move.

**Examples in this codebase:**

1. **Player action animations** - in the move handler:
   ```javascript
   moves: {
     magnet: ({ G, effects }, row, side) => {
       // State changes
       G.grid[row][col] = EMPTY;
       G.metals++;
       
       // Animation
       effects.magnetSweep({ ... });
       
       // End turn
       events.setPhase('gridTurn');
     }
   }
   ```

2. **Grid/enemy animations** - in phase `onBegin`:
   ```javascript
   phases: {
     gridTurn: {
       onBegin: ({ G, effects }) => {
         // State changes
         advanceGrid(G);
         
         // Animation
         effects.gridAdvance();
         
         // End phase
         events.endPhase();
       }
     }
   }
   ```

### Where Animations Should Wait

**In the UI components, NOT in game logic!**

Game logic should:
- ✅ Change state immediately
- ✅ Trigger effects
- ✅ End turns/phases

UI components should:
- ✅ Listen for effects
- ✅ Set animation state
- ✅ Disable input during animations
- ✅ Clear animation state after duration

### Constants Reference

In `src/game/constants.js`:

```javascript
// Magnet sweep animation
export const MAGNET_SWEEP_DURATION_SECONDS = 0.4;
export const MAGNET_SWEEP_DURATION_MS = MAGNET_SWEEP_DURATION_SECONDS * 1000;

// Grid advance animation  
export const GRID_ADVANCE_DURATION_SECONDS = 0.1; // Fast in production, set to 1+ for testing
export const GRID_ADVANCE_DURATION_MS = GRID_ADVANCE_DURATION_SECONDS * 1000;
```

**Testing tip:** Set durations to 1-3 seconds while developing to easily see timing issues, then dial down to 0.1-0.5 seconds for production.

## Common Issues and Solutions

### Issue: Can click buttons during animation

**Symptom:** Player can spam click Pass button, queueing up multiple turns

**Cause:** Phase changed to `playerTurn` immediately, buttons only check phase

**Solution:** Add `isAnimating` check to all buttons:
```javascript
disabled={ctx.phase !== 'playerTurn' || isAnimating}
```

### Issue: Animation timing is inconsistent

**Symptom:** Sometimes animation is too fast/slow compared to state updates

**Cause:** Duration constants not matching between effect definition and UI timeouts

**Solution:** Use the **same constant** everywhere:
```javascript
// Effect definition
duration: MY_ANIMATION_DURATION_SECONDS,

// UI timeout
setTimeout(() => ..., MY_ANIMATION_DURATION_MS);

// CSS animation
animation: `myAnim ${MY_ANIMATION_DURATION_MS}ms ...`
```

### Issue: Tiles jump or teleport instead of animating

**Symptom:** Tiles appear at new positions immediately instead of sliding

**Cause:** State updated before animation started, or `updateStateAfterEffects` is false

**Solution:** 
1. Verify `updateStateAfterEffects: true` in EffectsBoardWrapper
2. Ensure effect is triggered before `events.endPhase()`
3. Check that animation starts when effect is received

### Issue: Complex overlapping animations

**Symptom:** Multiple animations happening at once, hard to coordinate

**Solution:** Consider these approaches:
1. **Sequential effects** - each effect triggers the next when done
2. **Separate animation flags** - `isMagnetAnimating`, `isGridAnimating`, etc.
3. **Animation queue** - more complex but handles arbitrary sequences

For most games, keeping animations sequential (one at a time) is simplest and clearest for players.

## Debugging Animations

Add console logs at key points:

```javascript
// In game logic
effects.myAnimation();
console.log('[GAME] Triggered myAnimation effect');
events.endPhase();
console.log('[GAME] Phase changed');

// In UI effect listener
useEffectListener('myAnimation', () => {
  console.log('[UI] Animation starting - disabling input');
  setIsAnimating(true);
  setTimeout(() => {
    console.log('[UI] Animation complete - enabling input');
    setIsAnimating(false);
  }, DURATION);
});

// In phase change
useEffect(() => {
  console.log(`[UI] Phase changed to: ${ctx.phase}`);
}, [ctx.phase]);

// In state change
useEffect(() => {
  console.log('[UI] State updated');
}, [G]);
```

**Expected timeline:**
1. `[GAME] Triggered effect`
2. `[GAME] Phase changed`
3. `[UI] Phase changed`
4. `[UI] Animation starting`
5. ... wait N seconds ...
6. `[UI] Animation complete`
7. `[UI] State updated`

If order is different, you've found the problem!

## Best Practices Summary

1. ✅ **Centralize durations** in constants.js
2. ✅ **Use updateStateAfterEffects: true**
3. ✅ **Block UI during animations** with isAnimating flag
4. ✅ **Trigger effects before ending phase/turn**
5. ✅ **Match timings** between effect duration, setTimeout, and CSS
6. ✅ **Test with long durations** (1-3s) during development
7. ✅ **Add console logs** to verify timing
8. ✅ **Keep animations simple** - sequential is easier than parallel
9. ✅ **Document your effects** - what they do, when they trigger
10. ✅ **One source of truth** for each timing value

## References

- [bgio-effects documentation](https://delucis.github.io/bgio-effects/)
- [boardgame.io documentation](https://boardgame.io/documentation/)
- This project's `BestPractices.md` for UI/game separation principles

