Files
novafarma/docs/technical/ADVANCED_VISUAL_SYSTEMS_PLAN.md
David Kotnik eb78618ea1 📘 ADD: Advanced Visual Systems Master Plan
NEW DOC: ADVANCED_VISUAL_SYSTEMS_PLAN.md

🌊💨 Complete implementation plan for:
 Wind & Foliage System (DONE)
 Water Physics & Buoyancy
 Water Ripples (footsteps, splash)
 Water Displacement (refraction shader)
 Caustics (light networks)

📝 Includes:
- Full shader code (GLSL)
- Phaser 3 integration
- Performance optimization
- Style 32 compatibility
- 15-25h time estimate

🎯 Priority: WaterPhysics → Ripples → Displacement → Caustics

📁 Location: docs/technical/
2026-01-07 22:53:44 +01:00

22 KiB

🌊💨 ADVANCED VISUAL SYSTEMS - MASTER PLAN

Created: Jan 7, 2026 22:50 CET
Purpose: Complete implementation plan for advanced shader & particle systems
Style: Style 32 Dark-Chibi Noir compatible
Engine: Phaser 3 + Custom Shaders


📋 TABLE OF CONTENTS

  1. Wind & Foliage System
  2. Water Displacement Mapping
  3. Ripple Effects
  4. Caustics (Light Networks)
  5. Water Physics & Buoyancy
  6. Integration Plan
  7. Performance Optimization

🌬️ 1. WIND & FOLIAGE SYSTEM

Status: IMPLEMENTED (Jan 7, 2026)
File: src/systems/WindFoliageSystem.js

Features:

  • Vertex Shader for hair animation (Kai, Ana, Gronk dreads)
  • Perlin Noise algorithm for natural wind
  • Particle Emitter for falling leaves
  • Wobble physics
  • Biome-specific wind strength

Technical Details:

// Wind strength per biome
biomeWindSettings = {
    'grassland': { strength: 1.0, frequency: 2.0 },
    'mountains': { strength: 2.0, frequency: 3.5 },
    'swamp': { strength: 0.3, frequency: 1.0 },
    'forest': { strength: 0.8, frequency: 2.5 },
    'desert': { strength: 1.5, frequency: 2.8 }
}

Performance:

  • Uses shaders instead of sprite animations (RAM optimized)
  • CPU-side Perlin noise fallback
  • Particle pooling for leaves

Integration:

// In GameScene.js create()
this.windSystem = new WindFoliageSystem(this);
this.windSystem.init();

// Apply to sprites
this.windSystem.applyWindToSprite(kaiHairLayer, 'hair');
this.windSystem.applyWindToSprite(grassSprite, 'grass');

// Create leaf emitter
this.windSystem.createLeafEmitter(treeX, treeY, 0.5);

// In update()
this.windSystem.update(delta);

💧 2. WATER DISPLACEMENT MAPPING

Status: TODO - High Priority
Purpose: Light refraction when objects are underwater

Concept:

When Kai steps into water, the submerged part of her body should appear "wavy" due to light refraction (like looking at legs in a pool).

Technical Approach:

Displacement Shader:

// Fragment shader for underwater displacement
precision mediump float;

uniform sampler2D uMainSampler;
uniform float uTime;
uniform float uWaterLevel;      // Y coordinate of water surface
uniform float uDisplacementStrength; // 0.01 - 0.05

varying vec2 vTexCoord;
varying vec2 vPosition;

// Sine wave displacement
vec2 getDisplacement(vec2 uv, float time) {
    float wave1 = sin(uv.x * 10.0 + time * 2.0) * 0.005;
    float wave2 = sin(uv.y * 8.0 + time * 1.5) * 0.005;
    return vec2(wave1, wave2) * uDisplacementStrength;
}

void main() {
    // Check if pixel is underwater
    if (vPosition.y > uWaterLevel) {
        // Apply displacement to UV coordinates
        vec2 displacement = getDisplacement(vTexCoord, uTime);
        vec2 distortedUV = vTexCoord + displacement;
        
        vec4 color = texture2D(uMainSampler, distortedUV);
        
        // Add slight blue tint underwater
        color.rgb = mix(color.rgb, vec3(0.4, 0.5, 0.6), 0.15);
        
        gl_FragColor = color;
    } else {
        // Above water - no distortion
        gl_FragColor = texture2D(uMainSampler, vTexCoord);
    }
}

Implementation Plan:

File: src/systems/WaterDisplacementSystem.js

