Files
novafarma/docs/WATER_ANIMATION.md
2025-12-11 20:41:00 +01:00

13 KiB

b# 💧 Water Tile Animation Tutorial

Project: NovaFarma
Date: 11. December 2025
Author: Development Team


📚 Table of Contents:

  1. Overview
  2. How It Works
  3. Implementation Steps
  4. Code Breakdown
  5. Customization
  6. Troubleshooting
  7. Advanced Techniques

🌊 Overview:

Water tiles in NovaFarma use a 4-frame animation system to create realistic water movement with:

  • Isometric perspective (diamond-shaped tiles)
  • 3D depth effect (visible side faces)
  • Animated waves (sine wave pattern)
  • Sparkle effects (light reflections)
  • Smooth transitions (60 FPS animation)

Result: Living, breathing water that feels organic!


🔧 How It Works:

System Architecture:

TerrainSystem.js
├── createWaterFrames()    // Generates 4 animation frames
├── generate()             // Creates water tiles on map
└── update()               // Cycles through frames (60 FPS)

Animation Flow:

Frame 0 → Frame 1 → Frame 2 → Frame 3 → Loop back to Frame 0
  ↓         ↓         ↓         ↓
Wave     Wave      Wave      Wave
Offset   Offset    Offset    Offset
  0        +3        +6        +9

Frame Duration: ~200ms each
Total Loop Time: ~800ms
FPS: 60 (smooth transitions)


🚀 Implementation Steps:

Step 1: Generate Water Frames

Location: src/systems/TerrainSystem.js

createWaterFrames() {
    const tileWidth = 48;
    const tileHeight = 48;
    const P = 2; // Padding for anti-aliasing

    // Generate 4 frames
    for (let frame = 0; frame < 4; frame++) {
        const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });

        // ... drawing code ...

        graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);
        graphics.destroy();
    }
}

Call this in constructor:

constructor(scene) {
    // ...
    this.createWaterFrames();
}

Step 2: Create Isometric Diamond Shape

// Define isometric coordinates
const xs = P;               // Left edge
const xe = 48 + P;          // Right edge
const midX = 24 + P;        // Center X
const topY = P;             // Top point
const midY = 12 + P;        // Middle Y
const bottomY = 24 + P;     // Bottom point
const depth = 14;           // 3D depth for sides

// Draw diamond top surface
graphics.fillStyle(0x33ccff); // Light cyan
graphics.beginPath();
graphics.moveTo(xs, midY);      // Left point
graphics.lineTo(midX, topY);    // Top point
graphics.lineTo(xe, midY);      // Right point
graphics.lineTo(midX, bottomY); // Bottom point
graphics.closePath();
graphics.fill();

Step 3: Add 3D Side Faces

// LEFT FACE - Dark blue
const cLeft = 0x0066aa;
graphics.fillStyle(cLeft);
graphics.beginPath();
graphics.moveTo(midX, bottomY);
graphics.lineTo(midX, bottomY + depth);
graphics.lineTo(xs, midY + depth);
graphics.lineTo(xs, midY);
graphics.closePath();
graphics.fill();

// RIGHT FACE - Darker blue
const cRight = 0x004488;
graphics.fillStyle(cRight);
graphics.beginPath();
graphics.moveTo(xe, midY);
graphics.lineTo(xe, midY + depth);
graphics.lineTo(midX, bottomY + depth);
graphics.lineTo(midX, bottomY);
graphics.closePath();
graphics.fill();

Step 4: Add Wave Animation

// Offset changes per frame (0, 3, 6, 9)
const offset = frame * 3;

// Draw 3 wave lines
graphics.lineStyle(1, 0x66ddff, 0.3); // Semi-transparent white

for (let i = 0; i < 3; i++) {
    graphics.beginPath();
    const baseY = topY + 6 + i * 5; // Vertical spacing
    
    for (let px = xs; px <= xe; px += 2) {
        const relativeX = px - xs;
        const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.15) * 1.5;
        const py = baseY + waveOffset;
        
        if (px === xs) graphics.moveTo(px, py);
        else graphics.lineTo(px, py);
    }
    graphics.strokePath();
}

Wave Formula Explained:

