253 lines
6.6 KiB
JavaScript
253 lines
6.6 KiB
JavaScript
/**
|
|
* 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,
|
|
});
|
|
}
|
|
}
|