Initial commit: 3D Hommie RPG game
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
302
js/game/Particles.js
Normal file
302
js/game/Particles.js
Normal file
@@ -0,0 +1,302 @@
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user