Math.sin(
    (relativeX + offset + i * 10)  // Position + animation + wave offset
    * 0.15                          // Frequency (lower = wider waves)
) * 1.5                             // Amplitude (height of waves)

Step 5: Add Sparkle Effects

graphics.fillStyle(0xffffff); // White sparkles

const sparkles = [
    { x: midX - 10 + (frame * 2) % 20, y: midY + 3 },
    { x: midX + 8 - (frame * 3) % 16, y: midY + 8 },
    { x: midX - 4 + Math.floor(frame * 1.5) % 8, y: midY + 13 }
];

sparkles.forEach(s => {
    // Draw cross pattern (5 pixels)
    graphics.fillRect(s.x, s.y, 1, 1);       // Center
    graphics.fillRect(s.x - 2, s.y, 1, 1);   // Left
    graphics.fillRect(s.x + 2, s.y, 1, 1);   // Right
    graphics.fillRect(s.x, s.y - 2, 1, 1);   // Top
    graphics.fillRect(s.x, s.y + 2, 1, 1);   // Bottom
});

Sparkle Animation:

  • Each sparkle moves across the tile per frame
  • Uses modulo (%) to loop position
  • Creates realistic light reflection effect

Step 6: Animate Water Tiles

Location: src/systems/TerrainSystem.js - update() method

update(time, delta) {
    // Update every 200ms
    if (!this.lastWaterUpdate) this.lastWaterUpdate = 0;
    
    if (time - this.lastWaterUpdate > 200) {
        this.lastWaterUpdate = time;
        this.currentWaterFrame = (this.currentWaterFrame + 1) % 4;
        
        // Update all water tiles
        for (let key in this.tiles) {
            const tile = this.tiles[key];
            if (tile.type === 'WATER_DEEP') {
                tile.sprite.setTexture(`water_frame_${this.currentWaterFrame}`);
            }
        }
    }
}

Performance Note: Only updates texture reference, not recreating sprites!


🎨 Customization:

Change Water Color:

// Original (light cyan)
const waterColor = 0x33ccff;

// Variations:
const waterColor = 0x0099ff; // Deeper blue
const waterColor = 0x55eeff; // Bright cyan
const waterColor = 0x2266aa; // Dark ocean
const waterColor = 0x88ff88; // Toxic green (swamp)

Adjust Wave Speed:

// Faster animation (100ms per frame)
if (time - this.lastWaterUpdate > 100) { ... }

// Slower animation (500ms per frame)
if (time - this.lastWaterUpdate > 500) { ... }

Change Wave Pattern:

// More waves (5 instead of 3)
for (let i = 0; i < 5; i++) {
    const baseY = topY + 4 + i * 3; // Closer spacing
    // ...
}

// Bigger waves (higher amplitude)
const waveOffset = Math.sin(...) * 3.0; // Was 1.5

// Faster waves (higher frequency)
const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.3) * 1.5; // Was 0.15

More Sparkles:

const sparkles = [
    { x: midX - 15 + (frame * 2) % 30, y: midY + 2 },
    { x: midX - 5 + (frame * 3) % 10, y: midY + 6 },
    { x: midX + 5 - (frame * 2) % 10, y: midY + 10 },
    { x: midX + 10 + (frame * 4) % 20, y: midY + 14 },
    { x: midX - 8 + Math.floor(frame * 1.5) % 16, y: midY + 18 }
];

🐛 Troubleshooting:

Problem: Water not animating

Solution 1: Check if update() is called

// In GameScene.js update()
if (this.terrainSystem && this.terrainSystem.update) {
    this.terrainSystem.update(Date.now(), delta);
}

Solution 2: Verify frames exist

// In browser console:
game.textures.list
// Should show: water_frame_0, water_frame_1, water_frame_2, water_frame_3

Problem: Water tiles are black/missing

Solution: Ensure createWaterFrames() is called before generate()

constructor(scene) {
    // ...
    this.createWaterFrames();  // MUST come first
    this.generate();           // Then generate tiles
}

Problem: Animation is choppy

Solution: Reduce update frequency

// Too fast (every frame = choppy)
if (time - this.lastWaterUpdate > 16) { ... }

// Better (200ms = smooth)
if (time - this.lastWaterUpdate > 200) { ... }

Problem: Water looks pixelated

Solution: Add padding (anti-aliasing)

