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