refactored a lot of stuff into different files and generally fixed force application. #1
							
								
								
									
										72
									
								
								config/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								config/constants.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | # config/constants.py | ||||||
|  | """Configuration constants for the simulation.""" | ||||||
|  | 
 | ||||||
|  | # Screen settings | ||||||
|  | SCREEN_WIDTH = 1920 // 2 | ||||||
|  | SCREEN_HEIGHT = 1080 // 2 | ||||||
|  | 
 | ||||||
|  | # Colors | ||||||
|  | BLACK = (0, 0, 0) | ||||||
|  | DARK_GRAY = (64, 64, 64) | ||||||
|  | GRAY = (128, 128, 128) | ||||||
|  | WHITE = (255, 255, 255) | ||||||
|  | RED = (255, 0, 0) | ||||||
|  | BLUE = (0, 0, 255) | ||||||
|  | GREEN = (0, 255, 0) | ||||||
|  | LIGHT_BLUE = (52, 134, 235) | ||||||
|  | SELECTION_BLUE = (0, 128, 255) | ||||||
|  | SELECTION_GRAY = (128, 128, 128, 80) | ||||||
|  | SELECTION_BORDER = (80, 80, 90) | ||||||
|  | 
 | ||||||
|  | # Grid settings | ||||||
|  | GRID_WIDTH = 30 | ||||||
|  | GRID_HEIGHT = 25 | ||||||
|  | CELL_SIZE = 20 | ||||||
|  | RENDER_BUFFER = 50 | ||||||
|  | 
 | ||||||
|  | # Performance settings | ||||||
|  | DEFAULT_TPS = 20 | ||||||
|  | MAX_FPS = 180 | ||||||
|  | TURBO_MULTIPLIER = 4 | ||||||
|  | 
 | ||||||
|  | # Camera settings | ||||||
|  | DEFAULT_CAMERA_SPEED = 700 | ||||||
|  | CAMERA_SPEED_INCREMENT = 350 | ||||||
|  | MIN_CAMERA_SPEED = 350 | ||||||
|  | MAX_CAMERA_SPEED = 2100 | ||||||
|  | 
 | ||||||
|  | # UI settings | ||||||
|  | FONT_SIZE = 16 | ||||||
|  | LEGEND_FONT_SIZE = 14 | ||||||
|  | HUD_MARGIN = 10 | ||||||
|  | LINE_HEIGHT = 20 | ||||||
|  | SELECTION_THRESHOLD = 3  # pixels | ||||||
|  | 
 | ||||||
|  | # Simulation settings | ||||||
|  | FOOD_SPAWNING = True | ||||||
|  | RANDOM_SEED = 0 | ||||||
|  | 
 | ||||||
|  | # Vector visualization settings | ||||||
|  | ACCELERATION_SCALE = 1000 | ||||||
|  | VELOCITY_SCALE = 50 | ||||||
|  | ANGULAR_ACCELERATION_SCALE = 50 | ||||||
|  | ARROW_TIP_SIZE = 5 | ||||||
|  | ANGULAR_TIP_SIZE = 2.5 | ||||||
|  | DIRECTION_TIP_SIZE = 3 | ||||||
|  | 
 | ||||||
|  | KEYMAP_LEGEND = [ | ||||||
|  |     ("WASD", "Move camera"), | ||||||
|  |     ("Mouse wheel", "Zoom in/out"), | ||||||
|  |     ("Middle mouse", "Pan camera"), | ||||||
|  |     ("R", "Reset camera"), | ||||||
|  |     ("G", "Toggle grid"), | ||||||
|  |     ("I", "Toggle interaction radius"), | ||||||
|  |     ("ESC", "Deselect/Exit"), | ||||||
|  |     ("Left click", "Select object(s)"), | ||||||
|  |     ("Drag select", "Select multiple objects"), | ||||||
|  |     ("Click on object", "Select closest object in range"), | ||||||
|  |     ("Up/Down", "Increase/Decrease camera speed"), | ||||||
|  |     ("Shift", "Double TPS (for testing)"), | ||||||
|  |     ("L", "Toggle this legend"), | ||||||
|  |     ("Space", "Pause/Resume simulation"), | ||||||
|  | ] | ||||||
							
								
								
									
										163
									
								
								core/input_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								core/input_handler.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,163 @@ | |||||||
|  | # core/input_handler.py | ||||||
|  | """Handles all input events and camera controls.""" | ||||||
|  | 
 | ||||||
|  | import pygame | ||||||
|  | from config.constants import * | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InputHandler: | ||||||
|  |     def __init__(self, camera, world): | ||||||
|  |         self.camera = camera | ||||||
|  |         self.world = world | ||||||
|  | 
 | ||||||
|  |         # Selection state | ||||||
|  |         self.selecting = False | ||||||
|  |         self.select_start = None | ||||||
|  |         self.select_end = None | ||||||
|  |         self.selected_objects = [] | ||||||
|  | 
 | ||||||
|  |         # UI state flags | ||||||
|  |         self.show_grid = True | ||||||
|  |         self.show_interaction_radius = False | ||||||
|  |         self.show_legend = False | ||||||
|  |         self.is_paused = False | ||||||
|  | 
 | ||||||
|  |         # Speed control | ||||||
|  |         self.tps = DEFAULT_TPS | ||||||
|  |         self.default_tps = DEFAULT_TPS | ||||||
|  | 
 | ||||||
|  |     def handle_events(self, events): | ||||||
|  |         """Process all pygame events and return game state.""" | ||||||
|  |         running = True | ||||||
|  | 
 | ||||||
|  |         for event in events: | ||||||
|  |             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.camera.handle_zoom(event.y) | ||||||
|  |             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.is_paused = not self.is_paused | ||||||
|  |         elif event.key == pygame.K_LSHIFT: | ||||||
|  |             self.tps = self.default_tps * TURBO_MULTIPLIER | ||||||
|  |         elif event.key == pygame.K_r: | ||||||
|  |             self.camera.reset_position() | ||||||
|  | 
 | ||||||
|  |         return running | ||||||
|  | 
 | ||||||
|  |     def _handle_keyup(self, event): | ||||||
|  |         """Handle keyup events.""" | ||||||
|  |         if event.key == pygame.K_LSHIFT: | ||||||
|  |             self.tps = self.default_tps | ||||||
|  | 
 | ||||||
