LifeSimulator/js/particles.js

254 lines
6.6 KiB
JavaScript
Raw Permalink Normal View History

/**
* Particle Effects System
* Manages blood spray, eating crumbs, mating hearts, floating dandelion seeds, and splash ripples.
*/
export class ParticleSystem {
constructor() {
this.particles = [];
}
reset() {
this.particles = [];
}
update(dt, terrain) {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.life -= dt;
if (p.life <= 0) {
// If blood particle landed, paint small splatter on terrain
if (p.type === 'blood' && p.paintOnDeath && terrain) {
terrain.addBloodSplotch(p.x, p.y, p.radius || 2.5);
}
// If seed particle landed, signal germination callback
if (p.type === 'seed' && p.onLand) {
p.onLand(p.x, p.y);
}
this.particles.splice(i, 1);
continue;
}
// Physics integration
p.x += p.vx * dt;
p.y += p.vy * dt;
// Apply drag / gravity based on particle type
if (p.drag) {
p.vx *= Math.pow(p.drag, dt * 60);
p.vy *= Math.pow(p.drag, dt * 60);
}
if (p.gravity) {
p.vy += p.gravity * dt;
}
// Custom motion
if (p.type === 'seed') {
p.sway += dt * 4;
p.x += Math.sin(p.sway) * 12 * dt;
} else if (p.type === 'heart') {
p.sway += dt * 3;
p.x += Math.sin(p.sway) * 8 * dt;
}
}
}
draw(ctx) {
for (let i = 0; i < this.particles.length; i++) {
const p = this.particles[i];
const alpha = Math.max(0, Math.min(1, p.life / p.maxLife));
ctx.save();
ctx.globalAlpha = alpha;
switch (p.type) {
case 'blood': {
ctx.fillStyle = p.color || '#C91818';
const size = Math.max(1, Math.round(p.size));
ctx.fillRect(Math.round(p.x - size / 2), Math.round(p.y - size / 2), size, size);
break;
}
case 'crumb': {
ctx.fillStyle = p.color || '#57B547';
const size = Math.max(1, Math.round(p.size));
ctx.fillRect(Math.round(p.x - size / 2), Math.round(p.y - size / 2), size, size);
break;
}
case 'heart': {
// Pixelated little heart
ctx.fillStyle = '#FF3366';
const hx = Math.round(p.x);
const hy = Math.round(p.y);
const s = p.size || 2;
// Row 0
ctx.fillRect(hx - s * 2, hy - s, s, s);
ctx.fillRect(hx - s, hy - s * 2, s, s);
ctx.fillRect(hx + s, hy - s * 2, s, s);
ctx.fillRect(hx + s * 2, hy - s, s, s);
// Row 1
ctx.fillRect(hx - s * 2, hy, s * 5, s);
// Row 2
ctx.fillRect(hx - s, hy + s, s * 3, s);
// Row 3
ctx.fillRect(hx, hy + s * 2, s, s);
break;
}
case 'seed': {
// Dandelion seed puff
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(Math.round(p.x), Math.round(p.y), 3, 3);
ctx.fillStyle = '#C2B280';
ctx.fillRect(Math.round(p.x + 1), Math.round(p.y + 3), 1, 3);
break;
}
case 'sparkle': {
ctx.fillStyle = p.color || '#FFD700';
const s = p.size || 3;
ctx.fillRect(Math.round(p.x - s / 2), Math.round(p.y), s, 1);
ctx.fillRect(Math.round(p.x), Math.round(p.y - s / 2), 1, s);
break;
}
case 'splash': {
// Expanding ripple
const progress = 1 - (p.life / p.maxLife);
ctx.strokeStyle = '#8ED6FF';
ctx.lineWidth = 1;
ctx.strokeRect(
Math.round(p.x - p.radius * progress),
Math.round(p.y - p.radius * progress * 0.5),
Math.round(p.radius * progress * 2),
Math.round(p.radius * progress)
);
break;
}
}
ctx.restore();
}
}
// --- Emitter Methods ---
emitBloodSpray(x, y, angle, intensity = 14) {
const baseColors = ['#D41414', '#A80808', '#850000', '#FF3636'];
for (let i = 0; i < intensity; i++) {
const spread = (Math.random() - 0.5) * 1.2;
const speed = 70 + Math.random() * 140;
const particleAngle = angle + spread;
const life = 0.35 + Math.random() * 0.45;
this.particles.push({
type: 'blood',
x,
y,
vx: Math.cos(particleAngle) * speed,
vy: Math.sin(particleAngle) * speed,
drag: 0.88,
gravity: 120,
size: 2 + Math.floor(Math.random() * 3),
color: baseColors[Math.floor(Math.random() * baseColors.length)],
life,
maxLife: life,
paintOnDeath: Math.random() < 0.75,
radius: 3 + Math.random() * 4,
});
}
}
emitEatingCrumbs(x, y, color = '#5CB85C', count = 5) {
for (let i = 0; i < count; i++) {
const speed = 20 + Math.random() * 40;
const angle = Math.random() * Math.PI * 2;
const life = 0.3 + Math.random() * 0.3;
this.particles.push({
type: 'crumb',
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 15,
drag: 0.85,
gravity: 80,
size: 2,
color,
life,
maxLife: life,
});
}
}
emitHeart(x, y) {
const life = 1.2;
this.particles.push({
type: 'heart',
x,
y: y - 8,
vx: (Math.random() - 0.5) * 15,
vy: -25 - Math.random() * 20,
sway: Math.random() * Math.PI,
drag: 0.96,
size: 2,
life,
maxLife: life,
});
}
emitSeed(x, y, targetX, targetY, onLand) {
const dx = targetX - x;
const dy = targetY - y;
const dist = Math.hypot(dx, dy);
const speed = 35 + Math.random() * 25;
const life = Math.max(1.2, dist / speed);
this.particles.push({
type: 'seed',
x,
y,
vx: (dx / dist) * speed,
vy: (dy / dist) * speed,
sway: Math.random() * 6,
life,
maxLife: life,
onLand,
});
}
emitGrowthSparkles(x, y, count = 10) {
const colors = ['#FFDF00', '#FFFA8A', '#00FFFF', '#FF99EE'];
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 30 + Math.random() * 50;
const life = 0.5 + Math.random() * 0.4;
this.particles.push({
type: 'sparkle',
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
drag: 0.9,
size: 3 + Math.random() * 3,
color: colors[Math.floor(Math.random() * colors.length)],
life,
maxLife: life,
});
}
}
emitSplash(x, y) {
const life = 0.25;
this.particles.push({
type: 'splash',
x,
y,
vx: 0,
vy: 0,
radius: 4 + Math.random() * 4,
life,
maxLife: life,
});
}
}