# core/input_handler.py """Handles input events and camera controls - no state management.""" import pygame from config.constants import * class InputHandler: """Pure input handler - processes input without managing simulation state.""" def __init__(self, camera, world, sim_view_rect): self.camera = camera self.world = world # Selection state (input-specific, not simulation state) self.selecting = False self.select_start = None self.select_end = None self.selected_objects = [] # UI display flags (input-controlled visual settings) self.show_grid = True self.show_interaction_radius = False self.show_legend = False # Simulation state references (synchronized from external source) self.tps = DEFAULT_TPS self.default_tps = DEFAULT_TPS self.is_paused = False self.sprint_mode = False self.is_stepping = False self.speed_multiplier = 1.0 # sim-view rect for mouse position calculations self.sim_view_rect = sim_view_rect # HUD reference for viewport/inspector region checking self.hud = None # Action callbacks for simulation control self.action_callbacks = {} def update_sim_view_rect(self, sim_view_rect): """Update the sim_view rectangle.""" self.sim_view_rect = sim_view_rect def set_hud(self, hud): """Set HUD reference for viewport/inspector region checking.""" self.hud = hud def handle_events(self, events, ui_manager): """Process all pygame events and return game state.""" running = True for event in events: ui_manager.process_events(event) if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: running = self._handle_keydown(event, running) elif event.type == pygame.KEYUP: self._handle_keyup(event) elif event.type == pygame.MOUSEWHEEL: self._handle_mouse_wheel(event) elif event.type == pygame.MOUSEBUTTONDOWN: self._handle_mouse_down(event) elif event.type == pygame.MOUSEBUTTONUP: self._handle_mouse_up(event) elif event.type == pygame.MOUSEMOTION: self._handle_mouse_motion(event) return running def _handle_keydown(self, event, running): """Handle keydown events.""" if event.key == pygame.K_ESCAPE: if len(self.selected_objects) == 0: running = False else: self.selecting = False self.selected_objects = [] elif event.key == pygame.K_g: self.show_grid = not self.show_grid elif event.key == pygame.K_UP: if self.camera.speed < MAX_CAMERA_SPEED: self.camera.speed += CAMERA_SPEED_INCREMENT elif event.key == pygame.K_DOWN: if self.camera.speed > MIN_CAMERA_SPEED: self.camera.speed -= CAMERA_SPEED_INCREMENT elif event.key == pygame.K_i: self.show_interaction_radius = not self.show_interaction_radius elif event.key == pygame.K_l: self.show_legend = not self.show_legend elif event.key == pygame.K_SPACE: self.toggle_pause() elif event.key == pygame.K_LSHIFT: # Left Shift for temporary speed boost (turbo mode) self.set_speed_multiplier(2.0) elif event.key == pygame.K_r: self.camera.reset_position() elif event.key == pygame.K_RSHIFT: self.toggle_sprint_mode() # Right Shift toggles sprint mode elif event.key == pygame.K_s: self.step_forward() # Step forward return running def _handle_keyup(self, event): """Handle keyup events.""" if event.key == pygame.K_LSHIFT: # Reset speed multiplier when Left Shift is released self.set_speed_multiplier(1.0) # if event.key == pygame.K_RSHIFT: # self.sprint_mode = False # Exit sprint mode def _handle_mouse_wheel(self, event): """Handle mouse wheel events.""" mouse_x, mouse_y = pygame.mouse.get_pos() # Check if mouse is in viewport and HUD is available if self.hud: viewport_rect = self.hud.get_viewport_rect() inspector_rect = self.hud.inspector_panel.rect if self.hud.inspector_panel else None if viewport_rect.collidepoint(mouse_x, mouse_y): # Zoom in viewport self.camera.handle_zoom(event.y) elif inspector_rect and inspector_rect.collidepoint(mouse_x, mouse_y) and self.hud.tree_widget: # Scroll tree widget in inspector if not viewport_rect.collidepoint(mouse_x, mouse_y): # Convert to local coordinates if needed if not hasattr(event, 'pos'): event.pos = (mouse_x - inspector_rect.x, mouse_y - inspector_rect.y) else: local_x = mouse_x - inspector_rect.x local_y = mouse_y - inspector_rect.y event.pos = (local_x, local_y) self.hud.tree_widget.handle_event(event) else: # Fallback: always zoom if no HUD reference self.camera.handle_zoom(event.y) def _handle_mouse_down(self, event): """Handle mouse button down events.""" mouse_x, mouse_y = event.pos in_viewport = self.hud and self.hud.get_viewport_rect().collidepoint(mouse_x, mouse_y) if event.button == 2: # Middle mouse button # Only start panning if mouse is in viewport if in_viewport: self.camera.start_panning(event.pos) elif event.button == 1: # Left mouse button # Only start selection if mouse is in viewport if in_viewport: self.selecting = True self.select_start = event.pos self.select_end = event.pos def _handle_mouse_up(self, event): """Handle mouse button up events.""" if event.button == 2: self.camera.stop_panning() elif event.button == 1 and self.selecting: self._handle_selection() def _handle_mouse_motion(self, event): """Handle mouse motion events.""" # Only pan if camera was started in viewport (camera will handle this internally) self.camera.pan(event.pos) # Only update selection if we're actively selecting in viewport if self.selecting: self.select_end = event.pos def _handle_selection(self): """Process object selection logic.""" self.selecting = False # Map screen to sim_view coordinates sx1 = self.select_start[0] - self.sim_view_rect.left sy1 = self.select_start[1] - self.sim_view_rect.top sx2 = self.select_end[0] - self.sim_view_rect.left sy2 = self.select_end[1] - self.sim_view_rect.top # Convert sim_view to world coordinates x1, y1 = self.camera.get_real_coordinates(sx1, sy1) x2, y2 = self.camera.get_real_coordinates(sx2, sy2) # Check if selection is a click or drag if (abs(sx1 - sx2) < SELECTION_THRESHOLD and abs(sy1 - sy2) < SELECTION_THRESHOLD): self._handle_click_selection() else: self._handle_drag_selection(x1, y1, x2, y2) def _handle_click_selection(self): """Handle single click selection.""" sx, sy = self.select_start[0] - self.sim_view_rect.left, self.select_start[1] - self.sim_view_rect.top mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(sx, sy) obj = self.world.query_closest_object(mouse_world_x, mouse_world_y) self.selected_objects = [] if obj: obj_x, obj_y = obj.position.get_position() dx = obj_x - mouse_world_x dy = obj_y - mouse_world_y dist = (dx ** 2 + dy ** 2) ** 0.5 if dist <= obj.max_visual_width / 2: self.selected_objects = [obj] print(f"Clicked: selected {len(self.selected_objects)} object(s)") def _handle_drag_selection(self, x1, y1, x2, y2): """Handle drag selection.""" min_x, max_x = min(x1, x2), max(x1, x2) min_y, max_y = min(y1, y2), max(y1, y2) self.selected_objects = self.world.query_objects_in_range(min_x, min_y, max_x, max_y) print(f"Selected {len(self.selected_objects)} objects in range: {min_x}, {min_y} to {max_x}, {max_y}") def update_camera(self, keys, deltatime): """Update camera based on currently pressed keys.""" self.camera.update(keys, deltatime) def update_selected_objects(self): """Ensure selected objects are still valid.""" self.selected_objects = [ obj for obj in self.selected_objects if obj in self.world.get_objects() ] def get_selection_rect(self): """Get current selection rectangle for rendering.""" if self.selecting and self.select_start and self.select_end: left = min(self.select_start[0], self.select_end[0]) top = min(self.select_start[1], self.select_end[1]) width = abs(self.select_end[0] - self.select_start[0]) height = abs(self.select_end[1] - self.select_start[1]) return left, top, width, height return None def set_action_callback(self, action_name: str, callback): """Set callback for simulation control actions.""" self.action_callbacks[action_name] = callback def toggle_pause(self): """Toggle pause state via callback.""" if 'toggle_pause' in self.action_callbacks: self.action_callbacks['toggle_pause']() def step_forward(self): """Execute single simulation step via callback.""" self.is_stepping = True if 'step_forward' in self.action_callbacks: self.action_callbacks['step_forward']() def set_speed_multiplier(self, multiplier): """Set speed multiplier for simulation via callback.""" if 'set_speed' in self.action_callbacks: self.action_callbacks['set_speed'](multiplier) def set_custom_tps(self, tps): """Set custom TPS value via callback.""" if 'set_custom_tps' in self.action_callbacks: self.action_callbacks['set_custom_tps'](tps) def toggle_sprint_mode(self): """Toggle sprint mode via callback.""" if 'toggle_sprint' in self.action_callbacks: self.action_callbacks['toggle_sprint']() def get_current_speed_display(self): """Get current speed display string.""" if self.sprint_mode: return "Sprint" elif self.is_paused: return "Paused" elif self.speed_multiplier == 1.0: return "1x" elif self.speed_multiplier in [0.5, 2.0, 4.0, 8.0]: return f"{self.speed_multiplier}x" else: return f"{self.speed_multiplier:.1f}x"