|  |     def _handle_mouse_down(self, event): | ||||||
|  |         """Handle mouse button down events.""" | ||||||
|  |         if event.button == 2:  # Middle mouse button | ||||||
|  |             self.camera.start_panning(event.pos) | ||||||
|  |         elif event.button == 1:  # Left mouse button | ||||||
|  |             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.""" | ||||||
|  |         self.camera.pan(event.pos) | ||||||
|  |         if self.selecting: | ||||||
|  |             self.select_end = event.pos | ||||||
|  | 
 | ||||||
|  |     def _handle_selection(self): | ||||||
|  |         """Process object selection logic.""" | ||||||
|  |         self.selecting = False | ||||||
|  | 
 | ||||||
|  |         # Convert screen to world coordinates | ||||||
|  |         x1, y1 = self.camera.get_real_coordinates(*self.select_start) | ||||||
|  |         x2, y2 = self.camera.get_real_coordinates(*self.select_end) | ||||||
|  | 
 | ||||||
|  |         # Check if selection is a click or drag | ||||||
|  |         if (abs(self.select_start[0] - self.select_end[0]) < SELECTION_THRESHOLD and | ||||||
|  |                 abs(self.select_start[1] - self.select_end[1]) < SELECTION_THRESHOLD): | ||||||
|  |             self._handle_click_selection() | ||||||
|  |         else: | ||||||
|  |             self._handle_drag_selection(x1, y1, x2, y2) | ||||||
|  | 
 | ||||||
|  |     def _handle_click_selection(self): | ||||||
|  |         """Handle single click selection.""" | ||||||
|  |         mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(*self.select_start) | ||||||
|  |         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 | ||||||
							
								
								
									
										241
									
								
								core/renderer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								core/renderer.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,241 @@ | |||||||
|  | # core/renderer.py | ||||||
|  | """Handles all rendering operations.""" | ||||||
|  | 
 | ||||||
|  | import pygame | ||||||
|  | import math | ||||||
|  | from config.constants import * | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Renderer: | ||||||
|  |     def __init__(self, screen): | ||||||
|  |         self.screen = screen | ||||||
|  | 
 | ||||||
|  |     def clear_screen(self): | ||||||
|  |         """Clear the screen with a black background.""" | ||||||
|  |         self.screen.fill(BLACK) | ||||||
|  | 
 | ||||||
|  |     def draw_grid(self, camera, showing_grid=True): | ||||||
|  |         """Draw the reference grid.""" | ||||||
|  |         if not showing_grid: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # Calculate effective cell size with zoom | ||||||
|  |         effective_cell_size = CELL_SIZE * camera.zoom | ||||||
|  | 
 | ||||||
|  |         # Calculate grid boundaries in world coordinates (centered at 0,0) | ||||||
|  |         grid_world_width = GRID_WIDTH * effective_cell_size | ||||||
|  |         grid_world_height = GRID_HEIGHT * effective_cell_size | ||||||
|  | 
 | ||||||
|  |         # Calculate grid position relative to camera (with grid centered at 0,0) | ||||||
|  |         grid_center_x = SCREEN_WIDTH // 2 - camera.x * camera.zoom | ||||||
|  |         grid_center_y = SCREEN_HEIGHT // 2 - camera.y * camera.zoom | ||||||
|  | 
 | ||||||
|  |         grid_left = grid_center_x - grid_world_width // 2 | ||||||
|  |         grid_top = grid_center_y - grid_world_height // 2 | ||||||
|  |         grid_right = grid_left + grid_world_width | ||||||
|  |         grid_bottom = grid_top + grid_world_height | ||||||
|  | 
 | ||||||
|  |         # Check if grid is visible on screen | ||||||
|  |         if (grid_right < 0 or grid_left > SCREEN_WIDTH or | ||||||
|  |                 grid_bottom < 0 or grid_top > SCREEN_HEIGHT): | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # Fill the grid area with dark gray background | ||||||
|  |         grid_rect = pygame.Rect( | ||||||
|  |             max(0, grid_left), | ||||||
|  |             max(0, grid_top), | ||||||
|  |             min(SCREEN_WIDTH, grid_right) - max(0, grid_left), | ||||||
|  |             min(SCREEN_HEIGHT, grid_bottom) - max(0, grid_top), | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         if grid_rect.width > 0 and grid_rect.height > 0: | ||||||
|  |             pygame.draw.rect(self.screen, DARK_GRAY, grid_rect) | ||||||
|  | 
 | ||||||
|  |         # Draw grid lines only if zoom is high enough | ||||||
|  |         if effective_cell_size > 4: | ||||||
|  |             self._draw_grid_lines(grid_left, grid_top, grid_right, grid_bottom, effective_cell_size) | ||||||
|  | 
 | ||||||
|  |     def _draw_grid_lines(self, grid_left, grid_top, grid_right, grid_bottom, effective_cell_size): | ||||||
|  |         """Draw the grid lines.""" | ||||||
|  |         vertical_lines = [] | ||||||
|  |         horizontal_lines = [] | ||||||
|  | 
 | ||||||
|  |         for i in range(max(GRID_WIDTH, GRID_HEIGHT) + 1): | ||||||
|  |             # Vertical lines | ||||||
|  |             if i <= GRID_WIDTH: | ||||||
|  |                 line_x = grid_left + i * effective_cell_size | ||||||
|  |                 if 0 <= line_x <= SCREEN_WIDTH: | ||||||
|  |                     start_y = max(0, grid_top) | ||||||
|  |                     end_y = min(SCREEN_HEIGHT, grid_bottom) | ||||||
|  |                     if start_y < end_y: | ||||||
|  |                         vertical_lines.append(((line_x, start_y), (line_x, end_y))) | ||||||
|  | 
 | ||||||
|  |             # Horizontal lines | ||||||
|  |             if i <= GRID_HEIGHT: | ||||||
|  |                 line_y = grid_top + i * effective_cell_size | ||||||
|  |                 if 0 <= line_y <= SCREEN_HEIGHT: | ||||||
|  |                     start_x = max(0, grid_left) | ||||||
|  |                     end_x = min(SCREEN_WIDTH, grid_right) | ||||||
|  |                     if start_x < end_x: | ||||||
|  |                         horizontal_lines.append(((start_x, line_y), (end_x, line_y))) | ||||||
|  | 
 | ||||||
|  |         # Draw all lines | ||||||
|  |         for start, end in vertical_lines: | ||||||
|  |             pygame.draw.line(self.screen, GRAY, start, end) | ||||||
|  |         for start, end in horizontal_lines: | ||||||
|  |             pygame.draw.line(self.screen, GRAY, start, end) | ||||||
|  | 
 | ||||||
|  |     def render_world(self, world, camera): | ||||||
|  |         """Render all world objects.""" | ||||||
|  |         world.render_all(camera, self.screen) | ||||||
|  | 
 | ||||||
|  |     def render_interaction_radius(self, world, camera, selected_objects, show_radius=False): | ||||||
|  |         """Render interaction radius and debug vectors for objects.""" | ||||||
|  |         if not show_radius: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         for obj in world.get_objects(): | ||||||
|  |             obj_x, obj_y = obj.position.get_position() | ||||||
|  |             radius = obj.interaction_radius | ||||||
|  | 
 | ||||||
|  |             if radius > 0 and camera.is_in_view(obj_x, obj_y, margin=radius): | ||||||
|  |                 if selected_objects and obj not in selected_objects: | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |                 screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) | ||||||
|  |                 screen_radius = int(radius * camera.zoom) | ||||||
|  | 
 | ||||||
|  |                 if screen_radius > 0: | ||||||
|  |                     # Draw interaction radius circle | ||||||
|  |                     pygame.draw.circle(self.screen, RED, (screen_x, screen_y), screen_radius, 1) | ||||||
|  | 
 | ||||||
|  |                     # Draw direction arrow | ||||||
|  |                     self._draw_direction_arrow(obj, screen_x, screen_y, camera) | ||||||
|  | 
 | ||||||
|  |                     # Draw debug vectors | ||||||
|  |                     self._draw_debug_vectors(obj, screen_x, screen_y, camera) | ||||||
|  | 
 | ||||||
|  |     def _draw_direction_arrow(self, obj, screen_x, screen_y, camera): | ||||||
|  |         """Draw direction arrow for an object.""" | ||||||
|  |         rotation_angle = obj.rotation.get_rotation() | ||||||
|  |         arrow_length = obj.max_visual_width / 2 * camera.zoom | ||||||
|  | 
 | ||||||
|  |         end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) | ||||||
|  |         end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) | ||||||
|  | 
 | ||||||
|  |         # Draw arrow line | ||||||
|  |         pygame.draw.line(self.screen, WHITE, (screen_x, screen_y), (end_x, end_y), 2) | ||||||
|  | 
 | ||||||
|  |         # Draw arrowhead | ||||||
|  |         tip_size = DIRECTION_TIP_SIZE * camera.zoom | ||||||
|  |         left_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle + 150 + 180)) | ||||||
|  |         left_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle + 150 + 180)) | ||||||
|  |         right_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle - 150 + 180)) | ||||||
|  |         right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) | ||||||
|  | 
 | ||||||
|  |         pygame.draw.polygon( | ||||||
|  |             self.screen, WHITE, | ||||||
|  |             [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def _draw_debug_vectors(self, obj, screen_x, screen_y, camera): | ||||||
|  |         """Draw debug vectors (acceleration, velocity, angular acceleration).""" | ||||||
|  |         # Draw angular acceleration | ||||||
|  |         if hasattr(obj, 'angular_acceleration'): | ||||||
|  |             self._draw_angular_acceleration(obj, screen_x, screen_y, camera) | ||||||
|  | 
 | ||||||
|  |         # Draw acceleration vector | ||||||
|  |         if hasattr(obj, 'acceleration') and isinstance(obj.acceleration, tuple) and len(obj.acceleration) == 2: | ||||||
|  |             self._draw_acceleration_vector(obj, screen_x, screen_y, camera) | ||||||
|  | 
 | ||||||
|  |         # Draw velocity vector | ||||||
|  |         if hasattr(obj, 'velocity') and isinstance(obj.velocity, tuple) and len(obj.velocity) == 2: | ||||||
|  |             self._draw_velocity_vector(obj, screen_x, screen_y, camera) | ||||||
|  | 
 | ||||||
|  |     def _draw_angular_acceleration(self, obj, screen_x, screen_y, camera): | ||||||
|  |         """Draw angular acceleration vector.""" | ||||||
|  |         rotation_angle = obj.rotation.get_rotation() | ||||||
|  |         arrow_length = obj.max_visual_width / 2 * camera.zoom | ||||||
|  |         end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) | ||||||
|  |         end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) | ||||||
|  | 
 | ||||||
|  |         angular_acceleration = obj.angular_acceleration | ||||||
|  |         angular_accel_magnitude = abs(angular_acceleration) * ANGULAR_ACCELERATION_SCALE * camera.zoom | ||||||
|  |         angular_direction = rotation_angle + 90 if angular_acceleration >= 0 else rotation_angle - 90 | ||||||
|  | 
 | ||||||
|  |         angular_acc_end_x = end_x + angular_accel_magnitude * math.cos(math.radians(angular_direction)) | ||||||
|  |         angular_acc_end_y = end_y + angular_accel_magnitude * math.sin(math.radians(angular_direction)) | ||||||
|  | 
 | ||||||
|  |         pygame.draw.line(self.screen, LIGHT_BLUE, (end_x, end_y), (angular_acc_end_x, angular_acc_end_y), 2) | ||||||
|  | 
 | ||||||
|  |         # Draw arrowhead | ||||||
|  |         self._draw_arrowhead(angular_acc_end_x, angular_acc_end_y, angular_direction, | ||||||
|  |                              ANGULAR_TIP_SIZE * camera.zoom, LIGHT_BLUE) | ||||||
|  | 
 | ||||||
|  |     def _draw_acceleration_vector(self, obj, screen_x, screen_y, camera): | ||||||
|  |         """Draw acceleration vector.""" | ||||||
|  |         acc_x, acc_y = obj.acceleration | ||||||
|  |         acc_magnitude = math.sqrt(acc_x ** 2 + acc_y ** 2) | ||||||
|  | 
 | ||||||
|  |         if acc_magnitude > 0: | ||||||
|  |             acc_direction = math.degrees(math.atan2(acc_y, acc_x)) | ||||||
|  |             acc_vector_length = acc_magnitude * ACCELERATION_SCALE * camera.zoom | ||||||
|  |             acc_end_x = screen_x + acc_vector_length * math.cos(math.radians(acc_direction)) | ||||||
|  |             acc_end_y = screen_y + acc_vector_length * math.sin(math.radians(acc_direction)) | ||||||
|  | 
 | ||||||
|  |             pygame.draw.line(self.screen, RED, (screen_x, screen_y), (acc_end_x, acc_end_y), 2) | ||||||
|  |             self._draw_arrowhead(acc_end_x, acc_end_y, acc_direction, | ||||||
|  |                                  ARROW_TIP_SIZE * camera.zoom, RED) | ||||||
|  | 
 | ||||||
|  |     def _draw_velocity_vector(self, obj, screen_x, screen_y, camera): | ||||||
|  |         """Draw velocity vector.""" | ||||||
|  |         vel_x, vel_y = obj.velocity | ||||||
|  |         vel_magnitude = math.sqrt(vel_x ** 2 + vel_y ** 2) | ||||||
|  | 
 | ||||||
|  |         if vel_magnitude > 0: | ||||||
|  |             vel_direction = math.degrees(math.atan2(vel_y, vel_x)) | ||||||
|  |             vel_vector_length = vel_magnitude * VELOCITY_SCALE * camera.zoom | ||||||
|  |             vel_end_x = screen_x + vel_vector_length * math.cos(math.radians(vel_direction)) | ||||||
|  |             vel_end_y = screen_y + vel_vector_length * math.sin(math.radians(vel_direction)) | ||||||
|  | 
 | ||||||
|  |             pygame.draw.line(self.screen, BLUE, (screen_x, screen_y), (vel_end_x, vel_end_y), 2) | ||||||
|  |             self._draw_arrowhead(vel_end_x, vel_end_y, vel_direction, | ||||||
|  |                                  ARROW_TIP_SIZE * camera.zoom, BLUE) | ||||||
|  | 
 | ||||||
|  |     def _draw_arrowhead(self, end_x, end_y, direction, tip_size, color): | ||||||
|  |         """Draw an arrowhead at the specified position.""" | ||||||
|  |         left_tip_x = end_x - tip_size * math.cos(math.radians(direction + 150 + 180)) | ||||||
|  |         left_tip_y = end_y - tip_size * math.sin(math.radians(direction + 150 + 180)) | ||||||
|  |         right_tip_x = end_x - tip_size * math.cos(math.radians(direction - 150 + 180)) | ||||||
|  |         right_tip_y = end_y - tip_size * math.sin(math.radians(direction - 150 + 180)) | ||||||
|  | 
 | ||||||
|  |         pygame.draw.polygon( | ||||||
|  |             self.screen, color, | ||||||
|  |             [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def render_selection_rectangle(self, selection_rect): | ||||||
|  |         """Render the selection rectangle.""" | ||||||
|  |         if not selection_rect: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         left, top, width, height = selection_rect | ||||||
|  | 
 | ||||||
|  |         # Draw semi-transparent fill | ||||||
|  |         s = pygame.Surface((width, height), pygame.SRCALPHA) | ||||||
|  |         s.fill(SELECTION_GRAY) | ||||||
|  |         self.screen.blit(s, (left, top)) | ||||||
|  | 
 | ||||||
|  |         # Draw border | ||||||
|  |         pygame.draw.rect(self.screen, SELECTION_BORDER, | ||||||
|  |                          pygame.Rect(left, top, width, height), 1) | ||||||
|  | 
 | ||||||
|  |     def render_selected_objects_outline(self, selected_objects, camera): | ||||||
|  |         """Render blue outline for selected objects.""" | ||||||
|  |         for obj in selected_objects: | ||||||
|  |             obj_x, obj_y = obj.position.get_position() | ||||||
|  |             width = obj.max_visual_width if hasattr(obj, "max_visual_width") else 10 | ||||||
|  |             screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) | ||||||
|  |             size = camera.get_relative_size(width) | ||||||
|  |             rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) | ||||||
|  |             pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1) | ||||||
							
								
								
									
										546
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										546
									
								
								main.py
									
									
									
									
									
								
							| @ -6,116 +6,26 @@ import sys | |||||||
| import random | import random | ||||||
| 
 | 
 | ||||||
| from world.world import World, Position, Rotation | from world.world import World, Position, Rotation | ||||||
| from world.objects import DebugRenderObject, FoodObject, TestVelocityObject, DefaultCell | from world.objects import FoodObject, TestVelocityObject, DefaultCell | ||||||
| from world.simulation_interface import Camera | from world.simulation_interface import Camera | ||||||
| 
 | 
 | ||||||
|  | from config.constants import * | ||||||
|  | 
 | ||||||
|  | from core.input_handler import InputHandler | ||||||
|  | from core.renderer import Renderer | ||||||
|  | 
 | ||||||
|  | from ui.hud import HUD | ||||||
|  | 
 | ||||||
| # Initialize Pygame | # Initialize Pygame | ||||||
| pygame.init() | pygame.init() | ||||||
| 
 | 
 | ||||||
| # Constants |  | ||||||
| SCREEN_WIDTH = 1920 / 2 |  | ||||||
| SCREEN_HEIGHT = 1080 / 2 |  | ||||||
| BLACK = (0, 0, 0) |  | ||||||
| DARK_GRAY = (64, 64, 64) |  | ||||||
| GRAY = (128, 128, 128) |  | ||||||
| WHITE = (255, 255, 255) |  | ||||||
| RENDER_BUFFER = 50 |  | ||||||
| SPEED = 700 # Pixels per second |  | ||||||
| 
 |  | ||||||
| # Grid settings |  | ||||||
| GRID_WIDTH = 30  # Number of cells horizontally |  | ||||||
| GRID_HEIGHT = 25  # Number of cells vertically |  | ||||||
| CELL_SIZE = 20  # Size of each cell in pixels |  | ||||||
| 
 |  | ||||||
| DEFAULT_TPS = 20  # Number of ticks per second for the simulation |  | ||||||
| FOOD_SPAWNING = True |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def draw_grid(screen, camera, showing_grid=True): |  | ||||||
|     # Fill the screen with black |  | ||||||
|     screen.fill(BLACK) |  | ||||||
| 
 |  | ||||||
|     # Calculate effective cell size with zoom |  | ||||||
|     effective_cell_size = CELL_SIZE * camera.zoom |  | ||||||
| 
 |  | ||||||
|     # Calculate grid boundaries in world coordinates (centered at 0,0) |  | ||||||
|     grid_world_width = GRID_WIDTH * effective_cell_size |  | ||||||
|     grid_world_height = GRID_HEIGHT * effective_cell_size |  | ||||||
| 
 |  | ||||||
|     # Calculate grid position relative to camera (with grid centered at 0,0) |  | ||||||
|     grid_center_x = SCREEN_WIDTH // 2 - camera.x * camera.zoom |  | ||||||
|     grid_center_y = SCREEN_HEIGHT // 2 - camera.y * camera.zoom |  | ||||||
| 
 |  | ||||||
|     grid_left = grid_center_x - grid_world_width // 2 |  | ||||||
|     grid_top = grid_center_y - grid_world_height // 2 |  | ||||||
|     grid_right = grid_left + grid_world_width |  | ||||||
|     grid_bottom = grid_top + grid_world_height |  | ||||||
| 
 |  | ||||||
|     # Check if grid should be shown |  | ||||||
|     if not showing_grid: |  | ||||||
|         return  # Exit early if grid is not visible |  | ||||||
| 
 |  | ||||||
|     # Check if grid is visible on screen |  | ||||||
|     if ( |  | ||||||
|             grid_right < 0 |  | ||||||
|             or grid_left > SCREEN_WIDTH |  | ||||||
|             or grid_bottom < 0 |  | ||||||
|             or grid_top > SCREEN_HEIGHT |  | ||||||
|     ): |  | ||||||
|         return  # Grid is completely off-screen |  | ||||||
| 
 |  | ||||||
|     # Fill the grid area awith dark gray background |  | ||||||
|     grid_rect = pygame.Rect( |  | ||||||
|         max(0, grid_left), |  | ||||||
|         max(0, grid_top), |  | ||||||
|         min(SCREEN_WIDTH, grid_right) - max(0, grid_left), |  | ||||||
|         min(SCREEN_HEIGHT, grid_bottom) - max(0, grid_top), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     # Only draw if the rectangle has positive dimensions |  | ||||||
|     if grid_rect.width > 0 and grid_rect.height > 0: |  | ||||||
|         pygame.draw.rect(screen, DARK_GRAY, grid_rect) |  | ||||||
| 
 |  | ||||||
|     # Draw vertical grid lines (only if zoom is high enough to see them clearly) |  | ||||||
|     if effective_cell_size > 4: |  | ||||||
|         # Precompute grid boundaries |  | ||||||
|         vertical_lines = [] |  | ||||||
|         horizontal_lines = [] |  | ||||||
| 
 |  | ||||||
|         for i in range(max(GRID_WIDTH, GRID_HEIGHT) + 1): |  | ||||||
|             # Vertical lines |  | ||||||
|             if i <= GRID_WIDTH: |  | ||||||
|                 line_x = grid_left + i * effective_cell_size |  | ||||||
|                 if 0 <= line_x <= SCREEN_WIDTH: |  | ||||||
|                     start_y = max(0, grid_top) |  | ||||||
|                     end_y = min(SCREEN_HEIGHT, grid_bottom) |  | ||||||
|                     if start_y < end_y: |  | ||||||
|                         vertical_lines.append(((line_x, start_y), (line_x, end_y))) |  | ||||||
| 
 |  | ||||||
|             # Horizontal lines |  | ||||||
|             if i <= GRID_HEIGHT: |  | ||||||
|                 line_y = grid_top + i * effective_cell_size |  | ||||||
|                 if 0 <= line_y <= SCREEN_HEIGHT: |  | ||||||
|                     start_x = max(0, grid_left) |  | ||||||
|                     end_x = min(SCREEN_WIDTH, grid_right) |  | ||||||
|                     if start_x < end_x: |  | ||||||
|                         horizontal_lines.append(((start_x, line_y), (end_x, line_y))) |  | ||||||
| 
 |  | ||||||
|         # Draw all vertical lines in one batch |  | ||||||
|         for start, end in vertical_lines: |  | ||||||
|             pygame.draw.line(screen, GRAY, start, end) |  | ||||||
| 
 |  | ||||||
|         # Draw all horizontal lines in one batch |  | ||||||
|         for start, end in horizontal_lines: |  | ||||||
|             pygame.draw.line(screen, GRAY, start, end) |  | ||||||
| 
 | 
 | ||||||
| def setup(world: World): | def setup(world: World): | ||||||
|     if FOOD_SPAWNING: |     if FOOD_SPAWNING: | ||||||
|         world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) |         world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) | ||||||
| 
 | 
 | ||||||
|     world.add_object(TestVelocityObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) |     for i in range(100): | ||||||
| 
 |         world.add_object(DefaultCell(Position(x=random.randint(-100, 100),y=random.randint(-100, 100)), Rotation(angle=0))) | ||||||
|     world.add_object(DefaultCell(Position(x=0,y=0), Rotation(angle=0))) |  | ||||||
| 
 | 
 | ||||||
|     return world |     return world | ||||||
| 
 | 
 | ||||||
| @ -126,34 +36,12 @@ def main(): | |||||||
|     clock = pygame.time.Clock() |     clock = pygame.time.Clock() | ||||||
|     camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) |     camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) | ||||||
| 
 | 
 | ||||||
|     is_showing_grid = True  # Flag to control grid visibility |  | ||||||
|     show_interaction_radius = False  # Flag to control interaction radius visibility |  | ||||||
|     showing_legend = False  # Flag to control legend visibility |  | ||||||
|     is_paused = False  # Flag to control simulation pause state |  | ||||||
| 
 |  | ||||||
|     font = pygame.font.Font("freesansbold.ttf", 16) |  | ||||||
| 
 |  | ||||||
|     tps = DEFAULT_TPS  # Default ticks per second |  | ||||||
| 
 |  | ||||||
|     last_tick_time = time.perf_counter()  # Tracks the last tick time |     last_tick_time = time.perf_counter()  # Tracks the last tick time | ||||||
|     last_tps_time = time.perf_counter()  # Tracks the last TPS calculation time |     last_tps_time = time.perf_counter()  # Tracks the last TPS calculation time | ||||||
|     tick_counter = 0  # Counts ticks executed |     tick_counter = 0  # Counts ticks executed | ||||||
|     actual_tps = 0  # Stores the calculated TPS |     actual_tps = 0  # Stores the calculated TPS | ||||||
|     total_ticks = 0  # Total ticks executed |     total_ticks = 0  # Total ticks executed | ||||||
| 
 | 
 | ||||||
|     # Selection state |  | ||||||
|     selecting = False |  | ||||||
|     select_start = None  # (screen_x, screen_y) |  | ||||||
|     select_end = None  # (screen_x, screen_y) |  | ||||||
|     selected_objects = [] |  | ||||||
| 
 |  | ||||||
|     print("Controls:") |  | ||||||
|     print("WASD - Move camera") |  | ||||||
|     print("Mouse wheel - Zoom in/out") |  | ||||||
|     print("Middle mouse button - Pan camera") |  | ||||||
|     print("R - Reset camera to origin") |  | ||||||
|     print("ESC or close window - Exit") |  | ||||||
| 
 |  | ||||||
|     # Initialize world |     # Initialize world | ||||||
|     world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) |     world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) | ||||||
| 
 | 
 | ||||||
| @ -162,93 +50,19 @@ def main(): | |||||||
| 
 | 
 | ||||||
|     world = setup(world) |     world = setup(world) | ||||||
| 
 | 
 | ||||||
|  |     input_handler = InputHandler(camera, world) | ||||||
|  |     renderer = Renderer(screen) | ||||||
|  |     hud = HUD() | ||||||
|  | 
 | ||||||
|     running = True |     running = True | ||||||
|     while running: |     while running: | ||||||
|         deltatime = clock.get_time() / 1000.0  # Convert milliseconds to seconds |         deltatime = clock.get_time() / 1000.0  # Convert milliseconds to seconds | ||||||
|         tick_interval = 1.0 / tps  # Time per tick |         tick_interval = 1.0 / input_handler.tps # Time per tick | ||||||
| 
 | 
 | ||||||
|         # Handle events |         # Handle events | ||||||
|         for event in pygame.event.get(): |         running = input_handler.handle_events(pygame.event.get()) | ||||||
|             if event.type == pygame.QUIT: |  | ||||||
|                 running = False |  | ||||||
|             elif event.type == pygame.KEYDOWN: |  | ||||||
|                 if event.key == pygame.K_ESCAPE: |  | ||||||
|                     if len(selected_objects) == 0: |  | ||||||
|                         running = False |  | ||||||
|                     selecting = False |  | ||||||
|                     selected_objects = [] |  | ||||||
|                 if event.key == pygame.K_g: |  | ||||||
|                     is_showing_grid = not is_showing_grid |  | ||||||
|                 if event.key == pygame.K_UP: |  | ||||||
|                     if camera.speed < 2100: |  | ||||||
|                         camera.speed += 350 |  | ||||||
|                 if event.key == pygame.K_DOWN: |  | ||||||
|                     if camera.speed > 350: |  | ||||||
|                         camera.speed -= 350 |  | ||||||
|                 if event.key == pygame.K_i: |  | ||||||
|                     show_interaction_radius = not show_interaction_radius |  | ||||||
|                 if event.key == pygame.K_l: |  | ||||||
|                     showing_legend = not showing_legend |  | ||||||
|                 if event.key == pygame.K_SPACE: |  | ||||||
|                     is_paused = not is_paused |  | ||||||
|                 if event.key == pygame.K_LSHIFT: |  | ||||||
|                     tps = DEFAULT_TPS * 4 |  | ||||||
|             elif event.type == pygame.KEYUP: |  | ||||||
|                 if event.key == pygame.K_LSHIFT: |  | ||||||
|                     tps = DEFAULT_TPS |  | ||||||
|             elif event.type == pygame.MOUSEWHEEL: |  | ||||||
|                 camera.handle_zoom(event.y) |  | ||||||
|             elif event.type == pygame.MOUSEBUTTONDOWN: |  | ||||||
|                 if event.button == 2:  # Middle mouse button |  | ||||||
|                     camera.start_panning(event.pos) |  | ||||||
|                 elif event.button == 1:  # Left mouse button |  | ||||||
|                     selecting = True |  | ||||||
|                     select_start = event.pos |  | ||||||
|                     select_end = event.pos |  | ||||||
|             elif event.type == pygame.MOUSEBUTTONUP: |  | ||||||
|                 if event.button == 2: |  | ||||||
|                     camera.stop_panning() |  | ||||||
|                 elif event.button == 1 and selecting: |  | ||||||
|                     selecting = False |  | ||||||
|                     # Convert screen to world coordinates |  | ||||||
|                     x1, y1 = camera.get_real_coordinates(*select_start) |  | ||||||
|                     x2, y2 = camera.get_real_coordinates(*select_end) |  | ||||||
|                     # If the selection rectangle is very small, treat as a click |  | ||||||
|                     if ( |  | ||||||
|                             abs(select_start[0] - select_end[0]) < 3 |  | ||||||
|                             and abs(select_start[1] - select_end[1]) < 3 |  | ||||||
|                     ): |  | ||||||
|                         # Single click: select closest object if in range |  | ||||||
|                         mouse_world_x, mouse_world_y = camera.get_real_coordinates( |  | ||||||
|                             *select_start |  | ||||||
|                         ) |  | ||||||
|                         obj = world.query_closest_object(mouse_world_x, mouse_world_y) |  | ||||||
|                         selected_objects = [] |  | ||||||
|                         if obj: |  | ||||||
|                             obj_x, obj_y = obj.position.get_position() |  | ||||||
|                             # Calculate distance in world coordinates |  | ||||||
|                             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: |  | ||||||
|                                 selected_objects = [obj] |  | ||||||
|                         print(f"Clicked: selected {len(selected_objects)} object(s)") |  | ||||||
|                     else: |  | ||||||
|                         # Drag select: select all in rectangle |  | ||||||
|                         min_x, max_x = min(x1, x2), max(x1, x2) |  | ||||||
|                         min_y, max_y = min(y1, y2), max(y1, y2) |  | ||||||
|                         selected_objects = world.query_objects_in_range( |  | ||||||
|                             min_x, min_y, max_x, max_y |  | ||||||
|                         ) |  | ||||||
|                         print( |  | ||||||
|                             f"Selected {len(selected_objects)} objects in range: {min_x}, {min_y} to {max_x}, {max_y}" |  | ||||||
|                         ) |  | ||||||
|             elif event.type == pygame.MOUSEMOTION: |  | ||||||
|                 camera.pan(event.pos) |  | ||||||
|                 if selecting: |  | ||||||
|                     select_end = event.pos |  | ||||||
| 
 | 
 | ||||||
|         if not is_paused: |         if not input_handler.is_paused: | ||||||
|             # Tick logic (runs every tick interval) |             # Tick logic (runs every tick interval) | ||||||
|             current_time = time.perf_counter() |             current_time = time.perf_counter() | ||||||
|             while current_time - last_tick_time >= tick_interval: |             while current_time - last_tick_time >= tick_interval: | ||||||
| @ -256,15 +70,8 @@ def main(): | |||||||
|                 tick_counter += 1 |                 tick_counter += 1 | ||||||
|                 total_ticks += 1 |                 total_ticks += 1 | ||||||
| 
 | 
 | ||||||
|                 # gets every object in the world and returns amount of FoodObjects |  | ||||||
|                 objects = world.get_objects() |  | ||||||
|                 food = len([obj for obj in objects if isinstance(obj, FoodObject)]) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 # ensure selected objects are still valid or have not changed position, if so, reselect them |                 # ensure selected objects are still valid or have not changed position, if so, reselect them | ||||||
|                 selected_objects = [ |                 input_handler.update_selected_objects() | ||||||
|                     obj for obj in selected_objects if obj in world.get_objects() |  | ||||||
|                 ] |  | ||||||
| 
 | 
 | ||||||
|                 world.tick_all() |                 world.tick_all() | ||||||
| 
 | 
 | ||||||
| @ -279,315 +86,28 @@ def main(): | |||||||
| 
 | 
 | ||||||
|         # Get pressed keys for smooth movement |         # Get pressed keys for smooth movement | ||||||
|         keys = pygame.key.get_pressed() |         keys = pygame.key.get_pressed() | ||||||
|         camera.update(keys, deltatime) |         input_handler.update_camera(keys, deltatime) | ||||||
| 
 | 
 | ||||||
|         # Draw the reference grid |         renderer.clear_screen() | ||||||
|         draw_grid(screen, camera, is_showing_grid) |         renderer.draw_grid(camera, input_handler.show_grid) | ||||||
|  |         renderer.render_world(world, camera) | ||||||
| 
 | 
 | ||||||
|         # Render everything in the world |         renderer.render_interaction_radius(world, camera, input_handler.selected_objects, input_handler.show_interaction_radius) | ||||||
|         world.render_all(camera, screen) |  | ||||||
| 
 | 
 | ||||||
|         if show_interaction_radius: |         renderer.render_selection_rectangle(input_handler.get_selection_rect()) | ||||||
|             for obj in world.get_objects(): |         renderer.render_selected_objects_outline(input_handler.selected_objects, camera) | ||||||
|                 obj_x, obj_y = obj.position.get_position() |  | ||||||
|                 radius = obj.interaction_radius |  | ||||||
|                 if radius > 0 and camera.is_in_view(obj_x, obj_y, margin=radius): |  | ||||||
|                     if selected_objects and obj not in selected_objects: |  | ||||||
|                         continue # Skip if not selected and selecting |  | ||||||
|                     screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) |  | ||||||
|                     screen_radius = int(radius * camera.zoom) |  | ||||||
|                     if screen_radius > 0: |  | ||||||
|                         pygame.draw.circle( |  | ||||||
|                             screen, |  | ||||||
|                             (255, 0, 0),  # Red |  | ||||||
|                             (screen_x, screen_y), |  | ||||||
|                             screen_radius, |  | ||||||
|                             1  # 1 pixel thick |  | ||||||
|                         ) |  | ||||||
| 
 | 
 | ||||||
|                         # Draw direction arrow |         hud.render_mouse_position(screen, camera) | ||||||
|                         rotation_angle = obj.rotation.get_rotation() |         hud.render_fps(screen, clock) | ||||||
|                         arrow_length = obj.max_visual_width/2 * camera.zoom  # Scale arrow length with zoom |         hud.render_tps(screen, actual_tps) | ||||||
|                         arrow_color = (255, 255, 255)  # Green |         hud.render_tick_count(screen, total_ticks) | ||||||
| 
 |         hud.render_selected_objects_info(screen, input_handler.selected_objects) | ||||||
|                         # Calculate the arrow's end-point based on rotation angle |         hud.render_legend(screen, input_handler.show_legend) | ||||||
|                         end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) |         hud.render_pause_indicator(screen, input_handler.is_paused) | ||||||
|                         end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) |  | ||||||
| 
 |  | ||||||
|                         # Draw the arrow line |  | ||||||
|                         pygame.draw.line(screen, arrow_color, (screen_x, screen_y), (end_x, end_y), 2) |  | ||||||
| 
 |  | ||||||
|                         # Draw a rotated triangle for the arrowhead |  | ||||||
|                         tip_size = 3 * camera.zoom  # Scale triangle tip size with zoom |  | ||||||
|                         left_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle + 150 + 180)) |  | ||||||
|                         left_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle + 150 + 180)) |  | ||||||
|                         right_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle - 150 + 180)) |  | ||||||
|                         right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) |  | ||||||
| 
 |  | ||||||
|                         # Draw arrowhead (triangle) for direction |  | ||||||
|                         pygame.draw.polygon( |  | ||||||
|                             screen, |  | ||||||
|                             arrow_color, |  | ||||||
|                             [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] |  | ||||||
|                         ) |  | ||||||
| 
 |  | ||||||
|                         # Draw angular acceleration arrow (if present) |  | ||||||
|                         if hasattr(obj, 'angular_acceleration'): |  | ||||||
|                             angular_acceleration = obj.angular_acceleration |  | ||||||
| 
 |  | ||||||
|                             # Scale the angular acceleration value for visibility |  | ||||||
|                             angular_accel_magnitude = abs( |  | ||||||
|                                 angular_acceleration) * 50 * camera.zoom  # Use absolute magnitude for scaling |  | ||||||
| 
 |  | ||||||
|                             # Determine the perpendicular direction based on the sign of angular_acceleration |  | ||||||
|                             angular_direction = rotation_angle + 90 if angular_acceleration >= 0 else rotation_angle - 90 |  | ||||||
| 
 |  | ||||||
|                             # Calculate the end of the angular acceleration vector |  | ||||||
|                             angular_acc_end_x = end_x + angular_accel_magnitude * math.cos( |  | ||||||
|                                 math.radians(angular_direction)) |  | ||||||
|                             angular_acc_end_y = end_y + angular_accel_magnitude * math.sin( |  | ||||||
|                                 math.radians(angular_direction)) |  | ||||||
| 
 |  | ||||||
|                             # Draw the angular acceleration vector as a red line |  | ||||||
|                             pygame.draw.line(screen, (52, 134, 235), (end_x, end_y), |  | ||||||
|                                              (angular_acc_end_x, angular_acc_end_y), 2) |  | ||||||
| 
 |  | ||||||
|                             # Add an arrowhead to the angular acceleration vector |  | ||||||
|                             angular_tip_size = 2.5 * camera.zoom |  | ||||||
|                             left_angular_tip_x = angular_acc_end_x - angular_tip_size * math.cos( |  | ||||||
|                                 math.radians(angular_direction + 150 + 180)) |  | ||||||
|                             left_angular_tip_y = angular_acc_end_y - angular_tip_size * math.sin( |  | ||||||
|                                 math.radians(angular_direction + 150 + 180)) |  | ||||||
|                             right_angular_tip_x = angular_acc_end_x - angular_tip_size * math.cos( |  | ||||||
|                                 math.radians(angular_direction - 150 + 180)) |  | ||||||
|                             right_angular_tip_y = angular_acc_end_y - angular_tip_size * math.sin( |  | ||||||
|                                 math.radians(angular_direction - 150 + 180)) |  | ||||||
| 
 |  | ||||||
|                             # Draw arrowhead (triangle) for angular acceleration |  | ||||||
|                             pygame.draw.polygon( |  | ||||||
|                                 screen, |  | ||||||
|                                 (52, 134, 235),  # Red arrowhead |  | ||||||
|                                 [(angular_acc_end_x, angular_acc_end_y), (left_angular_tip_x, left_angular_tip_y), |  | ||||||
|                                  (right_angular_tip_x, right_angular_tip_y)] |  | ||||||
|                             ) |  | ||||||
| 
 |  | ||||||
|                         # If object has an acceleration attribute, draw a red vector with arrowhead |  | ||||||
|                         if hasattr(obj, 'acceleration') and isinstance(obj.acceleration, tuple) and len( |  | ||||||
|                                 obj.acceleration) == 2: |  | ||||||
|                             acc_x, acc_y = obj.acceleration |  | ||||||
| 
 |  | ||||||
|                             # Calculate acceleration magnitude and direction |  | ||||||
|                             acc_magnitude = math.sqrt(acc_x ** 2 + acc_y ** 2) |  | ||||||
|                             if acc_magnitude > 0: |  | ||||||
|                                 acc_direction = math.degrees(math.atan2(acc_y, acc_x))  # Get the angle in degrees |  | ||||||
| 
 |  | ||||||
|                                 # Calculate scaled acceleration vector's end point |  | ||||||
|                                 acc_vector_length = acc_magnitude * 1000 * camera.zoom  # Scale length with zoom |  | ||||||
|                                 acc_end_x = screen_x + acc_vector_length * math.cos(math.radians(acc_direction)) |  | ||||||
|                                 acc_end_y = screen_y + acc_vector_length * math.sin(math.radians(acc_direction)) |  | ||||||
| 
 |  | ||||||
|                                 # Draw the acceleration vector as a red line |  | ||||||
|                                 pygame.draw.line(screen, (255, 0, 0), (screen_x, screen_y), (acc_end_x, acc_end_y), 2) |  | ||||||
| 
 |  | ||||||
|                                 # Add arrowhead to acceleration vector |  | ||||||
|                                 acc_tip_size = 5 * camera.zoom |  | ||||||
|                                 left_tip_x = acc_end_x - acc_tip_size * math.cos(math.radians(acc_direction + 150 + 180)) |  | ||||||
|                                 left_tip_y = acc_end_y - acc_tip_size * math.sin(math.radians(acc_direction + 150 + 180)) |  | ||||||
|                                 right_tip_x = acc_end_x - acc_tip_size * math.cos(math.radians(acc_direction - 150 + 180)) |  | ||||||
|                                 right_tip_y = acc_end_y - acc_tip_size * math.sin(math.radians(acc_direction - 150 + 180)) |  | ||||||
| 
 |  | ||||||
|                                 pygame.draw.polygon( |  | ||||||
|                                     screen, |  | ||||||
|                                     (255, 0, 0),  # Red arrowhead |  | ||||||
|                                     [(acc_end_x, acc_end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] |  | ||||||
|                                 ) |  | ||||||
| 
 |  | ||||||
|                         # If object has a velocity attribute, draw a blue vector with arrowhead |  | ||||||
|                         if hasattr(obj, 'velocity') and isinstance(obj.velocity, tuple) and len(obj.velocity) == 2: |  | ||||||
|                             vel_x, vel_y = obj.velocity |  | ||||||
| 
 |  | ||||||
|                             # Calculate velocity magnitude and direction |  | ||||||
|                             vel_magnitude = math.sqrt(vel_x ** 2 + vel_y ** 2) |  | ||||||
|                             if vel_magnitude > 0: |  | ||||||
|                                 vel_direction = math.degrees(math.atan2(vel_y, vel_x))  # Get the angle in degrees |  | ||||||
| 
 |  | ||||||
|                                 # Calculate scaled velocity vector's end point |  | ||||||
|                                 vel_vector_length = vel_magnitude * 50 * camera.zoom  # Scale length with zoom |  | ||||||
|                                 vel_end_x = screen_x + vel_vector_length * math.cos(math.radians(vel_direction)) |  | ||||||
|                                 vel_end_y = screen_y + vel_vector_length * math.sin(math.radians(vel_direction)) |  | ||||||
| 
 |  | ||||||
|                                 # Draw the velocity vector as a blue line |  | ||||||
|                                 pygame.draw.line(screen, (0, 0, 255), (screen_x, screen_y), (vel_end_x, vel_end_y), 2) |  | ||||||
| 
 |  | ||||||
|                                 # Add arrowhead to velocity vector |  | ||||||
|                                 vel_tip_size = 5 * camera.zoom |  | ||||||
|                                 left_tip_x = vel_end_x - vel_tip_size * math.cos(math.radians(vel_direction + 150 + 180)) |  | ||||||
|                                 left_tip_y = vel_end_y - vel_tip_size * math.sin(math.radians(vel_direction + 150 + 180)) |  | ||||||
|                                 right_tip_x = vel_end_x - vel_tip_size * math.cos(math.radians(vel_direction - 150 + 180)) |  | ||||||
|                                 right_tip_y = vel_end_y - vel_tip_size * math.sin(math.radians(vel_direction - 150 + 180)) |  | ||||||
| 
 |  | ||||||
|                                 pygame.draw.polygon( |  | ||||||
|                                     screen, |  | ||||||
|                                     (0, 0, 255),  # Blue arrowhead |  | ||||||
|                                     [(vel_end_x, vel_end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] |  | ||||||
|                                 ) |  | ||||||
| 
 |  | ||||||
|         # Draw selection rectangle if selecting |  | ||||||
|         if selecting and select_start and select_end: |  | ||||||
|             rect_color = (128, 128, 128, 80)  # Gray, semi-transparent |  | ||||||
|             border_color = (80, 80, 90)  # Slightly darker gray for border |  | ||||||
| 
 |  | ||||||
|             left = min(select_start[0], select_end[0]) |  | ||||||
|             top = min(select_start[1], select_end[1]) |  | ||||||
|             width = abs(select_end[0] - select_start[0]) |  | ||||||
|             height = abs(select_end[1] - select_start[1]) |  | ||||||
| 
 |  | ||||||
|             s = pygame.Surface((width, height), pygame.SRCALPHA) |  | ||||||
|             s.fill(rect_color) |  | ||||||
|             screen.blit(s, (left, top)) |  | ||||||
| 
 |  | ||||||
|             # Draw 1-pixel border |  | ||||||
|             pygame.draw.rect( |  | ||||||
|                 screen, border_color, pygame.Rect(left, top, width, height), 1 |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|         # Draw blue outline for selected objects |  | ||||||
|         for obj in selected_objects: |  | ||||||
|             obj_x, obj_y = obj.position.get_position() |  | ||||||
|             width = obj.max_visual_width if hasattr(obj, "max_visual_width") else 10 |  | ||||||
|             screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) |  | ||||||
|             size = camera.get_relative_size(width) |  | ||||||
|             rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) |  | ||||||
|             pygame.draw.rect(screen, (0, 128, 255), rect, 1)  # Blue, 1px wide |  | ||||||
| 
 |  | ||||||
|         # Render mouse position as text in top left of screen |  | ||||||
|         mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) |  | ||||||
|         mouse_text = font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) |  | ||||||
|         text_rect = mouse_text.get_rect() |  | ||||||
|         text_rect.topleft = (10, 10) |  | ||||||
|         screen.blit(mouse_text, text_rect) |  | ||||||
| 
 |  | ||||||
|         # Render FPS in top right |  | ||||||
|         fps_text = font.render(f"FPS: {int(clock.get_fps())}", True, WHITE) |  | ||||||
|         fps_rect = fps_text.get_rect() |  | ||||||
|         fps_rect.topright = (SCREEN_WIDTH - 10, 10) |  | ||||||
|         screen.blit(fps_text, fps_rect) |  | ||||||
| 
 |  | ||||||
|         # Render TPS in bottom right |  | ||||||
|         tps_text = font.render(f"TPS: {actual_tps}", True, WHITE) |  | ||||||
|         tps_rect = tps_text.get_rect() |  | ||||||
|         tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10) |  | ||||||
|         screen.blit(tps_text, tps_rect) |  | ||||||
| 
 |  | ||||||
|         # Render tick count in bottom left |  | ||||||
|         tick_text = font.render(f"Ticks: {total_ticks}", True, WHITE) |  | ||||||
|         tick_rect = tick_text.get_rect() |  | ||||||
|         tick_rect.bottomleft = (10, SCREEN_HEIGHT - 10) |  | ||||||
|         screen.blit(tick_text, tick_rect) |  | ||||||
| 
 |  | ||||||
|         if len(selected_objects) >= 1: |  | ||||||
|             i = 0 |  | ||||||
|             max_width = SCREEN_WIDTH - 20  # Leave some padding from the right edge |  | ||||||
|             for each in selected_objects: |  | ||||||
|                 obj = each |  | ||||||
|                 text = f"Object: {str(obj)}" |  | ||||||
|                 words = text.split()  # Split text into words |  | ||||||
|                 line = "" |  | ||||||
|                 line_height = 20  # Height of each line of text |  | ||||||
|                 line_offset = 0 |  | ||||||
| 
 |  | ||||||
|                 for word in words: |  | ||||||
|                     test_line = f"{line} {word}".strip() |  | ||||||
|                     test_width, _ = font.size(test_line) |  | ||||||
| 
 |  | ||||||
|                     # Check if the line width exceeds the limit |  | ||||||
|                     if test_width > max_width and line: |  | ||||||
|                         obj_text = font.render(line, True, WHITE) |  | ||||||
|                         obj_rect = obj_text.get_rect() |  | ||||||
|                         obj_rect.topleft = (10, 30 + i * line_height + line_offset) |  | ||||||
|                         screen.blit(obj_text, obj_rect) |  | ||||||
|                         line = word  # Start a new line |  | ||||||
|                         line_offset += line_height |  | ||||||
|                     else: |  | ||||||
|                         line = test_line |  | ||||||
| 
 |  | ||||||
|                 # Render the last line |  | ||||||
|                 if line: |  | ||||||
|                     obj_text = font.render(line, True, WHITE) |  | ||||||
|                     obj_rect = obj_text.get_rect() |  | ||||||
|                     obj_rect.topleft = (10, 30 + i * line_height + line_offset) |  | ||||||
|                     screen.blit(obj_text, obj_rect) |  | ||||||
| 
 |  | ||||||
|                 i += 1 |  | ||||||
| 
 |  | ||||||
|         legend_font = pygame.font.Font("freesansbold.ttf", 14) |  | ||||||
| 
 |  | ||||||
|         keymap_legend = [ |  | ||||||
|             ("WASD", "Move camera"), |  | ||||||
|             ("Mouse wheel", "Zoom in/out"), |  | ||||||
|             ("Middle mouse", "Pan camera"), |  | ||||||
|             ("R", "Reset camera"), |  | ||||||
|             ("G", "Toggle grid"), |  | ||||||
|             ("I", "Toggle interaction radius"), |  | ||||||
|             ("ESC", "Deselect/Exit"), |  | ||||||
|             ("Left click", "Select object(s)"), |  | ||||||
|             ("Drag select", "Select multiple objects"), |  | ||||||
|             ("Click on object", "Select closest object in range"), |  | ||||||
|             ("Up/Down", "Increase/Decrease camera speed"), |  | ||||||
|             ("Shift", "Double TPS (for testing)"), |  | ||||||
|             ("L", "Toggle this legend"), |  | ||||||
|             ("Space", "Pause/Resume simulation"), |  | ||||||
|         ] |  | ||||||
| 
 |  | ||||||
|         if showing_legend: |  | ||||||
|             # Split into two columns |  | ||||||
|             mid = (len(keymap_legend) + 1) // 2 |  | ||||||
|             left_col = keymap_legend[:mid] |  | ||||||
|             right_col = keymap_legend[mid:] |  | ||||||
| 
 |  | ||||||
|             legend_font_height = legend_font.get_height() |  | ||||||
|             column_gap = 40  # Space between columns |  | ||||||
| 
 |  | ||||||
|             # Calculate max width for each column |  | ||||||
|             left_width = max(legend_font.size(f"{k}: {v}")[0] for k, v in left_col) |  | ||||||
|             right_width = max(legend_font.size(f"{k}: {v}")[0] for k, v in right_col) |  | ||||||
|             legend_width = left_width + right_width + column_gap |  | ||||||
|             legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10 |  | ||||||
| 
 |  | ||||||
|             legend_x = (SCREEN_WIDTH - legend_width) // 2 |  | ||||||
|             legend_y = SCREEN_HEIGHT - legend_height - 10 |  | ||||||
| 
 |  | ||||||
|             # Draw left column |  | ||||||
|             for i, (key, desc) in enumerate(left_col): |  | ||||||
|                 text = legend_font.render(f"{key}: {desc}", True, WHITE) |  | ||||||
|                 text_rect = text.get_rect() |  | ||||||
|                 text_rect.left = legend_x |  | ||||||
|                 text_rect.top = legend_y + 5 + i * legend_font_height |  | ||||||
|                 screen.blit(text, text_rect) |  | ||||||
| 
 |  | ||||||
|             # Draw right column |  | ||||||
|             for i, (key, desc) in enumerate(right_col): |  | ||||||
|                 text = legend_font.render(f"{key}: {desc}", True, WHITE) |  | ||||||
|                 text_rect = text.get_rect() |  | ||||||
|                 text_rect.left = legend_x + left_width + column_gap |  | ||||||
|                 text_rect.top = legend_y + 5 + i * legend_font_height |  | ||||||
|                 screen.blit(text, text_rect) |  | ||||||
|         else: |  | ||||||
|             # just show l to toggle legend |  | ||||||
|             legend_text = legend_font.render("Press 'L' to show controls", True, WHITE) |  | ||||||
|             legend_rect = legend_text.get_rect() |  | ||||||
|             legend_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 20) |  | ||||||
|             screen.blit(legend_text, legend_rect) |  | ||||||
| 
 |  | ||||||
|         if is_paused: |  | ||||||
|             pause_text = font.render("Press 'Space' to unpause", True, WHITE) |  | ||||||
|             pause_rect = pause_text.get_rect() |  | ||||||
|             pause_rect.center = (SCREEN_WIDTH // 2, 20) |  | ||||||
|             screen.blit(pause_text, pause_rect) |  | ||||||
| 
 | 
 | ||||||
|         # Update display |         # Update display | ||||||
|         pygame.display.flip() |         pygame.display.flip() | ||||||
|         clock.tick(180) |         clock.tick(MAX_FPS) | ||||||
| 
 | 
 | ||||||
|     pygame.quit() |     pygame.quit() | ||||||
|     sys.exit() |     sys.exit() | ||||||
|  | |||||||
							
								
								
									
										126
									
								
								ui/hud.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								ui/hud.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | # ui/hud.py | ||||||
|  | """Handles HUD elements and text overlays.""" | ||||||
|  | 
 | ||||||
|  | import pygame | ||||||
|  | from config.constants import * | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HUD: | ||||||
|  |     def __init__(self): | ||||||
|  |         self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE) | ||||||
|  |         self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |     def render_mouse_position(self, screen, camera): | ||||||
|  |         """Render mouse position in top left.""" | ||||||
|  |         mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) | ||||||
|  |         mouse_text = self.font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) | ||||||
|  |         text_rect = mouse_text.get_rect() | ||||||
|  |         text_rect.topleft = (HUD_MARGIN, HUD_MARGIN) | ||||||
|  |         screen.blit(mouse_text, text_rect) | ||||||
|  | 
 | ||||||
|  |     def render_fps(self, screen, clock): | ||||||
|  |         """Render FPS in top right.""" | ||||||
|  |         fps_text = self.font.render(f"FPS: {int(clock.get_fps())}", True, WHITE) | ||||||
|  |         fps_rect = fps_text.get_rect() | ||||||
|  |         fps_rect.topright = (SCREEN_WIDTH - HUD_MARGIN, HUD_MARGIN) | ||||||
|  |         screen.blit(fps_text, fps_rect) | ||||||
|  | 
 | ||||||
|  |     def render_tps(self, screen, actual_tps): | ||||||
|  |         """Render TPS in bottom right.""" | ||||||
|  |         tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE) | ||||||
|  |         tps_rect = tps_text.get_rect() | ||||||
|  |         tps_rect.bottomright = (SCREEN_WIDTH - HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) | ||||||
|  |         screen.blit(tps_text, tps_rect) | ||||||
|  | 
 | ||||||
|  |     def render_tick_count(self, screen, total_ticks): | ||||||
|  |         """Render total tick count in bottom left.""" | ||||||
|  |         tick_text = self.font.render(f"Ticks: {total_ticks}", True, WHITE) | ||||||
|  |         tick_rect = tick_text.get_rect() | ||||||
|  |         tick_rect.bottomleft = (HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) | ||||||
|  |         screen.blit(tick_text, tick_rect) | ||||||
|  | 
 | ||||||
|  |     def render_pause_indicator(self, screen, is_paused): | ||||||
|  |         """Render pause indicator when paused.""" | ||||||
|  |         if is_paused: | ||||||
|  |             pause_text = self.font.render("Press 'Space' to unpause", True, WHITE) | ||||||
|  |             pause_rect = pause_text.get_rect() | ||||||
|  |             pause_rect.center = (SCREEN_WIDTH // 2, 20) | ||||||
|  |             screen.blit(pause_text, pause_rect) | ||||||
|  | 
 | ||||||
|  |     def render_selected_objects_info(self, screen, selected_objects): | ||||||
|  |         """Render information about selected objects.""" | ||||||
|  |         if len(selected_objects) < 1: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         max_width = SCREEN_WIDTH - 20 | ||||||
|  |         i = 0 | ||||||
|  | 
 | ||||||
|  |         for obj in selected_objects: | ||||||
|  |             text = f"Object: {str(obj)}" | ||||||
|  |             words = text.split() | ||||||
|  |             line = "" | ||||||
|  |             line_offset = 0 | ||||||
|  | 
 | ||||||
|  |             for word in words: | ||||||
|  |                 test_line = f"{line} {word}".strip() | ||||||
|  |                 test_width, _ = self.font.size(test_line) | ||||||
|  | 
 | ||||||
|  |                 if test_width > max_width and line: | ||||||
|  |                     obj_text = self.font.render(line, True, WHITE) | ||||||
|  |                     obj_rect = obj_text.get_rect() | ||||||
|  |                     obj_rect.topleft = (HUD_MARGIN, 30 + i * LINE_HEIGHT + line_offset) | ||||||
|  |                     screen.blit(obj_text, obj_rect) | ||||||
|  |                     line = word | ||||||
|  |                     line_offset += LINE_HEIGHT | ||||||
|  |                 else: | ||||||
|  |                     line = test_line | ||||||
|  | 
 | ||||||
|  |             if line: | ||||||
|  |                 obj_text = self.font.render(line, True, WHITE) | ||||||
|  |                 obj_rect = obj_text.get_rect() | ||||||
|  |                 obj_rect.topleft = (HUD_MARGIN, 30 + i * LINE_HEIGHT + line_offset) | ||||||
|  |                 screen.blit(obj_text, obj_rect) | ||||||
|  | 
 | ||||||
|  |             i += 1 | ||||||
|  | 
 | ||||||
|  |     def render_legend(self, screen, showing_legend): | ||||||
|  |         """Render the controls legend.""" | ||||||
|  |         if not showing_legend: | ||||||
|  |             legend_text = self.legend_font.render("Press 'L' to show controls", True, WHITE) | ||||||
|  |             legend_rect = legend_text.get_rect() | ||||||
|  |             legend_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 20) | ||||||
|  |             screen.blit(legend_text, legend_rect) | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # Split into two columns | ||||||
|  |         mid = (len(KEYMAP_LEGEND) + 1) // 2 | ||||||
|  |         left_col = KEYMAP_LEGEND[:mid] | ||||||
|  |         right_col = KEYMAP_LEGEND[mid:] | ||||||
|  | 
 | ||||||
|  |         legend_font_height = self.legend_font.get_height() | ||||||
|  |         column_gap = 40  # Space between columns | ||||||
|  | 
 | ||||||
|  |         # Calculate max width for each column | ||||||
|  |         left_width = max(self.legend_font.size(f"{k}: {v}")[0] for k, v in left_col) | ||||||
|  |         right_width = max(self.legend_font.size(f"{k}: {v}")[0] for k, v in right_col) | ||||||
|  |         legend_width = left_width + right_width + column_gap | ||||||
|  |         legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10 | ||||||
|  | 
 | ||||||
|  |         legend_x = (SCREEN_WIDTH - legend_width) // 2 | ||||||
|  |         legend_y = SCREEN_HEIGHT - legend_height - 10 | ||||||
|  | 
 | ||||||
|  |         # Draw left column | ||||||
|  |         for i, (key, desc) in enumerate(left_col): | ||||||
|  |             text = self.legend_font.render(f"{key}: {desc}", True, WHITE) | ||||||
|  |             text_rect = text.get_rect() | ||||||
|  |             text_rect.left = legend_x | ||||||
|  |             text_rect.top = legend_y + 5 + i * legend_font_height | ||||||
|  |             screen.blit(text, text_rect) | ||||||
|  | 
 | ||||||
|  |         # Draw right column | ||||||
|  |         for i, (key, desc) in enumerate(right_col): | ||||||
|  |             text = self.legend_font.render(f"{key}: {desc}", True, WHITE) | ||||||
|  |             text_rect = text.get_rect() | ||||||
|  |             text_rect.left = legend_x + left_width + column_gap | ||||||
|  |             text_rect.top = legend_y + 5 + i * legend_font_height | ||||||
|  |             screen.blit(text, text_rect) | ||||||
| @ -17,7 +17,7 @@ class CellBrain(BehavioralModel): | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         self.weights = { |         self.weights = { | ||||||
|             'distance': 1, |             'distance': 0.1, | ||||||
|             'angle': 0.5 |             'angle': 0.5 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -33,15 +33,13 @@ class CellBrain(BehavioralModel): | |||||||
|         self.inputs['angle'] = input_data.get('angle', 0.0) |         self.inputs['angle'] = input_data.get('angle', 0.0) | ||||||
| 
 | 
 | ||||||
|         # Initialize output dictionary |         # Initialize output dictionary | ||||||
|         output_data = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'], |         self.outputs = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'], | ||||||
| 					   'angular_acceleration': self.inputs['angle'] * self.weights['angle']} | 					   'angular_acceleration': self.inputs['angle'] * self.weights['angle']} | ||||||
| 
 | 
 | ||||||
|         self.outputs = output_data |         return self.outputs | ||||||
| 
 |  | ||||||
|         return output_data |  | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         inputs = {key: round(value, 1) for key, value in self.inputs.items()} |         inputs = {key: round(value, 5) for key, value in self.inputs.items()} | ||||||
|         outputs = {key: round(value, 1) for key, value in self.outputs.items()} |         outputs = {key: round(value, 5) for key, value in self.outputs.items()} | ||||||
|         weights = {key: round(value, 1) for key, value in self.weights.items()} |         weights = {key: round(value, 5) for key, value in self.weights.items()} | ||||||
|         return f"CellBrain(inputs={inputs}, outputs={outputs}, weights={weights})" |         return f"CellBrain(inputs={inputs}, outputs={outputs}, weights={weights})" | ||||||
|  | |||||||
| @ -122,7 +122,9 @@ class FoodObject(BaseEntity): | |||||||
|         if interactable is None: |         if interactable is None: | ||||||
|             interactable = [] |             interactable = [] | ||||||
| 
 | 
 | ||||||
|         self.neighbors = len(interactable) |         # filter neighbors to only other food objects | ||||||
|  |         food_neighbors = [obj for obj in interactable if isinstance(obj, FoodObject)] | ||||||
|  |         self.neighbors = len(food_neighbors) | ||||||
| 
 | 
 | ||||||
|         if self.neighbors > 0: |         if self.neighbors > 0: | ||||||
|             self.decay += self.decay_rate * (1 + (self.neighbors / 10)) |             self.decay += self.decay_rate * (1 + (self.neighbors / 10)) | ||||||
| @ -306,25 +308,39 @@ class DefaultCell(BaseEntity): | |||||||
|         output_data["linear_acceleration"] = max(-0.1, min(0.02, output_data["linear_acceleration"])) |         output_data["linear_acceleration"] = max(-0.1, min(0.02, output_data["linear_acceleration"])) | ||||||
|         output_data["angular_acceleration"] = max(-0.1, min(0.1, output_data["angular_acceleration"])) |         output_data["angular_acceleration"] = max(-0.1, min(0.1, output_data["angular_acceleration"])) | ||||||
| 
 | 
 | ||||||
|         # output acceleration is acceleration along its current rotation. |         # 2. Apply drag force | ||||||
|         x_component = output_data["linear_acceleration"] * math.cos(math.radians(self.rotation.get_rotation())) |         drag_coefficient = 0.02 | ||||||
|         y_component = output_data["linear_acceleration"] * math.sin(math.radians(self.rotation.get_rotation())) |         drag_x = -self.velocity[0] * drag_coefficient | ||||||
|  |         drag_y = -self.velocity[1] * drag_coefficient | ||||||
| 
 | 
 | ||||||
|         self.acceleration = (x_component, y_component) |         # 3. Combine all forces | ||||||
|          |         total_linear_accel = output_data["linear_acceleration"] | ||||||
|         # # add drag according to current velocity |         total_linear_accel = max(-0.1, min(0.02, total_linear_accel)) | ||||||
|         # drag_coefficient = 0.3 | 
 | ||||||
|         # drag_x = -self.velocity[0] * drag_coefficient |         # 4. Convert to world coordinates | ||||||
|         # drag_y = -self.velocity[1] * drag_coefficient |         x_component = total_linear_accel * math.cos(math.radians(self.rotation.get_rotation())) | ||||||
|         # self.acceleration = (self.acceleration[0] + drag_x, self.acceleration[1] + drag_y) |         y_component = total_linear_accel * math.sin(math.radians(self.rotation.get_rotation())) | ||||||
|  | 
 | ||||||
|  |         # 5. Add drag to total acceleration | ||||||
|  |         total_accel_x = x_component + drag_x | ||||||
|  |         total_accel_y = y_component + drag_y | ||||||
|  | 
 | ||||||
|  |         self.acceleration = (total_accel_x, total_accel_y) | ||||||
|  | 
 | ||||||
|  |         rotational_drag = 0.05 | ||||||
|  |         self.angular_acceleration = output_data["angular_acceleration"] - self.rotational_velocity * rotational_drag | ||||||
| 
 | 
 | ||||||
|         # tick acceleration |         # tick acceleration | ||||||
|         velocity_x = self.velocity[0] + self.acceleration[0] |         velocity_x = self.velocity[0] + self.acceleration[0] | ||||||
|         velocity_y = self.velocity[1] + self.acceleration[1] |         velocity_y = self.velocity[1] + self.acceleration[1] | ||||||
|         self.velocity = (velocity_x, velocity_y) |         self.velocity = (velocity_x, velocity_y) | ||||||
| 
 | 
 | ||||||
|         # clamp velocity |         # # clamp velocity | ||||||
|         self.velocity = (max(-0.5, min(0.5, self.velocity[0])), max(-0.5, min(0.5, self.velocity[1]))) |         max_speed = 0.5 | ||||||
|  |         speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2) | ||||||
|  |         if speed > max_speed: | ||||||
|  |             scale = max_speed / speed | ||||||
|  |             self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale) | ||||||
| 
 | 
 | ||||||
|         # tick velocity |         # tick velocity | ||||||
|         x, y = self.position.get_position() |         x, y = self.position.get_position() | ||||||
| @ -338,7 +354,7 @@ class DefaultCell(BaseEntity): | |||||||
|         self.rotational_velocity += self.angular_acceleration |         self.rotational_velocity += self.angular_acceleration | ||||||
| 
 | 
 | ||||||
|         # clamp rotational velocity |         # clamp rotational velocity | ||||||
|         self.rotational_velocity = max(-0.5, min(0.5, self.rotational_velocity)) |         self.rotational_velocity = max(-3, min(3, self.rotational_velocity)) | ||||||
| 
 | 
 | ||||||
|         # tick rotational velocity |         # tick rotational velocity | ||||||
|         self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity) |         self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity) | ||||||
|  | |||||||
| @ -114,6 +114,13 @@ class Camera: | |||||||
|         self.is_panning = False |         self.is_panning = False | ||||||
|         self.last_mouse_pos = None |         self.last_mouse_pos = None | ||||||
| 
 | 
 | ||||||
|  |     def reset_position(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Resets the camera position to the origin. | ||||||
|  |         """ | ||||||
|  |         self.target_x = 0 | ||||||
|  |         self.target_y = 0 | ||||||
|  | 
 | ||||||
|     def pan(self, mouse_pos: Sequence[int]) -> None: |     def pan(self, mouse_pos: Sequence[int]) -> None: | ||||||
|         """ |         """ | ||||||
|         Pans the camera based on mouse movement. |         Pans the camera based on mouse movement. | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user