Particle.py/particle.py
OusmBlueNinja b422cd4621 fireeeee
2025-02-21 13:38:59 -06:00

182 lines
7.3 KiB
Python

import pygame
import random
class Particle:
def __init__(
self,
pos,
vel=(0, 0),
acceleration=(0, 0),
lifetime=1.0, # seconds the particle lives
decay_rate=1.0, # how fast the lifetime decreases
color=(255, 255, 255),
size=5,
size_decay=5, # rate at which the size decreases
glow=False,
glow_intensity=0, # multiplier for glow radius
friction=0.05, # slows down velocity over time
gravity=0, # constant acceleration (negative makes it rise)
bounce=False,
bounce_damping=0.5, # energy loss when bouncing off boundaries
random_spread=0, # randomness added to velocity each update
spin=0, # current rotation (unused for circles)
spin_rate=0, # rotation speed
spin_decay=0, # how spin_rate slows down
trail=False,
trail_length=10, # how many previous positions to store
fade=True, # if True, the particle will fade (alpha decreases)
shape='circle', # could be 'circle' or 'square'
particle_type="generic" # "fire" or "smoke"
):
# Position, velocity, and acceleration as vectors.
self.pos = pygame.math.Vector2(pos)
self.vel = pygame.math.Vector2(vel)
self.acceleration = pygame.math.Vector2(acceleration)
self.lifetime = lifetime
self.initial_lifetime = lifetime
self.decay_rate = decay_rate
self.color = color
self.size = size
self.initial_size = size
self.size_decay = size_decay
self.fade = fade
self.alpha = 255
self.glow = glow
self.glow_intensity = glow_intensity
self.friction = friction
self.gravity = gravity
self.bounce = bounce
self.bounce_damping = bounce_damping
self.random_spread = random_spread
self.spin = spin
self.spin_rate = spin_rate
self.spin_decay = spin_decay
self.trail = trail
self.trail_length = trail_length
self.positions = [self.pos.copy()] if trail else []
self.shape = shape
# Particle type for specialized behavior.
self.particle_type = particle_type
def update(self, dt, screen_rect):
# Decrease lifetime.
self.lifetime -= self.decay_rate * dt
if self.lifetime < 0:
self.lifetime = 0
# Compute life ratio (1 at birth, 0 at death).
life_ratio = self.lifetime / self.initial_lifetime if self.initial_lifetime else 0
# Update color based on particle type.
if self.particle_type == "fire":
# Realistic fire uses a multi-stage color gradient:
# At birth: bright white-yellow, mid-life: vivid orange, end: dark red.
if life_ratio > 0.5:
factor = (life_ratio - 0.5) / 0.5 # factor from 1 to 0 as life_ratio goes from 1 -> 0.5
r = 255
g = int(255 * factor + 180 * (1 - factor))
b = int(240 * factor + 50 * (1 - factor))
else:
factor = life_ratio / 0.5 # factor from 1 to 0 as life_ratio goes from 0.5 -> 0
r = int(255 * factor + 150 * (1 - factor))
g = int(180 * factor)
b = int(50 * factor)
self.color = (r, g, b)
elif self.particle_type == "smoke":
# Smoke transitions from a semi-transparent dark gray to a light gray.
start_shade = 100
end_shade = 230
shade = int(start_shade * life_ratio + end_shade * (1 - life_ratio))
self.color = (shade, shade, shade)
# Optionally add random spread.
if self.random_spread:
self.vel.x += random.uniform(-self.random_spread, self.random_spread) * dt
self.vel.y += random.uniform(-self.random_spread, self.random_spread) * dt
# Apply gravity (negative gravity makes particles rise).
self.acceleration.y += self.gravity
# Update velocity and position.
self.vel += self.acceleration * dt
self.vel *= (1 - self.friction * dt)
self.pos += self.vel * dt
# Bounce off screen edges if enabled.
if self.bounce:
if self.pos.x - self.size < screen_rect.left or self.pos.x + self.size > screen_rect.right:
self.vel.x = -self.vel.x * self.bounce_damping
if self.pos.x - self.size < screen_rect.left:
self.pos.x = screen_rect.left + self.size
elif self.pos.x + self.size > screen_rect.right:
self.pos.x = screen_rect.right - self.size
if self.pos.y - self.size < screen_rect.top or self.pos.y + self.size > screen_rect.bottom:
self.vel.y = -self.vel.y * self.bounce_damping
if self.pos.y - self.size < screen_rect.top:
self.pos.y = screen_rect.top + self.size
elif self.pos.y + self.size > screen_rect.bottom:
self.pos.y = screen_rect.bottom - self.size
# Update spin.
self.spin += self.spin_rate * dt
self.spin_rate *= (1 - self.spin_decay * dt)
# Decrease size.
if self.size_decay:
self.size = max(0, self.size - self.size_decay * dt)
# Fade out by adjusting alpha.
if self.fade:
self.alpha = int(255 * life_ratio)
if self.alpha < 0:
self.alpha = 0
# Update trail positions if enabled.
if self.trail:
self.positions.append(self.pos.copy())
if len(self.positions) > self.trail_length:
self.positions.pop(0)
# Reset acceleration for next update.
self.acceleration = pygame.math.Vector2(0, 0)
def draw(self, surface):
# Apply alpha to color if fading.
if self.fade:
draw_color = (*self.color, self.alpha)
else:
draw_color = self.color
# Optionally draw a trail.
if self.trail and len(self.positions) > 1:
for i in range(1, len(self.positions)):
start = self.positions[i - 1]
end = self.positions[i]
pygame.draw.line(surface, draw_color, start, end, int(self.size))
# Optionally draw glow.
if self.glow:
glow_radius = int(self.size * (1 + self.glow_intensity))
glow_color = (*self.color, self.alpha)
glow_surface = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
pygame.draw.circle(glow_surface, glow_color, (glow_radius, glow_radius), glow_radius)
surface.blit(glow_surface, (self.pos.x - glow_radius, self.pos.y - glow_radius), special_flags=pygame.BLEND_ADD)
# Draw the particle (default is circle).
if self.shape == 'circle':
pygame.draw.circle(surface, draw_color, (int(self.pos.x), int(self.pos.y)), int(self.size))
elif self.shape == 'square':
rect = pygame.Rect(self.pos.x - self.size, self.pos.y - self.size, self.size * 2, self.size * 2)
pygame.draw.rect(surface, draw_color, rect)
def is_dead(self):
"""Return True if the particle should be removed."""
return self.lifetime <= 0 or self.size <= 0