1717- Boundary collision handling with energy damping
1818- Inelastic merging of colliding bodies
1919
20- Usage:
21- python main.py
22-
2320Controls:
2421 • Left click: Create a new celestial body at mouse position
2522 • Mass/Radius inputs: Configure properties for new bodies
2623 • Hover over object: View detailed physical properties
24+ • Hover over object + "Delete": Delete celestial body
25+ • Num/*: Decrease G by 10, Num/*: Increase G by 10
26+ • Num-: Decrease dt by 0.01, Num+: Increase dt by 0.01
27+ • Spacebar: Pause simulation (dt = 0)
2728"""
2829
2930import math
3031import random
32+ import os
3133
34+ from typing import Optional , List
3235import pygame
3336
3437from gravity_simulator .physics import PhysicsEngine
3538from gravity_simulator .objects import Object
36- from gravity_simulator .ui import InputBox , draw_grid , find_object_under_mouse
39+ from gravity_simulator .ui import InputBox , find_object_under_mouse
3740from gravity_simulator .config import (SCREEN_WIDTH , SCREEN_HEIGHT , DEFAULT_MASS ,
38- DEFAULT_RADIUS , GRID_SIZE , GRID_COLOR )
39- from gravity_simulator .utils import random_color , safe_float_convert
41+ DEFAULT_RADIUS )
42+ from gravity_simulator .utils import apply_color_tint , safe_float_convert
43+
4044
4145pygame .init ()
4246
47+ icon : Optional [pygame .Surface ] = None
4348try :
44- import os
4549 icon_path = os .path .join ("resources" , "icons" , "gravity.png" )
4650 if os .path .exists (icon_path ):
4751 icon = pygame .image .load (icon_path )
4852 pygame .display .set_icon (icon )
4953except pygame .error as e :
5054 print (f"Failed to load icon: { e } " )
5155
56+
57+ background : Optional [pygame .Surface ] = None
58+ background_offset_x : int = 0
59+ background_offset_y : int = 0
60+
61+ try :
62+ background_path = os .path .join ("resources" , "background" , "back1.png" )
63+ if os .path .exists (background_path ):
64+ background = pygame .image .load (background_path )
65+ bg_width , bg_height = background .get_size ()
66+ scale_factor = max (SCREEN_WIDTH / bg_width , SCREEN_HEIGHT / bg_height )
67+ new_width = int (bg_width * scale_factor )
68+ new_height = int (bg_height * scale_factor )
69+ background = pygame .transform .smoothscale (background , (new_width , new_height ))
70+ background_offset_x = (SCREEN_WIDTH - new_width ) // 2
71+ background_offset_y = (SCREEN_HEIGHT - new_height ) // 2
72+ else :
73+ print (f"Background image not found at: { background_path } " )
74+ except pygame .error as e :
75+ print (f"Failed to load background: { e } " )
76+
77+
5278screen = pygame .display .set_mode ((SCREEN_WIDTH , SCREEN_HEIGHT ))
5379pygame .display .set_caption ("Physics Simulation" )
5480clock = pygame .time .Clock ()
55- running = True
81+ running : bool = True
5682font = pygame .font .Font (None , 24 )
5783input_font = pygame .font .Font (None , 32 )
5884
5985engine = PhysicsEngine (G = 100.0 , dt = 0.05 )
6086
87+ planet_textures : List [pygame .Surface ] = []
88+ texture_names = ["ice.png" , "lava.png" , "moon.png" , "terran.png" ]
89+
90+ for texture_name in texture_names :
91+ try :
92+ texture_path = os .path .join ("resources" , "objects" , texture_name )
93+ if os .path .exists (texture_path ):
94+ texture = pygame .image .load (texture_path )
95+ texture = texture .convert_alpha ()
96+ planet_textures .append (texture )
97+ print (f"Loaded texture: { texture_name } " )
98+ else :
99+ print (f"Texture not found: { texture_path } " )
100+ except pygame .error as e :
101+ print (f"Failed to load texture { texture_name } : { e } " )
102+
103+ print (f"Successfully loaded { len (planet_textures )} textures" )
104+
105+
61106mass_input = InputBox (
62107 position = (SCREEN_WIDTH - 130 , 10 ),
63108 size = (140 , 32 ),
68113 size = (140 , 32 ),
69114 text = str (DEFAULT_RADIUS )
70115)
116+ g_input = InputBox (
117+ position = (SCREEN_WIDTH - 130 , 90 ),
118+ size = (140 , 32 ),
119+ text = "100.0"
120+ )
121+ dt_input = InputBox (
122+ position = (SCREEN_WIDTH - 130 , 130 ),
123+ size = (140 , 32 ),
124+ text = "0.05"
125+ )
126+
127+ original_dt : float = engine .dt
128+ is_paused : bool = False
129+
71130
131+ def draw_background (surface : pygame .Surface ) -> None :
132+ """Draw background image"""
133+ if background :
134+ surface .blit (background , (background_offset_x , background_offset_y ))
135+ else :
136+ surface .fill ((0 , 0 , 0 ))
72137
73- def create_object (x , y ):
138+
139+ def create_object (x : float , y : float ) -> None :
74140 """Create a new celestial body at the specified position
75141
76142 Args:
@@ -80,29 +146,85 @@ def create_object(x, y):
80146 mass = safe_float_convert (mass_input .text , DEFAULT_MASS , 1 , 10000 )
81147 radius = safe_float_convert (radius_input .text , DEFAULT_RADIUS , 1 , 100 )
82148
149+ base_image = random .choice (planet_textures ) if planet_textures else None
150+
83151 new_object = Object (
84152 x = x ,
85153 y = y ,
86154 mass = mass ,
87155 radius = radius ,
88156 vx = random .uniform (- 10 , 10 ),
89- vy = random .uniform (- 10 , 10 )
157+ vy = random .uniform (- 10 , 10 ),
158+ base_image = base_image
90159 )
91160
92- new_object .color = random_color ()
161+ new_object .color = (
162+ random .randint (100 , 255 ),
163+ random .randint (100 , 255 ),
164+ random .randint (100 , 255 )
165+ )
93166
94167 engine .add_particle (new_object )
95168
96169
97- def draw_object_info (surface , hovered , text_font ):
170+ def update_physics_parameters () -> None :
171+ """Update physics engine parameters from input boxes"""
172+ new_g = safe_float_convert (g_input .text , 100.0 , 0.1 , 10000.0 )
173+ engine .G = new_g
174+
175+ new_dt = safe_float_convert (dt_input .text , 0.05 , 0.0 , 1.0 )
176+ engine .dt = new_dt
177+
178+
179+ def adjust_physics_parameters (g_delta : float = 0 , dt_delta : float = 0 ) -> None :
180+ """Adjust physics parameters by delta values and update input boxes
181+
182+ Args:
183+ g_delta: Change in G value
184+ dt_delta: Change in dt value
185+ """
186+ global is_paused , original_dt
187+
188+ if is_paused and dt_delta != 0 :
189+ is_paused = False
190+ original_dt = engine .dt
191+
192+ new_g = max (0.1 , engine .G + g_delta )
193+ engine .G = new_g
194+ g_input .text = str (round (new_g , 1 ))
195+
196+ new_dt = max (0.0 , engine .dt + dt_delta )
197+ engine .dt = new_dt
198+ dt_input .text = str (round (new_dt , 3 ))
199+ if new_dt == 0.0 :
200+ is_paused = True
201+
202+
203+ def toggle_pause () -> None :
204+ """Toggle simulation pause state"""
205+ global is_paused , original_dt
206+
207+ if is_paused :
208+ engine .dt = original_dt
209+ dt_input .text = str (round (original_dt , 3 ))
210+ is_paused = False
211+ else :
212+ original_dt = engine .dt
213+ engine .dt = 0.0
214+ dt_input .text = "0.0"
215+ is_paused = True
216+
217+
218+ def draw_object_info (surface : pygame .Surface , hovered : Optional [Object ],
219+ text_font : pygame .font .Font ) -> None :
98220 """Render information panel for the hovered celestial body
99221
100222 Args:
101223 surface: Pygame surface to draw on
102224 hovered: Currently hovered celestial body (or None)
103225 text_font: Font for rendering text
104226 """
105- if hovered_object :
227+ if hovered :
106228 info_text = [
107229 f"Mass: { hovered .mass :.1f} " ,
108230 f"Radius: { hovered .radius :.1f} " ,
@@ -122,17 +244,46 @@ def draw_object_info(surface, hovered, text_font):
122244 surface .blit (text_surface , (20 , 20 + i * 25 ))
123245
124246
125- def draw_ui_labels (surface , text_font ) :
126- """Draw UI labels for mass and radius inputs
247+ def draw_ui_labels (surface : pygame . Surface , text_font : pygame . font . Font ) -> None :
248+ """Draw UI labels for all input boxes
127249
128250 Args:
129251 surface: Pygame surface to draw on
130252 text_font: Font for rendering text
131253 """
132254 mass_label = text_font .render ("Mass:" , True , (255 , 255 , 255 ))
133255 radius_label = text_font .render ("Radius:" , True , (255 , 255 , 255 ))
256+ g_label = text_font .render ("G:" , True , (255 , 255 , 255 ))
257+ dt_label = text_font .render ("dt:" , True , (255 , 255 , 255 ))
258+
134259 surface .blit (mass_label , (SCREEN_WIDTH - 200 , 15 ))
135260 surface .blit (radius_label , (SCREEN_WIDTH - 211 , 55 ))
261+ surface .blit (g_label , (SCREEN_WIDTH - 180 , 95 ))
262+ surface .blit (dt_label , (SCREEN_WIDTH - 185 , 135 ))
263+
264+
265+ def draw_hotkey_info (surface : pygame .Surface , text_font : pygame .font .Font ) -> None :
266+ """Draw hotkey information panel
267+
268+ Args:
269+ surface: Pygame surface to draw on
270+ text_font: Font for rendering text
271+ """
272+ hotkey_text = [
273+ "Hotkeys:" ,
274+ "Num/* : G ±10" ,
275+ "Num-/+: dt ±0.01" ,
276+ "Space: Pause" ,
277+ "Delete: Delete Object"
278+ ]
279+
280+ text_height = len (hotkey_text ) * 20 + 10
281+ pygame .draw .rect (surface , (0 , 0 , 0 ), (10 , SCREEN_HEIGHT - 120 , 220 , text_height ))
282+ pygame .draw .rect (surface , (100 , 100 , 100 ), (10 , SCREEN_HEIGHT - 120 , 220 , text_height ), 1 )
283+
284+ for i , text in enumerate (hotkey_text ):
285+ text_surface = text_font .render (text , True , (200 , 200 , 200 ))
286+ surface .blit (text_surface , (15 , SCREEN_HEIGHT - 112 + i * 20 ))
136287
137288
138289while running :
@@ -148,34 +299,68 @@ def draw_ui_labels(surface, text_font):
148299
149300 mass_input .handle_event (event )
150301 radius_input .handle_event (event )
302+ g_input .handle_event (event )
303+ dt_input .handle_event (event )
151304
152305 if event .type == pygame .MOUSEBUTTONDOWN :
153306 if event .button == 1 : # left button
154- if not mass_input .is_hovered (event .pos ) and not radius_input .is_hovered (event .pos ):
307+ input_boxes = [mass_input , radius_input , g_input , dt_input ]
308+ clicked_on_input = any (box .is_hovered (event .pos ) for box in input_boxes )
309+
310+ if not clicked_on_input :
155311 create_object (mouse_x , mouse_y )
156312
157313 elif event .type == pygame .KEYDOWN :
158314 if event .key == pygame .K_DELETE :
159315 if hovered_object :
160316 engine .remove_particle (hovered_object )
317+ elif event .key == pygame .K_RETURN :
318+ update_physics_parameters ()
319+ elif event .key == pygame .K_SPACE :
320+ toggle_pause ()
321+ elif event .key == pygame .K_KP_MULTIPLY :
322+ adjust_physics_parameters (g_delta = 10 )
323+ elif event .key == pygame .K_KP_DIVIDE :
324+ adjust_physics_parameters (g_delta = - 10 )
325+ elif event .key == pygame .K_KP_MINUS :
326+ adjust_physics_parameters (dt_delta = - 0.01 )
327+ elif event .key == pygame .K_KP_PLUS :
328+ adjust_physics_parameters (dt_delta = 0.01 )
329+
330+ update_physics_parameters ()
161331
162332 engine .update ()
163333
164334 mass_input .update ()
165335 radius_input .update ()
336+ g_input .update ()
337+ dt_input .update ()
166338
167- screen .fill ((0 , 0 , 0 ))
168-
169- draw_grid (screen , grid_size = GRID_SIZE , grid_color = GRID_COLOR )
339+ draw_background (screen )
170340
171341 for particle in engine .particles :
172342 if particle .active :
173- pygame .draw .circle (
174- screen ,
175- particle .color ,
176- (int (particle .x ), int (particle .y )),
177- int (particle .radius )
178- )
343+ if hasattr (particle , 'base_image' ) and particle .base_image :
344+ scaled_size = int (particle .radius * 2 )
345+ if scaled_size > 0 :
346+ scaled_image = pygame .transform .smoothscale (
347+ particle .base_image ,
348+ (scaled_size , scaled_size )
349+ )
350+
351+ colored_image = apply_color_tint (scaled_image , particle .color )
352+
353+ screen .blit (
354+ colored_image ,
355+ (int (particle .x - particle .radius ), int (particle .y - particle .radius ))
356+ )
357+ else :
358+ pygame .draw .circle (
359+ screen ,
360+ particle .color ,
361+ (int (particle .x ), int (particle .y )),
362+ int (particle .radius )
363+ )
179364
180365 if hasattr (particle , 'trail' ) and len (particle .trail ) > 1 :
181366 pygame .draw .lines (
@@ -190,8 +375,11 @@ def draw_ui_labels(surface, text_font):
190375
191376 mass_input .draw (screen )
192377 radius_input .draw (screen )
378+ g_input .draw (screen )
379+ dt_input .draw (screen )
193380
194381 draw_ui_labels (screen , font )
382+ draw_hotkey_info (screen , font )
195383
196384 pygame .display .flip ()
197385 clock .tick (60 )
0 commit comments