Topdown Farming Demo

topdown_farming_demo.py
"""
topdown_farming_demo.py - Top-Down Farming Game Demo with Optimized Shadows

Updated for LunaEngine 0.2.0 Camera System:
- Unified world‑to‑screen conversion (camera.position = viewport centre)
- Constraints via CameraConstraints
- Compatible with legacy CameraMode / set_target
"""

import sys
import os
import random
import math
import numpy as np
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

from lunaengine.core import LunaEngine, Scene
from lunaengine.ui.elements import *
from lunaengine.graphics.camera import Camera, CameraMode
from lunaengine.graphics.particles import ParticleSystem, ParticleConfig, ExitPoint, PhysicsType
from lunaengine.graphics.shadows import ShadowSystem, Light, ShadowCaster
from lunaengine.utils import distance
import pygame

class TopDownFarmingGame(Scene):
    """Top-Down Farming and Collection Game with Optimized Shadows"""
    
    def __init__(self, engine: LunaEngine):
        super().__init__(engine)
        
        # Game state
        self.game_state = {
            'money': 100,
            'inventory': {
                'wood': 0,
                'stone': 0,
                'wheat': 0,
                'corn': 0
            },
            'seeds': {
                'wheat': 5,
                'corn': 3
            },
            'selected_tool': 'axe',
            'selected_seed': 'wheat',
            'day_time': 0.25,  # Start in morning
            'day_count': 1
        }
        
        # World configuration
        self.world_size = (2000, 2000)
        self.cell_size = 40
        
        # Initialize game entities
        self.player = None
        self.trees = []
        self.rocks = []
        self.farm_plots = []
        self.crops = []
        self.market_stall = None
        self.seed_shop = None
        
        # Shadow system control
        self.shadows_enabled = True
        self.shadow_quality = "medium"  # low, medium, high
        
        # Generate world (BEFORE setting up camera)
        self.setup_parallax()
        self.generate_world()
        
        # Configure camera (AFTER generating world)
        self.setup_camera()
        
        # Setup shadow system (AFTER camera and world)
        self.setup_shadows()
        
        # Setup UI
        self.setup_ui()

    def setup_shadows(self):
        """Setup optimized shadow system"""
        # Configure shadow system based on quality
        if self.shadow_quality == "low":
            self.shadow_system.max_cache_size = 3
            self.shadow_update_frequency = 3  # Update every 3 frames
        elif self.shadow_quality == "medium":
            self.shadow_system.max_cache_size = 5
            self.shadow_update_frequency = 2  # Update every 2 frames
        else:  # high
            self.shadow_system.max_cache_size = 8
            self.shadow_update_frequency = 1  # Update every frame
        
        # Add only essential shadow casters for better performance
        self.add_essential_shadow_casters()
        
        # Setup lights
        self.setup_lights()

    def add_essential_shadow_casters(self):
        """Add only the most important shadow casters for performance"""
        # Clear existing shadow casters first
        self.shadow_system.clear_shadow_casters()
        
        # Add trees as shadow casters (most visible)
        for tree in self.trees[:15]:  # Limit to 15 trees for performance
            tree_caster = self.create_tree_shadow_caster(tree)
            self.shadow_system.shadow_casters.append(tree_caster)
            
        # Add rocks as shadow casters
        for rock in self.rocks[:15]:  # Limit to 15 rocks for performance
            rock_caster = self.create_rock_shadow_caster(rock)
            self.shadow_system.shadow_casters.append(rock_caster)
        
        # Add buildings as shadow casters
        if self.market_stall:
            market_caster = self.create_building_shadow_caster(self.market_stall)
            self.shadow_system.shadow_casters.append(market_caster)
        
        if self.seed_shop:
            shop_caster = self.create_building_shadow_caster(self.seed_shop)
            self.shadow_system.shadow_casters.append(shop_caster)

    def setup_lights(self):
        """Setup optimized lighting system"""
        # Main directional light (sun/moon)
        self.sun_light = self.shadow_system.add_light(
            self.world_size[0] // 2,
            -300,
            1200,
            (255, 255, 200),
            intensity=0.8
        )
        
        # Player light (simple, small radius for performance)
        self.player_light = self.shadow_system.add_light(
            self.player['position'][0],
            self.player['position'][1],
            150,  # Smaller radius for performance
            (255, 220, 180),
            intensity=0.6
        )
        
        # Add some static lights for better illumination
        static_lights = [
            (500, 500, 200, (255, 200, 150), 0.4),
            (1500, 500, 200, (200, 220, 255), 0.4),
            (500, 1500, 200, (200, 255, 200), 0.4),
            (1500, 1500, 200, (255, 200, 255), 0.4)
        ]
        
        for x, y, radius, color, intensity in static_lights:
            self.shadow_system.add_light(x, y, radius, color, intensity)

    def create_tree_shadow_caster(self, tree):
        """Create simplified shadow caster for a tree"""
        x, y = tree['position']  # World position
        size = tree['size']
        
        # Simple square shadow caster (better performance than circle)
        half_size = size * 0.3
        vertices = [
            (x - half_size, y - half_size),
            (x + half_size, y - half_size),
            (x + half_size, y + half_size),
            (x - half_size, y + half_size)
        ]
        
        return ShadowCaster(vertices)
    
    def create_rock_shadow_caster(self, rock):
        """Create simplified shadow caster for a rock"""
        x, y = rock['position']  # World position
        size = rock['size']
        
        # Simple square shadow caster (better performance than circle)
        half_size = size * 0.3
        vertices = [
            (x - half_size, y - half_size),
            (x + half_size, y - half_size),
            (x + half_size, y + half_size),
            (x - half_size, y + half_size)
        ]
        
        return ShadowCaster(vertices)

    def create_building_shadow_caster(self, building):
        """Create shadow caster for building"""
        x, y = building['position']  # World position
        size = building['size']
        
        half_size = size // 2
        vertices = [
            (x - half_size, y - half_size),
            (x + half_size, y - half_size),
            (x + half_size, y + half_size),
            (x - half_size, y + half_size)
        ]
        return ShadowCaster(vertices)

    def update_shadows(self):
        """Update shadow system with proper world coordinates"""
        # Update player light - use world position
        if hasattr(self, 'player') and self.player:
            self.player_light.position.x = self.player['position'][0]
            self.player_light.position.y = self.player['position'][1]
        
        # Update sun based on time of day - use world position
        sun_angle = self.game_state['day_time'] * 2 * math.pi
        sun_x = self.world_size[0] // 2 + math.cos(sun_angle) * 1000
        sun_y = self.world_size[1] // 2 + math.sin(sun_angle) * 400 - 400
        
        self.sun_light.position.x = sun_x
        self.sun_light.position.y = sun_y
        
        # Day/night cycle - adjust light properties
        if 0.25 <= self.game_state['day_time'] <= 0.75:
            # Daytime
            self.sun_light.color = (255, 255, 200)
            self.sun_light.intensity = 0.8
            self.player_light.intensity = 0.3  # Dim player light during day
        else:
            # Nighttime
            self.sun_light.color = (150, 180, 255)
            self.sun_light.intensity = 0.2
            self.player_light.intensity = 0.8  # Bright player light at night

    def setup_camera(self):
        """Configure camera for top-down mode (updated for new camera system)"""
        # Reset camera position to player position
        self.camera.position = pygame.math.Vector2(self.player['position'])
        self.camera.target_position = pygame.math.Vector2(self.player['position'])
        
        self.camera.smooth_speed = 0.1
        self.camera.lead_factor = 0.2
        
        # Zoom limits – now stored in CameraConstraints
        self.camera.constraints.min_zoom = 0.7
        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)
        
        # Set camera to follow player (dict with position + velocity for look‑ahead)
        self.camera.set_target({
            'position': self.player['position'],
            'velocity': self.player['velocity']
        }, CameraMode.TOPDOWN)

    def generate_world(self):
        """Generate optimized game world"""
        grass_dark = (80, 160, 80)
        grass_light = (100, 180, 100)
        
        self.bg_surface = pygame.Surface((self.world_size[0], self.world_size[1]))
        # Generate world tiles
        for x in range(0, self.world_size[0], 100):
            for y in range(0, self.world_size[1], 100):
                grass_color = grass_dark if (x // 100 + y // 100) % 2 == 0 else grass_light
                pygame.draw.rect(self.bg_surface, grass_color, (x, y, 100, 100))
        
        # Create player in center
        self.player = {
            'position': [self.world_size[0] // 2, self.world_size[1] // 2],
            'velocity': [0, 0],
            'speed': 200,
            'size': 20
        }
        
        # Generate trees (fewer for better performance)
        for _ in range(20):
            x = random.randint(100, self.world_size[0] - 100)
            y = random.randint(100, self.world_size[1] - 100)
            self.trees.append({
                'position': [x, y],
                'size': random.randint(30, 50),
                'health': 3,
                'type': 'tree'
            })
        
        # Generate rocks
        for _ in range(15):
            x = random.randint(100, self.world_size[0] - 100)
            y = random.randint(100, self.world_size[1] - 100)
            self.rocks.append({
                'position': [x, y],
                'size': random.randint(25, 40),
                'health': 4,
                'type': 'rock'
            })
        
        # Generate farm plots
        plot_start_x = 400
        plot_start_y = 400
        for row in range(4):  # Smaller farm for performance
            for col in range(4):
                x = plot_start_x + col * 80
                y = plot_start_y + row * 80
                self.farm_plots.append({
                    'position': [x, y],
                    'size': 60,
                    'occupied': False,
                    'crop_type': None,
                    'growth_stage': 0,
                    'growth_timer': 0
                })
        
        # Place market and seed shop
        shop_y = self.world_size[1] // 2
        self.market_stall = {
            'position': [self.world_size[0] - 250, shop_y],
            'size': 80
        }
        
        self.seed_shop = {
            'position': [self.world_size[0] - 450, shop_y],
            'size': 80
        }

    def setup_parallax(self):
        """Setup optimized parallax background"""
        self.camera.clear_parallax_layers()
        
        # Only use one parallax layer for performance
        sky_surface = self.create_sky_surface()
        self.camera.add_parallax_layer(sky_surface, 0.1, tile_mode=True)
        self.camera.enable_parallax(True)

    def create_sky_surface(self):
        """Create simple sky background"""
        surface = pygame.Surface((800, 600))
        # Simple solid color sky (better performance than gradient)
        surface.fill((70, 70, 120))
        return surface

    def setup_ui(self):
        """Setup user interface"""
        screen_width, screen_height = self.engine.width, self.engine.height
        toolbar_height = 100  # Smaller toolbar
        toolbar_y = screen_height - toolbar_height
        
        # Simple toolbar background
        toolbar_bg = UiFrame(0, toolbar_y, screen_width, toolbar_height)
        self.add_ui_element(toolbar_bg)
        
        # Money display
        self.money_display = TextLabel(20, toolbar_y + 15, f"Money: ${self.game_state['money']}", 20, (255, 215, 0))
        self.add_ui_element(self.money_display)
        
        # Inventory display (simplified)
        self.inventory_display = TextLabel(20, toolbar_y + 45, "Inventory: ", 16, (200, 230, 255))
        self.add_ui_element(self.inventory_display)
        
        # Time display
        self.time_display = TextLabel(screen_width - 200, toolbar_y + 15, f"Day {self.game_state['day_count']}", 16, (255, 200, 150))
        self.add_ui_element(self.time_display)
        
        # Shadow toggle button
        self.shadow_toggle = Button(screen_width - 200, toolbar_y + 45, 120, 30, "Shadows: ON")
        self.shadow_toggle.set_on_click(self.toggle_shadows)
        self.add_ui_element(self.shadow_toggle)
        
        # Update initial selection
        self.select_tool('axe')

    def toggle_shadows(self):
        """Toggle shadows on/off"""
        self.shadows_enabled = not self.shadows_enabled
        self.shadow_toggle.set_text(f"Shadows: {'ON' if self.shadows_enabled else 'OFF'}")
        print(f"Shadows {'enabled' if self.shadows_enabled else 'disabled'}")

    def select_tool(self, tool):
        """Select tool"""
        self.game_state['selected_tool'] = tool

    def handle_key_press(self, event):
        """Handle key presses"""
        if event.key == pygame.K_F1:
            self.toggle_shadows()
        elif event.key == pygame.K_1:
            self.select_tool('axe')
        elif event.key == pygame.K_2:
            self.select_tool('pickaxe')
        elif event.key == pygame.K_3:
            self.select_tool('scythe')
        elif event.key == pygame.K_4:
            self.select_tool('seeds')
        elif event.key == pygame.K_q and self.game_state['selected_tool'] == 'seeds':
            self.game_state['selected_seed'] = 'wheat'
        elif event.key == pygame.K_e and self.game_state['selected_tool'] == 'seeds':
            self.game_state['selected_seed'] = 'corn'

    def get_interaction_distance(self):
        """Get maximum interaction distance"""
        return 80

    def update_player_movement(self, dt):
        """Update player movement"""
        keys = pygame.key.get_pressed()
        
        # Reset velocity
        self.player['velocity'] = [0, 0]
        
        # Movement input
        if keys[pygame.K_w]:
            self.player['velocity'][1] = -1
        if keys[pygame.K_s]:
            self.player['velocity'][1] = 1
        if keys[pygame.K_a]:
            self.player['velocity'][0] = -1
        if keys[pygame.K_d]:
            self.player['velocity'][0] = 1
        
        # Mouse wheel zoom (updated to use constraints)
        if self.engine.mouse_wheel != 0:
            zoom_speed = 0.05
            new_zoom = self.camera.zoom + (self.engine.mouse_wheel * zoom_speed)
            # Use constraints for min/max zoom
            new_zoom = max(self.camera.constraints.min_zoom, 
                          min(self.camera.constraints.max_zoom, new_zoom))
            self.camera.set_zoom(new_zoom, smooth=True)
        
        # Normalize diagonal movement
        if self.player['velocity'][0] != 0 and self.player['velocity'][1] != 0:
            self.player['velocity'][0] *= 0.7071
            self.player['velocity'][1] *= 0.7071
        
        # Apply movement
        self.player['position'][0] += self.player['velocity'][0] * self.player['speed'] * dt
        self.player['position'][1] += self.player['velocity'][1] * self.player['speed'] * dt
        
        # Keep player in bounds
        self.player['position'][0] = max(self.player['size'], min(self.world_size[0] - self.player['size'], self.player['position'][0]))
        self.player['position'][1] = max(self.player['size'], min(self.world_size[1] - self.player['size'], self.player['position'][1]))
        
        # Update camera target (look‑ahead works because we pass velocity)
        self.camera.set_target({
            'position': self.player['position'],
            'velocity': self.player['velocity']
        }, CameraMode.TOPDOWN)

    def handle_interaction(self):
        """Handle player interactions with world"""
        if self.engine.input_state.mouse_buttons_pressed.left:
            tool = self.game_state['selected_tool']
            ppos = self.player['position']
            mpos = self.camera.screen_to_world(self.engine.mouse_pos)
            
            if tool == 'axe':
                self.chop_tree(ppos, mpos)
            elif tool == 'pickaxe':
                self.mine_rock(ppos, mpos)
            elif tool == 'scythe':
                self.harvest_crop(ppos, mpos)
            elif tool == 'seeds':
                self.plant_seed(ppos, mpos)
            
            # Shop interactions
            self.interact_with_market(ppos, mpos)

    def chop_tree(self, position, mouse_pos):
        """Chop nearby tree"""
        interaction_distance = self.get_interaction_distance()
        
        for tree in self.trees[:]:
            dis = distance(tree['position'], position)
            mdis = distance(tree['position'], mouse_pos)
            
            if dis < interaction_distance and mdis < tree['size'] + 20:
                tree['health'] -= 1
                
                if tree['health'] <= 0:
                    self.trees.remove(tree)
                    self.game_state['inventory']['wood'] += 2
                    # Remove from shadow system and rebuild
                    self.add_essential_shadow_casters()
                
                break

    def mine_rock(self, position, mouse_pos):
        """Mine nearby rock"""
        interaction_distance = self.get_interaction_distance()
        
        for rock in self.rocks[:]:
            dis = distance(rock['position'], position)
            mdis = distance(rock['position'], mouse_pos)
            
            if dis < interaction_distance and mdis < rock['size'] + 20:
                rock['health'] -= 1
                
                if rock['health'] <= 0:
                    self.rocks.remove(rock)
                    self.game_state['inventory']['stone'] += 2
                
                break

    def plant_seed(self, position, mouse_pos):
        """Plant seed in empty plot"""
        if self.game_state['seeds'][self.game_state['selected_seed']] <= 0:
            return
            
        interaction_distance = self.get_interaction_distance()
        
        for plot in self.farm_plots:
            dis = distance(plot['position'], position)
            mdis = distance(plot['position'], mouse_pos)
            
            if dis < interaction_distance and mdis < plot['size'] + 20 and not plot['occupied']:
                plot['occupied'] = True
                plot['crop_type'] = self.game_state['selected_seed']
                plot['growth_stage'] = 1
                plot['growth_timer'] = 0
                
                self.game_state['seeds'][self.game_state['selected_seed']] -= 1
                break

    def harvest_crop(self, position, mouse_pos):
        """Harvest mature crop"""
        interaction_distance = self.get_interaction_distance()
        
        for plot in self.farm_plots:
            dis = distance(plot['position'], position)
            mdis = distance(plot['position'], mouse_pos)
            
            if dis < interaction_distance and mdis < plot['size'] + 20 and plot['occupied'] and plot['growth_stage'] == 3:
                crop_type = plot['crop_type']
                self.game_state['inventory'][crop_type] += 3
                
                # Reset plot
                plot['occupied'] = False
                plot['crop_type'] = None
                plot['growth_stage'] = 0
                plot['growth_timer'] = 0
                break

    def interact_with_market(self, position, mouse_pos):
        """Sell resources at market"""
        if not self.market_stall:
            return
        
        dis = distance(self.market_stall['position'], position)
        mdis = distance(self.market_stall['position'], mouse_pos)
        interaction_distance = self.get_interaction_distance()
        
        if dis < interaction_distance and mdis < self.market_stall['size'] + 20:
            # Selling prices
            prices = {
                'wood': 5,
                'stone': 8,
                'wheat': 12,
                'corn': 15
            }
            
            # Sell everything
            total_sale = 0
            for item, quantity in self.game_state['inventory'].items():
                if quantity > 0:
                    total_sale += quantity * prices[item]
                    self.game_state['inventory'][item] = 0
            
            if total_sale > 0:
                self.game_state['money'] += total_sale

    def update_crops(self, dt):
        """Update crop growth"""
        for plot in self.farm_plots:
            if plot['occupied']:
                plot['growth_timer'] += dt
                
                # Grow every 5 seconds
                if plot['growth_timer'] >= 5:
                    plot['growth_stage'] = min(3, plot['growth_stage'] + 1)
                    plot['growth_timer'] = 0

    def update_time(self, dt):
        """Update day/night cycle"""
        self.game_state['day_time'] += dt / 120  # 2 minutes per full day
        
        if self.game_state['day_time'] >= 1:
            self.game_state['day_time'] = 0
            self.game_state['day_count'] += 1

    def update_ui(self):
        """Update UI displays"""
        # Money
        self.money_display.set_text(f"Money: ${self.game_state['money']}")
        
        # Inventory
        inv_text = "Inventory: "
        for item, quantity in self.game_state['inventory'].items():
            if quantity > 0:
                inv_text += f"{item}:{quantity} "
        self.inventory_display.set_text(inv_text)
        
        # Time
        time_of_day = "Morning" if self.game_state['day_time'] < 0.25 else \
                     "Noon" if self.game_state['day_time'] < 0.5 else \
                     "Evening" if self.game_state['day_time'] < 0.75 else "Night"
        self.time_display.set_text(f"Day {self.game_state['day_count']} - {time_of_day}")

    def update(self, dt):
        """Update game logic"""
        # Update player movement
        self.update_player_movement(dt)
        
        # Update shadow system (less frequently for performance)
        static_frame_count = getattr(self, '_static_frame_count', 0)
        self._static_frame_count = static_frame_count + 1
        
        if self.shadows_enabled and static_frame_count % 2 == 0:  # Update every 2 frames
            self.update_shadows()
        
        # Update interactions
        self.handle_interaction()
        
        # Update crops
        self.update_crops(dt)
        
        # Update time
        self.update_time(dt)
        
        # Update UI
        self.update_ui()

    def apply_camera_offset(self, position):
        """Convert world coordinates to screen coordinates (uses new unified method)"""
        if isinstance(position, (list, tuple)):
            screen_pos = self.camera.world_to_screen(position)
            return (screen_pos.x, screen_pos.y)
        return position

    def get_ambient_color(self):
        """Get ambient color based on time"""
        time_of_day = self.game_state['day_time']
        if time_of_day < 0.25: return (120, 140, 180)
        elif time_of_day < 0.5: return (150, 170, 200)
        elif time_of_day < 0.75: return (180, 150, 140)
        else: return (80, 90, 120)

    def render_world(self, renderer):
        """Render the game world with proper coordinate conversion"""
        # Render parallax background
        self.camera.render_parallax(renderer)
        
        # Render base terrain
        screen_pos = self.apply_camera_offset((0, 0))
        renderer.blit(self.bg_surface, screen_pos)
        
        # Render farm plots
        for plot in self.farm_plots:
            screen_x, screen_y = self.apply_camera_offset(plot['position'])
            size = plot['size'] * self.camera.zoom
            
            # Plot color based on state
            if plot['occupied']:
                if plot['growth_stage'] == 1:
                    plot_color = (180, 160, 120)
                elif plot['growth_stage'] == 2:
                    plot_color = (140, 180, 100)
                else:
                    plot_color = (100, 160, 80)
            else:
                plot_color = (120, 80, 40)
            
            renderer.draw_rect(screen_x - size//2, screen_y - size//2, size, size, (80, 50, 20), fill=False)
            renderer.draw_rect(screen_x - size//2, screen_y - size//2, size, size, plot_color)
        
        # Render trees
        for tree in self.trees:
            screen_x, screen_y = self.apply_camera_offset(tree['position'])
            size = tree['size'] * self.camera.zoom
            
            # Trunk
            trunk_color = (90, 60, 30)
            renderer.draw_rect(screen_x - 5, screen_y - size//2, 10, size//2, trunk_color)
            # Canopy
            canopy_color = (40, 120, 40)
            renderer.draw_circle(screen_x, screen_y - size//4, size//2, canopy_color)
        
        # Render rocks
        for rock in self.rocks:
            screen_x, screen_y = self.apply_camera_offset(rock['position'])
            size = rock['size'] * self.camera.zoom
            rock_color = (100, 100, 120)
            renderer.draw_circle(screen_x, screen_y, size//2, rock_color)
        
        # Render player
        screen_x, screen_y = self.apply_camera_offset(self.player['position'])
        size = self.player['size'] * self.camera.zoom
        
        renderer.draw_circle(screen_x, screen_y, size//2, (170, 150, 240))

    def render(self, renderer):
        """Render the game with optimized shadows"""
        
        # Apply ambient color based on time of day
        ambient_color = self.get_ambient_color()
        renderer.get_surface().fill(ambient_color)
        
        # Render World
        self.render_world(renderer)
        
        # Render market
        if self.market_stall:
            screen_x, screen_y = self.apply_camera_offset(self.market_stall['position'])
            size = self.market_stall['size'] * self.camera.zoom
            market_color = (200, 160, 60)
            renderer.draw_rect(screen_x - size//2, screen_y - size//2, size, size, market_color)
            renderer.draw_text("Market", screen_x - size//2, screen_y - size//2, (255, 255, 255), pygame.font.SysFont("Arial", 20))
        
        # Render seed shop
        if self.seed_shop:
            screen_x, screen_y = self.apply_camera_offset(self.seed_shop['position'])
            size = self.seed_shop['size'] * self.camera.zoom
            shop_color = (120, 180, 100)
            renderer.draw_rect(screen_x - size//2, screen_y - size//2, size, size, shop_color)
            renderer.draw_text("Seed Shop", screen_x - size//2, screen_y - size//2, (255, 255, 255), pygame.font.SysFont("Arial", 20))
        
        # Render shadows only if enabled
        if self.shadows_enabled:
            try:
                # Use camera base position (without shake) for shadow calculation
                shadow_surface = self.shadow_system.render(self.camera.base_position, renderer)
                if shadow_surface and isinstance(shadow_surface, pygame.Surface):
                    # Apply shadows with proper blending
                    renderer.blit(shadow_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
            except Exception as e:
                # If shadows cause issues, disable them
                print(f"Shadow rendering error: {e}")
                import traceback
                traceback.print_exc()
                self.shadows_enabled = False
                self.shadow_toggle.set_text("Shadows: OFF")

def main():
    """Main function"""
    engine = LunaEngine("Top-Down Farming Game", 1024, 768)
    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)
    
    engine.add_scene("game", TopDownFarmingGame)
    engine.set_scene("game")
    
    print("=== Top-Down Farming Game with Optimized Shadows ===")
    print("Controls:")
    print("WASD - Move player")
    print("Mouse Click - Interact with objects") 
    print("1-4 - Select tools")
    print("Q/E - Select seeds (when seeds tool is active)")
    print("F1 - Toggle shadows on/off")
    print("\nPress F1 to disable shadows if performance is poor")
    
    engine.run()

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

topdown_farming_demo.py - Top-Down Farming Game Demo with Optimized Shadows