diff --git a/README.md b/README.md index 6bead46..ab64952 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,25 @@ the code is not finished yet, but i will update it as i go. ## Main Features - Particle Physics - - Gravity and wind effects + - Gravity and wind effects (maybe on the wind zones) - Temperature dynamics - State transitions (melting, freezing, evaporation) - Particle Interactions - Collision detection - - Chemical reactions (e.g., water + sand = mud) - - Heat transfer between particles + - Chemical reactions (e.g., water + sand = wet sand, Lava + lower temperature = molten rock = rock) + - Heat transfer between particles (e.g., Things seem to cool off as for heating up that's a different thing) - Special Effects - Fire propagation sorta - Smoke generation - - Liquid spreading + - Liquid spreading (Could be improved ) - Optimization Features - - Spatial partitioning grid - - Dormant particle tracking - - Batch processing - - Static User Interface + - Spatial partitioning grid (to reduce calculations) + - Dormant particle tracking (to reduce unnecessary calculations) + - Batch processing (to reduce unnecessary calculations) + - Static User Interface (to reduce unnecessary calculations) ### **Current Features** diff --git a/physics/REORGANIZATION.md b/physics/REORGANIZATION.md new file mode 100644 index 0000000..d1ec585 --- /dev/null +++ b/physics/REORGANIZATION.md @@ -0,0 +1,75 @@ +# Physics Module Reorganization Plan + +## 1. particle.py + +Contains core particle functionality: + +- Full Particle class definition +- particle_properties handling +- create_particle() +- create_particle_circle() +- transform_particle() +- get_particle_state() + +## 2. forces.py + +Physics and movement systems: + +- calculate_forces() +- _apply_neighbor_forces() +- apply_gravity() +- apply_physics() +- add_wind_zone() +- handle_gas_movement() + +## 3. interactions.py + +Particle interaction handling: + +- handle_particle_interactions() +- process_interaction() +- mix_liquids() +- create_mud() +- handle_particle_damage() +- _get_quick_neighbors() + +## 4. temperature.py + +Temperature and state management: + +- handle_temperature() +- handle_phase_transitions() +- burning() +- spread_fire() +- ignite_particle() +- handle_special_particles() + +## 5. grid.py + +Spatial management systems: + +- update_spatial_grid() +- get_cell_key() +- add_to_spatial_grid() +- remove_from_spatial_grid() +- _get_neighbors_from_grid() +- _wake_neighbors() +- clear_particles_circle() + +## 6. simulation.py + +Core simulation coordination: + +- Simulation class (main orchestrator) +- simulate_step() +- track_tps() +- reset_particle_count() +- get_accurate_particle_count() + +## Migration Steps + +1. Create new files +2. Move related code sections +3. Update imports +4. Test each component +5. Update main simulation.py references diff --git a/src/config/settings.py b/src/config/settings.py index d33cb39..3e7a043 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -22,7 +22,7 @@ import time import numpy as np engine_settings = { - 'pause_sim': False, + 'pause_sim': True, 'enable_cursor': True, 'enable_glow': False, 'enable_gas_effect': True, @@ -76,5 +76,5 @@ def load_particle_properties(): # Load particle properties once when module is imported particle_properties = load_particle_properties() -__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os', 'jit'] +__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os'] diff --git a/src/debug/__init__.py b/src/debug/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/part/coreparts/particles/solids.json b/src/part/coreparts/particles/solids.json index 2849fac..a512c44 100644 --- a/src/part/coreparts/particles/solids.json +++ b/src/part/coreparts/particles/solids.json @@ -60,8 +60,10 @@ "velocity": 0.5, "conductivity": 1, "heat_capacity": 1, - "color": [139, 69, 19, 255], + "color": [125, 45, 55, 255], "mass": 0.5, + "melt": "dirt", + "melt_temperature": 100, "flamability": 0, "temperature": 20, "explosive": false, diff --git a/src/part/coreparts/special/effects.json b/src/part/coreparts/special/effects.json index 4b69147..6e4ffb4 100644 --- a/src/part/coreparts/special/effects.json +++ b/src/part/coreparts/special/effects.json @@ -2,15 +2,13 @@ "plasma": { "name": "Plasma", "size": 1, - "hardness": 0.001, - "velocity": 0.0, - "conductivity": 0, - "heat_capacity": 10, + "hardness": 0.0, + "velocity": 0.8, + "conductivity": 1, + "heat_capacity": 1, "color": [255, 100, 200, 255], - "mass": 0.001, - "temperature": 3600, - "friction": 0.0, - "viscosity": 0.0, + "mass": 0.01, + "temperature": 3400, "liquid": false, "solid": false, "is_gas": true, @@ -24,6 +22,7 @@ "hardness": 0.1, "velocity": 0.8, "conductivity": 1, + "heat_capacity": 1, "color": [255, 255, 0, 255], "mass": 0.01, "temperature": 900, diff --git a/src/physics/particle.py b/src/physics/particle.py new file mode 100644 index 0000000..7228a90 --- /dev/null +++ b/src/physics/particle.py @@ -0,0 +1,69 @@ + +# Load particle properties from json so we know what particles we got and how they should be simulated. +class Particle: + def __init__(self, simulation, 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 + self.sim = simulation + + # 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) + self.durability = properties.get("durability", 100.0) + + # 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]) + self.explosion_force = properties.get("explosion_force", 0) + self.explosion_duration = properties.get("explosion_duration", 0) + + # Pressure properties + self.pressure_resistance = properties.get("pressure_resistance", 0) + self.pressure_tolerance = properties.get("pressure_tolerance", 0) + self.pressure_threshold = properties.get("pressure_threshold", 0) + self.pressure_threshold_duration = properties.get("pressure_threshold_duration", 0) + + # Burning properties + self.burning = properties.get("burning", False) + self.burn_temperature = properties.get("burn_temperature", 0) + self.burn_duration = properties.get("burn_duration", 0) + self.burn_color = properties.get("burn_color", [255, 0, 0]) + self.burn_rate = properties.get("burn_rate", 0) + self.burn_intensity = properties.get("burn_intensity", 0) + self.burn_rate_multiplier = properties.get("burn_rate_multiplier", 1.0) + + @classmethod + def from_type(cls, simulation, position, particle_type, properties): + default_velocity = [0, 0] + default_mass = properties.get("mass", 1.0) + return cls(simulation, position, default_velocity, default_mass, particle_type, properties) + + diff --git a/src/physics/sim.py b/src/physics/sim.py index 876b344..ec91442 100644 --- a/src/physics/sim.py +++ b/src/physics/sim.py @@ -20,15 +20,17 @@ Key Components: #Load the imports. from config.settings import np, time, particle_properties +from physics.particle import Particle # 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): + def __init__(self, simulation, 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 + self.sim = simulation # Core properties self.size = properties.get("size", 1) @@ -83,12 +85,12 @@ class Particle: self.burn_rate_multiplier = properties.get("burn_rate_multiplier", 1.0) @classmethod - def from_type(cls, position, particle_type, properties): + def from_type(cls, simulation, 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) + return cls(simulation, position, default_velocity, default_mass, particle_type, properties) + - class Simulation: # the main class of the simulation. @@ -200,8 +202,8 @@ class Simulation: if particle.particle_type == 'lava': if particle.temperature < particle.solidify_temperature: state_changed = self.transform_particle(x, y, particle.solidify) - return state_changed - + return state_changed + # Check evaporation if hasattr(particle, 'evaporate_temperature') and particle.evaporate_temperature is not None: if particle.temperature >= particle.evaporate_temperature and particle.evaporate: @@ -294,13 +296,20 @@ class Simulation: 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 + # Water + Sand = Wet Sand + 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.create_mud(x1, y1, 'wsand') # Pass wsand type self.particles[x2][y2] = None self.active_particles.discard((x2, y2)) - + + # Water + Dirt = Mud + if (particle1.particle_type == 'water' and particle2.particle_type == 'dirt' or + particle2.particle_type == 'water' and particle1.particle_type == 'dirt'): + self.create_mud(x1, y1, 'mud') # Pass mud type + 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 @@ -315,21 +324,30 @@ class Simulation: if np.random.random() < 0.3: # 30% chance to ignite self.transform_particle(target_x, target_y, 'fire') + # Add plasma effects + if particle1.particle_type == 'plasma' or particle2.particle_type == 'plasma': + target = particle2 if particle1.particle_type == 'plasma' else particle1 + target_x, target_y = (x2, y2) if particle1.particle_type == 'plasma' else (x1, y1) + + # Transfer high temperature to target + if hasattr(target, 'temperature'): + target.temperature += 100 # Rapid temperature increase - def create_mud(self, x, y): # this is where we create the mud. probably should be moved to handle_particle_interactions or process_interaction. - - if 'mud' in self.particle_properties: - properties = self.particle_properties['mud'] - new_particle = Particle.from_type((x, y), 'mud', properties) + + def create_mud(self, x, y, mud_type): + """Create either wet sand or mud based on the specified type""" + if mud_type in self.particle_properties: + properties = self.particle_properties[mud_type] + new_particle = Particle.from_type(self, (x, y), mud_type, 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. + def transform_particle(self, x, y, new_type): """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) + new_particle = Particle.from_type(self, (x, y), new_type, properties) self.particles[x][y] = new_particle self.active_particles.add((x, y)) @@ -495,12 +513,12 @@ class Simulation: continue - if particle.temperature > 1700: + if particle.temperature > 30000: # Transition to gas particle.is_gas = True - particle.temperature = 1700 + particle.temperature = 30000 particle.velocity = [np.random.uniform(-1, 1), np.random.uniform(-1, 1)] - particle.temperature < 1400 + particle.temperature < 30000 particle.is_gas = False # Temperature spread to neighbors @@ -511,7 +529,7 @@ class Simulation: 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 + heat_transfer = temp_diff * 0.3 * dt particle.temperature -= heat_transfer neighbor.temperature += heat_transfer @@ -543,6 +561,7 @@ class Simulation: properties = self.particle_properties[particle_type] position = (grid_x, grid_y) new_particle = Particle( + simulation=self, position=position, velocity=[0, 0], mass=properties.get('mass', 1.0), @@ -670,6 +689,7 @@ class Simulation: if np.random.random() < 0.25 and new_y > 0: properties = self.particle_properties['smoke'] new_smoke = Particle( + simulation=self, position=(new_x, new_y-1), velocity=[np.random.uniform(-0.5, 0.5), -1], mass=properties.get('mass', 0.1), diff --git a/src/physics/tests/base.py b/src/physics/tests/base.py new file mode 100644 index 0000000..10acb26 --- /dev/null +++ b/src/physics/tests/base.py @@ -0,0 +1,72 @@ + +from config.settings import np, time, engine_settings +from src.physics.particle import Particle, particle_properties + + + +class SimulationBase: + def __init__(self, width: int, height: int, x: int = 0, y: int = 0): + self.dormant_particles = set() + self.particle_movement_counter = {} + self.DORMANT_THRESHOLD = 10 + self.width = width + self.height = height + self.x = x + self.y = y + self.new_x = 0 + self.new_y = 0 + + self.particle_size = 3 + self.particles = [[None for _ in range(height)] for _ in range(width)] + print(f"Base init - particles type: {type(self.particles)}") + print(f"After initialization - particles type: {type(self.particles)}") + self.active_particles = set() + self.particle_count = 0 + self.brush_size = 1 + self.max_brush_size = 20 + self.particle_properties = particle_properties + self.particle_types = list(self.particle_properties.keys()) + self.current_particle_type = self.particle_types[0] if self.particle_types else 'sand' + self.gravity = 9.8 + self.wind_zones = [] + self.wind = [0.0, 0.0] + + + def create_particle(self, x, y): # this is where we create the particle. + if engine_settings ["enable_debug"]: + print(f"Before particle creation - particles type: {type(self.particles)}") + + """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 and + self.particles[grid_x][grid_y] is None): + + 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 + ) + + 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): + 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 + grid_x = (center_x + dx * self.particle_size) // self.particle_size + grid_y = (center_y + dy * self.particle_size) // self.particle_size + self.create_particle(grid_x, grid_y) \ No newline at end of file diff --git a/src/physics/tests/forces.py b/src/physics/tests/forces.py new file mode 100644 index 0000000..da72034 --- /dev/null +++ b/src/physics/tests/forces.py @@ -0,0 +1,296 @@ +""" +Physics force calculations and movement systems for Sandpypi +Handles particle movement, forces, and physics calculations +""" +from config.settings import np +from src.physics.particle import Particle + +class ForceSystem: + def __init__(self, simulation): + self.sim = simulation + self.wind_zones = [] + + + def calculate_forces(self, particle, x, y): + """Calculate net forces acting on a particle.""" + # Initialize forces as numpy array for vectorized operations + forces = np.zeros(2, dtype=np.float32) # [fx, fy] + + # Vectorized wind zone calculations + if self.wind_zones: + positions = np.array([[zone['x'], zone['y']] for zone in self.wind_zones]) + directions = np.array([zone['direction'] for zone in self.wind_zones]) + strengths = np.array([zone['strength'] for zone in self.wind_zones]) + radii = np.array([zone['radius'] for zone in self.wind_zones]) + + # Calculate distances vectorized + dx = x - positions[:, 0] + dy = y - positions[:, 1] + distances = np.sqrt(dx**2 + dy**2) + + # Apply wind zone forces where distance <= radius + mask = distances <= radii + scale_factors = np.where(mask, (1 - distances/radii) * strengths[:, np.newaxis], 0) + forces += np.sum(directions * scale_factors[:, np.newaxis], axis=0) + + # Apply global wind with vectorized operation + wind_factor = 0.5 if particle.is_gas else 1.0 + forces += np.array(self.wind) * wind_factor + + # Apply drag force vectorized + drag = particle.viscosity * -1 + forces += drag * np.array(particle.velocity) + + # Neighbor forces using numpy arrays + neighbors = np.array(self._get_neighbors_from_grid(x, y)) + if len(neighbors): + valid_mask = ( + (neighbors[:, 0] >= 0) & + (neighbors[:, 0] < self.width) & + (neighbors[:, 1] >= 0) & + (neighbors[:, 1] < self.height) + ) + neighbors = neighbors[valid_mask] + + for nx, ny in neighbors: + if (nx, ny) != (x, y): + neighbor = self.sim.particles[nx][ny] + if neighbor: + self._apply_neighbor_forces(particle, neighbor, forces[0], forces[1]) + + return forces[0], forces[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 apply_gravity(self, dt): + """Handle only gravity and basic particle movement""" + self.sim.spatial_grid.spatial_grid.clear() + + for x, y in list(self.sim.active_particles): + particle = self.sim.particles[x][y] + if not particle or particle.particle_type in ['wall', 'stone', 'wood']: + 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.sim.particles[x][new_y] is None: + new_x, new_y = x, y + 1 + else: + # Define diagonal directions first + diagonal_dirs = [(-1, 1), (1, 1)] + diagonal_dirs = np.array(diagonal_dirs) + # Randomize movement directions + diagonal_dirs = np.random.permutation(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.sim.particles[test_x][test_y] is None): + if np.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.sim.particles[x][new_y] is None: + new_x = x + new_y = y + 1 + else: + spread_directions = np.array([(-1, 0), (1, 0)]) + np.random.shuffle(spread_directions) + for dx, _ in spread_directions: + test_x = x + dx + if (0 <= test_x < self.width and + self.sim.particles[test_x][y] is None): + new_x = test_x + new_y = y + break + + # Move particle if destination is empty + if self.sim.particles[new_x][new_y] is None: + self.sim.particles[x][y] = None + self.sim.particles[new_x][new_y] = particle + self.sim.particles.add((new_x, new_y)) + self.sim.particles.discard((x, y)) + particle.position = (new_x, new_y) + + + def apply_physics(self, dt, engine_settings): + """Handle all physics effects""" + new_active_particles = set() + updates = [] + + for x, y in list(self.sim.active_particles): + particle = self.sim.particles[x][y] + if not particle: + 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.sim.particles[x][y] = None + self.sim.particles.discard((x, y)) + + # Handle fire movement using numpy random + dx = np.random.uniform(-0.5, 0.5) + dy = np.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.sim.particles[new_x][new_y] is None: + self.sim.particles[new_x][new_y] = particle + new_active_particles.add((new_x, new_y)) + + # Generate smoke above with numpy random + if np.random.random() < 0.25 and new_y > 0: + properties = self.sim.particle_properties['smoke'] + new_smoke = Particle( + position=(new_x, new_y-1), + velocity=[np.random.uniform(-0.5, 0.5), -1], + mass=properties.get('mass', 0.1), + particle_type='smoke', + properties=properties + ) + if self.sim.particles[new_x][new_y-1] is None: + self.sim.particles[new_x][new_y-1] = new_smoke + new_active_particles.add((new_x, new_y-1)) + + # Dissipation chance using numpy random + if np.random.random() < 0.02: + continue + continue + + # Air handling + 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 movement with numpy random + dx = np.random.uniform(2, -1) + dy = np.random.uniform(-2, 0) # Bias upward + new_x = int(x + dx) + new_y = int(y + dy) + + self.sim.particles[x][y] = None + self.sim.particles.discard((x, y)) + + if 0 <= new_x < self.width and 0 <= new_y < self.height: + if self.sim.particles[new_x][new_y] is None: + self.sim.particles[x][y] = None + self.sim.particles[new_x][new_y] = particle + new_active_particles.add((new_x, new_y)) + self.sim.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 with numpy random + if np.random.random() < 0.5: + dx = np.random.choice([-1, 1]) + if (0 <= x + dx < self.width and + self.sim.particles[x + dx][y] is None): + new_x = x + dx + new_y = y + self.sim.particles[x][y] = None + self.sim.particles[new_x][new_y] = particle + new_active_particles.add((new_x, new_y)) + continue + + new_x = int(x + particle.velocity[0] * dt) + new_y = int(y + particle.velocity[1] * dt) + + # Update position + if 0 <= new_x < self.width and 0 <= new_y < self.height: + if self.sim.particles[new_x][new_y] is None: + updates.append((x, y, new_x, new_y, particle)) + new_active_particles.add((new_x, new_y)) + self._wake_neighbors(new_x, new_y) + else: + new_active_particles.add((x, y)) + + # Apply updates + for old_x, old_y, new_x, new_y, particle in updates: + self.sim.particles[old_x][old_y] = None + self.sim.particles[new_x][new_y] = particle + particle.position = (new_x, new_y) + + # Handle boundaries + if engine_settings['outerwall']: + if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1: + if self.sim.particles[x][y] is None: + properties = self.sim.particle_properties['wall'] + wall = Particle.from_type((x, y), 'wall', properties) + self.sim.particles[x][y] = wall + new_active_particles.add((x, y)) + self.particle_count += 1 + continue + else: + if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1: + if self.sim.particles[x][y] is not None: + self.sim.particles[x][y] = None + self.sim.particles.discard((x, y)) + self.sim.particle_count -= 1 + continue + + self.sim.particles = new_active_particles + + + 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 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 = np.random.uniform(-1, 1) + dy = np.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.sim.particles[new_x][new_y] is None: + self.sim.particles[x][y] = None + self.sim.particles[new_x][new_y] = particle + self.sim.particles.add((new_x, new_y)) + self.sim.particles.discard((x, y)) diff --git a/src/physics/tests/grid.py b/src/physics/tests/grid.py new file mode 100644 index 0000000..c3cf42b --- /dev/null +++ b/src/physics/tests/grid.py @@ -0,0 +1,87 @@ + +""" +Spatial grid management systems for Sandpypi +Handles particle positioning and neighbor calculations +""" +from physics.base import SimulationBase + + +class SpatialGrid: + def __init__(self, simulation): + self.sim = simulation + self.width = simulation.width + self.height = simulation.height + self.cell_size = 32 + self.spatial_grid = {} + + + def update_spatial_grid(self): + """Enhanced spatial grid update""" + if len(self.sim.active_particles) > 100: + self.spatial_grid = {} + cell_lists = {} + + # Track temperature-sensitive particles + temp_sensitive_cells = set() + + for x, y in self.sim.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)) + + # Mark cells with temperature-sensitive particles + particle = self.sim.particles[x][y] + if hasattr(particle, 'solidify_temperature') or hasattr(particle, 'melt_temperature'): + temp_sensitive_cells.add(cell_key) + + self.spatial_grid = {k: set(v) for k, v in cell_lists.items()} + return temp_sensitive_cells + + + 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 _get_neighbors_from_grid(self, x, y): + """Get neighbors using spatial grid""" + cell_key = self.get_cell_key(x, y) + neighbors = [] + + # Check current and adjacent cells + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + check_key = (cell_key[0] + dx, cell_key[1] + dy) + if check_key in self.spatial_grid: + neighbors.extend(self.spatial_grid[check_key]) + + return neighbors + + + 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 _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 + diff --git a/src/physics/tests/interactions.py b/src/physics/tests/interactions.py new file mode 100644 index 0000000..02dc089 --- /dev/null +++ b/src/physics/tests/interactions.py @@ -0,0 +1,118 @@ + +""" +Particle interaction handling systems for Sandpypi +Manages how different particle types interact with each other +""" +from config.settings import np +from src.physics.particle import Particle +from physics.base import SimulationBase + + +class ParticleInteractions: + def __init__(self, simulation): + self.sim = simulation + + 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.sim.active_particles): + particle = self.sim.particles[x][y] + + if not particle: + continue + + # Handle damage for any particle with durability + self.handle_particle_damage(particle, x, y) + + # 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 = Wet Sand + 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, 'wsand') # Pass wsand type + self.particles[x2][y2] = None + self.active_particles.discard((x2, y2)) + + # Water + Dirt = Mud + if (particle1.particle_type == 'water' and particle2.particle_type == 'dirt' or + particle2.particle_type == 'water' and particle1.particle_type == 'dirt'): + self.create_mud(x1, y1, 'mud') # Pass mud type + 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 np.random.random() < 0.3: # 30% chance to ignite + self.transform_particle(target_x, target_y, 'fire') + + + def mix_liquids(self, liquid1, liquid2): # not implemented + """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 create_mud(self, x, y, mud_type): + """Create either wet sand or mud based on the specified type""" + if mud_type in self.particle_properties: + properties = self.particle_properties[mud_type] + new_particle = Particle.from_type((x, y), mud_type, properties) + self.particles[x][y] = new_particle + self.active_particles.add((x, y)) + + def handle_particle_damage(self, particle, x, y): + """Handle damage calculations for particles with durability""" + if not hasattr(particle, 'durability'): + return + + # Pressure damage + fx, fy = self.calculate_forces(particle, x, y) + pressure = (fx*fx + fy*fy)**0.5 # Calculate magnitude of force vector + + if hasattr(particle, 'pressure_threshold') and pressure > particle.pressure_threshold: + particle.durability -= 0.1 + + # Impact damage + neighbors = self._get_neighbors_from_grid(x, y) + for nx, ny in neighbors: + neighbor = self.particles[nx][ny] + if neighbor and neighbor.velocity[1] > 5.0: + particle.durability -= 0.2 + + # Heat damage + if particle.temperature > 900: + particle.durability -= 1 + + # Check if particle should break + if particle.durability <= 0 and hasattr(particle, 'broken'): + self.transform_particle(x, y, particle.broken) + return + + + 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)]] diff --git a/src/physics/tests/simulation.py b/src/physics/tests/simulation.py new file mode 100644 index 0000000..e3c4145 --- /dev/null +++ b/src/physics/tests/simulation.py @@ -0,0 +1,73 @@ + +from physics.base import SimulationBase, time, np +from physics.grid import SpatialGrid as sg +from physics.forces import ForceSystem as fs +from physics.temperature import TemperatureSystem as ts +from physics.interactions import ParticleInteractions as pi +from src.physics.particle import Particle, particle_properties +from physics.sim import Simulation + +class Simulation(SimulationBase): + def __init__(self, width, height, x=0, y=0): + print(f"Simulation init - before base init - particles type: {type(getattr(self, 'particles', None))}") + + SimulationBase.__init__(self, width, height, x, y) + + print(f"After base init - particles type: {type(self.particles)}") + self.dormant_particles = set() + self.particle_movement_counter = {} + self.DORMANT_THRESHOLD = 10 + self.sim = Simulation(width, height) + self.spatial_grid = sg(self) + self.force_system = fs(self) + self.temperature_system = ts(self) + self.particle_interactions = pi(self) + + def simulate_step(self, dt, engine_settings): + """Run simulation step with spatial grid updates""" + self.spatial_grid.update_spatial_grid() + + # Update particle positions and physics + self.force_system.apply_gravity(dt) + self.sim.Simulation.apply_physics(dt, engine_settings) + + # Handle state changes and interactions + self.temperature_system.handle_temperature(dt) + self.particle_interactions.handle_particle_interactions(dt) + self.temperature_system.burning() + self.temperature_system.spread_fire() + + + 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 reset_particle_count(self): + """Reset and recalculate accurate particle count""" + active_count = np.sum([1 for x, y in self.active_particles if self.particles[x][y] is not None]) + self.particle_count = int(active_count) + + + def get_accurate_particle_count(self): + """Get current accurate particle count using numpy""" + particle_mask = np.array([[self.particles[x][y] is not None + for y in range(self.height)] + for x in range(self.width)]) + return np.sum(particle_mask) + diff --git a/src/physics/tests/temperature.py b/src/physics/tests/temperature.py new file mode 100644 index 0000000..44c398e --- /dev/null +++ b/src/physics/tests/temperature.py @@ -0,0 +1,162 @@ + +""" +Temperature and state management systems for Sandpypi +Handles temperature changes, phase transitions, and burning mechanics +""" +from config.settings import np + + + +class TemperatureSystem: + def __init__(self, simulation): + self.sim = simulation + + def handle_temperature(self, dt): # this is where we handle the temperature. + """Handle temperature changes and state transitions""" + for x, y in list(self.sim.active_particles): + particle = self.sim.particles[x][y] + if particle and particle.is_gas: + particle.temperature -= 0.5 * dt + if not particle: + continue + + + if particle.temperature > 1700: + # Transition to gas + particle.is_gas = True + particle.temperature = 1700 + particle.velocity = [np.random.uniform(-1, 1), np.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.sim.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 handle_phase_transitions(self, particle, x, y): # this is where we handle all the phase transitions. + """Handle all phase transitions for a particle""" + state_changed = False + + if particle.particle_type == 'lava': + if particle.temperature < particle.solidify_temperature: + state_changed = self.transform_particle(x, y, particle.solidify) + return state_changed + + # 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) + + 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.sim.particle: + 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.sim.particle_properties: + self.transform_particle(x, y, new_type) + return new_type + + if particle.particle_type == 'steam': + # Steam should condense when it cools + if particle.temperature <= particle.solidify_temperature: + self.transform_particle(x, y, new_type) + return new_type + + # Check durability property from JSON + if (hasattr(particle, 'durability') and hasattr(particle, 'brk') + and particle.brk is not None): + if particle.durability <= 0: + self.transform_particle(x, y, particle.broken) + return new_type + + + def burning(self): # this is where we handle the burning. + """Handle burning of particles.""" + for x, y in list(self.sim.active_particles): + particle = self.sim.particles[x][y] + if particle and hasattr(particle, 'burning') and particle.burning: + particle.temperature += 10 + if particle.temperature > 1000: + self.sim.particles[x][y] = None + self.sim.active_particles.remove((x, y)) + self.spatial_grid.pop((x, y), None) + + + def spread_fire(self): # this is where we spread the fire. + """Spread fire to neighboring particles.""" + for x, y in list(self.sim.active_particles): + particle = self.sim.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.sim.particles[nx][ny] + if neighbor and hasattr(neighbor, 'flamability'): + if neighbor.particle_type == 'wood': + # Higher chance to ignite wood + if np.random.random() < 0.3: # 30% chance to spread + self.ignite_particle(neighbor) + elif neighbor.flamability > 0: + if np.random.random() < 0.1: # 10% chance for other materials + self.ignite_particle(neighbor) + + + 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 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 np.random.random() < 0.6: # % chance + self.sim.particles[x][y] = None + self.sim.active_particles.discard((x, y)) + + if particle.particle_type in ['fire', 'flame', 'lava']: + # Create smoke above with proper physics + if np.random.random() < 0.65 and y > 0: # % chance for smoke + properties = self.sim.particle_properties['smoke'] + new_smoke = self.sim.Particle.from_type((x, y-1), 'smoke', properties) + if self.sim.particles[x][y-1] is None: + self.sim.particles[x][y-1] = new_smoke + self.sim.active_particles.add((x, y-1)) + + else: + # Handle collision with water + if particle.particle_type == 'water': + self.sim.particles[x][y-1] = None + self.sim.active_particles.discard((x, y-1)) + self.sim.particles[x][y] = None + self.sim.active_particles.discard((x, y)) + self.sim.particles[x][y] = self.sim.Particle.from_type((x, y), 'water', self.sim.particle_properties['water']) + self.sim.active_particles.add((x, y)) \ No newline at end of file diff --git a/src/rendering/rendering.py b/src/rendering/rendering.py index 034e549..05f7b89 100644 --- a/src/rendering/rendering.py +++ b/src/rendering/rendering.py @@ -23,7 +23,6 @@ The `clear_screen` function is used to reset the simulation grid and clear the d from config.settings import pygame, random, particle_properties, engine_settings -from debug.debugger_system import DebuggerSystem class Rendering: @@ -49,7 +48,6 @@ class Rendering: 'zoom_window': pygame.font.SysFont(None, 20), 'zoom_window_text': pygame.font.SysFont(None, 20) } - self.debug = DebuggerSystem() # Pre-render static UI elements self.button_surfaces = {} @@ -286,13 +284,52 @@ class Rendering: def draw_debug_overlay(self, fps, sim): - """Draws debug overlay on the screen.""" - if engine_settings['enable_fps'] or engine_settings['enable_debug']: - self.debug.screen = self.screen # Pass screen reference - self.debug.cached_fonts = self.cached_fonts # Pass font references - self.debug.track_fps() - self.debug.update_performance_metrics(sim) - self.debug.draw_debug_overlay(self.screen, sim) + if not engine_settings['enable_fps'] and not engine_settings['enable_debug']: + return + + # Create static debug surface if not exists + if not hasattr(self, 'debug_surface'): + self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA) + + # Only update when values change significantly + mouse_x, mouse_y = pygame.mouse.get_pos() + grid_x, grid_y = mouse_x // 3, mouse_y // 3 + + current_info = ( + int(fps), + int(sim.track_tps()), + mouse_x // 10, # Reduced update frequency + mouse_y // 10, + sim.particle_count + ) + + if not hasattr(self, '_last_debug_info') or current_info != self._last_debug_info: + self._last_debug_info = current_info + self.debug_surface.fill((0, 0, 0, 0)) + + font = self.cached_fonts['debug'] + y_offset = 10 + + if engine_settings['enable_fps']: + fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255)) + self.debug_surface.blit(fps_surf, (10, y_offset)) + y_offset += 25 + + if engine_settings['enable_debug']: + debug_lines = [ + f"Mouse: ({mouse_x}, {mouse_y})", + f"Grid: ({grid_x}, {grid_y})", + f"Particles: {sim.particle_count}" + ] + + for line in debug_lines: + text_surf = font.render(line, True, (255, 255, 255)) + self.debug_surface.blit(text_surf, (10, y_offset)) + y_offset += 25 + + # Single blit of cached surface + self.screen.blit(self.debug_surface, (0, 0)) + def draw_buttons(self): # this is the function that draws the buttons self.buttons = {} diff --git a/src/sandpypi.py b/src/sandpypi.py index 910626a..0563a24 100644 --- a/src/sandpypi.py +++ b/src/sandpypi.py @@ -20,12 +20,6 @@ from config.settings import pygame, engine_settings, cProfile, pstats, time from rendering.rendering import Rendering from physics.sim import Simulation -""" -This is for the future physics engine until i figure out a better method used for testing right now. -#import os -#import sys -#sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -""" def handle_input(event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos):