refactored a lot of stuff into different files and generally fixed force application.

This commit is contained in:
Sam 2025-06-15 23:44:50 -05:00
parent f0576e52d6
commit 26f166ebee
8 changed files with 678 additions and 535 deletions

72
config/constants.py Normal file
View 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
View 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
View 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
View File

@ -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
View 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)

View File

@ -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})"

View File

@ -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)

View File

@ -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.