Files
Hommie_RPG_Game/js/game/Particles.js
Maxim Dolgolyov fb5f09212b Initial commit: 3D Hommie RPG game
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:04:09 +03:00

303 lines
9.6 KiB
JavaScript

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 = [];
}
}