Parallax Demo

parallax_demo.py
"""
parallax_demo.py - Parallax System Demonstration

Updated for LunaEngine 0.2.0 Camera System:
- Uses CameraConstraints for zoom limits
- Unified world‑to‑screen conversion (camera.position = viewport centre)
"""

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

import pygame
from lunaengine.core import LunaEngine, Scene
from lunaengine.graphics.camera import Camera, CameraMode

class ParallaxDemo(Scene):
    """Demo scene to test the parallax background system"""
    
    def __init__(self, engine: LunaEngine):
        super().__init__(engine)
        
        # Game state
        self.game_state = {
            'camera_speed': 200,
            'parallax_enabled': True,
            'debug_info': True
        }
        
        # World configuration
        self.world_size = (4000, 720)  # Wide world for horizontal movement
        
        # Player position (for camera control)
        self.player_x = 0
        
        # Load background image
        self.bg_image = self.load_background_image()
        
        # Setup camera and parallax
        self.setup_camera()
        self.setup_parallax()
        
        # Setup UI
        self.setup_ui()
        
        print("Parallax Demo Controls:")
        print("A/D or Left/Right Arrow - Move camera horizontally")
        print("W/S or Up/Down Arrow - Move camera vertically") 
        print("Mouse Wheel - Zoom in/out")
        print("P - Toggle parallax on/off")
        print("I - Toggle debug info")
        print("R - Reset camera position")

    def load_background_image(self):
        """Load the background image for parallax testing"""
        try:
            # Try to load from current directory first
            image_path = "background.jpg"
            if os.path.exists(image_path):
                image = pygame.image.load(image_path)
                print(f"Loaded background image: {image_path}")
                return image.convert_alpha()
            else:
                # Create a placeholder image if file not found
                print("Background image not found. Creating placeholder...")
                return self.create_placeholder_image()
        except Exception as e:
            print(f"Error loading background image: {e}")
            return self.create_placeholder_image()

    def create_placeholder_image(self):
        """Create a placeholder image with gradient and grid for testing"""
        surface = pygame.Surface((1280, 720), pygame.SRCALPHA)
        
        # Create gradient background
        for y in range(720):
            # Sky gradient (blue to light blue)
            if y < 360:
                color = (100, 150, 255 - y // 3)
                pygame.draw.line(surface, color, (0, y), (1280, y))
            # Ground gradient (green to dark green)
            else:
                color = (50, 200 - (y - 360) // 2, 50)
                pygame.draw.line(surface, color, (0, y), (1280, y))
        
        # Add grid lines for better parallax effect visualization
        grid_color = (255, 255, 255, 50)
        for x in range(0, 1280, 100):
            pygame.draw.line(surface, grid_color, (x, 0), (x, 720), 2)
        for y in range(0, 720, 100):
            pygame.draw.line(surface, grid_color, (0, y), (1280, y), 2)
        
        # Add some landmarks
        # Mountains in background
        pygame.draw.polygon(surface, (150, 150, 200, 200), 
                           [(200, 360), (400, 200), (600, 360)])
        pygame.draw.polygon(surface, (120, 120, 180, 200), 
                           [(600, 360), (800, 150), (1000, 360)])
        
        # Trees in midground
        for x in [150, 450, 750, 1050]:
            # Trunk
            pygame.draw.rect(surface, (139, 69, 19, 255), (x, 360, 20, 60))
            # Leaves
            pygame.draw.circle(surface, (50, 150, 50, 255), (x + 10, 330), 40)
        
        # Foreground elements
        for x in [100, 300, 500, 700, 900, 1100]:
            pygame.draw.rect(surface, (100, 100, 100, 255), (x, 420, 10, 100))
        
        return surface

    def setup_camera(self):
        """Configure camera for parallax testing (updated for new camera system)"""
        self.camera.position = pygame.math.Vector2(0, 0)
        self.camera.target_position = pygame.math.Vector2(0, 0)
        
        self.camera.mode = CameraMode.TOPDOWN
        self.camera.smooth_speed = 0.05
        self.camera.lead_factor = 0.0
        
        # Set zoom limits via CameraConstraints
        self.camera.constraints.min_zoom = 0.5
        self.camera.constraints.max_zoom = 2.0
        self.camera.zoom = 1.0
        self.camera.target_zoom = 1.0
        
        # Set camera bounds to world size
        world_rect = pygame.Rect(0, 0, self.world_size[0], self.world_size[1])
        self.camera.set_bounds(world_rect)

    def setup_parallax(self):
        """Setup parallax background with multiple layers"""
        # Clear any existing layers
        self.camera.clear_parallax_layers()
        
        # Create different parallax layers from the same image with different speeds
        # This simulates depth by having background elements move slower than foreground
        
        # Layer 1: Far background (mountains, sky) - moves very slowly
        far_bg = self.create_parallax_layer(self.bg_image, 0.1)
        self.camera.add_parallax_layer(far_bg, 0.2, tile_mode=True)
        
        # Layer 2: Mid background (distant trees) - moves slowly
        mid_bg = self.create_parallax_layer(self.bg_image, 0.3)
        self.camera.add_parallax_layer(mid_bg, 0.4, tile_mode=True)
        
        # Layer 3: Near background (close trees) - moves at medium speed
        near_bg = self.create_parallax_layer(self.bg_image, 0.6)
        self.camera.add_parallax_layer(near_bg, 0.7, tile_mode=True)
        
        # Layer 4: Foreground (grass, rocks) - moves almost with camera
        foreground = self.create_parallax_layer(self.bg_image, 0.9)
        self.camera.add_parallax_layer(foreground, 0.9, tile_mode=True)
        
        self.camera.enable_parallax(True)
        
        print("Parallax layers created:")
        print("- Far background (speed: 0.2)")
        print("- Mid background (speed: 0.4)") 
        print("- Near background (speed: 0.7)")
        print("- Foreground (speed: 0.9)")

    def create_parallax_layer(self, base_image, brightness_factor=1.0):
        """Create a modified version of the base image for parallax layers"""
        # Create a copy of the image
        layer = base_image.copy()
        
        # Adjust brightness to simulate depth (darker = further away)
        if brightness_factor != 1.0:
            # Fill with semi-transparent color to darken/brighten
            overlay = pygame.Surface(layer.get_size(), pygame.SRCALPHA)
            brightness_value = int(255 * (1 - brightness_factor))
            overlay.fill((brightness_value, brightness_value, brightness_value, 100))
            layer.blit(overlay, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
        
        return layer

    def setup_ui(self):
        """Setup user interface for the demo"""
        from lunaengine.ui.elements import TextLabel, Button
        
        screen_width, screen_height = self.engine.width, self.engine.height
        
        # Debug info display
        self.debug_label = TextLabel(10, 10, "", 16, (255, 255, 255))
        self.add_ui_element(self.debug_label)
        
        # Toggle parallax button
        self.parallax_toggle = Button(screen_width - 150, 10, 140, 30, "Parallax: ON")
        self.parallax_toggle.set_on_click(self.toggle_parallax)
        self.add_ui_element(self.parallax_toggle)
        
        # Debug info toggle
        self.debug_toggle = Button(screen_width - 150, 50, 140, 30, "Debug Info: ON")
        self.debug_toggle.set_on_click(self.toggle_debug_info)
        self.add_ui_element(self.debug_toggle)
        
        # Reset camera button
        self.reset_button = Button(screen_width - 150, 90, 140, 30, "Reset Camera")
        self.reset_button.set_on_click(self.reset_camera)
        self.add_ui_element(self.reset_button)

    def toggle_parallax(self):
        """Toggle parallax effect on/off"""
        self.game_state['parallax_enabled'] = not self.game_state['parallax_enabled']
        self.camera.enable_parallax(self.game_state['parallax_enabled'])
        self.parallax_toggle.set_text(f"Parallax: {'ON' if self.game_state['parallax_enabled'] else 'OFF'}")

    def toggle_debug_info(self):
        """Toggle debug information display"""
        self.game_state['debug_info'] = not self.game_state['debug_info']
        self.debug_toggle.set_text(f"Debug Info: {'ON' if self.game_state['debug_info'] else 'OFF'}")

    def reset_camera(self):
        """Reset camera to starting position"""
        self.player_x = 0
        self.camera.position = pygame.math.Vector2(0, 0)
        self.camera.target_position = pygame.math.Vector2(0, 0)
        self.camera.set_zoom(1.0, smooth=False)

    def handle_key_press(self, event):
        """Handle key presses"""
        if event.key == pygame.K_p:
            self.toggle_parallax()
        elif event.key == pygame.K_i:
            self.toggle_debug_info()
        elif event.key == pygame.K_r:
            self.reset_camera()

    def update_player_movement(self, dt):
        """Update camera movement based on input"""
        keys = pygame.key.get_pressed()
        
        movement_x = 0
        movement_y = 0
        
        # Horizontal movement
        if keys[pygame.K_a] or keys[pygame.K_LEFT]:
            movement_x = -1
        if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
            movement_x = 1
            
        # Vertical movement  
        if keys[pygame.K_w] or keys[pygame.K_UP]:
            movement_y = -1
        if keys[pygame.K_s] or keys[pygame.K_DOWN]:
            movement_y = 1
        
        # Apply movement
        self.player_x += movement_x * self.game_state['camera_speed'] * dt
        movement_y = movement_y * self.game_state['camera_speed'] * dt
        
        # Update camera target
        self.camera.target_position.x = self.player_x
        self.camera.target_position.y += movement_y
        
        # Mouse wheel zoom – now uses constraints for clamping
        if self.engine.mouse_wheel != 0:
            zoom_speed = 0.1
            new_zoom = self.camera.zoom + (self.engine.mouse_wheel * zoom_speed)
            # Clamp using constraints
            new_zoom = max(self.camera.constraints.min_zoom,
                          min(self.camera.constraints.max_zoom, new_zoom))
            self.camera.set_zoom(new_zoom, smooth=True)

    def update_debug_info(self):
        """Update debug information display"""
        if self.game_state['debug_info']:
            info_text = (
                f"Camera Position: ({self.camera.position.x:.1f}, {self.camera.position.y:.1f})\n"
                f"Camera Zoom: {self.camera.zoom:.2f}\n"
                f"Player X: {self.player_x:.1f}\n"
                f"Parallax Layers: {self.camera.get_parallax_layer_count()}\n"
                f"World Size: {self.world_size[0]}x{self.world_size[1]}"
            )
            self.debug_label.set_text(info_text)
        else:
            self.debug_label.set_text("")

    def update(self, dt):
        """Update game logic"""
        # Update player movement
        self.update_player_movement(dt)
        
        # Update camera
        self.camera.update(dt)
        
        # Update debug info
        self.update_debug_info()

    def render(self, renderer):
        """Render the scene"""
        # Clear screen
        renderer.get_surface().fill((30, 30, 50))
        
        # Render parallax background
        if self.game_state['parallax_enabled']:
            self.camera.render_parallax(renderer)
        else:
            # Fallback: render static background when parallax is disabled
            bg_x = -self.camera.position.x * 0.5  # Simple parallax effect
            renderer.blit(self.bg_image, (bg_x % 1280 - 1280, 0))
            renderer.blit(self.bg_image, (bg_x % 1280, 0))
        
        # Render world boundaries for reference
        self.render_world_boundaries(renderer)
        
        # Render player position indicator
        self.render_player_indicator(renderer)

    def render_world_boundaries(self, renderer):
        """Render world boundaries for visual reference"""
        screen_width, screen_height = self.engine.width, self.engine.height
        
        # Convert world coordinates to screen coordinates (returns pygame.Vector2)
        left_pos = self.camera.world_to_screen((0, 0))
        right_pos = self.camera.world_to_screen((self.world_size[0], 0))
        
        # Draw boundary lines – cast to int for rendering safety
        boundary_color = (255, 0, 0, 100)
        renderer.draw_line(int(left_pos.x), 0, int(left_pos.x), screen_height, boundary_color, 2)
        renderer.draw_line(int(right_pos.x), 0, int(right_pos.x), screen_height, boundary_color, 2)

    def render_player_indicator(self, renderer):
        """Render a simple indicator for player position"""
        player_screen_pos = self.camera.world_to_screen((self.player_x, self.world_size[1] // 2))
        
        # Draw player indicator
        indicator_color = (255, 255, 0)
        renderer.draw_circle(int(player_screen_pos.x), int(player_screen_pos.y), 10, indicator_color)
        
        # Draw direction indicator
        direction_length = 30
        renderer.draw_line(
            int(player_screen_pos.x), int(player_screen_pos.y),
            int(player_screen_pos.x + direction_length), int(player_screen_pos.y),
            indicator_color, 3
        )

def main():
    """Main function to run the parallax demo"""
    engine = LunaEngine("Parallax System Demo - Use A/D to move, P to toggle parallax", 1024, 576)
    engine.fps = 60
    
    # Register event handlers
    @engine.on_event(pygame.KEYDOWN)
    def on_key_press(event):
        if engine.current_scene and hasattr(engine.current_scene, 'handle_key_press'):
            engine.current_scene.handle_key_press(event)
    
    # Add and start the parallax demo scene
    engine.add_scene("parallax_demo", ParallaxDemo)
    engine.set_scene("parallax_demo")
    
    print("=== Parallax System Demo ===")
    print("This demo tests the parallax background system with horizontal movement.")
    print("The background image will be loaded from 'background.jpg' if available.")
    print("If no image is found, a placeholder will be generated automatically.")
    
    engine.run()

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

parallax_demo.py - Parallax System Demonstration