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