Particle Demo

particle_demo.py
"""
Particle System Demo - Fixed Version
"""

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

import pygame
from lunaengine.core import LunaEngine, Scene
from lunaengine.ui.elements import *
from lunaengine.graphics.particles import *
from lunaengine.graphics.camera import Camera, CameraMode

class ParticleDemoScene(Scene):
    """Interactive particle system demo scene"""
    
    def on_exit(self, next_scene = None):
        return super().on_exit(next_scene)
    
    def on_enter(self, previous_scene: str = None):
        return super().on_enter(previous_scene)
    
    def __init__(self, engine: LunaEngine):
        super().__init__(engine)
        self.demo_state = {
            'active_particles': 0,
            'fps': 0,
            'memory_usage': 0,
            'emission_rate': 50,
            'selected_particle': "dust",  # Uses string
            'selected_exit': "top",
            'selected_physics': "topdown", 
            'spread': 45.0,
            'auto_emit': True,
            'show_metrics': True,
        }
        self.emitter_positions = [
            (-200, -200), (-200, 200), (0, 50), (200, -200), (200, 200)
        ]
        self.setup_ui()
        
        # Register some custom particles
        self.setup_custom_particles()
        
        self.particle_system.get_physics_names()
        
    def setup_custom_particles(self):
        """Setup custom particle types for testing"""
        # Magic particles
        magic_config = ParticleConfig(
            color_start=(200, 0, 255),
            color_end=(100, 0, 200),
            size_start=6.0,
            size_end=10.0,
            lifetime=2.0,
            speed=120.0,
            gravity=-80.0,
            spread=90.0,
            fade_out=True,
            grow=True
        )
        self.particle_system.register_custom_particle("magic", magic_config)
        
        # Electric sparks
        electric_config = ParticleConfig(
            color_start=(0, 200, 255),
            color_end=(0, 100, 255),
            size_start=3.0,
            size_end=1.0,
            lifetime=0.8,
            speed=250.0,
            gravity=50.0,
            spread=15.0,
            fade_out=True
        )
        self.particle_system.register_custom_particle("electric", electric_config)

    def setup_ui(self):
        """Setup all UI controls and displays"""
        self.camera.mode = CameraMode.FIXED
        print(self.camera.viewport_width, self.camera.viewport_height)
        
        print(self.camera.world_to_screen((0,0)))
        print(self.camera.screen_to_world((0,0)))
        
        # Title
        title = TextLabel(512, 20, "LunaEngine - Particle System Demo", 32, root_point=(0.5, 0))
        self.add_ui_element(title)
        
        # FPS and Performance Display
        self.fps_display = TextLabel(20, 20, "FPS: --", 18, (100, 255, 100))
        self.add_ui_element(self.fps_display)
        
        self.particle_count_display = TextLabel(20, 45, "Particles: 0", 16, (200, 200, 255))
        self.add_ui_element(self.particle_count_display)
        
        self.memory_display = TextLabel(20, 70, "Memory: 0.0 MB", 16, (255, 200, 100))
        self.add_ui_element(self.memory_display)
        
        # Section 1: Particle Type Selection
        section1_title = TextLabel(20, 110, "Particle Type", 20, (255, 255, 0))
        self.add_ui_element(section1_title)
        
        # Particle type selector
        self.particle_dropdown = Dropdown(20, 140, 150, 30, self.particle_system.get_particles_names(True,True))
        self.particle_dropdown.set_on_selection_changed(
            lambda i, v: self.update_state('selected_particle', v)
        )
        self.add_ui_element(self.particle_dropdown)
        
        # Section 2: Emission Controls
        section2_title = TextLabel(20, 190, "Emission Controls", 20, (255, 255, 0))
        self.add_ui_element(section2_title)
        
        # Emission rate slider
        emission_label = TextLabel(20, 220, "Emission Rate:", 16)
        self.add_ui_element(emission_label)
        
        self.emission_slider = Slider(20, 240, 150, 20, 1, 500, 50)
        self.emission_slider.on_value_changed = lambda v: self.update_state('emission_rate', int(v))
        self.add_ui_element(self.emission_slider)
        
        self.emission_display = TextLabel(180, 240, "50 p/s", 14)
        self.add_ui_element(self.emission_display)
        
        # Spread control
        spread_label = TextLabel(20, 270, "Spread Angle:", 16)
        self.add_ui_element(spread_label)
        
        self.spread_slider = Slider(20, 290, 150, 20, 0, 360, 45)
        self.spread_slider.on_value_changed = lambda v: self.update_state('spread', v)
        self.add_ui_element(self.spread_slider)
        
        self.spread_display = TextLabel(180, 290, "45°", 14)
        self.add_ui_element(self.spread_display)
        
        # Section 3: Physics and Exit Points
        section3_title = TextLabel(20, 330, "Physics & Emission", 20, (255, 255, 0))
        self.add_ui_element(section3_title)
        
        exit_points = ["top", "bottom", "left", "right", "center", "circular"]
        self.exit_dropdown = Dropdown(20, 360, 150, 30, exit_points)
        self.exit_dropdown.set_on_selection_changed(
            lambda i, v: self.update_state('selected_exit', v)
        )
        self.add_ui_element(self.exit_dropdown)
        
        # Physics type selector
        self.physics_dropdown = Dropdown(20, 400, 150, 30, self.particle_system.get_physics_names(True, True))
        self.physics_dropdown.set_on_selection_changed(
            lambda i, v: self.update_state('selected_physics', v)
        )
        self.add_ui_element(self.physics_dropdown)
        
        # Section 4: Controls
        section4_title = TextLabel(20, 450, "Controls", 20, (255, 255, 0))
        self.add_ui_element(section4_title)
        
        # Auto emission toggle
        self.auto_emit_switch = Switch(20, 480, 60, 30, True)
        self.auto_emit_switch.set_on_toggle(lambda s: self.update_state('auto_emit', s))
        self.add_ui_element(self.auto_emit_switch)
        
        auto_emit_label = TextLabel(90, 485, "Auto Emission", 16)
        self.add_ui_element(auto_emit_label)
        
        # Burst mode button
        burst_button = Button(20, 520, 120, 30, "Burst Emission")
        burst_button.set_on_click(lambda: self.emit_burst())
        self.add_ui_element(burst_button)
        
        # Clear particles button
        clear_button = Button(150, 520, 120, 30, "Clear All")
        clear_button.set_on_click(lambda: self.particle_system.clear())
        self.add_ui_element(clear_button)
        
        # Manual emit button
        manual_button = Button(20, 560, 250, 30, "Click Here to Emit Manually")
        manual_button.set_on_click(lambda: self.emit_manual())
        self.add_ui_element(manual_button)

    def update_state(self, key, value):
        """Update demo state and refresh displays"""
        if key in ['particle_type', 'physics_type']: value = str(value).lower()
        self.demo_state[key] = value
        print(f"Updated {key} to {value}")  # DEBUG
        
        # Update UI displays
        self.emission_display.set_text(f"{self.demo_state['emission_rate']} p/s")
        self.spread_display.set_text(f"{self.demo_state['spread']:.0f}°")

    def emit_manual(self):
        """Emit particles manually from center"""
        particle_type = ParticleType[self.demo_state['selected_particle'].upper()]
        exit_point = ExitPoint[self.demo_state['selected_exit'].upper()]
        physics_type = PhysicsType[self.demo_state['selected_physics'].upper()]
        
        # Emit at world center (0,0)
        px,py = 0, 0
        print(self.camera.world_to_screen((px, py)))
        
        self.particle_system.emit(
            x=px, y=py,
            particle_type=particle_type,
            count=100,
            exit_point=exit_point,
            physics_type=physics_type,
            spread=self.demo_state['spread']
        )
        print(f"Emitted 100 {self.demo_state['selected_particle']} particles")

    def emit_burst(self):
        """Emit a burst of particles from all emitter positions"""
        particle_type = ParticleType[self.demo_state['selected_particle'].upper()]
        exit_point = ExitPoint[self.demo_state['selected_exit'].upper()]
        physics_type = PhysicsType[self.demo_state['selected_physics'].upper()]
        
        for x, y in self.emitter_positions:
            self.particle_system.emit(
                x=x, y=y,
                particle_type=particle_type,
                count=50,
                exit_point=exit_point,
                physics_type=physics_type,
                spread=self.demo_state['spread']
            )
        print(f"Emitted burst of {self.demo_state['selected_particle']} particles")

    def on_enter(self, previous_scene: str = None):
        """Called when scene becomes active"""
        print("=== Particle System Demo ===")
        print("Controls:")
        print("- Use dropdowns to select particle type, exit point, and physics")
        print("- Adjust sliders for emission rate and spread")
        print("- Click buttons to emit particles")
        print("- Toggle auto-emission on/off")

    def update(self, dt):
        """Update scene logic"""
        
        # Auto emission from fixed positions
        if self.demo_state['auto_emit']:
            particle_type = ParticleType[self.demo_state['selected_particle'].upper()]
            exit_point = ExitPoint[self.demo_state['selected_exit'].upper()]
            physics_type = PhysicsType[self.demo_state['selected_physics'].upper()]
            for x, y in self.emitter_positions:
                # print(f'X: {x} > {pos_x}, Y: {y} > {pos_y}')
                self.particle_system.emit(
                    x=x, y=y,
                    particle_type=particle_type,
                    count=int(self.demo_state['emission_rate'] * dt),
                    exit_point=exit_point,
                    physics_type=physics_type,
                    spread=self.demo_state['spread']
                )
        
        # Update performance displays
        stats = self.particle_system.get_stats()
        fps_stats = self.engine.get_fps_stats()
        
        self.demo_state['active_particles'] = stats['active_particles']
        self.demo_state['fps'] = fps_stats['current_fps']
        self.demo_state['memory_usage'] = stats['memory_usage_mb']
        
        # Update UI displays
        self.fps_display.set_text(f"FPS: {self.demo_state['fps']:.1f}")
        self.particle_count_display.set_text(f"Particles: {self.demo_state['active_particles']}")
        self.memory_display.set_text(f"Memory: {self.demo_state['memory_usage']:.2f} MB")

    def render(self, renderer):
        """Render scene"""
        # Draw background
        renderer.draw_rect(0, 0, 1024, 768, (20, 20, 30))
        
        # Draw emitter position markers (in world coordinates, so convert to screen)
        for x, y in self.emitter_positions:
            screen_pos = self.camera.world_to_screen((x,y))
            renderer.draw_rect(screen_pos.x-2, screen_pos.y-2, 4, 4, (255, 255, 255))
            renderer.draw_rect(screen_pos.x-1, screen_pos.y-1, 2, 2, (100, 200, 255))
        
        # Draw center marker (world (0,0) is the center)
        center_screen = self.camera.world_to_screen((0, 0))
        renderer.draw_rect(center_screen.x-2, center_screen.y-2, 4, 4, (255, 255, 0))

def main():
    """Main function"""
    # Test
    engine = LunaEngine("LunaEngine - Particle System Demo", 1024, 768)
    engine.fps = 144
    
    @engine.on_event(pygame.KEYDOWN)
    def handle_keydown(event):
        if event.key == pygame.K_r:
            print("Resetting particle system...")
            engine.scenes["main"].particle_system.clear()
        elif event.key == pygame.K_SPACE:
            engine.scenes["main"].emit_manual()
    
    engine.add_scene("main", ParticleDemoScene)
    engine.set_scene("main")
    engine.run()

if __name__ == "__main__":
    main()
About This Example