b# 💧 Water Tile Animation Tutorial **Project:** NovaFarma **Date:** 11. December 2025 **Author:** Development Team --- ## 📚 **Table of Contents:** 1. [Overview](#overview) 2. [How It Works](#how-it-works) 3. [Implementation Steps](#implementation-steps) 4. [Code Breakdown](#code-breakdown) 5. [Customization](#customization) 6. [Troubleshooting](#troubleshooting) 7. [Advanced Techniques](#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` ```javascript 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:** ```javascript constructor(scene) { // ... this.createWaterFrames(); } ``` --- ### **Step 2: Create Isometric Diamond Shape** ```javascript // 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** ```javascript // 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** ```javascript // 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:** ```javascript 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** ```javascript 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 ```javascript 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:** ```javascript // 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:** ```javascript // Faster animation (100ms per frame) if (time - this.lastWaterUpdate > 100) { ... } // Slower animation (500ms per frame) if (time - this.lastWaterUpdate > 500) { ... } ``` --- ### **Change Wave Pattern:** ```javascript // 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:** ```javascript 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 ```javascript // In GameScene.js update() if (this.terrainSystem && this.terrainSystem.update) { this.terrainSystem.update(Date.now(), delta); } ``` **Solution 2:** Verify frames exist ```javascript // 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()` ```javascript constructor(scene) { // ... this.createWaterFrames(); // MUST come first this.generate(); // Then generate tiles } ``` --- ### **Problem: Animation is choppy** **Solution:** Reduce update frequency ```javascript // 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) ```javascript 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: ```javascript 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: ```javascript 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: ```javascript // 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: ```javascript 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 --- ## 📚 **Related Files:** ``` 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*