class WaterDisplacementSystem {
    constructor(scene) {
        this.scene = scene;
        this.waterLevel = 0; // Y coordinate
        this.displacementStrength = 0.03;
    }
    
    createDisplacementShader() {
        // Create custom Phaser pipeline
        const DisplacementPipeline = new Phaser.Class({
            Extends: Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline,
            
            initialize: function() {
                Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline.call(this, {
                    game: this.game,
                    renderer: this.renderer,
                    fragShader: [fragment shader code from above]
                });
            }
        });
        
        this.scene.game.renderer.addPipeline('waterDisplacement', new DisplacementPipeline());
    }
    
    applyToSprite(sprite, waterY) {
        sprite.setPipeline('waterDisplacement');
        sprite.pipeline.set1f('uWaterLevel', waterY);
        sprite.pipeline.set1f('uDisplacementStrength', this.displacementStrength);
    }
    
    update(time) {
        // Update time uniform for animation
        this.scene.children.list.forEach(child => {
            if (child.pipeline && child.pipeline.name === 'waterDisplacement') {
                child.pipeline.set1f('uTime', time * 0.001);
            }
        });
    }
}

Usage:

// In GameScene
this.waterSystem = new WaterDisplacementSystem(this);
this.waterSystem.createDisplacementShader();

// Apply to player when in water
if (this.player.y > waterSurfaceY) {
    this.waterSystem.applyToSprite(this.player, waterSurfaceY);
}

Style 32 Compatibility:

  • Displacement is SUBTLE (0.03 strength)
  • Maintains thick black outlines
  • Blue tint is minimal (15% mix)
  • Works with cell-shaded sprites

🌊 3. RIPPLE EFFECTS

Status: TODO - Medium Priority
Purpose: Create circular ripples when walking in water or when objects fall

Concept:

When Kai walks through shallow water, or when a leaf falls from a tree into water, circular ripples should emanate from the impact point.

Technical Approach:

Particle-Based Ripples:

File: src/systems/WaterRipplesSystem.js

class WaterRipplesSystem {
    constructor(scene) {
        this.scene = scene;
        this.ripplesParticles = null;
    }
    
    init() {
        // Create ripple texture (Style 32 noir aesthetic)
        this.createRippleTexture();
        
        // Initialize particle manager
        this.ripplesParticles = this.scene.add.particles('waterRipple');
    }
    
    createRippleTexture() {
        const graphics = this.scene.add.graphics();
        
        // Style 32: Thick black outline with subtle fill
        graphics.lineStyle(3, 0x000000, 1);      // 3px black outline
        graphics.strokeCircle(32, 32, 28);       // Outer circle
        
        graphics.lineStyle(2, 0x000000, 0.6);    // Thinner inner ring
        graphics.strokeCircle(32, 32, 24);
        
        // Generate texture
        graphics.generateTexture('waterRipple', 64, 64);
        graphics.destroy();
    }
    
    createRipple(x, y, size = 1.0) {
        const emitter = this.ripplesParticles.createEmitter({
            x: x,
            y: y,
            lifespan: 1500,
            speed: 0,
            scale: { start: 0.1 * size, end: 2.0 * size },
            alpha: { start: 0.7, end: 0 },
            blendMode: 'NORMAL',
            frequency: -1,  // Emit once
            quantity: 1,
            rotate: 0
        });
        
        emitter.explode(1);
        
        // Auto-destroy emitter after animation
        this.scene.time.delayedCall(1500, () => {
            emitter.stop();
            this.ripplesParticles.removeEmitter(emitter);
        });
    }
    
    createFootstepRipples(x, y) {
        // Smaller ripples for footsteps
        this.createRipple(x, y, 0.5);
    }
    
    createSplashRipples(x, y) {
        // Larger ripples for falling objects
        this.createRipple(x, y, 1.5);
        
        // Create multiple concentric ripples
        this.scene.time.delayedCall(150, () => this.createRipple(x, y, 1.2));
        this.scene.time.delayedCall(300, () => this.createRipple(x, y, 0.9));
    }
}

Integration with Movement:

// In Player update()
update(delta) {
    // Check if player is in water
    if (this.isInWater && this.velocity > 0) {
        // Create ripple at player position every 300ms
        if (this.scene.time.now - this.lastRippleTime > 300) {
            this.scene.waterRipples.createFootstepRipples(this.x, this.y + 16);
            this.lastRippleTime = this.scene.time.now;
        }
    }
}

Integration with Wind System:

// In WindFoliageSystem - when leaf hits water
onLeafLand(leaf) {
    if (this.isOverWater(leaf.x, leaf.y)) {
        this.scene.waterRipples.createSplashRipples(leaf.x, leaf.y);
    }
}

