376 lines
12 KiB
JavaScript
376 lines
12 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|
|
}
|