import * as THREE from 'three'; export class ParticleSystem { constructor(game) { this.game = game; this.systems = []; } createFire(position, opts = {}) { const count = opts.count || 60; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const velocities = new Float32Array(count * 3); const lifetimes = new Float32Array(count); const sizes = new Float32Array(count); for (let i = 0; i < count; i++) { this.resetFireParticle(positions, velocities, lifetimes, sizes, i, position); } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const mat = new THREE.PointsMaterial({ color: 0xff6622, size: 0.25, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true }); const mesh = new THREE.Points(geo, mat); this.game.scene.add(mesh); this.systems.push({ type: 'fire', mesh, geo, positions, velocities, lifetimes, sizes, count, origin: position.clone(), resetFn: (i) => this.resetFireParticle(positions, velocities, lifetimes, sizes, i, position) }); return mesh; } resetFireParticle(positions, velocities, lifetimes, sizes, i, origin) { const i3 = i * 3; positions[i3] = origin.x + (Math.random() - 0.5) * 0.4; positions[i3 + 1] = origin.y + Math.random() * 0.2; positions[i3 + 2] = origin.z + (Math.random() - 0.5) * 0.4; velocities[i3] = (Math.random() - 0.5) * 0.3; velocities[i3 + 1] = 1.0 + Math.random() * 2.0; velocities[i3 + 2] = (Math.random() - 0.5) * 0.3; lifetimes[i] = Math.random() * 1.5; sizes[i] = 0.15 + Math.random() * 0.2; } createRain() { const count = 3000; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const velocities = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const i3 = i * 3; positions[i3] = (Math.random() - 0.5) * 120; positions[i3 + 1] = Math.random() * 40; positions[i3 + 2] = (Math.random() - 0.5) * 120; velocities[i3] = -0.5; velocities[i3 + 1] = -15 - Math.random() * 10; velocities[i3 + 2] = -1; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: 0xaaccff, size: 0.08, transparent: true, opacity: 0.5, depthWrite: false }); const mesh = new THREE.Points(geo, mat); mesh.visible = false; this.game.scene.add(mesh); this.systems.push({ type: 'rain', mesh, geo, positions, velocities, count }); return mesh; } createSnow() { const count = 2000; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const velocities = new Float32Array(count * 3); const phases = new Float32Array(count); for (let i = 0; i < count; i++) { const i3 = i * 3; positions[i3] = (Math.random() - 0.5) * 120; positions[i3 + 1] = Math.random() * 40; positions[i3 + 2] = (Math.random() - 0.5) * 120; velocities[i3] = 0; velocities[i3 + 1] = -1.5 - Math.random() * 1.5; velocities[i3 + 2] = 0; phases[i] = Math.random() * Math.PI * 2; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.15, transparent: true, opacity: 0.8, depthWrite: false }); const mesh = new THREE.Points(geo, mat); mesh.visible = false; this.game.scene.add(mesh); this.systems.push({ type: 'snow', mesh, geo, positions, velocities, count, phases, time: 0 }); return mesh; } createSparks(position) { const count = 20; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const velocities = new Float32Array(count * 3); const lifetimes = new Float32Array(count); for (let i = 0; i < count; i++) { const i3 = i * 3; positions[i3] = position.x; positions[i3 + 1] = position.y + 0.5; positions[i3 + 2] = position.z; const angle = Math.random() * Math.PI * 2; const speed = 1 + Math.random() * 3; velocities[i3] = Math.cos(angle) * speed * 0.3; velocities[i3 + 1] = 2 + Math.random() * 4; velocities[i3 + 2] = Math.sin(angle) * speed * 0.3; lifetimes[i] = 0.5 + Math.random() * 1.0; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color: 0xffaa00, size: 0.08, transparent: true, opacity: 1, blending: THREE.AdditiveBlending, depthWrite: false }); const mesh = new THREE.Points(geo, mat); this.game.scene.add(mesh); this.systems.push({ type: 'sparks', mesh, geo, positions, velocities, lifetimes, count, origin: position.clone(), age: 0, maxAge: 1.5 }); } update(dt) { const playerPos = this.game.player.position; for (let s = this.systems.length - 1; s >= 0; s--) { const sys = this.systems[s]; if (sys.type === 'fire') { this.updateFire(sys, dt); } else if (sys.type === 'rain') { this.updateWeatherParticles(sys, dt, playerPos); } else if (sys.type === 'snow') { this.updateSnow(sys, dt, playerPos); } else if (sys.type === 'sparks') { sys.age += dt; if (sys.age > sys.maxAge) { this.game.scene.remove(sys.mesh); sys.geo.dispose(); sys.mesh.material.dispose(); this.systems.splice(s, 1); continue; } this.updateSparks(sys, dt); } } } updateFire(sys, dt) { const pos = sys.positions; const vel = sys.velocities; const life = sys.lifetimes; for (let i = 0; i < sys.count; i++) { const i3 = i * 3; life[i] -= dt; if (life[i] <= 0) { sys.resetFn(i); continue; } pos[i3] += vel[i3] * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt; vel[i3 + 1] += dt * 0.5; // подъём } sys.geo.attributes.position.needsUpdate = true; sys.mesh.material.opacity = 0.5 + Math.sin(Date.now() * 0.01) * 0.3; } updateWeatherParticles(sys, dt, playerPos) { const pos = sys.positions; const vel = sys.velocities; for (let i = 0; i < sys.count; i++) { const i3 = i * 3; pos[i3] += vel[i3] * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt; if (pos[i3 + 1] < 0) { pos[i3] = playerPos.x + (Math.random() - 0.5) * 120; pos[i3 + 1] = 30 + Math.random() * 10; pos[i3 + 2] = playerPos.z + (Math.random() - 0.5) * 120; } } sys.geo.attributes.position.needsUpdate = true; } updateSnow(sys, dt, playerPos) { const pos = sys.positions; const vel = sys.velocities; sys.time += dt; for (let i = 0; i < sys.count; i++) { const i3 = i * 3; pos[i3] += vel[i3] * dt + Math.sin(sys.time * 2 + sys.phases[i]) * 0.3 * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt + Math.cos(sys.time * 1.5 + sys.phases[i]) * 0.3 * dt; if (pos[i3 + 1] < 0) { pos[i3] = playerPos.x + (Math.random() - 0.5) * 120; pos[i3 + 1] = 30 + Math.random() * 10; pos[i3 + 2] = playerPos.z + (Math.random() - 0.5) * 120; } } sys.geo.attributes.position.needsUpdate = true; } updateSparks(sys, dt) { const pos = sys.positions; const vel = sys.velocities; for (let i = 0; i < sys.count; i++) { const i3 = i * 3; pos[i3] += vel[i3] * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt; vel[i3 + 1] -= 9.8 * dt; // гравитация } sys.geo.attributes.position.needsUpdate = true; sys.mesh.material.opacity = 1 - (sys.age / sys.maxAge); } setWeather(type) { this.systems.forEach(sys => { if (sys.type === 'rain') sys.mesh.visible = (type === 'rain'); if (sys.type === 'snow') sys.mesh.visible = (type === 'snow'); }); } clear() { this.systems.forEach(sys => { this.game.scene.remove(sys.mesh); sys.geo.dispose(); sys.mesh.material.dispose(); }); this.systems = []; } }