4. CAUSTICS (LIGHT NETWORKS)

Status: TODO - Medium Priority
Purpose: Animated dancing light patterns on underwater surfaces

Concept:

Those beautiful, shimmering light patterns you see on the bottom of a pool or cenote when sunlight refracts through moving water.

Technical Approach:

Animated Texture with Additive Blending:

File: src/systems/WaterCausticsSystem.js

class WaterCausticsSystem {
    constructor(scene) {
        this.scene = scene;
        this.causticsLayers = [];
    }
    
    init() {
        // Generate procedural caustics texture
        this.generateCausticsTexture();
    }
    
    generateCausticsTexture() {
        // Create 4 frames for animation
        for (let frame = 0; frame < 4; frame++) {
            const graphics = this.scene.add.graphics();
            
            // Style 32 noir caustics: Subtle light patterns
            graphics.fillStyle(0xffffff, 0.15);
            
            // Draw organic light patterns (simplified)
            const offset = frame * 20;
            for (let i = 0; i < 20; i++) {
                const x = (Math.sin(i * 0.5 + offset) * 30) + 64;
                const y = (Math.cos(i * 0.7 + offset) * 30) + 64;
                const size = 15 + Math.sin(i) * 5;
                
                graphics.fillCircle(x, y, size);
            }
            
            graphics.generateTexture(`caustics_${frame}`, 128, 128);
            graphics.destroy();
        }
        
        // Create animation
        this.scene.anims.create({
            key: 'caustics_anim',
            frames: [
                { key: 'caustics_0' },
                { key: 'caustics_1' },
                { key: 'caustics_2' },
                { key: 'caustics_3' }
            ],
            frameRate: 6,
            repeat: -1
        });
    }
    
    addCausticsLayer(x, y, width, height, depthLayer = 0) {
        const caustics = this.scene.add.sprite(x, y, 'caustics_0')
            .setOrigin(0, 0)
            .setDisplaySize(width, height)
            .setBlendMode(Phaser.BlendModes.ADD)  // Additive blend!
            .setAlpha(0.3)
            .setDepth(depthLayer);
        
        caustics.play('caustics_anim');
        
        this.causticsLayers.push(caustics);
        return caustics;
    }
    
    setCausticsIntensity(intensity) {
        // Adjust based on water depth or time of day
        this.causticsLayers.forEach(layer => {
            layer.setAlpha(intensity * 0.3);
        });
    }
}

Usage:

// In WaterBiomeScene (Mexican Cenotes, Atlantis, etc.)
create() {
    // Add caustics to water floor
    this.caustics = new WaterCausticsSystem(this);
    this.caustics.init();
    
    // Add caustics layer to cenote floor
    this.caustics.addCausticsLayer(
        0, waterFloorY,      // Position
        mapWidth, 200,       // Size
        -1                   // Below player
    );
    
    // Dynamic intensity based on depth
    if (playerDepth > 50) {
        this.caustics.setCausticsIntensity(0.2);  // Darker at depth
    }
}

Style 32 Compatibility:

  • Uses white light with low alpha (15-30%)
  • Additive blend mode for light effect
  • Subtle, not overpowering
  • Works on dark gothic floors

🏊 5. WATER PHYSICS & BUOYANCY

Status: TODO - High Priority
Purpose: Realistic movement underwater with drag and buoyancy

Concept:

  • Kai moves 30% slower in water
  • Hair floats upward (dreads like in zero gravity)
  • Jumping is reduced underwater
  • Swimming animation triggers

Technical Approach:

File: src/systems/WaterPhysicsSystem.js

class WaterPhysicsSystem {
    constructor(scene) {
        this.scene = scene;
        
        // Physics constants
        this.waterDragFactor = 0.7;        // 30% slower
        this.waterJumpReduction = 0.5;     // 50% jump power
        this.buoyancyForce = -20;          // Upward force
        this.hairFloatStrength = 1.5;      // Hair rises more
    }
    
    applyWaterPhysics(player, isInWater) {
        if (isInWater) {
            // Apply drag to movement
            player.body.velocity.x *= this.waterDragFactor;
            player.body.velocity.y *= this.waterDragFactor;
            
            // Apply buoyancy (slow upward drift)
            player.body.velocity.y += this.buoyancyForce * this.scene.game.loop.delta * 0.001;
            
            // Modify jump power
            if (player.isJumping) {
                player.jumpVelocity *= this.waterJumpReduction;
            }
            
            // Trigger swimming animation
            if (!player.anims.currentAnim || player.anims.currentAnim.key !== 'swim') {
                player.play('swim');
            }
            
            // Float hair upward
            this.floatHair(player, true);
            
        } else {
            // Reset hair
            this.floatHair(player, false);
        }
    }
    