const P = 2; // Padding MUST be at least 2
graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);

🚀 Advanced Techniques:

1. Shore Transitions:

Create special water tiles for edges:

createShoreWaterFrame(frame, edgeType) {
    // edgeType: 'north', 'south', 'east', 'west'
    const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
    
    // Draw water diamond
    // ... (same as before)
    
    // Add sand/grass edge overlay
    if (edgeType === 'north') {
        graphics.fillStyle(0xddaa77); // Sand color
        graphics.beginPath();
        graphics.moveTo(midX - 10, topY);
        graphics.lineTo(midX + 10, topY);
        graphics.lineTo(midX, topY + 5);
        graphics.closePath();
        graphics.fill();
    }
    
    graphics.generateTexture(`water_shore_${edgeType}_${frame}`, 52, 52);
    graphics.destroy();
}

2. Depth Variations:

Different tile heights for shallow/deep water:

createWaterFrames() {
    // Shallow water (lighter, less depth)
    this.createWaterVariant('shallow', {
        topColor: 0x66ddff,
        sideColor: 0x33aadd,
        depth: 8
    });
    
    // Deep water (darker, more depth)
    this.createWaterVariant('deep', {
        topColor: 0x0066aa,
        sideColor: 0x003366,
        depth: 20
    });
}

3. Particle Effects:

Add water droplets on click:

// In GameScene.js
this.input.on('pointerdown', (pointer) => {
    const tile = this.terrainSystem.getTileAt(pointer.x, pointer.y);
    
    if (tile && tile.type === 'WATER_DEEP') {
        this.createWaterSplash(pointer.x, pointer.y);
    }
});

createWaterSplash(x, y) {
    for (let i = 0; i < 10; i++) {
        const particle = this.add.circle(x, y, 2, 0x66ddff);
        this.tweens.add({
            targets: particle,
            x: x + (Math.random() - 0.5) * 30,
            y: y - Math.random() * 20,
            alpha: 0,
            duration: 500,
            onComplete: () => particle.destroy()
        });
    }
}

4. Reflection Effect:

Mirror sprites above water:

createReflection(sprite, gridX, gridY) {
    const waterTile = this.tiles[`${gridX},${gridY}`];
    if (waterTile && waterTile.type === 'WATER_DEEP') {
        const reflection = this.scene.add.sprite(
            sprite.x,
            sprite.y + 20, // Offset below sprite
            sprite.texture.key
        );
        reflection.setOrigin(0.5, 0);
        reflection.setAlpha(0.3);
        reflection.setFlipY(true); // Mirror vertically
        reflection.setDepth(waterTile.sprite.depth - 1);
        
        return reflection;
    }
}

📖 Best Practices:

Performance:

  • Generate frames ONCE in constructor
  • Use texture swapping (not sprite recreation)
  • Update at 200ms intervals (not every frame)
  • Use object pooling for particles
  • Don't recreate graphics every update

Visual Quality:

  • Use padding (P = 2) for smooth edges
  • Add sparkle effects for realism
  • Use isometric perspective for depth
  • Vary side face colors (left darker than right)
  • Don't make waves too fast (looks jittery)

Code Organization:

  • Separate frame generation from animation
  • Use constants for colors/sizes
  • Comment complex math formulas
  • Log creation success (console.log)
  • Don't hardcode magic numbers

💡 Pro Tips:

  1. Debug mode: Press F12 and type game.textures.list to see all frames
  2. Performance: Monitor FPS with game.loop.actualFps
  3. Testing: Change this.currentWaterFrame manually in console
  4. Variations: Create multiple water types (ocean, river, swamp)
  5. Polish: Add sound effects (water splash, waves)

🎓 Summary:

What you learned:

  • How to create animated isometric tiles
  • Sine wave animation technique
  • Texture generation in Phaser
  • Frame-based animation system
  • 3D depth effect with side faces

Next steps:

  • Experiment with colors and speeds
  • Add shore transitions
  • Create particle effects
  • Implement reflection system

c:\novafarma\src\systems\TerrainSystem.js  👈 Main implementation
c:\novafarma\docs\WATER_ANIMATION.md       👈 This file

Reference implementation:
Lines 237-324 in TerrainSystem.js


Happy animating! 💧🌊

Last updated: 11.12.2025 - 20:12