import pygame import math import random # Initialize PyGame and create window pygame.init() width, height = 800, 600 screen = pygame.display.set_mode((width, height)) pygame.display.set_caption("Drag to Shoot with Prediction & Trails") clock = pygame.time.Clock() # Constants GRAVITY = 300 # pixels per second^2 VELOCITY_SCALE = 0.8 # multiplier for the drag-to-velocity conversion (a stronger shot) PREDICTION_TIME = 3.0 # seconds to simulate the predicted trajectory PREDICTION_DT = 0.02 # simulation time step for prediction BALL_RADIUS = 8 # radius for each ball PREDICTION_DISPLAY_TIME = 5.0 # seconds to display the prediction after launch MAX_TRAIL_LENGTH = PREDICTION_DISPLAY_TIME * VELOCITY_SCALE * PREDICTION_TIME * 1000 # maximum number of positions stored for ball trail # List to hold active projectiles (each is a dict with position, velocity, lifetime, and a trail) projectiles = [] # For storing the last prediction (list of positions) and a timer to display it after shot last_prediction = None prediction_timer = 0 # ----------------------- # Collision Handling Code # ----------------------- def handle_collisions(balls): """ For each pair of balls in the list, check if they overlap. If so, separate them and update their velocities using a simple elastic collision formula (assuming equal masses). """ for i in range(len(balls)): for j in range(i + 1, len(balls)): ball1 = balls[i] ball2 = balls[j] dx = ball1["pos"][0] - ball2["pos"][0] dy = ball1["pos"][1] - ball2["pos"][1] dist_sq = dx * dx + dy * dy min_dist = ball1["radius"] + ball2["radius"] if dist_sq < min_dist * min_dist: dist = math.sqrt(dist_sq) if dist_sq != 0 else 0.1 # Calculate overlap amount overlap = 0.5 * (min_dist - dist) nx = dx / dist ny = dy / dist # Separate the balls so they just touch ball1["pos"][0] += nx * overlap ball1["pos"][1] += ny * overlap ball2["pos"][0] -= nx * overlap ball2["pos"][1] -= ny * overlap # Relative velocity in normal direction dvx = ball1["vel"][0] - ball2["vel"][0] dvy = ball1["vel"][1] - ball2["vel"][1] dot = dvx * nx + dvy * ny if dot < 0: # Adjust velocities for a simple elastic collision ball1["vel"][0] -= dot * nx ball1["vel"][1] -= dot * ny ball2["vel"][0] += dot * nx ball2["vel"][1] += dot * ny # ----------------------- # Prediction Simulation # ----------------------- def simulate_prediction(initial_pos, initial_vel, other_balls, prediction_time=PREDICTION_TIME, dt_sim=PREDICTION_DT): """ Simulate the predicted path of a new ball starting at 'initial_pos' with initial velocity 'initial_vel'. The simulation takes into account gravity, wall bounces (using the ball radius), and collisions with other balls. Returns a list of (x, y) positions for the predicted ball. """ # Create a copy for the predicted ball (with the same BALL_RADIUS) predicted_ball = {"pos": [initial_pos[0], initial_pos[1]], "vel": [initial_vel[0], initial_vel[1]], "radius": BALL_RADIUS} # Copy the other balls from the current simulation (so as not to modify them) predicted_others = [] for ball in other_balls: new_ball = {"pos": [ball["pos"][0], ball["pos"][1]], "vel": [ball["vel"][0], ball["vel"][1]], "radius": ball.get("radius", BALL_RADIUS)} predicted_others.append(new_ball) balls = [predicted_ball] + predicted_others positions = [] steps = int(prediction_time / dt_sim) for _ in range(steps): # Update every ball in simulation for ball in balls: ball["vel"][1] += GRAVITY * dt_sim ball["pos"][0] += ball["vel"][0] * dt_sim ball["pos"][1] += ball["vel"][1] * dt_sim # Bounce off left/right walls (accounting for ball radius) if ball["pos"][0] < ball["radius"]: ball["pos"][0] = ball["radius"] ball["vel"][0] = -ball["vel"][0] elif ball["pos"][0] > width - ball["radius"]: ball["pos"][0] = width - ball["radius"] ball["vel"][0] = -ball["vel"][0] # Bounce off top/bottom walls if ball["pos"][1] < ball["radius"]: ball["pos"][1] = ball["radius"] ball["vel"][1] = -ball["vel"][1] elif ball["pos"][1] > height - ball["radius"]: ball["pos"][1] = height - ball["radius"] ball["vel"][1] = -ball["vel"][1] # Handle collisions among balls handle_collisions(balls) # Record the predicted ball's position positions.append((int(predicted_ball["pos"][0]), int(predicted_ball["pos"][1]))) return positions # ----------------------- # Main Simulation Loop # ----------------------- # Variables for the drag-to-aim mechanic dragging = False start_pos = (0, 0) current_pos = (0, 0) running = True while running: dt = clock.tick(60) / 1000.0 # delta time in seconds for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Start dragging elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: dragging = True start_pos = event.pos current_pos = event.pos # Update drag position while holding down the mouse elif event.type == pygame.MOUSEMOTION and dragging: current_pos = event.pos # On mouse release, launch a new ball and compute its prediction. elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: if dragging: # Compute initial velocity (invert drag vector so dragging backward shoots forward) v0 = ((start_pos[0] - current_pos[0]) * VELOCITY_SCALE, (start_pos[1] - current_pos[1]) * VELOCITY_SCALE) # Compute and store the prediction simulation (will be displayed for a short time) last_prediction = simulate_prediction(start_pos, v0, projectiles) prediction_timer = PREDICTION_DISPLAY_TIME # Create a new ball with an initial empty trail new_ball = {"pos": [start_pos[0], start_pos[1]], "vel": [v0[0], v0[1]], "radius": BALL_RADIUS, "lifetime": random.uniform(3, 5), "trail": []} projectiles.append(new_ball) dragging = False # Update active projectiles (balls) for ball in projectiles: # Append current position to the trail ball["trail"].append((ball["pos"][0], ball["pos"][1])) if len(ball["trail"]) > MAX_TRAIL_LENGTH: ball["trail"].pop(0) # Update velocity (apply gravity) and position ball["vel"][1] += GRAVITY * dt ball["pos"][0] += ball["vel"][0] * dt ball["pos"][1] += ball["vel"][1] * dt # Bounce off the walls, accounting for ball radius if ball["pos"][0] < ball["radius"]: ball["pos"][0] = ball["radius"] ball["vel"][0] = -ball["vel"][0] elif ball["pos"][0] > width - ball["radius"]: ball["pos"][0] = width - ball["radius"] ball["vel"][0] = -ball["vel"][0] if ball["pos"][1] < ball["radius"]: ball["pos"][1] = ball["radius"] ball["vel"][1] = -ball["vel"][1] elif ball["pos"][1] > height - ball["radius"]: ball["pos"][1] = height - ball["radius"] ball["vel"][1] = -ball["vel"][1] # Decrease lifetime (ball will vanish after its lifetime expires) ball["lifetime"] -= dt # Remove expired balls projectiles = [ball for ball in projectiles if ball["lifetime"] > 0] # Handle collisions among active projectiles handle_collisions(projectiles) # Decrease the prediction display timer if prediction_timer > 0: prediction_timer -= dt if prediction_timer <= 0: last_prediction = None # ----------------------- # Drawing Section # ----------------------- screen.fill((255, 255, 255)) # While dragging, draw drag line and live predicted trajectory if dragging: pygame.draw.line(screen, (0, 0, 0), start_pos, current_pos, 2) v0 = ((start_pos[0] - current_pos[0]) * VELOCITY_SCALE, (start_pos[1] - current_pos[1]) * VELOCITY_SCALE) live_prediction = simulate_prediction(start_pos, v0, projectiles) if len(live_prediction) > 1: pygame.draw.lines(screen, (0, 0, 255), False, live_prediction, 2) # Draw the stored prediction (after shot) if available if last_prediction is not None: pygame.draw.lines(screen, (128, 0, 128), False, last_prediction, 2) # Draw projectiles and their trails for ball in projectiles: # Draw the trail as a line (if there are at least 2 points) if len(ball["trail"]) > 1: pygame.draw.lines(screen, (150, 150, 150), False, ball["trail"], 2) # Draw the ball as a filled circle pos = (int(ball["pos"][0]), int(ball["pos"][1])) pygame.draw.circle(screen, (255, 0, 0), pos, ball["radius"]) pygame.display.flip() pygame.quit()