    floatHair(player, float) {
        if (!player.hairLayer) return;
        
        if (float) {
            // Modify wind system to make hair float upward
            if (this.scene.windSystem) {
                // Override wind direction for hair to point UP
                this.scene.windSystem.windAffectedLayers.forEach(layer => {
                    if (layer.sprite === player.hairLayer) {
                        layer.buoyantMode = true;
                        layer.floatStrength = this.hairFloatStrength;
                    }
                });
            }
        } else {
            // Restore normal wind behavior
            if (this.scene.windSystem) {
                this.scene.windSystem.windAffectedLayers.forEach(layer => {
                    if (layer.sprite === player.hairLayer) {
                        layer.buoyantMode = false;
                        layer.floatStrength = 0;
                    }
                });
            }
        }
    }
    
    checkWaterDepth(x, y) {
        // Return depth: 0 = surface, 100 = deep
        // Can use tilemap water layer or zone detection
        const waterTile = this.scene.waterLayer.getTileAtWorldXY(x, y);
        
        if (!waterTile) return 0;  // No water
        
        // Use tile properties for depth
        return waterTile.properties.depth || 50;
    }
}

Enhanced Wind System Integration:

Update src/systems/WindFoliageSystem.js:

// Add to WindFoliageSystem.update()
update(delta) {
    this.time += delta * 0.001;
    
    this.windAffectedLayers.forEach(layer => {
        const { sprite, type, baseX, baseY, buoyantMode, floatStrength } = layer;
        
        if (!sprite || !sprite.active) return;
        
        if (buoyantMode && floatStrength > 0) {
            // UNDERWATER BUOYANCY MODE
            // Hair floats upward with gentle wave
            const floatOffset = Math.sin(this.time * 0.5) * 5 * floatStrength;
            sprite.y = baseY - floatOffset;  // Negative = upward
            sprite.angle = floatOffset * 0.3; // Slight rotation
            
        } else {
            // NORMAL WIND MODE
            const noiseX = baseX * 0.01 * this.wind.frequency + this.time * this.wind.speed;
            const noiseY = baseY * 0.01 * this.wind.frequency + this.time * this.wind.speed * 0.7;
            const windNoise = this.perlinNoise(noiseX, noiseY);
            const displacement = windNoise * this.wind.strength * this.wind.amplitude;
            
            if (type === 'hair') {
                sprite.x = baseX + displacement * 1.5;
                sprite.angle = displacement * 0.5;
            } else if (type === 'grass') {
                sprite.x = baseX + displacement;
            }
        }
    });
    
    // ... rest of update
}

Usage:

// In GameScene update()
update(time, delta) {
    const playerDepth = this.waterPhysics.checkWaterDepth(this.player.x, this.player.y);
    const isInWater = playerDepth > 10;
    
    this.waterPhysics.applyWaterPhysics(this.player, isInWater);
    
    // Update other systems
    this.windSystem.update(delta);
}

🔧 INTEGRATION PLAN

Phase 1 - Core Systems (Already Done)

  • WindFoliageSystem

Phase 2 - Water Effects (Next Priority)

  1. WaterPhysicsSystem - Drag, buoyancy, hair float

    • Estimated time: 4-6 hours
    • Dependencies: WindFoliageSystem integration
  2. WaterRipplesSystem - Footstep and splash ripples

    • Estimated time: 2-3 hours
    • Dependencies: None
  3. WaterDisplacementSystem - Underwater refraction

    • Estimated time: 6-8 hours
    • Dependencies: Custom shader pipeline

Phase 3 - Advanced Effects (Polish)

  1. WaterCausticsSystem - Light networks
    • Estimated time: 3-4 hours
    • Dependencies: None

File Structure:

src/systems/
├── WindFoliageSystem.js          ✅ Done
├── WaterPhysicsSystem.js         ❌ TODO
├── WaterRipplesSystem.js         ❌ TODO
├── WaterDisplacementSystem.js    ❌ TODO
└── WaterCausticsSystem.js        ❌ TODO

Integration in GameScene:

