"""
Shadow System Test Demo - LunaEngine
"""
import sys
import os
import math
import random
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.camera import Camera, CameraMode
from lunaengine.graphics.shadows import ShadowSystem, Light, ShadowCaster
class ShadowTestScene(Scene):
"""Simple scene to test the shadow system"""
def __init__(self, engine: LunaEngine):
super().__init__(engine)
# Test objects
self.test_objects = []
self.lights = []
self.shadow_casters = []
# Camera setup
self.camera.position = pygame.math.Vector2(400, 300)
self.camera.target_position = pygame.math.Vector2(400, 300)
self.camera.mode = CameraMode.TOPDOWN
self.camera.zoom = 1.0
self.camera.target_zoom = 1.0
# Player for movement
self.player = {
'position': [400, 300],
'velocity': [0, 0],
'speed': 200,
'size': 15
}
# Generate test objects
self.generate_test_objects()
# Setup UI
self.setup_ui()
# Debug info
self.debug_info = {
'mouse_world_pos': (0, 0),
'camera_pos': (0, 0),
'shadow_stats': {}
}
def generate_test_objects(self):
"""Generate test objects for shadow casting"""
# Create some rectangular objects
rectangles = [
(100, 100, 80, 40), # x, y, width, height
(600, 200, 60, 100),
(300, 400, 120, 60),
(200, 500, 70, 70),
(500, 100, 50, 150)
]
for x, y, w, h in rectangles:
rect_data = {
'type': 'rectangle',
'position': [x, y],
'size': [w, h],
'color': (random.randint(100, 200), random.randint(100, 200), random.randint(100, 200))
}
self.test_objects.append(rect_data)
# Add as shadow caster
caster = self.shadow_system.add_rectangle_caster(x, y, w, h)
self.shadow_casters.append(caster)
# Create some circular objects
circles = [
(400, 200, 30),
(150, 300, 40),
(550, 350, 25),
(250, 150, 35)
]
for x, y, radius in circles:
circle_data = {
'type': 'circle',
'position': [x, y],
'radius': radius,
'color': (random.randint(100, 200), random.randint(100, 200), random.randint(100, 200))
}
self.test_objects.append(circle_data)
# Add as shadow caster
caster = self.shadow_system.add_circle_caster(x, y, radius)
self.shadow_casters.append(caster)
# Create lights
# Main light (sun)
self.sun_light = self.shadow_system.add_light(400, -100, 600, (255, 255, 200), 0.8)
self.lights.append(self.sun_light)
# Player light
self.player_light = self.shadow_system.add_light(
self.player['position'][0],
self.player['position'][1],
150,
(255, 220, 180),
0.6
)
self.lights.append(self.player_light)
# Some static lights
static_lights = [
(200, 200, 120, (255, 200, 150), 0.7),
(600, 400, 180, (200, 220, 255), 0.5),
(100, 450, 100, (200, 255, 200), 0.6)
]
for x, y, radius, color, intensity in static_lights:
light = self.shadow_system.add_light(x, y, radius, color, intensity)
self.lights.append(light)
def setup_ui(self):
"""Setup user interface"""
screen_width, screen_height = self.engine.width, self.engine.height
# Control panel
control_bg = UiFrame(10, 10, 300, 160)
self.add_ui_element(control_bg)
# Title
title = TextLabel(20, 15, "Shadow System Test", 20, (255, 255, 255))
self.add_ui_element(title)
# Instructions
instructions = [
"WASD: Move camera",
"Mouse Wheel: Zoom",
"Click: Add light at mouse",
"R: Reset lights",
"C: Clear shadow casters",
"T: Toggle shadows"
]
for i, instruction in enumerate(instructions):
label = TextLabel(20, 45 + i * 18, instruction, 14, (200, 200, 200))
self.add_ui_element(label)
# Debug info display
self.debug_label = TextLabel(10, screen_height - 100, "Debug Info", 14, (255, 255, 200))
self.add_ui_element(self.debug_label)
def handle_key_press(self, event):
"""Handle key presses"""
if event.key == pygame.K_r:
# Reset lights
self.shadow_system.clear_lights()
self.lights.clear()
# Recreate player light
self.player_light = self.shadow_system.add_light(
self.player['position'][0],
self.player['position'][1],
150,
(255, 220, 180),
0.6
)
self.lights.append(self.player_light)
print("Lights reset")
elif event.key == pygame.K_c:
# Clear shadow casters
self.shadow_system.clear_shadow_casters()
self.shadow_casters.clear()
print("Shadow casters cleared")
elif event.key == pygame.K_t:
# Toggle shadows
self.engine.current_scene.shadows_enabled = not self.engine.current_scene.shadows_enabled
print(f"Shadows {'enabled' if self.engine.current_scene.shadows_enabled else 'disabled'}")
def handle_mouse_click(self, pos):
"""Handle mouse clicks to add lights"""
world_pos = self.camera.screen_to_world(pos)
# Add a new light at mouse position
new_light = self.shadow_system.add_light(
world_pos.x,
world_pos.y,
random.randint(80, 200),
(random.randint(150, 255), random.randint(150, 255), random.randint(150, 255)),
random.uniform(0.4, 0.8)
)
self.lights.append(new_light)
print(f"Added light at ({world_pos.x:.1f}, {world_pos.y:.1f})")
def update_player_movement(self, dt):
"""Update player/camera 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
if self.engine.mouse_wheel != 0:
zoom_speed = 0.1
new_zoom = self.camera.zoom + (self.engine.mouse_wheel * zoom_speed)
new_zoom = max(0.3, min(3.0, 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
# Update camera to follow player
self.camera.position.x = self.player['position'][0]
self.camera.position.y = self.player['position'][1]
self.camera.target_position.x = self.player['position'][0]
self.camera.target_position.y = self.player['position'][1]
# Update player light
self.player_light.position.x = self.player['position'][0]
self.player_light.position.y = self.player['position'][1]
# Animate sun light
self.sun_light.position.x = 400 + math.cos(pygame.time.get_ticks() * 0.0005) * 300
self.sun_light.position.y = -100 + math.sin(pygame.time.get_ticks() * 0.0005) * 200
def update(self, dt):
"""Update game logic"""
# Update player movement
self.update_player_movement(dt)
# Update camera
self.camera.update(dt)
# Update mouse world position for debug
mouse_screen_pos = self.engine.mouse_pos
mouse_world_pos = self.camera.screen_to_world(mouse_screen_pos)
self.debug_info['mouse_world_pos'] = (mouse_world_pos.x, mouse_world_pos.y)
self.debug_info['camera_pos'] = (self.camera.position.x, self.camera.position.y)
# Get shadow system stats
self.debug_info['shadow_stats'] = self.shadow_system.get_stats()
# Handle mouse clicks
if self.engine.input_state.mouse_buttons_pressed.left:
self.handle_mouse_click(mouse_screen_pos)
# Update debug display
self.update_debug_info()
def update_debug_info(self):
"""Update debug information display"""
stats = self.debug_info['shadow_stats']
mouse_pos = self.debug_info['mouse_world_pos']
camera_pos = self.debug_info['camera_pos']
debug_text = f"Mouse World: ({mouse_pos[0]:.1f}, {mouse_pos[1]:.1f}), Camera: ({camera_pos[0]:.1f}, {camera_pos[1]:.1f}), Zoom: {self.camera.zoom:.2f}, Lights: {stats.get('total_lights', 0)} (visible: {stats.get('visible_lights', 0)}), Casters: {stats.get('total_casters', 0)} (visible: {stats.get('visible_casters', 0)}), Render Time: {stats.get('render_time_ms', 0):.1f}ms, FPS: {stats.get('current_fps', 0):.1f}"
self.debug_label.set_text(debug_text)
def render(self, renderer):
"""Render the scene"""
# Clear screen
renderer.get_surface().fill((30, 30, 50))
# Draw grid for reference
self.draw_grid(renderer)
# Draw test objects
self.draw_objects(renderer)
# Draw player
self.draw_player(renderer)
# Draw lights (for visualization)
self.draw_lights_debug(renderer)
# Render shadows
if hasattr(self, 'shadows_enabled') and self.shadows_enabled:
try:
shadow_surface = self.shadow_system.render(self.camera.position, renderer)
if shadow_surface and isinstance(shadow_surface, pygame.Surface):
renderer.blit(shadow_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
except Exception as e:
print(f"Shadow rendering error: {e}")
import traceback
traceback.print_exc()
def draw_grid(self, renderer):
"""Draw a grid for spatial reference"""
grid_size = 100
grid_color = (60, 60, 80)
# Get visible area
visible_rect = self.camera.get_visible_rect()
start_x = int(visible_rect.left // grid_size) * grid_size
start_y = int(visible_rect.top // grid_size) * grid_size
end_x = int(visible_rect.right) + grid_size
end_y = int(visible_rect.bottom) + grid_size
for x in range(start_x, end_x, grid_size):
screen_pos = self.camera.world_to_screen((x, start_y))
screen_end = self.camera.world_to_screen((x, end_y))
renderer.draw_line(
screen_pos.x, screen_pos.y,
screen_end.x, screen_end.y,
grid_color, 1
)
for y in range(start_y, end_y, grid_size):
screen_pos = self.camera.world_to_screen((start_x, y))
screen_end = self.camera.world_to_screen((end_x, y))
renderer.draw_line(
screen_pos.x, screen_pos.y,
screen_end.x, screen_end.y,
grid_color, 1
)
def draw_objects(self, renderer):
"""Draw all test objects"""
for obj in self.test_objects:
screen_pos = self.camera.world_to_screen(obj['position'])
if obj['type'] == 'rectangle':
width, height = obj['size']
screen_width = width * self.camera.zoom
screen_height = height * self.camera.zoom
renderer.draw_rect(
screen_pos.x - screen_width / 2,
screen_pos.y - screen_height / 2,
screen_width,
screen_height,
obj['color'],
fill=True
)
elif obj['type'] == 'circle':
radius = obj['radius'] * self.camera.zoom
renderer.draw_circle(
screen_pos.x,
screen_pos.y,
radius,
obj['color'],
fill=True
)
def draw_player(self, renderer):
"""Draw the player"""
screen_pos = self.camera.world_to_screen(self.player['position'])
size = self.player['size'] * self.camera.zoom
renderer.draw_circle(
screen_pos.x,
screen_pos.y,
size,
(255, 100, 100),
fill=True
)
# Draw direction indicator
if self.player['velocity'][0] != 0 or self.player['velocity'][1] != 0:
end_x = screen_pos.x + self.player['velocity'][0] * size * 1.5
end_y = screen_pos.y + self.player['velocity'][1] * size * 1.5
renderer.draw_line(
screen_pos.x, screen_pos.y,
end_x, end_y,
(255, 255, 255), 3
)
def draw_lights_debug(self, renderer):
"""Draw light positions for visualization"""
for light in self.lights:
screen_pos = self.camera.world_to_screen(light.position)
radius = light.radius * self.camera.zoom
# Draw light center
renderer.draw_circle(
screen_pos.x,
screen_pos.y,
5,
light.color,
fill=True
)
# Draw light radius (outline)
renderer.draw_circle(
screen_pos.x,
screen_pos.y,
radius,
(*light.color, 128), # Semi-transparent
fill=False,
border_width=2
)
def main():
"""Main function to run the shadow test demo"""
engine = LunaEngine("Shadow System Test - LunaEngine", 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)
# Add and set the test scene
engine.add_scene("shadow_test", ShadowTestScene)
engine.set_scene("shadow_test")
# Enable shadows by default
engine.current_scene.shadows_enabled = True
print("=== Shadow System Test Demo ===")
print("Controls:")
print("WASD - Move camera/player")
print("Mouse Wheel - Zoom in/out")
print("Click - Add light at mouse position")
print("R - Reset lights")
print("C - Clear shadow casters")
print("T - Toggle shadows on/off")
print("\nDebug information shown at bottom of screen")
engine.run()
if __name__ == "__main__":
main()