/** * WindFoliageSystem.js * * Dynamic Wind & Foliage System for Style 32 Dark-Chibi Noir * * Features: * - Vertex Shader for hair (Kai, Ana, Gronk dreads) * - Grass/foliage wave animation using Perlin Noise * - Particle Emitter for falling leaves from trees * - Biome-specific wind strength * - Wobble physics for natural movement * * Performance: Uses shaders instead of thousands of sprites for RAM optimization */ export default class WindFoliageSystem { constructor(scene) { this.scene = scene; // Global wind settings this.wind = { strength: 1.0, // Default wind strength (0.0 - 2.0) direction: 0, // Wind direction in radians speed: 0.5, // Wind animation speed frequency: 2.0, // Perlin noise frequency amplitude: 8.0 // Maximum displacement in pixels }; // Biome-specific wind configurations this.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 }, 'tundra': { strength: 1.8, frequency: 3.0 }, 'volcanic': { strength: 1.2, frequency: 2.0 } }; // Particle systems for leaves this.leafEmitters = []; // Hair/foliage layers with wind effect this.windAffectedLayers = []; // Time accumulator for noise this.time = 0; } /** * Initialize the wind system */ init() { console.log('🌬️ WindFoliageSystem: Initializing...'); // Create wind shader pipeline this.createWindShader(); // Initialize leaf particle system this.initLeafParticles(); console.log('✅ WindFoliageSystem: Ready!'); } /** * Create custom wind shader for vertex displacement */ createWindShader() { // Vertex shader code for wind effect const vertexShader = ` precision mediump float; uniform mat4 uProjectionMatrix; uniform mat4 uViewMatrix; uniform float uTime; uniform float uWindStrength; uniform float uWindFrequency; uniform float uWindAmplitude; uniform vec2 uWindDirection; attribute vec2 inPosition; attribute vec2 inTexCoord; varying vec2 vTexCoord; // Perlin-like noise function (simplified for performance) float noise(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); } float smoothNoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); // Smoothstep float a = noise(i); float b = noise(i + vec2(1.0, 0.0)); float c = noise(i + vec2(0.0, 1.0)); float d = noise(i + vec2(1.0, 1.0)); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } float perlinNoise(vec2 p) { float total = 0.0; float frequency = 1.0; float amplitude = 1.0; float maxValue = 0.0; for(int i = 0; i < 4; i++) { total += smoothNoise(p * frequency) * amplitude; maxValue += amplitude; amplitude *= 0.5; frequency *= 2.0; } return total / maxValue; } void main() { vTexCoord = inTexCoord; // Apply wind displacement based on vertex height (Y position) // Higher vertices (like hair tips) move more float heightFactor = inTexCoord.y; // 0.0 at bottom, 1.0 at top // Calculate Perlin noise for natural wind vec2 noiseInput = inPosition * 0.01 * uWindFrequency + vec2(uTime * 0.3, uTime * 0.2); float windOffset = perlinNoise(noiseInput); // Apply wind displacement vec2 displacement = uWindDirection * windOffset * uWindStrength * uWindAmplitude * heightFactor; vec2 finalPosition = inPosition + displacement; gl_Position = uProjectionMatrix * uViewMatrix * vec4(finalPosition, 0.0, 1.0); } `; // Fragment shader (simple passthrough with texture) const fragmentShader = ` precision mediump float; uniform sampler2D uMainSampler; varying vec2 vTexCoord; void main() { gl_FragColor = texture2D(uMainSampler, vTexCoord); } `; // Store shader code for later use this.windShader = { vertex: vertexShader, fragment: fragmentShader }; console.log('🎨 Wind shader created'); } /** * Apply wind effect to a sprite layer (hair, grass, etc.) * @param {Phaser.GameObjects.Sprite} sprite - The sprite to affect * @param {string} layerType - Type: 'hair', 'grass', 'foliage' */ applyWindToSprite(sprite, layerType = 'grass') { if (!sprite) return; // Store reference this.windAffectedLayers.push({ sprite: sprite, type: layerType, offsetX: 0, offsetY: 0, baseX: sprite.x, baseY: sprite.y }); console.log(`🌬️ Wind applied to ${layerType} layer`); } /** * Initialize falling leaf particle system */ initLeafParticles() { // Configuration for different leaf types this.leafConfig = { lifespan: 5000, // 5 seconds fall time speed: { min: 20, max: 50 }, gravityY: 100, rotate: { min: -180, max: 180 }, wobbleStrength: 30, // Horizontal wobble colors: [0x8B4513, 0xA0522D, 0xD2691E, 0x654321] // Brown tones }; console.log('🍃 Leaf particle system initialized'); } /** * Create leaf emitter for a tree * @param {number} x - Tree X position * @param {number} y - Tree Y position * @param {number} emitRate - Leaves per second */ createLeafEmitter(x, y, emitRate = 0.5) { const particles = this.scene.add.particles(x, y); // Create simple leaf graphics (can be replaced with sprite) const leafGraphics = this.scene.add.graphics(); leafGraphics.fillStyle(0x8B4513, 1); leafGraphics.fillEllipse(0, 0, 8, 12); leafGraphics.generateTexture('leaf', 8, 12); leafGraphics.destroy(); const emitter = particles.createEmitter({ frame: 'leaf', lifespan: this.leafConfig.lifespan, speed: this.leafConfig.speed, gravityY: this.leafConfig.gravityY, rotate: this.leafConfig.rotate, frequency: 1000 / emitRate, // Convert to milliseconds scale: { start: 1, end: 0.8 }, alpha: { start: 1, end: 0.3 }, tint: Phaser.Utils.Array.GetRandom(this.leafConfig.colors) }); this.leafEmitters.push({ particles: particles, emitter: emitter, x: x, y: y }); return emitter; } /** * Set wind strength (affects all wind effects) * @param {number} strength - Wind strength (0.0 - 2.0) */ setWindStrength(strength) { this.wind.strength = Phaser.Math.Clamp(strength, 0, 2.0); console.log(`🌬️ Wind strength set to: ${this.wind.strength.toFixed(2)}`); } /** * Apply biome-specific wind settings * @param {string} biomeName - Name of the biome */ setBiomeWind(biomeName) { const settings = this.biomeWindSettings[biomeName.toLowerCase()]; if (settings) { this.wind.strength = settings.strength; this.wind.frequency = settings.frequency; console.log(`🌍 Wind adjusted for ${biomeName}: strength=${settings.strength}, frequency=${settings.frequency}`); } else { console.warn(`⚠️ Unknown biome: ${biomeName}, using default wind`); } } /** * Perlin noise implementation (for CPU-side calculations) */ perlinNoise(x, y) { const xi = Math.floor(x); const yi = Math.floor(y); const xf = x - xi; const yf = y - yi; // Simple hash function const hash = (x, y) => { const h = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; return h - Math.floor(h); }; const a = hash(xi, yi); const b = hash(xi + 1, yi); const c = hash(xi, yi + 1); const d = hash(xi + 1, yi + 1); // Smoothstep interpolation const u = xf * xf * (3 - 2 * xf); const v = yf * yf * (3 - 2 * yf); return Phaser.Math.Linear( Phaser.Math.Linear(a, b, u), Phaser.Math.Linear(c, d, u), v ); } /** * Update wind effect (call every frame) * @param {number} delta - Time since last frame in ms */ update(delta) { this.time += delta * 0.001; // Convert to seconds // Update all wind-affected sprites this.windAffectedLayers.forEach(layer => { const { sprite, type, baseX, baseY } = layer; if (!sprite || !sprite.active) return; // Calculate Perlin noise for smooth wind 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); // Apply displacement const displacement = windNoise * this.wind.strength * this.wind.amplitude; // Different displacement for different layer types if (type === 'hair') { // Hair sways more at the tips sprite.x = baseX + displacement * 1.5; sprite.angle = displacement * 0.5; // Slight rotation } else if (type === 'grass') { // Grass bends from the base sprite.x = baseX + displacement; } else { // Generic foliage sprite.x = baseX + displacement * 0.8; } }); // Update leaf particles with wobble this.leafEmitters.forEach(emitterData => { if (emitterData.emitter) { // Add horizontal wobble to falling leaves const wobble = Math.sin(this.time * 2) * this.leafConfig.wobbleStrength * this.wind.strength; emitterData.emitter.setSpeedX({ min: wobble - 10, max: wobble + 10 }); } }); } /** * Debug: Show wind strength indicator */ showWindDebug() { if (!this.windDebugText) { this.windDebugText = this.scene.add.text(10, 10, '', { font: '16px Arial', fill: '#ffffff', stroke: '#000000', strokeThickness: 4 }).setScrollFactor(0).setDepth(10000); } this.windDebugText.setText([ `🌬️ Wind System`, `Strength: ${this.wind.strength.toFixed(2)}`, `Frequency: ${this.wind.frequency.toFixed(2)}`, `Layers: ${this.windAffectedLayers.length}`, `Emitters: ${this.leafEmitters.length}` ]); } /** * Cleanup */ destroy() { this.windAffectedLayers = []; this.leafEmitters.forEach(e => { if (e.particles) e.particles.destroy(); }); this.leafEmitters = []; if (this.windDebugText) { this.windDebugText.destroy(); } } }