// In GameScene.js
create() {
    // Initialize all systems
    this.windSystem = new WindFoliageSystem(this);
    this.windSystem.init();
    
    this.waterPhysics = new WaterPhysicsSystem(this);
    this.waterRipples = new WaterRipplesSystem(this);
    this.waterRipples.init();
    
    this.waterDisplacement = new WaterDisplacementSystem(this);
    this.waterDisplacement.createDisplacementShader();
    
    this.caustics = new WaterCausticsSystem(this);
    this.caustics.init();
    
    // Set biome wind
    this.windSystem.setBiomeWind(this.currentBiome);
    
    // Apply systems to player
    this.windSystem.applyWindToSprite(this.player.hairLayer, 'hair');
}

update(time, delta) {
    // Update all systems
    this.windSystem.update(delta);
    
    // Check if in water
    const depth = this.waterPhysics.checkWaterDepth(this.player.x, this.player.y);
    const isInWater = depth > 10;
    
    this.waterPhysics.applyWaterPhysics(this.player, isInWater);
    
    if (isInWater) {
        this.waterDisplacement.update(time);
    }
}

PERFORMANCE OPTIMIZATION

RAM Optimization:

  • Shaders use GPU instead of sprite sheets
  • Particle pooling (max 50 active ripples)
  • Texture atlasing for caustics (4 frames only)
  • TODO: Level-of-Detail (LOD) - disable effects on mobile

GPU Optimization:

  • Minimize shader complexity (max 20 instructions)
  • Use low-precision floats (mediump)
  • TODO: Batch draw calls
  • TODO: Frustum culling for particles

CPU Optimization:

  • Perlin noise cached per frame
  • Water depth checks only on movement
  • TODO: Spatial partitioning for ripples

Memory Budget:

System Memory Usage GPU Load
Wind ~2MB Low
Ripples ~5MB Medium
Displacement ~1MB High
Caustics ~3MB Low
TOTAL ~11MB Medium-High

Target Performance:

  • 60 FPS on desktop (GTX 1060 equivalent)
  • 30 FPS on mobile (Snapdragon 855+)
  • Graceful degradation on low-end devices

📊 IMPLEMENTATION CHECKLIST

Immediate (Next Session):

  • Implement WaterPhysicsSystem
  • Test hair buoyancy with WindFoliageSystem
  • Implement WaterRipplesSystem
  • Test ripples with player movement

Short-Term (This Week):

  • Implement WaterDisplacementSystem
  • Create custom shader pipeline
  • Test underwater refraction effect
  • Style 32 compatibility testing

Medium-Term (Next Week):

  • Implement WaterCausticsSystem
  • Generate procedural caustics textures
  • Integrate all systems in test scene
  • Performance profiling

Long-Term (Phase 2):

  • Mobile optimization
  • Water surface waves (optional)
  • Underwater fog/particles (optional)
  • Rain effects integration

🎨 STYLE 32 COMPATIBILITY NOTES

All systems MUST maintain Style 32 Dark-Chibi Noir aesthetic:

  1. Thick Black Outlines: Ripples have 3px black strokes
  2. Cell-Shaded: Displacement maintains flat color regions
  3. Subtle Effects: All effects at 15-30% opacity
  4. Gothic Palette: Use dark blues/greens for water tints
  5. No Realism: Effects are stylized, not photorealistic

🔗 SYSTEM DEPENDENCIES

WindFoliageSystem (✅ Done)
    ↓
WaterPhysicsSystem (uses WindSystem for hair float)
    ↓
WaterRipplesSystem (triggered by WaterPhysics movement)
    ↓
WaterDisplacementSystem (visual only, independent)
    ↓
WaterCausticsSystem (visual only, independent)

📝 NOTES TO SELF (ADHD-Friendly)

🌬️ Wind = DONE! (Check WindFoliageSystem.js)

💧 Water Physics NEXT → Start here!

  • Drag factor = player moves slow
  • Hair floats UP (modify WindSystem)
  • Swimming animation trigger

🌊 Ripples EASY → Do this second!

  • Just particles with scale+alpha animation
  • Style 32 = black circles
  • Trigger on footsteps

Displacement HARD → Do this third!

  • Need custom shader pipeline
  • Test extensively
  • Fallback for mobile

💎 Caustics OPTIONAL → Polish phase

  • Low priority
  • Looks amazing but not critical
  • Mexican Cenotes will LOVE this!

TOTAL ESTIMATED TIME: 15-25 hours across all systems

PRIORITY ORDER:

  1. WaterPhysicsSystem (critical for gameplay)
  2. WaterRipplesSystem (polish, medium effort)
  3. WaterDisplacementSystem (polish, high effort)
  4. WaterCausticsSystem (optional, medium effort)

DolinaSmrti - Where even the water is gothic. 🌊💀