""" #File Name: sim.py Particle-based Physics Simulation System ====================================== This module implements a 2D particle simulation with physics, interactions, and state changes. Key Components: -------------- 1. Particle Class - Handles individual particle properties and behaviors - Supports multiple particle types (solid, liquid, gas) - Manages temperature and state transitions 2. Simulation Class - Core simulation engine - Manages particle creation, movement and interactions - Handles physics calculations and spatial partitioning """ # Load the imports. Pygame is what makes this even work and so simple may consider other engines for performance depends on learning curve. from settings import particle_properties, random, time # Load particle properties from json so we know what particles we got and how they should be simulated. class Particle: def __init__( self, position, velocity, mass, particle_type, properties, temperature=20, ): self.position = position # (x, y) self.velocity = velocity # (vx, vy) self.mass = mass self.particle_type = particle_type # Core properties self.size = properties.get("size", 1) self.hardness = properties.get("hardness", 0.5) self.color = properties.get("color", [255, 255, 255, 255]) self.temperature = properties.get("temperature", temperature) # Physics properties self.conductivity = properties.get("conductivity", 0) self.heat_capacity = properties.get("heat_capacity", 1) self.flamability = properties.get("flamability", 0.0) self.friction = properties.get("friction", 0.5) self.viscosity = properties.get("viscosity", 1.0) self.pressure = properties.get("pressure", 0) # State properties self.liquid = properties.get("liquid", False) self.solid = properties.get("solid", True) self.is_gas = properties.get("is_gas", False) # Temperature transition properties self.melt = properties.get("melt", None) self.melt_temperature = properties.get("melt_temperature", None) self.solidify = properties.get("solidify", None) self.solidify_temperature = properties.get( "solidify_temperature", None ) self.evaporate = properties.get("evaporate", None) self.evaporate_temperature = properties.get( "evaporate_temperature", None ) self.freeze = properties.get("freeze", None) self.freeze_temperature = properties.get("freeze_temperature", None) # Special properties self.explosive = properties.get("explosive", False) self.explosion_radius = properties.get("explosion_radius", 0) self.explosion_color = properties.get("explosion_color", [0, 0, 0]) @classmethod def from_type(cls, position, particle_type, properties): default_velocity = [0, 0] default_mass = properties.get("mass", 1.0) return cls( position, default_velocity, default_mass, particle_type, properties ) class Simulation: # the main class of the simulation. def __init__(self, width, height, x=0, y=0): self.dormant_particles = set() self.particle_movement_counter = {} self.DORMANT_THRESHOLD = 10 self.x = x self.y = y self.new_x = 0 self.new_y = 0 self.width = width self.height = height self.particle_size = 3 self.particles = [[None for _ in range(height)] for _ in range(width)] self.particle_count = 0 self.active_particles = set() self.cell_size = 32 self.spatial_grid = {} self.brush_size = 1 self.max_brush_size = 20 self.particle_properties = particle_properties self.current_particle_type = "sand" self.gravity = ( 9.8 # m/s^2, adjustable based on the scale of simulation ) self.wind_zones = [] self.wind = [0.0, 0.0] # Global wind vector (x, y) self._acceleration_wrapper = None def reset_particle_count(self): self.particle_count = 0 def get_cell_key(self, x, y): # this is where we get the cell key. # Convert coordinates to grid cell cell_x = x // self.cell_size cell_y = y // self.cell_size return (cell_x, cell_y) def add_to_spatial_grid( self, particle, x, y ): # this is where we add to the spatial grid. cell_key = self.get_cell_key(x, y) if cell_key not in self.spatial_grid: self.spatial_grid[cell_key] = set() self.spatial_grid[cell_key].add((x, y)) return cell_key def remove_from_spatial_grid( self, x, y ): # this is where we remove from the spatial grid. cell_key = self.get_cell_key(x, y) if cell_key in self.spatial_grid: self.spatial_grid[cell_key].discard((x, y)) return cell_key def update_spatial_grid(self): # this is where we update the spatial grid. """Update spatial grid for optimized collision detection""" if len(self.active_particles) > 100: self.spatial_grid = {} cell_lists = {} for x, y in self.active_particles: cell_key = (x // self.cell_size, y // self.cell_size) if cell_key not in cell_lists: cell_lists[cell_key] = [] cell_lists[cell_key].append((x, y)) self.spatial_grid = {k: set(v) for k, v in cell_lists.items()} def _check_dormant_state(self, x, y, particle): key = (x, y) if particle.particle_type == "wall": self.dormant_particles.add(key) return True if not hasattr(particle, "last_position"): particle.last_position = (x, y) self.particle_movement_counter[key] = 0 return False if particle.last_position == (x, y): self.particle_movement_counter[key] = ( self.particle_movement_counter.get(key, 0) + 1 ) if self.particle_movement_counter[key] >= self.DORMANT_THRESHOLD: self.dormant_particles.add(key) return True else: particle.last_position = (x, y) self.particle_movement_counter[key] = 0 self.dormant_particles.discard(key) return False def handle_phase_transitions( self, particle, x, y ): # this is where we handle all the phase transitions. """Handle all phase transitions for a particle""" # Check evaporation if ( hasattr(particle, "evaporate_temperature") and particle.evaporate_temperature is not None ): if ( particle.temperature >= particle.evaporate_temperature and particle.evaporate ): self.transform_particle(x, y, particle.evaporate) # Check freezing if ( hasattr(particle, "freeze_temperature") and particle.freeze_temperature is not None ): if ( particle.temperature <= particle.freeze_temperature and particle.freeze ): self.transform_particle(x, y, particle.freeze) # Check for melting with proper attribute validation if ( hasattr(particle, "melt") and hasattr(particle, "melt_temperature") and particle.melt_temperature is not None ): if particle.temperature >= particle.melt_temperature: new_type = particle.melt if new_type in self.particle_properties: self.transform_particle(x, y, new_type) # Check for solidification with proper attribute validation if ( hasattr(particle, "solidify") and hasattr(particle, "solidify_temperature") and particle.solidify_temperature is not None ): if particle.temperature <= particle.solidify_temperature: new_type = particle.solidify if new_type in self.particle_properties: self.transform_particle(x, y, new_type) def handle_particle_interactions( self, dt ): # this is where we handle all the particle interactions. """Handle interactions between different particle types""" for x, y in list(self.active_particles): particle = self.particles[x][y] if not particle: continue # Check neighboring particles for dx, dy in [ (-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1), ]: nx, ny = x + dx, y + dy if 0 <= nx < self.width and 0 <= ny < self.height: neighbor = self.particles[nx][ny] if neighbor: self.process_interaction( particle, neighbor, x, y, nx, ny ) def process_interaction( self, particle1, particle2, x1, y1, x2, y2 ): # this function is part 2 of handle_particle_interactions. """Process specific interactions between two particles""" # Water + Sand = Mud if ( particle1.particle_type == "water" and particle2.particle_type == "sand" or particle2.particle_type == "water" and particle1.particle_type == "sand" ): self.create_mud(x1, y1) self.particles[x2][y2] = None self.active_particles.discard((x2, y2)) # Lava/Fire effects if particle1.particle_type in [ "lava", "fire", "flame", ] or particle2.particle_type in ["lava", "fire", "flame"]: target = ( particle2 if particle1.particle_type in ["lava", "fire", "flame"] else particle1 ) target_x, target_y = ( (x2, y2) if particle1.particle_type in ["lava", "fire", "flame"] else (x1, y1) ) # Water to Steam if target.particle_type == "water": self.transform_particle(target_x, target_y, "steam") # Wood to Fire elif target.particle_type == "wood": if random.random() < 0.3: # 30% chance to ignite self.transform_particle(target_x, target_y, "fire") def create_mud( self, x, y ): # this is where we create the mud. probably should be moved to handle_particle_interactions or process_interaction. """Create mud particle from water and sand interaction""" if "mud" in self.particle_properties: properties = self.particle_properties["mud"] new_particle = Particle.from_type((x, y), "mud", properties) self.particles[x][y] = new_particle self.active_particles.add((x, y)) def transform_particle( self, x, y, new_type ): # this is where we transform the particle. """Transform a particle into a different type""" if new_type in self.particle_properties: properties = self.particle_properties[new_type] new_particle = Particle.from_type((x, y), new_type, properties) self.particles[x][y] = new_particle self.active_particles.add((x, y)) def handle_gas_movement( self, particle, x, y ): # this is where we handle the gas movement. this function sucks. wip """Handle gas particle movement""" if particle.is_gas: dx = random.uniform(-1, 1) dy = random.uniform(-2, 0) # Bias upward movement new_x = int(x + dx) new_y = int(y + dy) if 0 <= new_x < self.width and 0 <= new_y < self.height: if self.particles[new_x][new_y] is None: self.particles[x][y] = None self.particles[new_x][new_y] = particle self.active_particles.add((new_x, new_y)) self.active_particles.discard((x, y)) def add_wind_zone(self, x, y): # Instead of creating particles, store wind zone data wind_zone = { "x": x, "y": y, "radius": 50, "strength": 2.0, "direction": [1, 0], } self.wind_zones.append(wind_zone) def calculate_forces( self, particle, x, y ): # this is where we calculate the forces. """Calculate net forces acting on a particle.""" fx, fy = 0.0, 0.0 # Initialize forces # Check wind zones for zone in self.wind_zones: dx = x - zone["x"] dy = y - zone["y"] distance = (dx * dx + dy * dy) ** 0.5 if distance <= zone["radius"]: fx += ( zone["direction"][0] * zone["strength"] * (1 - distance / zone["radius"]) ) fy += ( zone["direction"][1] * zone["strength"] * (1 - distance / zone["radius"]) ) # Apply wind force if particle.is_gas: fx += self.wind[0] * 0.5 fy += self.wind[1] * 0.5 else: fx += self.wind[0] fy += self.wind[1] # Apply drag force drag = particle.viscosity * -1 fx += drag * particle.velocity[0] fy += drag * particle.velocity[1] # Check neighboring particles neighbors = self._get_quick_neighbors(x, y) for nx, ny in neighbors: if 0 <= nx < self.width and 0 <= ny < self.height: neighbor = self.particles[nx][ny] if neighbor: self._apply_neighbor_forces(particle, neighbor, fx, fy) return fx, fy """ def _process_particle_batch(self, batch, dt): updates = [] new_active = set() # Filter out dormant particles from the batch active_batch = [pos for pos in batch if pos not in self.dormant_particles] for x, y in active_batch: particle = self.particles[x][y] if not particle: continue if particle.particle_type == 'wall': new_active.add((x, y)) continue # Check if particle should become dormant if self._check_dormant_state(x, y, particle): new_active.add((x, y)) continue # physics calculations fx, fy = self.calculate_forces(particle, x, y) # Use max() to ensure mass is never zero mass = max(particle.mass, 0.001) particle.velocity[0] += (fx / mass) * dt particle.velocity[1] += (fy / mass) * dt new_x = int(x + particle.velocity[0] * dt) new_y = int(y + particle.velocity[1] * dt) if 0 <= new_x < self.width and 0 <= new_y < self.height: if self.particles[new_x][new_y] is None: updates.append((x, y, new_x, new_y, particle)) new_active.add((new_x, new_y)) # Wake up neighboring dormant particles self._wake_neighbors(new_x, new_y) else: new_active.add((x, y)) # Apply updates and return new active set for old_x, old_y, new_x, new_y, particle in updates: self.particles[old_x][old_y] = None self.particles[new_x][new_y] = particle particle.position = (new_x, new_y) return new_active """ def _get_quick_neighbors(self, x, y): """Quick neighbor lookup without full spatial grid""" return [ (x + dx, y + dy) for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)] ] def _apply_neighbor_forces(self, particle, neighbor, fx, fy): """Optimized neighbor force calculation""" if hasattr(neighbor, "temperature") and hasattr( particle, "temperature" ): temp_diff = neighbor.temperature - particle.temperature fy += temp_diff * 0.05 def ignite_particle( self, particle ): # this is where we ignite the particle. """Handle ignition and burning of flammable particles.""" if hasattr(particle, "flamability") and particle.flamability > 0.5: if hasattr(particle, "temperature") and particle.temperature > 150: particle.type = "fire" particle.temperature += 200 # Add burning effect for wood if particle.type == "wood": particle.burning = True particle.burn_time = 100 # burn time def spread_fire(self): # this is where we spread the fire. """Spread fire to neighboring particles.""" for x, y in list(self.active_particles): particle = self.particles[x][y] if particle and ( particle.particle_type == "fire" or getattr(particle, "burning", False) ): # Check all neighboring cells including diagonals for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: nx, ny = x + dx, y + dy if 0 <= nx < self.width and 0 <= ny < self.height: neighbor = self.particles[nx][ny] if neighbor and hasattr(neighbor, "flamability"): if neighbor.particle_type == "wood": # Higher chance to ignite wood if ( random.random() < 0.3 ): # 30% chance to spread self.ignite_particle(neighbor) elif neighbor.flamability > 0: if ( random.random() < 0.1 ): # 10% chance for other materials self.ignite_particle(neighbor) def handle_special_particles( self, particle, x, y ): # this is where we handle special particles. """Handle special particle behaviors""" if particle.particle_type in ["fire", "flame", "smoke"]: if random.random() < 0.6: # % chance self.particles[x][y] = None self.active_particles.discard((x, y)) if particle.particle_type in ["fire", "flame", "lava"]: # Create smoke above with proper physics if random.random() < 0.65 and y > 0: # % chance for smoke properties = self.particle_properties["smoke"] new_smoke = Particle.from_type( (x, y - 1), "smoke", properties ) if self.particles[x][y - 1] is None: self.particles[x][y - 1] = new_smoke self.active_particles.add((x, y - 1)) else: # Handle collision with water if particle.particle_type == "water": self.particles[x][y - 1] = None self.active_particles.discard((x, y - 1)) self.particles[x][y] = None self.active_particles.discard((x, y)) self.particles[x][y] = Particle.from_type( (x, y), "water", self.particle_properties["water"] ) self.active_particles.add((x, y)) def handle_temperature( self, dt ): # this is where we handle the temperature. """Handle temperature changes and state transitions""" for x, y in list(self.active_particles): particle = self.particles[x][y] if not particle: continue if particle.temperature > 1700: # Transition to gas particle.is_gas = True particle.temperature = 1700 particle.velocity = [ random.uniform(-1, 1), random.uniform(-1, 1), ] particle.temperature < 1400 particle.is_gas = False # Temperature spread to neighbors for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nx, ny = x + dx, y + dy if 0 <= nx < self.width and 0 <= ny < self.height: neighbor = self.particles[nx][ny] if ( neighbor and hasattr(neighbor, "temperature") and neighbor.temperature is not None ): temp_diff = particle.temperature - neighbor.temperature heat_transfer = temp_diff * 0.1 * dt particle.temperature -= heat_transfer neighbor.temperature += heat_transfer def burning(self): # this is where we handle the burning. """Handle burning of particles.""" for x, y in list(self.active_particles): particle = self.particles[x][y] if particle and hasattr(particle, "burning") and particle.burning: particle.temperature += 10 if particle.temperature > 1000: self.particles[x][y] = None self.active_particles.remove((x, y)) self.spatial_grid.pop((x, y), None) def create_particle(self, x, y): # this is where we create the particle. """Create a new particle with full property support""" particle_type = self.current_particle_type.lower() # Check if the particle is within the grid boundaries if particle_type in self.particle_properties: grid_x = x // self.particle_size grid_y = y // self.particle_size if 0 <= grid_x < self.width and 0 <= grid_y < self.height: properties = self.particle_properties[particle_type] position = (grid_x, grid_y) new_particle = Particle( position=position, velocity=[0, 0], mass=properties.get("mass", 1.0), particle_type=particle_type, properties=properties, ) # Add to the grid if 0 <= grid_x < len(self.particles) and 0 <= grid_y < len( self.particles[0] ): self.particles[grid_x][grid_y] = new_particle self.active_particles.add((grid_x, grid_y)) self.particle_count += 1 def create_particle_circle( self, center_x, center_y ): # this is where we create the particle circle. brush_size = int(self.brush_size) for dx in range(-brush_size, brush_size + 1): for dy in range(-brush_size, brush_size + 1): if ( dx * dx + dy * dy <= brush_size * brush_size ): # Circle check self.create_particle( center_x + dx * self.particle_size, center_y + dy * self.particle_size, ) def get_particle_state( self, x, y ): # this is where we get the particle state. """Get the state of a particle at a given position""" particle = self.particles[x][y] if particle: return particle.particle_type return None def apply_gravity(self, dt): # this is where we apply gravity. """Handle only gravity and basic particle movement""" self.spatial_grid.clear() for x, y in list(self.active_particles): particle = self.particles[x][y] if not particle or particle.particle_type == "wall": continue # Apply gravity new_y = y + 1 new_x = x # Check boundaries if not (0 <= new_x < self.width and 0 <= new_y < self.height): continue # Handle granular materials (sand, dirt) if particle.particle_type in ["sand", "dirt", "snow", "ice"]: if self.particles[x][new_y] is None: new_x, new_y = x, y + 1 else: # Try diagonal movement with randomization diagonal_dirs = [(-1, 1), (1, 1)] random.shuffle(diagonal_dirs) for dx, dy in diagonal_dirs: test_x = x + dx test_y = y + dy if ( 0 <= test_x < self.width and 0 <= test_y < self.height and self.particles[test_x][test_y] is None ): if ( random.random() < 0.8 ): # 80% chance to move diagonally new_x = test_x new_y = test_y break # Handle liquid movement (water, lava) elif particle.liquid: if self.particles[x][new_y] is None: new_x = x new_y = y + 1 else: spread_directions = [(-1, 0), (1, 0)] random.shuffle(spread_directions) for dx, _ in spread_directions: test_x = x + dx if ( 0 <= test_x < self.width and self.particles[test_x][y] is None ): new_x = test_x new_y = y break # Move particle if destination is empty if self.particles[new_x][new_y] is None: self.particles[x][y] = None self.particles[new_x][new_y] = particle self.active_particles.add((new_x, new_y)) self.active_particles.discard((x, y)) particle.position = (new_x, new_y) def apply_physics( self, dt, engine_settings ): # this is where we apply physics. """Handle all physics effects""" new_active_particles = set() for x, y in list(self.active_particles): particle = self.particles[x][y] if not particle: continue # Handle boundaries based on settings if engine_settings["outerwall"]: if ( x <= 0 or x >= self.width - 1 or y <= 0 or y >= self.height - 1 ): # Create wall particle at boundary if none exists if self.particles[x][y] is None: properties = self.particle_properties["wall"] wall = Particle.from_type((x, y), "wall", properties) self.particles[x][y] = wall new_active_particles.add((x, y)) self.particle_count += 1 # Track new wall particle continue else: # Delete particles that go out of bounds if ( x <= 0 or x >= self.width - 1 or y <= 0 or y >= self.height - 1 ): if self.particles[x][y] is not None: self.particles[x][y] = None self.active_particles.discard((x, y)) self.particle_count -= 1 continue # Skip wall physics - walls are immutable if particle.particle_type == "wall": new_active_particles.add((x, y)) continue # Handle dissipating particles if particle.particle_type in ["fire", "flame"]: # Clear current position first self.particles[x][y] = None self.active_particles.discard((x, y)) # Handle fire movement dx = random.uniform(-0.5, 0.5) dy = random.uniform(-1.5, -0.5) # Upward drift new_x = int(x + dx) new_y = int(y + dy) if 0 <= new_x < self.width and 0 <= new_y < self.height: if self.particles[new_x][new_y] is None: self.particles[new_x][new_y] = particle new_active_particles.add((new_x, new_y)) # Generate smoke above the new fire position if random.random() < 0.25 and new_y > 0: properties = self.particle_properties["smoke"] new_smoke = Particle( position=(new_x, new_y - 1), velocity=[random.uniform(-0.5, 0.5), -1], mass=properties.get("mass", 0.1), particle_type="smoke", properties=properties, ) if self.particles[new_x][new_y - 1] is None: self.particles[new_x][new_y - 1] = new_smoke new_active_particles.add((new_x, new_y - 1)) # Dissipation chance if random.random() < 0.02: continue continue # Air handling - particles can pass through should implement proper air instead of a particle if particle.particle_type == "air": continue # Handle phase transitions self.handle_phase_transitions(particle, x, y) # Calculate forces fx, fy = self.calculate_forces(particle, x, y) # handle gas particles if particle.is_gas: # Gas-specific movement dx = random.uniform(2, -1) dy = random.uniform(-2, 0) # Bias upward new_x = int(x + dx) new_y = int(y + dy) self.particles[x][y] = None self.active_particles.discard((x, y)) if 0 <= new_x < self.width and 0 <= new_y < self.height: if self.particles[new_x][new_y] is None: self.particles[x][y] = None self.particles[new_x][new_y] = particle new_active_particles.add((new_x, new_y)) self.active_particles.discard((x, y)) continue else: # Regular particle physics mass = max(particle.mass, 0.001) particle.velocity[0] += (fx / mass) * dt particle.velocity[1] += (fy / mass) * dt if particle.liquid: # Enhanced liquid spreading spread_chance = 0.5 if random.random() < spread_chance: dx = random.choice([-1, 1]) if ( 0 <= x + dx < self.width and self.particles[x + dx][y] is None ): new_x = x + dx new_y = y self.particles[x][y] = None self.particles[new_x][new_y] = particle new_active_particles.add((new_x, new_y)) continue # Update position for non-liquid particles new_x = int(x + particle.velocity[0] * dt) new_y = int(y + particle.velocity[1] * dt) if 0 <= new_x < self.width and 0 <= new_y < self.height: if self.particles[new_x][new_y] is None: self.particles[x][y] = None self.particles[new_x][new_y] = particle else: new_active_particles.add((x, y)) self.active_particles = new_active_particles def clear_particles_circle( self, center_x, center_y ): # this is for the brush tool """Clear particles in a circle around the given point based on brush size""" brush_size = int(self.brush_size) particles_cleared = 0 # Track how many particles we clear for dx in range(-brush_size, brush_size + 1): for dy in range(-brush_size, brush_size + 1): if ( dx * dx + dy * dy <= brush_size * brush_size ): # Circle check grid_x = ( center_x + dx * self.particle_size ) // self.particle_size grid_y = ( center_y + dy * self.particle_size ) // self.particle_size if 0 <= grid_x < self.width and 0 <= grid_y < self.height: if self.particles[grid_x][grid_y]: self.particles[grid_x][grid_y] = None self.active_particles.discard((grid_x, grid_y)) self.remove_from_spatial_grid(grid_x, grid_y) particles_cleared += 1 self.particle_count = max( 0, self.particle_count - particles_cleared ) # Update count, ensure it doesn't go negative def mix_liquids(self, liquid1, liquid2): # this is for the mix tool """Handle liquid mixing interactions""" if liquid1.temperature != liquid2.temperature: avg_temp = (liquid1.temperature + liquid2.temperature) / 2 liquid1.temperature = avg_temp liquid2.temperature = avg_temp liquid1.density = self.calculate_density(liquid1.temperature) liquid2.density = self.calculate_density(liquid2.temperature) liquid1.viscosity = self.calculate_viscosity(liquid1.temperature) liquid2.viscosity = self.calculate_viscosity(liquid2.temperature) liquid1.color = self.calculate_color(liquid1.temperature) liquid2.color = self.calculate_color(liquid2.temperature) def _wake_neighbors(self, x, y): for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: nx, ny = x + dx, y + dy key = (nx, ny) if key in self.dormant_particles: self.dormant_particles.discard(key) self.particle_movement_counter[key] = 0 def track_tps(self): """Track Ticks Per Second for simulation performance monitoring""" if not hasattr(self, "_tps_counter"): self._tps_counter = 0 self._tps_timer = time.time() self._current_tps = 0 self._tps_counter += 1 current_time = time.time() elapsed = current_time - self._tps_timer # Update TPS count every second if elapsed >= 1.0: self._current_tps = self._tps_counter / elapsed self._tps_counter = 0 self._tps_timer = current_time return self._current_tps def simulate_step(self, dt, engine_settings): """Run a single step of the simulation""" """ active_list = list(self.active_particles) batch_size = 1000 for i in range(0, len(active_list), batch_size): batch = active_list[i:i + batch_size] self._process_particle_batch(batch, dt) """ # Update spatial grid only when needed if len(self.active_particles) > 100: self.update_spatial_grid() # Update particle positions and physics self.apply_gravity(dt) self.apply_physics(dt, engine_settings) # Handle state changes and interactions self.handle_temperature(dt) self.handle_particle_interactions(dt) self.burning() self.spread_fire()