Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 14m56s
279 lines
11 KiB
Python
279 lines
11 KiB
Python
# 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 - 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" |